From d4acdfb59cf32493d25bf54cd017b2c464af5d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9=E8=BF=9C?= Date: Sat, 20 Jun 2020 15:09:26 +0800 Subject: [PATCH] keyboard shortcuts --- dist/article/article.css | 3 ++ dist/icons/logo-outline-dark.svg | 60 +++++++++++++++++++++++++++ dist/icons/logo-outline.svg | 60 +++++++++++++++++++++++++++ dist/{ => icons}/logo.svg | 0 dist/styles.css | 25 ++++++++++- src/components/article.tsx | 27 ++++++++++-- src/components/cards/card.tsx | 5 +++ src/components/cards/default-card.tsx | 2 + src/components/cards/list-card.tsx | 5 ++- src/components/feeds/cards-feed.tsx | 1 + src/components/feeds/feed.tsx | 1 + src/components/feeds/list-feed.tsx | 1 + src/components/nav.tsx | 35 ++++++++++++++++ src/components/page.tsx | 23 ++++++---- src/components/settings.tsx | 10 ++--- src/components/settings/about.tsx | 2 +- src/containers/article-container.tsx | 6 ++- src/containers/feed-container.tsx | 3 +- src/scripts/models/item.ts | 22 +++++++++- src/scripts/models/page.ts | 8 ++-- 20 files changed, 271 insertions(+), 28 deletions(-) create mode 100644 dist/icons/logo-outline-dark.svg create mode 100644 dist/icons/logo-outline.svg rename dist/{ => icons}/logo.svg (100%) diff --git a/dist/article/article.css b/dist/article/article.css index 10d7b27..5893fc0 100644 --- a/dist/article/article.css +++ b/dist/article/article.css @@ -60,4 +60,7 @@ article figure figcaption { font-size: .875rem; color: var(--gray); -webkit-user-modify: read-only; +} +article iframe { + width: 100%; } \ No newline at end of file diff --git a/dist/icons/logo-outline-dark.svg b/dist/icons/logo-outline-dark.svg new file mode 100644 index 0000000..782ce23 --- /dev/null +++ b/dist/icons/logo-outline-dark.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + F + + diff --git a/dist/icons/logo-outline.svg b/dist/icons/logo-outline.svg new file mode 100644 index 0000000..c05c252 --- /dev/null +++ b/dist/icons/logo-outline.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + F + + diff --git a/dist/logo.svg b/dist/icons/logo.svg similarity index 100% rename from dist/logo.svg rename to dist/icons/logo.svg diff --git a/dist/styles.css b/dist/styles.css index 6326571..52d8e2f 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -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; diff --git a/src/components/article.tsx b/src/components/article.tsx index ec2fb23..6917c58 100644 --- a/src/components/article.tsx +++ b/src/components/article.tsx @@ -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 { 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 { }) 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 { 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() } } diff --git a/src/components/cards/card.tsx b/src/components/cards/card.tsx index 780df09..9040126 100644 --- a/src/components/cards/card.tsx +++ b/src/components/cards/card.tsx @@ -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 { this.props.contextMenu(this.props.feedId, this.props.item, e) } } + + onKeyDown = (e: React.KeyboardEvent) => { + this.props.shortcuts(this.props.item, e.key) + } } \ No newline at end of file diff --git a/src/components/cards/default-card.tsx b/src/components/cards/default-card.tsx index aa6fb49..ac9a68c 100644 --- a/src/components/cards/default-card.tsx +++ b/src/components/cards/default-card.tsx @@ -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 ? ( diff --git a/src/components/cards/list-card.tsx b/src/components/cards/list-card.tsx index a6b4f89..a4cb4b5 100644 --- a/src/components/cards/list-card.tsx +++ b/src/components/cards/list-card.tsx @@ -14,8 +14,11 @@ class ListCard extends Card { return (
event.preventDefault()} + onKeyDown={this.onKeyDown} + data-iid={this.props.item._id} data-is-focusable> {this.props.item.thumb ? (
diff --git a/src/components/feeds/cards-feed.tsx b/src/components/feeds/cards-feed.tsx index 69569cb..8c4e20a 100644 --- a/src/components/feeds/cards-feed.tsx +++ b/src/components/feeds/cards-feed.tsx @@ -39,6 +39,7 @@ class CardsFeed extends React.Component { 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} /> diff --git a/src/components/feeds/feed.tsx b/src/components/feeds/feed.tsx index 79a3b1f..9101b3d 100644 --- a/src/components/feeds/feed.tsx +++ b/src/components/feeds/feed.tsx @@ -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 diff --git a/src/components/feeds/list-feed.tsx b/src/components/feeds/list-feed.tsx index 624d0f0..4a164d9 100644 --- a/src/components/feeds/list-feed.tsx +++ b/src/components/feeds/list-feed.tsx @@ -15,6 +15,7 @@ class ListFeed extends React.Component { 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} /> diff --git a/src/components/nav.tsx b/src/components/nav.tsx index 827f2e4..699f829 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -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 { } } + 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() } diff --git a/src/components/page.tsx b/src/components/page.tsx index 2ae6f42..0ae354c 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -16,20 +16,18 @@ type PageProps = { } class Page extends React.Component { - 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 : -
+
{this.props.feeds.map(fid => ( @@ -53,17 +51,24 @@ class Page extends React.Component { : ( <> {this.props.settingsOn ? null : -
+
{this.props.feeds.map(fid => ( ))}
- {this.props.itemId && ( + {this.props.itemId + ? (
+ ) + : ( +
+ + +
)}
} diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 0622c7f..6bcbdff 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -22,15 +22,15 @@ class Settings extends React.Component { render = () => this.props.display && (
+
+ + + +
{this.props.blocked &&
} -
- - - -
diff --git a/src/components/settings/about.tsx b/src/components/settings/about.tsx index 79ab282..4908d07 100644 --- a/src/components/settings/about.tsx +++ b/src/components/settings/about.tsx @@ -8,7 +8,7 @@ class AboutTab extends React.Component { render = () => (
- +

Fluent Reader

{intl.get("settings.version")} {remote.app.getVersion()}

Copyright © 2020 Haoyuan Liu. All rights reserved.

diff --git a/src/containers/article-container.tsx b/src/containers/article-container.tsx index 73431ed..90b35f4 100644 --- a/src/containers/article-container.tsx +++ b/src/containers/article-container.tsx @@ -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)), diff --git a/src/containers/feed-container.tsx b/src/containers/feed-container.tsx index 13e21bb..6fe84ff 100644 --- a/src/containers/feed-container.tsx +++ b/src/containers/feed-container.tsx @@ -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)), diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index bb3c034..e689943 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -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) { diff --git a/src/scripts/models/page.ts b/src/scripts/models/page.ts index 406429e..7b9696f 100644 --- a/src/scripts/models/page.ts +++ b/src/scripts/models/page.ts @@ -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,