mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-03-17 11:50:06 +01:00
prev / next article & default open target
This commit is contained in:
parent
3fb9252b58
commit
165597a454
20
dist/styles.css
vendored
20
dist/styles.css
vendored
@ -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 {
|
||||
|
@ -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" />
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
@ -354,7 +354,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
: <p className="settings-hint">双击分组以修改订阅源,可通过拖拽排序</p>
|
||||
: <span className="settings-hint">双击分组以修改订阅源,可通过拖拽排序</span>
|
||||
}
|
||||
</> : null}
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user