mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-03-13 09:50:18 +01:00
commit
a2d93543e8
@ -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.
|
- 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.
|
- Search for articles with regular expressions or filter by read status.
|
||||||
- Organize your subscriptions with folder-like groupings.
|
- Organize your subscriptions with folder-like groupings.
|
||||||
|
- Single-key [keyboard shortcuts](https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts).
|
||||||
|
|
||||||
## Development
|
## 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).
|
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).
|
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.
|
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.
|
||||||
|
|
||||||
|
3
dist/article/article.css
vendored
3
dist/article/article.css
vendored
@ -60,4 +60,7 @@ article figure figcaption {
|
|||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
color: var(--gray);
|
color: var(--gray);
|
||||||
-webkit-user-modify: read-only;
|
-webkit-user-modify: read-only;
|
||||||
|
}
|
||||||
|
article iframe {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
60
dist/icons/logo-outline-dark.svg
vendored
Normal file
60
dist/icons/logo-outline-dark.svg
vendored
Normal 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
60
dist/icons/logo-outline.svg
vendored
Normal 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 |
0
dist/logo.svg → dist/icons/logo.svg
vendored
0
dist/logo.svg → dist/icons/logo.svg
vendored
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
44
dist/styles.css
vendored
44
dist/styles.css
vendored
@ -277,6 +277,7 @@ nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system {
|
|||||||
height: calc(100% - 64px);
|
height: calc(100% - 64px);
|
||||||
background-color: var(--white);
|
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);
|
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"] {
|
div[role="toolbar"] {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -469,12 +470,34 @@ img.favicon {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.side-article-wrapper {
|
.side-article-wrapper, .side-logo-wrapper {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-top: 32px;
|
padding-top: 32px;
|
||||||
height: calc(100% - 32px);
|
height: calc(100% - 32px);
|
||||||
background: var(--white);
|
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 {
|
.side-article-wrapper .article {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
@ -631,8 +654,19 @@ img.favicon {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
animation-fill-mode: none;
|
animation-fill-mode: none;
|
||||||
}
|
}
|
||||||
.card:hover {
|
.card:hover, .card:focus {
|
||||||
box-shadow: #0006 0px 5px 40px;
|
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 {
|
.card:active {
|
||||||
transform: scale(.97);
|
transform: scale(.97);
|
||||||
@ -663,7 +697,8 @@ img.favicon {
|
|||||||
.card img.head, .card p, .card h3 {
|
.card img.head, .card p, .card h3 {
|
||||||
transition: transform ease-out .12s;
|
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);
|
transform: translateY(-144px);
|
||||||
}
|
}
|
||||||
.card h3.title {
|
.card h3.title {
|
||||||
@ -717,8 +752,9 @@ img.favicon {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: #0000 0px 5px 15px;
|
box-shadow: #0000 0px 5px 15px;
|
||||||
}
|
}
|
||||||
.list-card:hover {
|
.list-card:hover, .list-card:focus {
|
||||||
box-shadow: #0004 0px 5px 15px;
|
box-shadow: #0004 0px 5px 15px;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
.list-card:active {
|
.list-card:active {
|
||||||
box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px;
|
box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px;
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<img class="elevate" src="imgs/opml.png" />
|
<img class="elevate" src="imgs/opml.png" />
|
||||||
</section>
|
</section>
|
||||||
<section class="elevate">
|
<section class="elevate">
|
||||||
<h2>Read comfortably.</h2>
|
<h2>Read fluently.</h2>
|
||||||
<p>
|
<p>
|
||||||
Enjoy your contents like never before with the built-in article view
|
Enjoy your contents like never before with the built-in article view
|
||||||
for RSS full text tailored to maximize focus. Source only comes with
|
for RSS full text tailored to maximize focus. Source only comes with
|
||||||
@ -56,7 +56,7 @@
|
|||||||
<h3>No personal information collected, ever.</h3>
|
<h3>No personal information collected, ever.</h3>
|
||||||
<h3>Behavior tracking limited.</h3>
|
<h3>Behavior tracking limited.</h3>
|
||||||
<h3>Strict Content Security Policy enforced.</h3>
|
<h3>Strict Content Security Policy enforced.</h3>
|
||||||
<h3>Sandboxed webview.</h3>
|
<h3>Proxy support with PAC.</h3>
|
||||||
<br />
|
<br />
|
||||||
<h3><span>■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■</span></h3>
|
<h3><span>■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■</span></h3>
|
||||||
<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><a href="https://github.com/yang991178/fluent-reader/releases">Download for macOS or older Windows</a></div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://github.com/yang991178/fluent-reader/">GitHub</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fluent-reader",
|
"name": "fluent-reader",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"description": "A simplistic, modern desktop RSS reader",
|
"description": "A simplistic, modern desktop RSS reader",
|
||||||
"main": "./dist/electron.js",
|
"main": "./dist/electron.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -3,10 +3,10 @@ import intl = require("react-intl-universal")
|
|||||||
import { renderToString } from "react-dom/server"
|
import { renderToString } from "react-dom/server"
|
||||||
import { RSSItem } from "../scripts/models/item"
|
import { RSSItem } from "../scripts/models/item"
|
||||||
import { openExternal } from "../scripts/utils"
|
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 { RSSSource, SourceOpenTarget } from "../scripts/models/source"
|
||||||
import { store } from "../scripts/settings"
|
import { store } from "../scripts/settings"
|
||||||
import { clipboard } from "electron"
|
import { clipboard, remote } from "electron"
|
||||||
|
|
||||||
const FONT_SIZE_STORE_KEY = "fontSize"
|
const FONT_SIZE_STORE_KEY = "fontSize"
|
||||||
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
|
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||||
@ -15,7 +15,9 @@ type ArticleProps = {
|
|||||||
item: RSSItem
|
item: RSSItem
|
||||||
source: RSSSource
|
source: RSSSource
|
||||||
locale: string
|
locale: string
|
||||||
|
shortcuts: (item: RSSItem, key: string) => void
|
||||||
dismiss: () => void
|
dismiss: () => void
|
||||||
|
offsetItem: (offset: number) => void
|
||||||
toggleHasRead: (item: RSSItem) => void
|
toggleHasRead: (item: RSSItem) => void
|
||||||
toggleStarred: (item: RSSItem) => void
|
toggleStarred: (item: RSSItem) => void
|
||||||
toggleHidden: (item: RSSItem) => void
|
toggleHidden: (item: RSSItem) => void
|
||||||
@ -28,7 +30,8 @@ type ArticleState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Article extends React.Component<ArticleProps, ArticleState> {
|
class Article extends React.Component<ArticleProps, ArticleState> {
|
||||||
webview: HTMLWebViewElement
|
webview: Electron.WebviewTag
|
||||||
|
shouldRefocus = false
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
@ -100,15 +103,42 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
openExternal(event.url)
|
openExternal(event.url)
|
||||||
this.props.dismiss()
|
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 = () => {
|
componentDidMount = () => {
|
||||||
let webview = document.getElementById("article")
|
let webview = document.getElementById("article") as Electron.WebviewTag
|
||||||
if (webview != this.webview) {
|
if (webview != this.webview) {
|
||||||
if (this.webview) this.componentWillUnmount()
|
|
||||||
webview.addEventListener("ipc-message", this.ipcHandler)
|
webview.addEventListener("ipc-message", this.ipcHandler)
|
||||||
webview.addEventListener("new-window", this.popUpHandler)
|
webview.addEventListener("new-window", this.popUpHandler)
|
||||||
webview.addEventListener("will-navigate", this.navigationHandler)
|
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
|
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) => {
|
componentDidUpdate = (prevProps: ArticleProps) => {
|
||||||
@ -119,9 +149,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
this.webview.removeEventListener("ipc-message", this.ipcHandler)
|
if (this.shouldRefocus) {
|
||||||
this.webview.removeEventListener("new-window", this.popUpHandler)
|
let refocus = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement
|
||||||
this.webview.removeEventListener("will-navigate", this.navigationHandler)
|
if (refocus) refocus.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openInBrowser = () => {
|
openInBrowser = () => {
|
||||||
@ -143,7 +174,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
</>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}`
|
</>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}`
|
||||||
|
|
||||||
render = () => (
|
render = () => (
|
||||||
<div className="article">
|
<FocusZone className="article">
|
||||||
<Stack horizontal style={{height: 36}}>
|
<Stack horizontal style={{height: 36}}>
|
||||||
<span style={{width: 96}}></span>
|
<span style={{width: 96}}></span>
|
||||||
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
|
<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()}
|
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
|
||||||
preload={this.state.loadWebpage ? null : "article/preload.js"}
|
preload={this.state.loadWebpage ? null : "article/preload.js"}
|
||||||
partition="sandbox" />
|
partition="sandbox" />
|
||||||
</div>
|
</FocusZone>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ export interface CardProps {
|
|||||||
feedId: string
|
feedId: string
|
||||||
item: RSSItem
|
item: RSSItem
|
||||||
source: RSSSource
|
source: RSSSource
|
||||||
|
shortcuts: (item: RSSItem, key: string) => void
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||||
showItem: (fid: string, item: RSSItem) => 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)
|
this.props.contextMenu(this.props.feedId, this.props.item, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
this.props.shortcuts(this.props.item, e.key)
|
||||||
|
}
|
||||||
}
|
}
|
@ -13,8 +13,14 @@ class DefaultCard extends Card {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={this.className()}
|
<div
|
||||||
onClick={this.onClick} onMouseUp={this.onMouseUp} >
|
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 ? (
|
{this.props.item.thumb ? (
|
||||||
<img className="bg" src={this.props.item.thumb} />
|
<img className="bg" src={this.props.item.thumb} />
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -12,8 +12,14 @@ class ListCard extends Card {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={this.className()}
|
<div
|
||||||
onClick={this.onClick} onMouseUp={this.onMouseUp} >
|
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 ? (
|
{this.props.item.thumb ? (
|
||||||
<div className="head"><img src={this.props.item.thumb} /></div>
|
<div className="head"><img src={this.props.item.thumb} /></div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -2,7 +2,7 @@ import * as React from "react"
|
|||||||
import intl = require("react-intl-universal")
|
import intl = require("react-intl-universal")
|
||||||
import { FeedProps } from "./feed"
|
import { FeedProps } from "./feed"
|
||||||
import DefaultCard from "../cards/default-card"
|
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> {
|
class CardsFeed extends React.Component<FeedProps> {
|
||||||
state = { width: window.innerWidth - 12 }
|
state = { width: window.innerWidth - 12 }
|
||||||
@ -31,7 +31,7 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.props.feed.loaded && (
|
return this.props.feed.loaded && (
|
||||||
<div className="cards-feed-container">
|
<FocusZone as="div" id="refocus" className="cards-feed-container">
|
||||||
{
|
{
|
||||||
this.props.items.map((item) => (
|
this.props.items.map((item) => (
|
||||||
<DefaultCard
|
<DefaultCard
|
||||||
@ -39,6 +39,7 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
key={item._id}
|
key={item._id}
|
||||||
item={item}
|
item={item}
|
||||||
source={this.props.sourceMap[item.source]}
|
source={this.props.sourceMap[item.source]}
|
||||||
|
shortcuts={this.props.shortcuts}
|
||||||
markRead={this.props.markRead}
|
markRead={this.props.markRead}
|
||||||
contextMenu={this.props.contextMenu}
|
contextMenu={this.props.contextMenu}
|
||||||
showItem={this.props.showItem} />
|
showItem={this.props.showItem} />
|
||||||
@ -56,7 +57,7 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
{ this.props.items.length === 0 && (
|
{ this.props.items.length === 0 && (
|
||||||
<div className="empty">{intl.get("article.empty")}</div>
|
<div className="empty">{intl.get("article.empty")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</FocusZone>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ export type FeedProps = FeedReduxProps & {
|
|||||||
viewType: ViewType
|
viewType: ViewType
|
||||||
items: RSSItem[]
|
items: RSSItem[]
|
||||||
sourceMap: Object
|
sourceMap: Object
|
||||||
|
shortcuts: (item: RSSItem, key: string) => void
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||||
loadMore: (feed: RSSFeed) => void
|
loadMore: (feed: RSSFeed) => void
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import intl = require("react-intl-universal")
|
import intl = require("react-intl-universal")
|
||||||
import { FeedProps } from "./feed"
|
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";
|
import ListCard from "../cards/list-card";
|
||||||
|
|
||||||
class ListFeed extends React.Component<FeedProps> {
|
class ListFeed extends React.Component<FeedProps> {
|
||||||
render() {
|
render() {
|
||||||
return this.props.feed.loaded && (
|
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) => (
|
this.props.items.map((item) => (
|
||||||
<ListCard
|
<ListCard
|
||||||
@ -15,6 +15,7 @@ class ListFeed extends React.Component<FeedProps> {
|
|||||||
key={item._id}
|
key={item._id}
|
||||||
item={item}
|
item={item}
|
||||||
source={this.props.sourceMap[item.source]}
|
source={this.props.sourceMap[item.source]}
|
||||||
|
shortcuts={this.props.shortcuts}
|
||||||
markRead={this.props.markRead}
|
markRead={this.props.markRead}
|
||||||
contextMenu={this.props.contextMenu}
|
contextMenu={this.props.contextMenu}
|
||||||
showItem={this.props.showItem} />
|
showItem={this.props.showItem} />
|
||||||
@ -31,7 +32,7 @@ class ListFeed extends React.Component<FeedProps> {
|
|||||||
{ this.props.items.length === 0 && (
|
{ this.props.items.length === 0 && (
|
||||||
<div className="empty">{intl.get("article.empty")}</div>
|
<div className="empty">{intl.get("article.empty")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</FocusZone>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export type MenuProps = {
|
|||||||
export class Menu extends React.Component<MenuProps> {
|
export class Menu extends React.Component<MenuProps> {
|
||||||
countOverflow = (count: number) => count >= 1000 ? "999+" : String(count)
|
countOverflow = (count: number) => count >= 1000 ? "999+" : String(count)
|
||||||
|
|
||||||
getItems = (): INavLinkGroup[] => [
|
getLinkGroups = (): INavLinkGroup[] => [
|
||||||
{
|
{
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@ -46,28 +46,28 @@ export class Menu extends React.Component<MenuProps> {
|
|||||||
url: null
|
url: null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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])
|
||||||
|
return {
|
||||||
|
name: g.name,
|
||||||
|
ariaLabel: this.countOverflow(sources.map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
||||||
|
key: "g-" + i,
|
||||||
|
url: null,
|
||||||
|
isExpanded: g.expanded,
|
||||||
|
onClick: () => this.props.selectSourceGroup(g, "g-" + i),
|
||||||
|
links: sources.map(this.getSource)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.getSource(this.props.sources[g.sids[0]])
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
getGroups = (): INavLinkGroup[] => [{
|
|
||||||
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])
|
|
||||||
return {
|
|
||||||
name: g.name,
|
|
||||||
ariaLabel: this.countOverflow(sources.map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
|
||||||
key: "g-" + i,
|
|
||||||
url: null,
|
|
||||||
isExpanded: g.expanded,
|
|
||||||
onClick: () => this.props.selectSourceGroup(g, "g-" + i),
|
|
||||||
links: sources.map(this.getSource)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return this.getSource(this.props.sources[g.sids[0]])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)}]
|
|
||||||
|
|
||||||
getSource = (s: RSSSource): INavLink => ({
|
getSource = (s: RSSSource): INavLink => ({
|
||||||
name: s.name,
|
name: s.name,
|
||||||
ariaLabel: this.countOverflow(s.unreadCount),
|
ariaLabel: this.countOverflow(s.unreadCount),
|
||||||
@ -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() {
|
render() {
|
||||||
return this.props.status && (
|
return this.props.status && (
|
||||||
<div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}>
|
<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>
|
||||||
<div className="nav-wrapper">
|
<div className="nav-wrapper">
|
||||||
<Nav
|
<Nav
|
||||||
|
onRenderGroupHeader={this._onRenderGroupHeader}
|
||||||
onRenderLink={this._onRenderLink}
|
onRenderLink={this._onRenderLink}
|
||||||
groups={this.getItems()}
|
groups={this.getLinkGroups()}
|
||||||
selectedKey={this.props.selected} />
|
|
||||||
<p className={"subs-header " + AnimationClassNames.slideDownIn10}>{intl.get("menu.subscriptions")}</p>
|
|
||||||
<Nav
|
|
||||||
selectedKey={this.props.selected}
|
selectedKey={this.props.selected}
|
||||||
onRenderLink={this._onRenderLink}
|
onLinkExpandClick={(event, item) => this.props.updateGroupExpansion(event, item.key, this.props.selected)} />
|
||||||
onLinkExpandClick={(event, item) => this.props.updateGroupExpansion(event, item.key, this.props.selected)}
|
|
||||||
groups={this.getGroups()} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@ import { remote } from "electron"
|
|||||||
import { Icon } from "@fluentui/react/lib/Icon"
|
import { Icon } from "@fluentui/react/lib/Icon"
|
||||||
import { AppState } from "../scripts/models/app"
|
import { AppState } from "../scripts/models/app"
|
||||||
import { ProgressIndicator } from "@fluentui/react"
|
import { ProgressIndicator } from "@fluentui/react"
|
||||||
|
import { getWindowBreakpoint } from "../scripts/utils"
|
||||||
|
|
||||||
type NavProps = {
|
type NavProps = {
|
||||||
state: AppState,
|
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 = () => {
|
minimize = () => {
|
||||||
this.state.window.minimize()
|
this.state.window.minimize()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { FeedContainer } from "../containers/feed-container"
|
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 ArticleContainer from "../containers/article-container"
|
||||||
import { ViewType } from "../scripts/models/page"
|
import { ViewType } from "../scripts/models/page"
|
||||||
import ArticleSearch from "./utils/article-search"
|
import ArticleSearch from "./utils/article-search"
|
||||||
@ -16,50 +16,59 @@ type PageProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Page extends React.Component<PageProps> {
|
class Page extends React.Component<PageProps> {
|
||||||
prevItem = (event: React.MouseEvent) => {
|
offsetItem = (event: React.MouseEvent, offset: number) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
this.props.offsetItem(-1)
|
this.props.offsetItem(offset)
|
||||||
}
|
|
||||||
nextItem = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
this.props.offsetItem(1)
|
|
||||||
}
|
}
|
||||||
|
prevItem = (event: React.MouseEvent) => this.offsetItem(event, -1)
|
||||||
|
nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1)
|
||||||
|
|
||||||
render = () => this.props.viewType == ViewType.Cards
|
render = () => this.props.viewType == ViewType.Cards
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
{this.props.settingsOn ? null :
|
{this.props.settingsOn ? null :
|
||||||
<div className={"main" + (this.props.menuOn ? " menu-on" : "")}>
|
<div key="card" className={"main" + (this.props.menuOn ? " menu-on" : "")}>
|
||||||
<ArticleSearch />
|
<ArticleSearch />
|
||||||
{this.props.feeds.map(fid => (
|
{this.props.feeds.map(fid => (
|
||||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||||
))}
|
))}
|
||||||
</div>}
|
</div>}
|
||||||
{this.props.itemId && (
|
{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()}>
|
<div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}>
|
||||||
<ArticleContainer itemId={this.props.itemId} />
|
<ArticleContainer itemId={this.props.itemId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="btn-group prev"><a className="btn" onClick={this.prevItem}><Icon iconName="Back" /></a></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 className="btn-group next"><a className="btn" onClick={this.nextItem}><Icon iconName="Forward" /></a></div>
|
||||||
</div>
|
</FocusTrapZone>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
{this.props.settingsOn ? null :
|
{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 />
|
<ArticleSearch />
|
||||||
<div className="list-feed-container">
|
<div className="list-feed-container">
|
||||||
{this.props.feeds.map(fid => (
|
{this.props.feeds.map(fid => (
|
||||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{this.props.itemId && (
|
{this.props.itemId
|
||||||
|
? (
|
||||||
<div className="side-article-wrapper">
|
<div className="side-article-wrapper">
|
||||||
<ArticleContainer itemId={this.props.itemId} />
|
<ArticleContainer itemId={this.props.itemId} />
|
||||||
</div>
|
</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>}
|
</div>}
|
||||||
</>
|
</>
|
||||||
|
@ -22,15 +22,15 @@ class Settings extends React.Component<SettingsProps> {
|
|||||||
|
|
||||||
render = () => this.props.display && (
|
render = () => this.props.display && (
|
||||||
<div className="settings-container">
|
<div className="settings-container">
|
||||||
|
<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}>
|
<div className={"settings " + AnimationClassNames.slideUpIn20}>
|
||||||
{this.props.blocked && <div className="loading">
|
{this.props.blocked && <div className="loading">
|
||||||
<Spinner label={intl.get("settings.fetching")} />
|
<Spinner label={intl.get("settings.fetching")} />
|
||||||
</div>}
|
</div>}
|
||||||
<div className="btn-group" style={{position: "absolute", top: 6, left: -64}}>
|
|
||||||
<a className={"btn" + (this.props.exitting ? " disabled" : "")} title={intl.get("settings.exit")} onClick={this.props.close}>
|
|
||||||
<Icon iconName="Back" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<Pivot>
|
<Pivot>
|
||||||
<PivotItem headerText={intl.get("settings.sources")} itemIcon="Source">
|
<PivotItem headerText={intl.get("settings.sources")} itemIcon="Source">
|
||||||
<SourcesTabContainer />
|
<SourcesTabContainer />
|
||||||
|
@ -8,11 +8,12 @@ class AboutTab extends React.Component {
|
|||||||
render = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
<Stack className="settings-about" horizontalAlign="center">
|
<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>
|
<h3>Fluent Reader</h3>
|
||||||
<small>{intl.get("settings.version")} {remote.app.getVersion()}</small>
|
<small>{intl.get("settings.version")} {remote.app.getVersion()}</small>
|
||||||
<p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
|
<p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
|
||||||
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 12}}>
|
<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")}>{intl.get("settings.openSource")}</Link></small>
|
||||||
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
|
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
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 { AppDispatch } from "../scripts/utils"
|
||||||
import { dismissItem } from "../scripts/models/page"
|
import { dismissItem, showOffsetItem } from "../scripts/models/page"
|
||||||
import Article from "../components/article"
|
import Article from "../components/article"
|
||||||
import { openTextMenu } from "../scripts/models/app"
|
import { openTextMenu } from "../scripts/models/app"
|
||||||
|
|
||||||
@ -28,7 +28,9 @@ const makeMapStateToProps = () => {
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||||
return {
|
return {
|
||||||
|
shortcuts: (item: RSSItem, key: string) => dispatch(itemShortcuts(item, key)),
|
||||||
dismiss: () => dispatch(dismissItem()),
|
dismiss: () => dispatch(dismissItem()),
|
||||||
|
offsetItem: (offset: number) => dispatch(showOffsetItem(offset)),
|
||||||
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
|
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
|
||||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||||
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
|
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
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 { openItemMenu } from "../scripts/models/app"
|
||||||
import { loadMore, RSSFeed } from "../scripts/models/feed"
|
import { loadMore, RSSFeed } from "../scripts/models/feed"
|
||||||
import { showItem, ViewType } from "../scripts/models/page"
|
import { showItem, ViewType } from "../scripts/models/page"
|
||||||
@ -30,6 +30,7 @@ const makeMapStateToProps = () => {
|
|||||||
}
|
}
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
|
shortcuts: (item: RSSItem, key: string) => dispatch(itemShortcuts(item, key)),
|
||||||
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
||||||
contextMenu: (feedId: string, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)),
|
contextMenu: (feedId: string, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)),
|
||||||
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),
|
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),
|
||||||
|
@ -77,6 +77,7 @@
|
|||||||
"app": "Preferences",
|
"app": "Preferences",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
|
"shortcuts": "Shortcuts",
|
||||||
"openSource": "Open source",
|
"openSource": "Open source",
|
||||||
"feedback": "Feedback"
|
"feedback": "Feedback"
|
||||||
},
|
},
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
"copyTitle": "复制标题",
|
"copyTitle": "复制标题",
|
||||||
"copyURL": "复制链接",
|
"copyURL": "复制链接",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"search": "使用Google搜索“{text}”",
|
"search": "使用 Google 搜索“{text}”",
|
||||||
"view": "视图",
|
"view": "视图",
|
||||||
"cardView": "卡片视图",
|
"cardView": "卡片视图",
|
||||||
"listView": "列表视图",
|
"listView": "列表视图",
|
||||||
@ -77,6 +77,7 @@
|
|||||||
"app": "应用偏好",
|
"app": "应用偏好",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
|
"shortcuts": "快捷键",
|
||||||
"openSource": "开源项目",
|
"openSource": "开源项目",
|
||||||
"feedback": "反馈"
|
"feedback": "反馈"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as db from "../db"
|
import * as db from "../db"
|
||||||
import intl = require("react-intl-universal")
|
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 { RSSSource } from "./source"
|
||||||
import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
|
import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
|
||||||
import Parser = require("@yang991178/rss-parser")
|
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
|
((s.lastFetched ? s.lastFetched.getTime() : 0) + (s.fetchFrequency || 0) * 60000) <= timenow
|
||||||
)
|
)
|
||||||
for (let source of sources) {
|
for (let source of sources) {
|
||||||
let promise = RSSSource.fetchItems(source, rssParser)
|
let promise = RSSSource.fetchItems(source)
|
||||||
promise.finally(() => dispatch(fetchItemsIntermediate()))
|
promise.finally(() => dispatch(fetchItemsIntermediate()))
|
||||||
promises.push(promise)
|
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) {
|
export function applyItemReduction(item: RSSItem, type: string) {
|
||||||
let nextItem = { ...item }
|
let nextItem = { ...item }
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -221,18 +221,20 @@ export function pageReducer(
|
|||||||
switch (action.pageType) {
|
switch (action.pageType) {
|
||||||
case PageType.AllArticles: return {
|
case PageType.AllArticles: return {
|
||||||
...state,
|
...state,
|
||||||
feedId: ALL
|
feedId: ALL,
|
||||||
|
itemId: null
|
||||||
}
|
}
|
||||||
case PageType.Sources: return {
|
case PageType.Sources: return {
|
||||||
...state,
|
...state,
|
||||||
feedId: SOURCE
|
feedId: SOURCE,
|
||||||
|
itemId: null
|
||||||
}
|
}
|
||||||
default: return state
|
default: return state
|
||||||
}
|
}
|
||||||
case SWITCH_VIEW: return {
|
case SWITCH_VIEW: return {
|
||||||
...state,
|
...state,
|
||||||
viewType: action.viewType,
|
viewType: action.viewType,
|
||||||
itemId: action.viewType === ViewType.List ? state.itemId : null
|
itemId: null
|
||||||
}
|
}
|
||||||
case APPLY_FILTER: return {
|
case APPLY_FILTER: return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Parser = require("@yang991178/rss-parser")
|
import Parser = require("@yang991178/rss-parser")
|
||||||
import intl = require("react-intl-universal")
|
import intl = require("react-intl-universal")
|
||||||
import * as db from "../db"
|
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 { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item"
|
||||||
import { SourceGroup } from "./group"
|
import { SourceGroup } from "./group"
|
||||||
import { saveSettings } from "./app"
|
import { saveSettings } from "./app"
|
||||||
@ -28,16 +28,16 @@ export class RSSSource {
|
|||||||
this.lastFetched = new Date()
|
this.lastFetched = new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMetaData(parser: Parser) {
|
static async fetchMetaData(source: RSSSource) {
|
||||||
let feed = await parseRSS(this.url)
|
let feed = await parseRSS(source.url)
|
||||||
if (!this.name) {
|
if (!source.name) {
|
||||||
if (feed.title) this.name = feed.title.trim()
|
if (feed.title) source.name = feed.title.trim()
|
||||||
this.name = this.name || intl.get("sources.untitled")
|
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 {
|
try {
|
||||||
let f = await fetchFavicon(domain)
|
let f = await fetchFavicon(domain)
|
||||||
if (f !== null) this.iconurl = f
|
if (f !== null) source.iconurl = f
|
||||||
} finally {
|
} finally {
|
||||||
return feed
|
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)
|
let feed = await parseRSS(source.url)
|
||||||
db.sdb.update({ sid: source.sid }, { $set: { lastFetched: new Date() } })
|
db.sdb.update({ sid: source.sid }, { $set: { lastFetched: new Date() } })
|
||||||
return await this.checkItems(source, feed.items)
|
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) {
|
if (app.sourceInit) {
|
||||||
dispatch(addSourceRequest(batch))
|
dispatch(addSourceRequest(batch))
|
||||||
let source = new RSSSource(url, name)
|
let source = new RSSSource(url, name)
|
||||||
return source.fetchMetaData(rssParser)
|
return RSSSource.fetchMetaData(source)
|
||||||
.then(feed => {
|
.then(feed => {
|
||||||
return dispatch(insertSource(source))
|
return dispatch(insertSource(source))
|
||||||
.then(inserted => {
|
.then(inserted => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { remote, ipcRenderer } from "electron"
|
import { remote, ipcRenderer } from "electron"
|
||||||
import { ViewType } from "./models/page"
|
import { ViewType } from "./models/page"
|
||||||
import { IPartialTheme, loadTheme, values } from "@fluentui/react"
|
import { IPartialTheme, loadTheme } from "@fluentui/react"
|
||||||
import locales from "./i18n/_locales"
|
import locales from "./i18n/_locales"
|
||||||
import Store = require("electron-store")
|
import Store = require("electron-store")
|
||||||
import { schemaTypes } from "./config-schema"
|
import { schemaTypes } from "./config-schema"
|
||||||
@ -51,7 +51,7 @@ export const setDefaultView = (viewType: ViewType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lightTheme: IPartialTheme = {
|
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 = {
|
const darkTheme: IPartialTheme = {
|
||||||
...lightTheme,
|
...lightTheme,
|
||||||
|
@ -17,7 +17,7 @@ export type AppThunk<ReturnType = void> = ThunkAction<
|
|||||||
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
||||||
|
|
||||||
import Parser = require("@yang991178/rss-parser")
|
import Parser = require("@yang991178/rss-parser")
|
||||||
export const rssParser = new Parser({
|
const rssParser = new Parser({
|
||||||
customFields: {
|
customFields: {
|
||||||
item: ["thumb", "image", ["content:encoded", "fullContent"]] as Parser.CustomFieldItem[]
|
item: ["thumb", "image", ["content:encoded", "fullContent"]] as Parser.CustomFieldItem[]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user