keyboard shortcuts

This commit is contained in:
刘浩远 2020-06-20 15:09:26 +08:00
parent 3578721d6c
commit d4acdfb59c
20 changed files with 271 additions and 28 deletions

View File

@ -60,4 +60,7 @@ article figure figcaption {
font-size: .875rem;
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

25
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;

View File

@ -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") {
this.shouldRefocus = true
this.props.dismiss()
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()
}
}

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

@ -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} />

View File

@ -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>

View File

@ -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} />

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

@ -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} />

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

@ -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>}
</>

View File

@ -22,15 +22,15 @@ class Settings extends React.Component<SettingsProps> {
render = () => this.props.display && (
<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}>
{this.props.blocked && <div className="loading">
<Spinner label={intl.get("settings.fetching")} />
</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>
<PivotItem headerText={intl.get("settings.sources")} itemIcon="Source">
<SourcesTabContainer />

View File

@ -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>

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

@ -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) {

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,