Merge pull request #9 from yang991178/0.3.3

Version 0.3.3
This commit is contained in:
Haoyuan Liu 2020-06-21 14:57:07 +08:00 committed by GitHub
commit 06abd8bc21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 113 additions and 65 deletions

View File

@ -29,7 +29,7 @@ If you are using macOS or an older version of Windows, you can also [get Fluent
### Contribute
Make Fluent Reader better by reporting bugs or opening feature requests through [GitHub issues](https://github.com/yang991178/fluent-reader/issues).
Help make Fluent Reader better by reporting bugs or opening feature requests through [GitHub issues](https://github.com/yang991178/fluent-reader/issues).
You can also help internationalize the app by providing [translations into additional languages](https://github.com/yang991178/fluent-reader/tree/master/src/scripts/i18n).
Refer to the repo of [react-intl-universal](https://github.com/alibaba/react-intl-universal) to get started on internationalization.

54
dist/styles.css vendored
View File

@ -45,9 +45,6 @@ html, body {
overflow: hidden;
margin: 0;
}
body {
background-color: var(--neutralLighterAlt);
}
#root {
height: 100%;
}
@ -157,6 +154,9 @@ nav .progress {
text-align: center;
vertical-align: middle;
}
body.darwin .btn-group .seperator {
line-height: 32px;
}
.btn-group .seperator::before {
content: "|";
}
@ -184,12 +184,15 @@ nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system {
nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system {
color: var(--white);
}
.btn-group .btn:hover {
.btn-group .btn:hover, .ms-Nav-compositeLink:hover {
background-color: #0001;
}
.btn-group .btn:active {
.btn-group .btn:active, .ms-Nav-compositeLink:active {
background-color: #0002;
}
.ms-Nav-compositeLink:hover .ms-Nav-link {
background: none;
}
.btn-group .btn.disabled, .btn-group .btn.fetching {
background-color: unset !important;
color: var(--neutralSecondaryAlt);
@ -263,7 +266,7 @@ nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system {
.settings-container {
position: fixed;
z-index: 5;
z-index: 7;
left: 0;
top: 0;
width: 100%;
@ -295,7 +298,7 @@ div[role="tabpanel"] {
width: 100%;
height: 100%;
background-color: #fffa;
z-index: 6;
z-index: 8;
}
.settings .loading .ms-Spinner {
margin-top: 180px;
@ -346,16 +349,18 @@ img.favicon {
}
.main {
height: calc(100% - 32px);
overflow-y: scroll;
margin-top: -32px;
height: 100%;
overflow: hidden;
background-color: var(--neutralLighterAlt);
}
.main::before {
content: "";
display: block;
position: sticky;
top: 0;
top: 32px;
left: 0;
width: 100%;
width: calc(100% - 16px);
height: 32px;
margin-bottom: -32px;
background: linear-gradient(var(--neutralLighterAlt), #faf9f800);
@ -390,10 +395,15 @@ img.favicon {
}
.menu-container {
width: 280px;
background: none;
backdrop-filter: none;
}
.menu-container .menu {
background-color: var(--neutralLight);
}
body.darwin .menu-container .menu {
background: none;
}
.menu-container .menu::after {
content: "";
display: block;
@ -564,6 +574,9 @@ img.favicon {
flex-wrap: wrap;
justify-content: space-around;
padding: 12px;
height: calc(100% - 56px);
overflow: hidden scroll;
margin-top: 32px;
}
.cards-feed-container > div.load-more-wrapper, .flex-fix {
text-align: center;
@ -654,11 +667,13 @@ img.favicon {
cursor: pointer;
animation-fill-mode: none;
}
.card:hover, .card:focus {
box-shadow: #0006 0px 5px 40px;
.card:focus, .list-card:focus {
outline: none;
}
.card:focus::after, .list-card:focus::after {
.card:hover, .ms-Fabric--isFocusVisible .card:focus {
box-shadow: #0006 0px 5px 40px;
}
.ms-Fabric--isFocusVisible .card:focus::after, .ms-Fabric--isFocusVisible .list-card:focus::after {
content: "";
position: absolute;
top: 2px;
@ -698,7 +713,9 @@ img.favicon {
transition: transform ease-out .12s;
}
.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 {
.ms-Fabric--isFocusVisible .card.transform:focus img.head,
.ms-Fabric--isFocusVisible .card.transform:focus p,
.ms-Fabric--isFocusVisible .card.transform:focus h3 {
transform: translateY(-144px);
}
.card h3.title {
@ -752,9 +769,8 @@ img.favicon {
cursor: pointer;
box-shadow: #0000 0px 5px 15px;
}
.list-card:hover, .list-card:focus {
.list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus {
box-shadow: #0004 0px 5px 15px;
outline: none;
}
.list-card:active {
box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px;
@ -793,10 +809,10 @@ img.favicon {
.ms-Button--commandBar.active .ms-Button-icon {
color: #c7e0f4;
}
.btn-group .btn:hover {
.btn-group .btn:hover, .ms-Nav-compositeLink:hover {
background-color: #fff1;
}
.btn-group .btn:active {
.btn-group .btn:active, .ms-Nav-compositeLink:active {
background-color: #fff2;
}
.settings .loading {

View File

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

View File

@ -31,7 +31,6 @@ type ArticleState = {
class Article extends React.Component<ArticleProps, ArticleState> {
webview: Electron.WebviewTag
shouldRefocus = false
constructor(props) {
super(props)
@ -107,18 +106,28 @@ class Article extends React.Component<ArticleProps, ArticleState> {
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":
case "l": case "L":
this.toggleWebpage()
break
default:
this.props.shortcuts(this.props.item, input.key)
const keyboardEvent = new KeyboardEvent("keydown", {
code: input.code,
key: input.key,
shiftKey: input.shift,
altKey: input.alt,
ctrlKey: input.control,
metaKey: input.meta,
repeat: input.isAutoRepeat,
bubbles: true
})
document.dispatchEvent(keyboardEvent)
break
}
}
@ -149,10 +158,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
}
componentWillUnmount = () => {
if (this.shouldRefocus) {
let refocus = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement
if (refocus) refocus.focus()
}
let refocus = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement
if (refocus) refocus.focus()
}
openInBrowser = () => {

View File

@ -17,7 +17,6 @@ class DefaultCard extends Card {
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>

View File

@ -16,7 +16,6 @@ class ListCard extends Card {
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>

View File

@ -91,8 +91,10 @@ export class Menu extends React.Component<MenuProps> {
let [type, index] = item.key.split("-")
if (type === "s") {
sids = [parseInt(index)]
} else {
} else if (type === "g") {
sids = this.props.groups[parseInt(index)].sids
} else {
return
}
this.props.groupContextMenu(sids, event)
}

View File

@ -7,14 +7,15 @@ import { ProgressIndicator } from "@fluentui/react"
import { getWindowBreakpoint } from "../scripts/utils"
type NavProps = {
state: AppState,
itemShown: boolean,
fetch: () => void,
menu: () => void,
logs: () => void,
views: () => void,
settings: () => void,
state: AppState
itemShown: boolean
menu: () => void
search: () => void
markAllRead: () => void
fetch: () => void
logs: () => void
views: () => void
settings: () => void
}
type NavState = {
@ -44,6 +45,9 @@ class Nav extends React.Component<NavProps, NavState> {
case "F1":
this.props.menu()
break
case "F2":
this.props.search()
break
case "F5":
this.fetch()
break

View File

@ -7,6 +7,7 @@ import ArticleSearch from "./utils/article-search"
type PageProps = {
menuOn: boolean
contextOn: boolean
settingsOn: boolean
feeds: string[]
itemId: string
@ -35,6 +36,7 @@ class Page extends React.Component<PageProps> {
</div>}
{this.props.itemId && (
<FocusTrapZone
disabled={this.props.contextOn}
ignoreExternalFocusing={true}
isClickableOutsideFocusTrap={true}
className="article-container"

View File

@ -214,12 +214,13 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
handleInputChange = (event) => {
const name: string = event.target.name
this.setState({[name]: event.target.value.trim()})
this.setState({[name]: event.target.value})
}
createGroup = (event: React.FormEvent) => {
event.preventDefault()
if (this.state.newGroupName.length > 0) this.props.createGroup(this.state.newGroupName)
let trimmed = this.state.newGroupName.trim()
if (trimmed.length > 0) this.props.createGroup(trimmed)
}
addToGroup = () => {
@ -239,7 +240,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
updateGroupName = () => {
let group = this.state.selectedGroup
group = { ...group, name: this.state.editGroupName }
group = { ...group, name: this.state.editGroupName.trim() }
this.props.updateGroup(group)
}
@ -292,7 +293,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
</Stack.Item>
<Stack.Item>
<PrimaryButton
disabled={this.state.newGroupName.length == 0}
disabled={this.state.newGroupName.trim().length == 0}
type="sumbit"
text={intl.get("create")} />
</Stack.Item>
@ -325,7 +326,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
</Stack.Item>
<Stack.Item>
<DefaultButton
disabled={this.state.editGroupName.length == 0}
disabled={this.state.editGroupName.trim().length == 0}
onClick={this.updateGroupName}
text={intl.get("groups.editName")} />
</Stack.Item>

View File

@ -101,12 +101,13 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
handleInputChange = (event) => {
const name: string = event.target.name
this.setState({[name]: event.target.value.trim()})
this.setState({[name]: event.target.value})
}
addSource = (event: React.FormEvent) => {
event.preventDefault()
if (urlTest(this.state.newUrl)) this.props.addSource(this.state.newUrl)
let trimmed = this.state.newUrl.trim()
if (urlTest(trimmed)) this.props.addSource(trimmed)
}
onOpenTargetChange = (_, option: IChoiceGroupOption) => {
@ -142,7 +143,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
</Stack.Item>
<Stack.Item>
<PrimaryButton
disabled={!urlTest(this.state.newUrl)}
disabled={!urlTest(this.state.newUrl.trim())}
type="submit"
text={intl.get("add")} />
</Stack.Item>
@ -171,8 +172,8 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
</Stack.Item>
<Stack.Item>
<DefaultButton
disabled={this.state.newSourceName.length == 0}
onClick={() => this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName)}
disabled={this.state.newSourceName.trim().length == 0}
onClick={() => this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName.trim())}
text={intl.get("sources.editName")} />
</Stack.Item>
</Stack>

View File

@ -41,6 +41,7 @@ class ArticleSearch extends React.Component<SearchProps, SearchState> {
componentDidUpdate(prevProps: SearchProps) {
if (this.props.searchOn && !prevProps.searchOn) {
this.setState({ query: this.props.initQuery })
this.inputRef.current.focus()
}
}

View File

@ -5,7 +5,7 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { fetchItems, markAllRead } from "../scripts/models/item"
import { toggleMenu, toggleLogMenu, toggleSettings, openViewMenu } from "../scripts/models/app"
import { ViewType } from "../scripts/models/page"
import { ViewType, toggleSearch } from "../scripts/models/page"
import Nav from "../components/nav"
const getState = (state: RootState) => state.app
@ -25,6 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
logs: () => dispatch(toggleLogMenu()),
views: () => dispatch(openViewMenu()),
settings: () => dispatch(toggleSettings()),
search: () => dispatch(toggleSearch()),
markAllRead: () => {
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
title: intl.get("nav.markAllRead"),

View File

@ -4,17 +4,20 @@ import { RootState } from "../scripts/reducer"
import Page from "../components/page"
import { AppDispatch } from "../scripts/utils"
import { dismissItem, showOffsetItem } from "../scripts/models/page"
import { ContextMenuType } from "../scripts/models/app"
const getPage = (state: RootState) => state.page
const getSettings = (state: RootState) => state.app.settings.display
const getMenu = (state: RootState) => state.app.menu
const getContext = (state: RootState) => state.app.contextMenu.type != ContextMenuType.Hidden
const mapStateToProps = createSelector(
[getPage, getSettings, getMenu],
(page, settingsOn, menuOn) => ({
[getPage, getSettings, getMenu, getContext],
(page, settingsOn, menuOn, contextOn) => ({
feeds: [page.feedId],
settingsOn: settingsOn,
menuOn: menuOn,
contextOn: contextOn,
itemId: page.itemId,
viewType: page.viewType
})

View File

@ -29,7 +29,8 @@ function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
title: "Fluent Reader",
backgroundColor: nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8",
backgroundColor: process.platform === "darwin" ? "#00000000" : (nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8"),
vibrancy: "sidebar",
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
@ -55,7 +56,7 @@ function createWindow() {
mainWindow.loadFile((app.isPackaged ? "dist/" : "") + "index.html")
}
if (process.platform === 'darwin') {
if (process.platform === "darwin") {
const template = [
{
label: "Application",
@ -66,8 +67,12 @@ if (process.platform === 'darwin') {
{
label: "Edit",
submenu: [
{ label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
{ label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
{ label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
]
}
]

View File

@ -199,6 +199,7 @@ export function initIntl(): AppThunk<Promise<void>> {
export function initApp(): AppThunk {
return (dispatch) => {
document.body.classList.add(process.platform)
dispatch(initIntl()).then(() =>
dispatch(initSources())
).then(() =>
@ -266,10 +267,7 @@ export function appReducer(
}
case INIT_FEEDS:
switch (action.status) {
case ActionStatus.Request: return {
...state,
feedInit: false
}
case ActionStatus.Request: return state
default: return {
...state,
feedInit: true

View File

@ -266,17 +266,18 @@ export function toggleHidden(item: RSSItem): AppThunk {
export function itemShortcuts(item: RSSItem, key: string): AppThunk {
return (dispatch) => {
switch (key) {
case "m":
case "m": case "M":
if (item.hasRead) dispatch(markUnread(item))
else dispatch(markRead(item))
break
case "b":
case "b": case "B":
if (!item.hasRead) dispatch(markRead(item))
openExternal(item.link)
break
case "s":
case "s": case "S":
dispatch(toggleStarred(item))
break
case "h":
case "h": case "H":
dispatch(toggleHidden(item))
break
}

View File

@ -1,5 +1,5 @@
import { ALL, SOURCE, loadMore, FeedFilter, FilterType, initFeeds } from "./feed"
import { getWindowBreakpoint, AppThunk } from "../utils"
import { ALL, SOURCE, loadMore, FeedFilter, FilterType, initFeeds, FeedActionTypes, INIT_FEED } from "./feed"
import { getWindowBreakpoint, AppThunk, ActionStatus } from "../utils"
import { getDefaultView } from "../settings"
import { RSSItem, markRead } from "./item"
import { SourceActionTypes, DELETE_SOURCE } from "./source"
@ -104,7 +104,7 @@ export const toggleSearch = (): AppThunk => {
return (dispatch, getState) => {
let state = getState()
dispatch(({ type: TOGGLE_SEARCH }))
if (!getWindowBreakpoint()) {
if (!getWindowBreakpoint() && state.app.menu) {
dispatch(toggleMenu())
}
if (state.page.searchOn) {
@ -214,7 +214,7 @@ export class PageState {
export function pageReducer(
state = new PageState(),
action: PageActionTypes | SourceActionTypes
action: PageActionTypes | SourceActionTypes | FeedActionTypes
): PageState {
switch (action.type) {
case SELECT_PAGE:
@ -244,6 +244,14 @@ export function pageReducer(
...state,
itemId: action.item._id
}
case INIT_FEED: switch (action.status) {
case ActionStatus.Success: return {
...state,
itemId: (action.feed._id === state.feedId && action.items.filter(i => i._id === state.itemId).length === 0)
? null : state.itemId
}
default: return state
}
case DELETE_SOURCE:
case DISMISS_ITEM: return {
...state,