article view optimization

This commit is contained in:
刘浩远 2020-07-20 12:18:10 +08:00
parent 7fde620d00
commit 9f85be51dc
9 changed files with 82 additions and 13 deletions

26
dist/styles/feeds.css vendored
View File

@ -22,11 +22,16 @@
} }
.article { .article {
height: 100%; height: 100%;
user-select: none;
} }
.article webview { .article webview, .article .error-prompt {
width: 100%; width: 100%;
height: calc(100% - 36px); height: calc(100% - 36px);
border: none; border: none;
color: var(--black);
}
.article webview.error {
display: none;
} }
.article i.ms-Icon { .article i.ms-Icon {
color: var(--neutralDarker); color: var(--neutralDarker);
@ -35,18 +40,31 @@
color: var(--black); color: var(--black);
border-bottom: 1px solid var(--neutralQuaternaryAlt); border-bottom: 1px solid var(--neutralQuaternaryAlt);
} }
.article .actions .favicon { .article .actions .favicon, .article .actions .ms-Spinner {
margin-right: 8px; margin: 8px 8px 11px 0;
}
.article .actions .ms-Spinner {
display: inline-block;
vertical-align: middle;
} }
.article .actions .source-name { .article .actions .source-name {
line-height: 35px; line-height: 35px;
user-select: none; user-select: none;
max-width: 280px; max-width: 320px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
display: inline-block; display: inline-block;
} }
.article .actions .creator {
color: var(--neutralSecondaryAlt);
user-select: text;
}
.article .actions .creator::before {
display: inline-block;
content: "/";
margin: 0 6px;
}
.side-article-wrapper, .side-logo-wrapper { .side-article-wrapper, .side-logo-wrapper {
flex-grow: 1; flex-grow: 1;
padding-top: var(--navHeight); padding-top: var(--navHeight);

View File

@ -52,6 +52,9 @@ html, body {
height: 100%; height: 100%;
} }
.ms-Link {
user-select: none;
}
.ms-ContextualMenu-link, .ms-Button, .ms-ContextualMenu-item button { .ms-ContextualMenu-link, .ms-Button, .ms-ContextualMenu-item button {
cursor: default; cursor: default;
font-size: 13px; font-size: 13px;

View File

@ -42,6 +42,12 @@ const utilsBridge = {
await ipcRenderer.invoke("clear-cache") await ipcRenderer.invoke("clear-cache")
}, },
addMainContextListener: (callback: (pos: [number, number], text: string) => any) => {
ipcRenderer.removeAllListeners("window-context-menu")
ipcRenderer.on("window-context-menu", (_, pos, text) => {
callback(pos, text)
})
},
addWebviewContextListener: (callback: (pos: [number, number], text: string) => any) => { addWebviewContextListener: (callback: (pos: [number, number], text: string) => any) => {
ipcRenderer.removeAllListeners("webview-context-menu") ipcRenderer.removeAllListeners("webview-context-menu")
ipcRenderer.on("webview-context-menu", (_, pos, text) => { ipcRenderer.on("webview-context-menu", (_, pos, text) => {

View File

@ -2,7 +2,7 @@ import * as React from "react"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { renderToString } from "react-dom/server" import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item" import { RSSItem } from "../scripts/models/item"
import { Stack, CommandBarButton, IContextualMenuProps, FocusZone, ContextualMenuItemType } from "@fluentui/react" import { Stack, CommandBarButton, IContextualMenuProps, FocusZone, ContextualMenuItemType, Spinner, Icon, Link } from "@fluentui/react"
import { RSSSource, SourceOpenTarget } from "../scripts/models/source" import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
import { shareSubmenu } from "./context-menu" import { shareSubmenu } from "./context-menu"
@ -25,6 +25,8 @@ type ArticleProps = {
type ArticleState = { type ArticleState = {
fontSize: number fontSize: number
loadWebpage: boolean loadWebpage: boolean
loaded: boolean
error: boolean
} }
class Article extends React.Component<ArticleProps, ArticleState> { class Article extends React.Component<ArticleProps, ArticleState> {
@ -34,7 +36,9 @@ class Article extends React.Component<ArticleProps, ArticleState> {
super(props) super(props)
this.state = { this.state = {
fontSize: this.getFontSize(), fontSize: this.getFontSize(),
loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage,
loaded: false,
error: false,
} }
window.utils.addWebviewContextListener(this.contextMenuHandler) window.utils.addWebviewContextListener(this.contextMenuHandler)
window.utils.addWebviewKeydownListener(this.keyDownHandler) window.utils.addWebviewKeydownListener(this.keyDownHandler)
@ -125,11 +129,27 @@ class Article extends React.Component<ArticleProps, ArticleState> {
} }
} }
webviewLoaded = () => {
this.setState({loaded: true})
}
webviewError = () => {
this.setState({error: true})
}
webviewReload = () => {
if (this.webview) {
this.setState({loaded: false, error: false})
this.webview.reload()
}
}
componentDidMount = () => { componentDidMount = () => {
let webview = document.getElementById("article") as Electron.WebviewTag let webview = document.getElementById("article") as Electron.WebviewTag
if (webview != this.webview) { if (webview != this.webview) {
this.webview = webview this.webview = webview
webview.focus() webview.focus()
this.setState({loaded: false, error: false})
webview.addEventListener("did-stop-loading", this.webviewLoaded)
webview.addEventListener("did-fail-load", this.webviewError)
let card = document.querySelector(`#refocus div[data-iid="${this.props.item._id}"]`) as HTMLElement let card = document.querySelector(`#refocus div[data-iid="${this.props.item._id}"]`) as HTMLElement
// @ts-ignore // @ts-ignore
if (card) card.scrollIntoViewIfNeeded() if (card) card.scrollIntoViewIfNeeded()
@ -172,8 +192,11 @@ class Article extends React.Component<ArticleProps, ArticleState> {
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}> <Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
<Stack.Item grow> <Stack.Item grow>
<span className="source-name"> <span className="source-name">
{this.props.source.iconurl && <img className="favicon" src={this.props.source.iconurl} />} {this.state.loaded
? (this.props.source.iconurl && <img className="favicon" src={this.props.source.iconurl} />)
: <Spinner size={1} />}
{this.props.source.name} {this.props.source.name}
{this.props.item.creator && <span className="creator">{this.props.item.creator}</span>}
</span> </span>
</Stack.Item> </Stack.Item>
<CommandBarButton <CommandBarButton
@ -212,10 +235,20 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</Stack> </Stack>
<webview <webview
id="article" id="article"
className={this.state.error ? "error" : ""}
key={this.props.item._id + (this.state.loadWebpage ? "_" : "")} key={this.props.item._id + (this.state.loadWebpage ? "_" : "")}
src={this.state.loadWebpage ? this.props.item.link : this.articleView()} src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required" webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required"
partition={this.state.loadWebpage ? "sandbox" : undefined} /> partition={this.state.loadWebpage ? "sandbox" : undefined} />
{this.state.error && (
<Stack className="error-prompt" verticalAlign="center" horizontalAlign="center" tokens={{childrenGap: 12}}>
<Icon iconName="HeartBroken" style={{fontSize: 32}} />
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 7}}>
<small>{intl.get("article.error")}</small>
<small><Link onClick={this.webviewReload}>{intl.get("article.reload")}</Link></small>
</Stack>
</Stack>
)}
</FocusZone> </FocusZone>
) )
} }

View File

@ -12,11 +12,7 @@ import { RootState } from "../scripts/reducer"
const Root = ({ locale, dispatch }) => locale && ( const Root = ({ locale, dispatch }) => locale && (
<div id="root" <div id="root"
key={locale} key={locale}
onMouseDown={() => dispatch(closeContextMenu())} onMouseDown={() => dispatch(closeContextMenu())}>
onContextMenu={event => {
let text = document.getSelection().toString()
if (text) dispatch(openTextMenu(text, [event.clientX, event.clientY]))
}}>
<NavContainer /> <NavContainer />
<PageContainer /> <PageContainer />
<LogMenuContainer /> <LogMenuContainer />

View File

@ -8,7 +8,7 @@ import { rootReducer, RootState } from "./scripts/reducer"
import Root from "./components/root" import Root from "./components/root"
import { AppDispatch } from "./scripts/utils" import { AppDispatch } from "./scripts/utils"
import { applyThemeSettings } from "./scripts/settings" import { applyThemeSettings } from "./scripts/settings"
import { initApp } from "./scripts/models/app" import { initApp, openTextMenu } from "./scripts/models/app"
window.settings.setProxy() window.settings.setProxy()
@ -22,6 +22,10 @@ const store = createStore(
store.dispatch(initApp()) store.dispatch(initApp())
window.utils.addMainContextListener((pos, text) => {
store.dispatch(openTextMenu(text, pos))
})
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<Root /> <Root />

View File

@ -78,6 +78,11 @@ export class WindowManager {
this.mainWindow.on("unmaximize", () => { this.mainWindow.on("unmaximize", () => {
this.mainWindow.webContents.send("unmaximized") this.mainWindow.webContents.send("unmaximized")
}) })
this.mainWindow.webContents.on("context-menu", (_, params) => {
if (params.selectionText) {
this.mainWindow.webContents.send("window-context-menu", [params.x, params.y], params.selectionText)
}
})
} }
} }

View File

@ -49,6 +49,8 @@
"subscriptions": "Subscriptions" "subscriptions": "Subscriptions"
}, },
"article": { "article": {
"error": "Failed to load article.",
"reload": "Reload?",
"empty": "No articles", "empty": "No articles",
"untitled": "(Untitled)", "untitled": "(Untitled)",
"hide": "Hide article", "hide": "Hide article",

View File

@ -49,6 +49,8 @@
"subscriptions": "订阅源" "subscriptions": "订阅源"
}, },
"article": { "article": {
"error": "文章加载失败",
"reload": "重新加载",
"empty": "无文章", "empty": "无文章",
"untitled": "(无标题)", "untitled": "(无标题)",
"hide": "隐藏文章", "hide": "隐藏文章",