prev / next article & default open target

This commit is contained in:
刘浩远 2020-06-08 11:48:58 +08:00
parent 3fb9252b58
commit 165597a454
13 changed files with 148 additions and 32 deletions

20
dist/styles.css vendored
View File

@ -247,6 +247,8 @@ img.favicon {
margin-bottom: 16px;
}
.settings-hint {
user-select: none;
line-height: 32px;
font-size: 12px;
color: #605e5c;
}
@ -320,6 +322,19 @@ img.favicon {
border-radius: 5px;
overflow: hidden;
}
.article-container .btn-group .btn {
color: #fff;
}
.article-container .btn-group {
position: absolute;
top: calc(50% - 32px)
}
.article-container .btn-group.prev {
left: calc(50% - 486px);
}
.article-container .btn-group.next {
right: calc(50% - 486px);
}
.article webview {
width: 100%;
height: calc(100vh - 86px);
@ -337,6 +352,11 @@ img.favicon {
.article .actions .source-name {
line-height: 35px;
user-select: none;
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
.cards-feed-container {

View File

@ -3,7 +3,7 @@ import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item"
import { openExternal } from "../scripts/utils"
import { Stack, CommandBarButton, IContextualMenuProps } from "@fluentui/react"
import { RSSSource } from "../scripts/models/source"
import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
const FONT_SIZE_STORE_KEY = "fontSize"
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
@ -28,7 +28,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
super(props)
this.state = {
fontSize: this.getFontSize(),
loadWebpage: false
loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage
}
}
@ -74,10 +74,20 @@ class Article extends React.Component<ArticleProps, ArticleState> {
}
componentDidMount = () => {
this.webview = document.getElementById("article")
this.webview.addEventListener("ipc-message", this.ipcHandler)
this.webview.addEventListener("new-window", this.popUpHandler)
this.webview.addEventListener("will-navigate", this.navigationHandler)
let webview = document.getElementById("article")
if (webview != this.webview) {
if (this.webview) this.componentWillUnmount()
webview.addEventListener("ipc-message", this.ipcHandler)
webview.addEventListener("new-window", this.popUpHandler)
webview.addEventListener("will-navigate", this.navigationHandler)
this.webview = webview
}
}
componentDidUpdate = (prevProps: ArticleProps) => {
if (prevProps.item.id != this.props.item.id) {
this.setState({loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage})
}
this.componentDidMount()
}
componentWillUnmount = () => {
@ -110,8 +120,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
<span style={{width: 96}}></span>
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
<Stack.Item grow>
{this.props.source.iconurl && <img className="favicon" src={this.props.source.iconurl} />}
<span className="source-name">{this.props.source.name}</span>
<span className="source-name">
{this.props.source.iconurl && <img className="favicon" src={this.props.source.iconurl} />}
{this.props.source.name}
</span>
</Stack.Item>
<CommandBarButton
title={this.props.item.hasRead ? "标为未读" : "标为已读"}
@ -146,6 +158,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</Stack>
<webview
id="article"
key={this.props.item.id + (this.state.loadWebpage ? "_" : "")}
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
preload={this.state.loadWebpage ? null : "article/preload.js"}
partition="sandbox" />

View File

@ -1,6 +1,6 @@
import * as React from "react"
import { openExternal } from "../../scripts/utils"
import { RSSSource } from "../../scripts/models/source"
import { RSSSource, SourceOpenTarget } from "../../scripts/models/source"
import { RSSItem } from "../../scripts/models/item"
import { FeedIdType } from "../../scripts/models/feed"
@ -22,8 +22,18 @@ export class Card extends React.Component<CardProps> {
onClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
this.props.markRead(this.props.item)
this.props.showItem(this.props.feedId, this.props.item)
switch (this.props.source.openTarget) {
case SourceOpenTarget.Local:
case SourceOpenTarget.Webpage: {
this.props.markRead(this.props.item)
this.props.showItem(this.props.feedId, this.props.item)
break
}
case SourceOpenTarget.External: {
this.openInBrowser()
break
}
}
}
onMouseUp = (e: React.MouseEvent) => {

View File

@ -20,9 +20,9 @@ class CardsFeed extends Feed {
flexFix = () => {
let elemPerRow = Math.floor(this.state.width / 280)
let elemLastRow = this.props.items.length % elemPerRow
//let elemLastRow = this.props.items.length % elemPerRow
let fixes = new Array<JSX.Element>()
for (let i = 0; i < elemPerRow - elemLastRow; i += 1) {
for (let i = 0; i < elemPerRow; i += 1) {
fixes.push(<div className="flex-fix" key={"f-"+i}></div>)
}
return fixes

View File

@ -1,10 +1,7 @@
import * as React from "react"
import { FeedIdType } from "../scripts/models/feed"
import { FeedContainer } from "../containers/feed-container"
import { RSSItem } from "../scripts/models/item"
import Article from "./article"
import { dismissItem } from "../scripts/models/page"
import { AnimationClassNames } from "@fluentui/react"
import { AnimationClassNames, Icon } from "@fluentui/react"
import ArticleContainer from "../containers/article-container"
type PageProps = {
@ -13,9 +10,19 @@ type PageProps = {
feeds: FeedIdType[]
itemId: number
dismissItem: () => void
offsetItem: (offset: number) => void
}
class Page extends React.Component<PageProps> {
prevItem = (event: React.MouseEvent) => {
event.stopPropagation()
this.props.offsetItem(-1)
}
nextItem = (event: React.MouseEvent) => {
event.stopPropagation()
this.props.offsetItem(1)
}
render = () => (
<>
{this.props.settingsOn ? null :
@ -29,6 +36,8 @@ class Page extends React.Component<PageProps> {
<div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}>
<ArticleContainer itemId={this.props.itemId} />
</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>
)}
</>

View File

@ -354,7 +354,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
</Stack>
</>
)
: <p className="settings-hint"></p>
: <span className="settings-hint"></span>
}
</> : null}
</div>

View File

@ -1,15 +1,16 @@
import * as React from "react"
import { Label, DefaultButton, TextField, Stack, PrimaryButton, DetailsList,
IColumn, SelectionMode, Selection } from "@fluentui/react"
import { SourceState, RSSSource } from "../../scripts/models/source"
IColumn, SelectionMode, Selection, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react"
import { SourceState, RSSSource, SourceOpenTarget } from "../../scripts/models/source"
import { urlTest } from "../../scripts/utils"
import DangerButton from "../utils/danger-button"
type SourcesTabProps = {
sources: SourceState,
addSource: (url: string) => void,
updateSourceName: (source: RSSSource, name: string) => void,
deleteSource: (source: RSSSource) => void,
sources: SourceState
addSource: (url: string) => void
updateSourceName: (source: RSSSource, name: string) => void
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => void
deleteSource: (source: RSSSource) => void
importOPML: () => void
}
@ -49,6 +50,12 @@ const columns: IColumn[] = [
}
]
const sourceOpenTargetChoices: IChoiceGroupOption[] = [
{ key: String(SourceOpenTarget.Local), text: "RSS正文" },
{ key: String(SourceOpenTarget.Webpage), text: "加载网页" },
{ key: String(SourceOpenTarget.External), text: "在浏览器中打开" }
]
class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
selection: Selection
@ -81,6 +88,12 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
if (urlTest(this.state.newUrl)) this.props.addSource(this.state.newUrl)
}
onOpenTargetChange = (_, option: IChoiceGroupOption) => {
let newTarget = parseInt(option.key) as SourceOpenTarget
this.props.updateSourceOpenTarget(this.state.selectedSource, newTarget)
this.setState({selectedSource: {...this.state.selectedSource, openTarget: newTarget} as RSSSource})
}
render = () => (
<div className="tab-body">
<Label>OPML文件</Label>
@ -141,12 +154,22 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
onClick={() => this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName)}
text="修改名称" />
</Stack.Item>
</Stack>
<ChoiceGroup
label="订阅源文章打开方式"
options={sourceOpenTargetChoices}
selectedKey={String(this.state.selectedSource.openTarget)}
onChange={this.onOpenTargetChange} />
<Stack horizontal style={{marginTop: 24}}>
<Stack.Item>
<DangerButton
onClick={() => this.props.deleteSource(this.state.selectedSource)}
key={this.state.selectedSource.sid}
text={`删除订阅源`} />
</Stack.Item>
<Stack.Item>
<span className="settings-hint"></span>
</Stack.Item>
</Stack>
</>}
</div>

View File

@ -3,7 +3,7 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import Page from "../components/page"
import { AppDispatch } from "../scripts/utils"
import { dismissItem } from "../scripts/models/page"
import { dismissItem, showOffsetItem } from "../scripts/models/page"
const getPage = (state: RootState) => state.page
const getSettings = (state: RootState) => state.app.settings.display
@ -20,7 +20,8 @@ const mapStateToProps = createSelector(
)
const mapDispatchToProps = (dispatch: AppDispatch) => ({
dismissItem: () => dispatch(dismissItem())
dismissItem: () => dispatch(dismissItem()),
offsetItem: (offset: number) => dispatch(showOffsetItem(offset))
})
const PageContainer = connect(mapStateToProps, mapDispatchToProps)(Page)

View File

@ -3,7 +3,7 @@ import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../../scripts/reducer"
import SourcesTab from "../../components/settings/sources"
import { addSource, RSSSource, updateSource, deleteSource } from "../../scripts/models/source"
import { addSource, RSSSource, updateSource, deleteSource, SourceOpenTarget } from "../../scripts/models/source"
import { importOPML } from "../../scripts/models/group"
import { AppDispatch } from "../../scripts/utils"
@ -22,6 +22,9 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
updateSourceName: (source: RSSSource, name: string) => {
dispatch(updateSource({ ...source, name: name } as RSSSource))
},
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => {
dispatch(updateSource({ ...source, openTarget: target } as RSSSource))
},
deleteSource: (source: RSSSource) => dispatch(deleteSource(source)),
importOPML: () => {
let path = remote.dialog.showOpenDialogSync(

View File

@ -18,6 +18,7 @@ function createWindow() {
minWidth: 992,
minHeight: 600,
frame: false,
fullscreenable: false,
webPreferences: {
nodeIntegration: true,
webviewTag: true
@ -34,6 +35,7 @@ Menu.setApplicationMenu(null)
app.on('ready', createWindow)
app.on('window-all-closed', function () {
mainWindow = null
if (process.platform !== 'darwin') {
app.quit()
}

View File

@ -1,9 +1,10 @@
import { ALL, SOURCE, FeedIdType } from "./feed"
import { getWindowBreakpoint } from "../utils"
import { RSSItem, ItemActionTypes, MARK_READ, MARK_UNREAD } from "./item"
import { ALL, SOURCE, FeedIdType, loadMore } from "./feed"
import { getWindowBreakpoint, AppThunk } from "../utils"
import { RSSItem, ItemActionTypes, MARK_READ, MARK_UNREAD, markRead } from "./item"
export const SELECT_PAGE = "SELECT_PAGE"
export const SHOW_ITEM = "SHOW_ITEM"
export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
export const DISMISS_ITEM = "DISMISS_ITEM"
export enum PageType {
@ -61,6 +62,33 @@ export function showItem(feedId: FeedIdType, item: RSSItem): PageActionTypes {
export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM })
export function showOffsetItem(offset: number): AppThunk {
return (dispatch, getState) => {
let state = getState()
let [itemId, feedId] = [state.page.itemId, state.page.feedId]
let feed = state.feeds[feedId]
let iids = feed.iids
let itemIndex = iids.indexOf(itemId)
let newIndex = itemIndex + offset
if (itemIndex >= 0 && newIndex >= 0) {
if (newIndex < iids.length) {
let item = state.items[iids[newIndex]]
dispatch(markRead(item))
dispatch(showItem(feedId, item))
return
} else if (!feed.allLoaded){
dispatch(loadMore(feed)).then(() => {
dispatch(showOffsetItem(offset))
}).catch(() =>
dispatch(dismissItem())
)
return
}
}
dispatch(dismissItem())
}
}
export class PageState {
feedId = ALL as FeedIdType
itemId = -1

View File

@ -5,18 +5,22 @@ import { RSSItem, insertItems } from "./item"
import { SourceGroup } from "./group"
import { saveSettings } from "./app"
export enum SourceOpenTarget {
Local, Webpage, External
}
export class RSSSource {
sid: number
url: string
iconurl: string
name: string
description: string
useProxy: boolean
openTarget: SourceOpenTarget
constructor(url: string, name: string = null) {
this.url = url
this.name = name
this.useProxy = false
this.openTarget = SourceOpenTarget.Local
}
async fetchMetaData(parser: Parser) {

View File

@ -42,6 +42,9 @@ export function setProxy(address = null) {
remote.getCurrentWebContents().session.setProxy({
pacScript: getProxyStatus() ? address : ""
})
remote.session.fromPartition("sandbox").setProxy({
pacScript: getProxyStatus() ? address : ""
})
}
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")