mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-07 23:38:41 +01:00
keyboard shortcuts
This commit is contained in:
parent
3578721d6c
commit
d4acdfb59c
3
dist/article/article.css
vendored
3
dist/article/article.css
vendored
@ -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
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 |
25
dist/styles.css
vendored
25
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);
|
||||
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;
|
||||
|
@ -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
|
||||
@ -102,9 +104,23 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
this.props.dismiss()
|
||||
}
|
||||
keyDownHandler = (_, input) => {
|
||||
if (input.type === "keyDown" && input.key === "Escape") {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,6 +136,9 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
})
|
||||
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) => {
|
||||
@ -131,7 +150,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
|
||||
componentWillUnmount = () => {
|
||||
if (this.shouldRefocus) {
|
||||
let refocus = document.querySelector("#refocus>div[tabindex='0']") as HTMLElement
|
||||
let refocus = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement
|
||||
if (refocus) refocus.focus()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ class DefaultCard extends Card {
|
||||
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} />
|
||||
|
@ -14,8 +14,11 @@ class ListCard extends Card {
|
||||
return (
|
||||
<div
|
||||
className={this.className()}
|
||||
onClick={this.onClick} onMouseUp={this.onMouseUp}
|
||||
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>
|
||||
|
@ -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} />
|
||||
|
@ -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
|
||||
|
@ -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} />
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -16,20 +16,18 @@ 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} />
|
||||
@ -53,17 +51,24 @@ class Page extends React.Component<PageProps> {
|
||||
: (
|
||||
<>
|
||||
{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>}
|
||||
</>
|
||||
|
@ -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 />
|
||||
|
@ -8,7 +8,7 @@ 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>
|
||||
|
@ -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)),
|
||||
|
@ -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)),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as db from "../db"
|
||||
import intl = require("react-intl-universal")
|
||||
import { 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")
|
||||
@ -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) {
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user