mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-24 23:18:46 +02:00
commit
5927bf3441
2
.github/workflows/release-linux.yml
vendored
2
.github/workflows/release-linux.yml
vendored
@ -25,6 +25,8 @@ jobs:
|
|||||||
- name: Get release
|
- name: Get release
|
||||||
id: get_release
|
id: get_release
|
||||||
uses: bruceadams/get-release@v1.2.0
|
uses: bruceadams/get-release@v1.2.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload AppImage to release assets
|
- name: Upload AppImage to release assets
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,5 @@ dist/*.js
|
|||||||
dist/*.js.map
|
dist/*.js.map
|
||||||
dist/*.html
|
dist/*.html
|
||||||
bin/*
|
bin/*
|
||||||
package-lock.json
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.provisionprofile
|
*.provisionprofile
|
16
dist/styles/cards.css
vendored
16
dist/styles/cards.css
vendored
@ -1,4 +1,5 @@
|
|||||||
.info {
|
.info {
|
||||||
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px 12px;
|
margin: 10px 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
@ -12,13 +13,20 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 144px;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.info span.creator {
|
||||||
|
color: var(--neutralSecondaryAlt);
|
||||||
|
}
|
||||||
|
.info span.creator::before {
|
||||||
|
display: inline-block;
|
||||||
|
content: "/";
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
.info span.time {
|
.info span.time {
|
||||||
float: right;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +34,6 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
float: right;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.read-indicator::after {
|
.read-indicator::after {
|
||||||
@ -80,6 +87,9 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
background: #0003;
|
background: #0003;
|
||||||
}
|
}
|
||||||
|
.card span.h {
|
||||||
|
background: #fce10080;
|
||||||
|
}
|
||||||
|
|
||||||
.default-card {
|
.default-card {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
7368
package-lock.json
generated
Normal file
7368
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fluent-reader",
|
"name": "fluent-reader",
|
||||||
"version": "0.6.2",
|
"version": "0.6.3",
|
||||||
"description": "A simplistic, modern desktop RSS reader",
|
"description": "A simplistic, modern desktop RSS reader",
|
||||||
"main": "./dist/electron.js",
|
"main": "./dist/electron.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ipcRenderer } from "electron"
|
import { ipcRenderer } from "electron"
|
||||||
|
import { ImageCallbackTypes } from "../schema-types"
|
||||||
|
|
||||||
const utilsBridge = {
|
const utilsBridge = {
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
@ -7,8 +8,8 @@ const utilsBridge = {
|
|||||||
return ipcRenderer.sendSync("get-version")
|
return ipcRenderer.sendSync("get-version")
|
||||||
},
|
},
|
||||||
|
|
||||||
openExternal: (url: string) => {
|
openExternal: (url: string, background=false) => {
|
||||||
ipcRenderer.invoke("open-external", url)
|
ipcRenderer.invoke("open-external", url, background)
|
||||||
},
|
},
|
||||||
|
|
||||||
showErrorBox: (title: string, content: string) => {
|
showErrorBox: (title: string, content: string) => {
|
||||||
@ -54,6 +55,9 @@ const utilsBridge = {
|
|||||||
callback(pos, text)
|
callback(pos, text)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
imageCallback: (type: ImageCallbackTypes) => {
|
||||||
|
ipcRenderer.invoke("image-callback", type)
|
||||||
|
},
|
||||||
|
|
||||||
addWebviewKeydownListener: (callback: (event: Electron.Input) => any) => {
|
addWebviewKeydownListener: (callback: (event: Electron.Input) => any) => {
|
||||||
ipcRenderer.removeAllListeners("webview-keydown")
|
ipcRenderer.removeAllListeners("webview-keydown")
|
||||||
|
@ -19,6 +19,7 @@ type ArticleProps = {
|
|||||||
toggleStarred: (item: RSSItem) => void
|
toggleStarred: (item: RSSItem) => void
|
||||||
toggleHidden: (item: RSSItem) => void
|
toggleHidden: (item: RSSItem) => void
|
||||||
textMenu: (text: string, position: [number, number]) => void
|
textMenu: (text: string, position: [number, number]) => void
|
||||||
|
imageMenu: (position: [number, number]) => void
|
||||||
dismissContextMenu: () => void
|
dismissContextMenu: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +72,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
key: "openInBrowser",
|
key: "openInBrowser",
|
||||||
text: intl.get("openExternal"),
|
text: intl.get("openExternal"),
|
||||||
iconProps: { iconName: "NavigateExternalInline" },
|
iconProps: { iconName: "NavigateExternalInline" },
|
||||||
onClick: this.openInBrowser
|
onClick: e => { window.utils.openExternal(this.props.item.link, window.utils.platform === "darwin" ? e.metaKey : e.ctrlKey) }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "copyURL",
|
key: "copyURL",
|
||||||
@ -95,7 +96,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
|
|
||||||
contextMenuHandler = (pos: [number, number], text: string) => {
|
contextMenuHandler = (pos: [number, number], text: string) => {
|
||||||
if (pos) {
|
if (pos) {
|
||||||
this.props.textMenu(text, pos)
|
if (text) this.props.textMenu(text, pos)
|
||||||
|
else this.props.imageMenu(pos)
|
||||||
} else {
|
} else {
|
||||||
this.props.dismissContextMenu()
|
this.props.dismissContextMenu()
|
||||||
}
|
}
|
||||||
@ -169,10 +171,6 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
if (refocus) refocus.focus()
|
if (refocus) refocus.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
openInBrowser = () => {
|
|
||||||
window.utils.openExternal(this.props.item.link)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleWebpage = () => {
|
toggleWebpage = () => {
|
||||||
if (this.state.loadWebpage) {
|
if (this.state.loadWebpage) {
|
||||||
this.setState({loadWebpage: false})
|
this.setState({loadWebpage: false})
|
||||||
|
@ -7,18 +7,25 @@ export namespace Card {
|
|||||||
feedId: string
|
feedId: string
|
||||||
item: RSSItem
|
item: RSSItem
|
||||||
source: RSSSource
|
source: RSSSource
|
||||||
|
keyword: string
|
||||||
shortcuts: (item: RSSItem, key: string) => void
|
shortcuts: (item: RSSItem, key: string) => void
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||||
showItem: (fid: string, item: RSSItem) => void
|
showItem: (fid: string, item: RSSItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openInBrowser = (props: Props) => {
|
const openInBrowser = (props: Props, e: React.MouseEvent) => {
|
||||||
props.markRead(props.item)
|
props.markRead(props.item)
|
||||||
window.utils.openExternal(props.item.link)
|
window.utils.openExternal(props.item.link, window.utils.platform === "darwin" ? e.metaKey : e.ctrlKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onClick = (props: Props, e: React.MouseEvent) => {
|
export const bindEventsToProps = (props: Props) => ({
|
||||||
|
onClick: (e: React.MouseEvent) => onClick(props, e),
|
||||||
|
onMouseUp: (e: React.MouseEvent) => onMouseUp(props, e),
|
||||||
|
onKeyDown: (e: React.KeyboardEvent) => onKeyDown(props, e),
|
||||||
|
})
|
||||||
|
|
||||||
|
const onClick = (props: Props, e: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
switch (props.source.openTarget) {
|
switch (props.source.openTarget) {
|
||||||
@ -29,25 +36,25 @@ export namespace Card {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case SourceOpenTarget.External: {
|
case SourceOpenTarget.External: {
|
||||||
openInBrowser(props)
|
openInBrowser(props, e)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onMouseUp = (props: Props, e: React.MouseEvent) => {
|
const onMouseUp = (props: Props, e: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
switch (e.button) {
|
switch (e.button) {
|
||||||
case 1:
|
case 1:
|
||||||
openInBrowser(props)
|
openInBrowser(props, e)
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
props.contextMenu(props.feedId, props.item, e)
|
props.contextMenu(props.feedId, props.item, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onKeyDown = (props: Props, e: React.KeyboardEvent) => {
|
const onKeyDown = (props: Props, e: React.KeyboardEvent) => {
|
||||||
props.shortcuts(props.item, e.key)
|
props.shortcuts(props.item, e.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ import * as React from "react"
|
|||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
import Time from "../utils/time"
|
import Time from "../utils/time"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "compact-card"]
|
let cn = ["card", "compact-card"]
|
||||||
@ -12,15 +13,13 @@ const className = (props: Card.Props) => {
|
|||||||
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
<CardInfo source={props.source} item={props.item} hideTime />
|
<CardInfo source={props.source} item={props.item} hideTime />
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<span className="title">{props.item.title}</span>
|
<span className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></span>
|
||||||
<span className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</span>
|
<span className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></span>
|
||||||
</div>
|
</div>
|
||||||
<Time date={props.item.date} />
|
<Time date={props.item.date} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "default-card"]
|
let cn = ["card", "default-card"]
|
||||||
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
|
|||||||
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb ? (
|
{props.item.thumb ? (
|
||||||
@ -25,8 +24,10 @@ const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
<img className="head" src={props.item.thumb} />
|
<img className="head" src={props.item.thumb} />
|
||||||
) : null}
|
) : null}
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<CardInfo source={props.source} item={props.item} />
|
||||||
<h3 className="title">{props.item.title}</h3>
|
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
|
||||||
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
|
<p className={"snippet" + (props.item.thumb ? "" : " show")}>
|
||||||
|
<Highlights text={props.item.snippet} keyword={props.keyword} />
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
51
src/components/cards/highlights.tsx
Normal file
51
src/components/cards/highlights.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { validateRegex } from "../../scripts/utils"
|
||||||
|
|
||||||
|
type HighlightsProps = {
|
||||||
|
text: string
|
||||||
|
keyword: string
|
||||||
|
title?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
||||||
|
const spans: [string, boolean][] = new Array()
|
||||||
|
let regex: RegExp
|
||||||
|
if (props.keyword === "" || !(regex = validateRegex(props.keyword, "g"))) {
|
||||||
|
if (props.title) spans.push([props.text, false])
|
||||||
|
else spans.push([props.text.substr(0, 325), false])
|
||||||
|
} else if (props.title) {
|
||||||
|
let match: RegExpExecArray
|
||||||
|
do {
|
||||||
|
const startIndex = regex.lastIndex
|
||||||
|
match = regex.exec(props.text)
|
||||||
|
if (match) {
|
||||||
|
if (startIndex != match.index) {
|
||||||
|
spans.push([props.text.substring(startIndex, match.index), false])
|
||||||
|
}
|
||||||
|
spans.push([match[0], true])
|
||||||
|
} else {
|
||||||
|
spans.push([props.text.substr(startIndex), false])
|
||||||
|
}
|
||||||
|
} while (match && regex.lastIndex < props.text.length)
|
||||||
|
} else {
|
||||||
|
const match = regex.exec(props.text)
|
||||||
|
if (match) {
|
||||||
|
if (match.index != 0) {
|
||||||
|
const startIndex = Math.max(match.index - 25, props.text.lastIndexOf(" ", Math.max(match.index - 10, 0)))
|
||||||
|
spans.push([props.text.substring(Math.max(0, startIndex), match.index), false])
|
||||||
|
}
|
||||||
|
spans.push([match[0], true])
|
||||||
|
if (regex.lastIndex < props.text.length) {
|
||||||
|
spans.push([props.text.substr(regex.lastIndex, 300), false])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spans.push([props.text.substr(0, 325), false])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{spans.map(([text, flag]) => flag ? <span className="h">{text}</span> : text)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Highlights
|
@ -7,15 +7,21 @@ type CardInfoProps = {
|
|||||||
source: RSSSource
|
source: RSSSource
|
||||||
item: RSSItem
|
item: RSSItem
|
||||||
hideTime?: boolean
|
hideTime?: boolean
|
||||||
|
showCreator?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
|
const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
|
||||||
<p className="info">
|
<p className="info">
|
||||||
{props.source.iconurl ? <img src={props.source.iconurl} /> : null}
|
{props.source.iconurl ? <img src={props.source.iconurl} /> : null}
|
||||||
<span className="name">{props.source.name}</span>
|
<span className="name">
|
||||||
{props.hideTime ? null : <Time date={props.item.date} />}
|
{props.source.name}
|
||||||
{props.item.hasRead ? null : <span className="read-indicator"></span>}
|
{props.showCreator && props.item.creator && (
|
||||||
|
<span className="creator">{props.item.creator}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
{props.item.starred ? <span className="starred-indicator"></span> : null}
|
{props.item.starred ? <span className="starred-indicator"></span> : null}
|
||||||
|
{props.item.hasRead ? null : <span className="read-indicator"></span>}
|
||||||
|
{props.hideTime ? null : <Time date={props.item.date} />}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "list-card"]
|
let cn = ["card", "list-card"]
|
||||||
@ -11,9 +12,7 @@ const className = (props: Card.Props) => {
|
|||||||
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb ? (
|
{props.item.thumb ? (
|
||||||
@ -21,7 +20,7 @@ const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<CardInfo source={props.source} item={props.item} />
|
||||||
<h3 className="title">{props.item.title}</h3>
|
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "magazine-card"]
|
let cn = ["card", "magazine-card"]
|
||||||
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
|
|||||||
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb ? (
|
{props.item.thumb ? (
|
||||||
@ -22,10 +21,10 @@ const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="title">{props.item.title}</h3>
|
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
|
||||||
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
|
<p className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></p>
|
||||||
</div>
|
</div>
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<CardInfo source={props.source} item={props.item} showCreator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, Directiona
|
|||||||
import { ContextMenuType } from "../scripts/models/app"
|
import { ContextMenuType } from "../scripts/models/app"
|
||||||
import { RSSItem } from "../scripts/models/item"
|
import { RSSItem } from "../scripts/models/item"
|
||||||
import { ContextReduxProps } from "../containers/context-menu-container"
|
import { ContextReduxProps } from "../containers/context-menu-container"
|
||||||
import { ViewType } from "../schema-types"
|
import { ViewType, ImageCallbackTypes } from "../schema-types"
|
||||||
import { FilterType } from "../scripts/models/feed"
|
import { FilterType } from "../scripts/models/feed"
|
||||||
|
|
||||||
export type ContextMenuProps = ContextReduxProps & {
|
export type ContextMenuProps = ContextReduxProps & {
|
||||||
@ -75,9 +75,9 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||||||
key: "openInBrowser",
|
key: "openInBrowser",
|
||||||
text: intl.get("openExternal"),
|
text: intl.get("openExternal"),
|
||||||
iconProps: { iconName: "NavigateExternalInline" },
|
iconProps: { iconName: "NavigateExternalInline" },
|
||||||
onClick: () => {
|
onClick: (e) => {
|
||||||
this.props.markRead(this.props.item)
|
this.props.markRead(this.props.item)
|
||||||
window.utils.openExternal(this.props.item.link)
|
window.utils.openExternal(this.props.item.link, window.utils.platform === "darwin" ? e.metaKey : e.ctrlKey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -152,6 +152,38 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||||||
},
|
},
|
||||||
getSearchItem(this.props.text)
|
getSearchItem(this.props.text)
|
||||||
]
|
]
|
||||||
|
case ContextMenuType.Image: return [
|
||||||
|
{
|
||||||
|
key: "openInBrowser",
|
||||||
|
text: intl.get("openExternal"),
|
||||||
|
iconProps: { iconName: "NavigateExternalInline" },
|
||||||
|
onClick: (e) => {
|
||||||
|
if (window.utils.platform === "darwin" ? e.metaKey : e.ctrlKey) {
|
||||||
|
window.utils.imageCallback(ImageCallbackTypes.OpenExternalBg)
|
||||||
|
} else {
|
||||||
|
window.utils.imageCallback(ImageCallbackTypes.OpenExternal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "saveImageAs",
|
||||||
|
text: intl.get("context.saveImageAs"),
|
||||||
|
iconProps: { iconName: "SaveTemplate" },
|
||||||
|
onClick: () => { window.utils.imageCallback(ImageCallbackTypes.SaveAs) }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "copyImage",
|
||||||
|
text: intl.get("context.copyImage"),
|
||||||
|
iconProps: { iconName: "FileImage" },
|
||||||
|
onClick: () => { window.utils.imageCallback(ImageCallbackTypes.Copy) }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "copyImageURL",
|
||||||
|
text: intl.get("context.copyImageURL"),
|
||||||
|
iconProps: { iconName: "Link" },
|
||||||
|
onClick: () => { window.utils.imageCallback(ImageCallbackTypes.CopyLink) }
|
||||||
|
}
|
||||||
|
]
|
||||||
case ContextMenuType.View: return [
|
case ContextMenuType.View: return [
|
||||||
{
|
{
|
||||||
key: "section_1",
|
key: "section_1",
|
||||||
|
@ -47,6 +47,7 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
key={item._id}
|
key={item._id}
|
||||||
item={item}
|
item={item}
|
||||||
source={this.props.sourceMap[item.source]}
|
source={this.props.sourceMap[item.source]}
|
||||||
|
keyword={this.props.keyword}
|
||||||
shortcuts={this.props.shortcuts}
|
shortcuts={this.props.shortcuts}
|
||||||
markRead={this.props.markRead}
|
markRead={this.props.markRead}
|
||||||
contextMenu={this.props.contextMenu}
|
contextMenu={this.props.contextMenu}
|
||||||
|
@ -11,6 +11,7 @@ export type FeedProps = FeedReduxProps & {
|
|||||||
viewType: ViewType
|
viewType: ViewType
|
||||||
items: RSSItem[]
|
items: RSSItem[]
|
||||||
sourceMap: Object
|
sourceMap: Object
|
||||||
|
keyword: string
|
||||||
shortcuts: (item: RSSItem, key: string) => void
|
shortcuts: (item: RSSItem, key: string) => void
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||||
|
@ -16,6 +16,7 @@ class ListFeed extends React.Component<FeedProps> {
|
|||||||
key: item._id,
|
key: item._id,
|
||||||
item: item,
|
item: item,
|
||||||
source: this.props.sourceMap[item.source],
|
source: this.props.sourceMap[item.source],
|
||||||
|
keyword: this.props.keyword,
|
||||||
shortcuts: this.props.shortcuts,
|
shortcuts: this.props.shortcuts,
|
||||||
markRead: this.props.markRead,
|
markRead: this.props.markRead,
|
||||||
contextMenu: this.props.contextMenu,
|
contextMenu: this.props.contextMenu,
|
||||||
|
@ -5,7 +5,7 @@ import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden, itemShortcu
|
|||||||
import { AppDispatch } from "../scripts/utils"
|
import { AppDispatch } from "../scripts/utils"
|
||||||
import { dismissItem, showOffsetItem } from "../scripts/models/page"
|
import { dismissItem, showOffsetItem } from "../scripts/models/page"
|
||||||
import Article from "../components/article"
|
import Article from "../components/article"
|
||||||
import { openTextMenu, closeContextMenu } from "../scripts/models/app"
|
import { openTextMenu, closeContextMenu, openImageMenu } from "../scripts/models/app"
|
||||||
|
|
||||||
type ArticleContainerProps = {
|
type ArticleContainerProps = {
|
||||||
itemId: string
|
itemId: string
|
||||||
@ -35,6 +35,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
|||||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||||
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
|
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
|
||||||
textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)),
|
textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)),
|
||||||
|
imageMenu: (position: [number, number]) => dispatch(openImageMenu(position)),
|
||||||
dismissContextMenu: () => dispatch(closeContextMenu())
|
dismissContextMenu: () => dispatch(closeContextMenu())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,10 @@ const mapStateToProps = createSelector(
|
|||||||
event: context.event,
|
event: context.event,
|
||||||
sids: context.target
|
sids: context.target
|
||||||
}
|
}
|
||||||
|
case ContextMenuType.Image: return {
|
||||||
|
type: context.type,
|
||||||
|
position: context.position
|
||||||
|
}
|
||||||
default: return { type: ContextMenuType.Hidden }
|
default: return { type: ContextMenuType.Hidden }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,15 +16,17 @@ interface FeedContainerProps {
|
|||||||
const getSources = (state: RootState) => state.sources
|
const getSources = (state: RootState) => state.sources
|
||||||
const getItems = (state: RootState) => state.items
|
const getItems = (state: RootState) => state.items
|
||||||
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
|
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
|
||||||
|
const getKeyword = (state: RootState) => state.page.filter.search
|
||||||
const getView = (_, props: FeedContainerProps) => props.viewType
|
const getView = (_, props: FeedContainerProps) => props.viewType
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[getSources, getItems, getFeed, getView],
|
[getSources, getItems, getFeed, getView, getKeyword],
|
||||||
(sources, items, feed, viewType) => ({
|
(sources, items, feed, viewType, keyword) => ({
|
||||||
feed: feed,
|
feed: feed,
|
||||||
items: feed.iids.map(iid => items[iid]),
|
items: feed.iids.map(iid => items[iid]),
|
||||||
sourceMap: sources,
|
sourceMap: sources,
|
||||||
|
keyword: keyword,
|
||||||
viewType: viewType
|
viewType: viewType
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,6 @@ import { ThemeSettings, SchemaTypes } from "./schema-types"
|
|||||||
import { store } from "./main/settings"
|
import { store } from "./main/settings"
|
||||||
import performUpdate from "./main/update-scripts"
|
import performUpdate from "./main/update-scripts"
|
||||||
import { WindowManager } from "./main/window"
|
import { WindowManager } from "./main/window"
|
||||||
import { openExternal } from "./main/utils"
|
|
||||||
|
|
||||||
if (!process.mas) {
|
if (!process.mas) {
|
||||||
const locked = app.requestSingleInstanceLock()
|
const locked = app.requestSingleInstanceLock()
|
||||||
@ -85,14 +84,3 @@ ipcMain.handle("import-all-settings", (_, configs: SchemaTypes) => {
|
|||||||
winManager.mainWindow.close()
|
winManager.mainWindow.close()
|
||||||
}, process.platform === "darwin" ? 1000 : 0); // Why ???
|
}, process.platform === "darwin" ? 1000 : 0); // Why ???
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on("web-contents-created", (_, contents) => {
|
|
||||||
contents.on("new-window", (event, url) => {
|
|
||||||
if (winManager.hasWindow()) event.preventDefault()
|
|
||||||
if (contents.getType() === "webview") openExternal(url)
|
|
||||||
})
|
|
||||||
contents.on("will-navigate", (event, url) => {
|
|
||||||
event.preventDefault()
|
|
||||||
if (contents.getType() === "webview") openExternal(url)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
@ -1,19 +1,41 @@
|
|||||||
import { ipcMain, shell, dialog, app, session, webContents, clipboard } from "electron"
|
import { ipcMain, shell, dialog, app, session, clipboard } from "electron"
|
||||||
import { WindowManager } from "./window"
|
import { WindowManager } from "./window"
|
||||||
import fs = require("fs")
|
import fs = require("fs")
|
||||||
|
import { ImageCallbackTypes } from "../schema-types"
|
||||||
export function openExternal(url: string) {
|
|
||||||
if (url.startsWith("https://") || url.startsWith("http://"))
|
|
||||||
shell.openExternal(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setUtilsListeners(manager: WindowManager) {
|
export function setUtilsListeners(manager: WindowManager) {
|
||||||
|
async function openExternal(url: string, background=false) {
|
||||||
|
if (url.startsWith("https://") || url.startsWith("http://")) {
|
||||||
|
if (background && process.platform === "darwin") {
|
||||||
|
shell.openExternal(url, { activate: false })
|
||||||
|
} else if (background && manager.hasWindow()) {
|
||||||
|
manager.mainWindow.setAlwaysOnTop(true)
|
||||||
|
await shell.openExternal(url)
|
||||||
|
setTimeout(() => manager.mainWindow.setAlwaysOnTop(false), 1000)
|
||||||
|
} else {
|
||||||
|
shell.openExternal(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on("web-contents-created", (_, contents) => {
|
||||||
|
contents.on("new-window", (event, url, _, disposition) => {
|
||||||
|
if (manager.hasWindow()) event.preventDefault()
|
||||||
|
if (contents.getType() === "webview") openExternal(url, disposition === "background-tab")
|
||||||
|
})
|
||||||
|
contents.on("will-navigate", (event, url) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (contents.getType() === "webview") openExternal(url)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
ipcMain.on("get-version", (event) => {
|
ipcMain.on("get-version", (event) => {
|
||||||
event.returnValue = app.getVersion()
|
event.returnValue = app.getVersion()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle("open-external", (_, url: string) => {
|
ipcMain.handle("open-external", (_, url: string, background: boolean) => {
|
||||||
openExternal(url)
|
openExternal(url, background)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle("show-error-box", (_, title, content) => {
|
ipcMain.handle("show-error-box", (_, title, content) => {
|
||||||
@ -88,8 +110,30 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
contents.on("context-menu", (_, params) => {
|
contents.on("context-menu", (_, params) => {
|
||||||
if (params.selectionText && manager.hasWindow()) {
|
if ((params.hasImageContents || params.selectionText) && manager.hasWindow()) {
|
||||||
|
if (params.hasImageContents) {
|
||||||
|
ipcMain.removeHandler("image-callback")
|
||||||
|
ipcMain.handleOnce("image-callback", (_, type: ImageCallbackTypes) => {
|
||||||
|
switch (type) {
|
||||||
|
case ImageCallbackTypes.OpenExternal:
|
||||||
|
case ImageCallbackTypes.OpenExternalBg:
|
||||||
|
openExternal(params.srcURL, type === ImageCallbackTypes.OpenExternalBg)
|
||||||
|
break
|
||||||
|
case ImageCallbackTypes.SaveAs:
|
||||||
|
contents.session.downloadURL(params.srcURL)
|
||||||
|
break
|
||||||
|
case ImageCallbackTypes.Copy:
|
||||||
|
contents.copyImageAt(params.x, params.y)
|
||||||
|
break
|
||||||
|
case ImageCallbackTypes.CopyLink:
|
||||||
|
clipboard.writeText(params.srcURL)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y])
|
||||||
|
} else {
|
||||||
manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y], params.selectionText)
|
manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y], params.selectionText)
|
||||||
|
}
|
||||||
contents.executeJavaScript(`new Promise(resolve => {
|
contents.executeJavaScript(`new Promise(resolve => {
|
||||||
const dismiss = () => {
|
const dismiss = () => {
|
||||||
document.removeEventListener("mousedown", dismiss)
|
document.removeEventListener("mousedown", dismiss)
|
||||||
@ -142,6 +186,10 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
if (manager.hasWindow()) {
|
if (manager.hasWindow()) {
|
||||||
const win = manager.mainWindow
|
const win = manager.mainWindow
|
||||||
if (win.isMinimized()) win.restore()
|
if (win.isMinimized()) win.restore()
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
win.setAlwaysOnTop(true)
|
||||||
|
win.setAlwaysOnTop(false)
|
||||||
|
}
|
||||||
win.focus()
|
win.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -32,6 +32,10 @@ export const enum SearchEngines {
|
|||||||
Google, Bing, Baidu, DuckDuckGo
|
Google, Bing, Baidu, DuckDuckGo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum ImageCallbackTypes {
|
||||||
|
OpenExternal, OpenExternalBg, SaveAs, Copy, CopyLink
|
||||||
|
}
|
||||||
|
|
||||||
export type SchemaTypes = {
|
export type SchemaTypes = {
|
||||||
version: string
|
version: string
|
||||||
theme: ThemeSettings
|
theme: ThemeSettings
|
||||||
|
@ -83,7 +83,10 @@
|
|||||||
"starredOnly": "Starred only",
|
"starredOnly": "Starred only",
|
||||||
"fullSearch": "Search in full text",
|
"fullSearch": "Search in full text",
|
||||||
"showHidden": "Show hidden articles",
|
"showHidden": "Show hidden articles",
|
||||||
"manageSources": "Manage sources"
|
"manageSources": "Manage sources",
|
||||||
|
"saveImageAs": "Save image as …",
|
||||||
|
"copyImage": "Copy image",
|
||||||
|
"copyImageURL": "Copy image link"
|
||||||
},
|
},
|
||||||
"searchEngine": {
|
"searchEngine": {
|
||||||
"name": "Search engine",
|
"name": "Search engine",
|
||||||
@ -113,6 +116,7 @@
|
|||||||
"errorParse": "An error has occurred when parsing the OPML file.",
|
"errorParse": "An error has occurred when parsing the OPML file.",
|
||||||
"errorParseHint": "Please ensure that the file isn't corrupted and is encoded with UTF-8.",
|
"errorParseHint": "Please ensure that the file isn't corrupted and is encoded with UTF-8.",
|
||||||
"errorImport": "Error importing {count, plural, =1 {# source} other {# sources}}.",
|
"errorImport": "Error importing {count, plural, =1 {# source} other {# sources}}.",
|
||||||
|
"exist": "This source already exists.",
|
||||||
"opmlFile": "OPML File",
|
"opmlFile": "OPML File",
|
||||||
"name": "Source name",
|
"name": "Source name",
|
||||||
"editName": "Edit name",
|
"editName": "Edit name",
|
||||||
|
@ -83,7 +83,10 @@
|
|||||||
"starredOnly": "仅星标文章",
|
"starredOnly": "仅星标文章",
|
||||||
"fullSearch": "在正文中搜索",
|
"fullSearch": "在正文中搜索",
|
||||||
"showHidden": "显示隐藏文章",
|
"showHidden": "显示隐藏文章",
|
||||||
"manageSources": "管理订阅源"
|
"manageSources": "管理订阅源",
|
||||||
|
"saveImageAs": "将图像另存为",
|
||||||
|
"copyImage": "复制图像",
|
||||||
|
"copyImageURL": "复制图像链接"
|
||||||
},
|
},
|
||||||
"searchEngine": {
|
"searchEngine": {
|
||||||
"name": "搜索引擎",
|
"name": "搜索引擎",
|
||||||
@ -111,6 +114,7 @@
|
|||||||
"errorParse": "解析OPML文件时出错",
|
"errorParse": "解析OPML文件时出错",
|
||||||
"errorParseHint": "请确保OPML文件完整且使用UTF-8编码。",
|
"errorParseHint": "请确保OPML文件完整且使用UTF-8编码。",
|
||||||
"errorImport": "导入{count}项订阅源时出错",
|
"errorImport": "导入{count}项订阅源时出错",
|
||||||
|
"exist": "该订阅源已存在",
|
||||||
"opmlFile": "OPML文件",
|
"opmlFile": "OPML文件",
|
||||||
"name": "订阅源名称",
|
"name": "订阅源名称",
|
||||||
"editName": "修改名称",
|
"editName": "修改名称",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import intl from "react-intl-universal"
|
import intl from "react-intl-universal"
|
||||||
import { INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE, initSources } from "./source"
|
import { INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE, initSources, SourceOpenTarget } from "./source"
|
||||||
import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item"
|
import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item"
|
||||||
import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils"
|
import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils"
|
||||||
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
|
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
|
||||||
@ -10,7 +10,7 @@ import locales from "../i18n/_locales"
|
|||||||
import * as db from "../db"
|
import * as db from "../db"
|
||||||
|
|
||||||
export const enum ContextMenuType {
|
export const enum ContextMenuType {
|
||||||
Hidden, Item, Text, View, Group
|
Hidden, Item, Text, View, Group, Image
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum AppLogType {
|
export const enum AppLogType {
|
||||||
@ -74,6 +74,7 @@ export const OPEN_ITEM_MENU = "OPEN_ITEM_MENU"
|
|||||||
export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU"
|
export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU"
|
||||||
export const OPEN_VIEW_MENU = "OPEN_VIEW_MENU"
|
export const OPEN_VIEW_MENU = "OPEN_VIEW_MENU"
|
||||||
export const OPEN_GROUP_MENU = "OPEN_GROUP_MENU"
|
export const OPEN_GROUP_MENU = "OPEN_GROUP_MENU"
|
||||||
|
export const OPEN_IMAGE_MENU = "OPEN_IMAGE_MENU"
|
||||||
|
|
||||||
interface CloseContextMenuAction {
|
interface CloseContextMenuAction {
|
||||||
type: typeof CLOSE_CONTEXT_MENU
|
type: typeof CLOSE_CONTEXT_MENU
|
||||||
@ -102,8 +103,13 @@ interface OpenGroupMenuAction {
|
|||||||
sids: number[]
|
sids: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OpenImageMenuAction {
|
||||||
|
type: typeof OPEN_IMAGE_MENU
|
||||||
|
position: [number, number]
|
||||||
|
}
|
||||||
|
|
||||||
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction
|
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction
|
||||||
| OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction
|
| OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction | OpenImageMenuAction
|
||||||
|
|
||||||
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
||||||
export const PUSH_NOTIFICATION = "PUSH_NOTIFICATION"
|
export const PUSH_NOTIFICATION = "PUSH_NOTIFICATION"
|
||||||
@ -163,6 +169,13 @@ export function openGroupMenu(sids: number[], event: React.MouseEvent): ContextM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openImageMenu(position: [number, number]): ContextMenuActionTypes {
|
||||||
|
return {
|
||||||
|
type: OPEN_IMAGE_MENU,
|
||||||
|
position: position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function toggleMenu(): AppThunk {
|
export function toggleMenu(): AppThunk {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: TOGGLE_MENU })
|
dispatch({ type: TOGGLE_MENU })
|
||||||
@ -213,14 +226,16 @@ export function setupAutoFetch(): AppThunk {
|
|||||||
|
|
||||||
export function pushNotification(item: RSSItem): AppThunk {
|
export function pushNotification(item: RSSItem): AppThunk {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState()
|
const sourceName = getState().sources[item.source].name
|
||||||
const sourceName = state.sources[item.source].name
|
|
||||||
if (!window.utils.isFocused()) {
|
if (!window.utils.isFocused()) {
|
||||||
const options = { body: sourceName } as any
|
const options = { body: sourceName } as any
|
||||||
if (item.thumb) options.icon = item.thumb
|
if (item.thumb) options.icon = item.thumb
|
||||||
const notification = new Notification(item.title, options)
|
const notification = new Notification(item.title, options)
|
||||||
notification.onclick = () => {
|
notification.onclick = () => {
|
||||||
if (!getState().app.settings.display) {
|
const state = getState()
|
||||||
|
if (state.sources[item.source].openTarget === SourceOpenTarget.External) {
|
||||||
|
window.utils.openExternal(item.link)
|
||||||
|
} else if (!state.app.settings.display) {
|
||||||
window.utils.focus()
|
window.utils.focus()
|
||||||
dispatch(showItemFromId(item._id))
|
dispatch(showItemFromId(item._id))
|
||||||
}
|
}
|
||||||
@ -422,6 +437,13 @@ export function appReducer(
|
|||||||
target: action.sids
|
target: action.sids
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case OPEN_IMAGE_MENU: return {
|
||||||
|
...state,
|
||||||
|
contextMenu: {
|
||||||
|
type: ContextMenuType.Image,
|
||||||
|
position: action.position
|
||||||
|
}
|
||||||
|
}
|
||||||
case TOGGLE_MENU: return {
|
case TOGGLE_MENU: return {
|
||||||
...state,
|
...state,
|
||||||
menu: !state.menu
|
menu: !state.menu
|
||||||
|
@ -204,29 +204,26 @@ export function addSourceFailure(err, batch: boolean): SourceActionTypes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertSource(source: RSSSource, trials = 0): AppThunk<Promise<RSSSource>> {
|
let insertPromises = Promise.resolve()
|
||||||
return (dispatch, getState) => {
|
function insertSource(source: RSSSource): AppThunk<Promise<RSSSource>> {
|
||||||
|
return (_, getState) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (trials >= 25) {
|
insertPromises = insertPromises.then(() => new Promise(innerResolve => {
|
||||||
reject("Failed to insert the source into NeDB.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let sids = Object.values(getState().sources).map(s => s.sid)
|
let sids = Object.values(getState().sources).map(s => s.sid)
|
||||||
source.sid = Math.max(...sids, -1) + 1
|
source.sid = Math.max(...sids, -1) + 1
|
||||||
db.sdb.insert(source, (err, inserted) => {
|
db.sdb.insert(source, (err, inserted) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (/^Can't insert key [0-9]+,/.test(err.message)) {
|
if ((new RegExp(`^Can't insert key ${source.url},`)).test(err.message)) {
|
||||||
console.log("sid conflict")
|
reject(intl.get("sources.exist"))
|
||||||
dispatch(insertSource(source, trials + 1))
|
|
||||||
.then(inserted => resolve(inserted))
|
|
||||||
.catch(err => reject(err))
|
|
||||||
} else {
|
} else {
|
||||||
reject(err)
|
reject(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(inserted)
|
resolve(inserted)
|
||||||
}
|
}
|
||||||
|
innerResolve()
|
||||||
})
|
})
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export async function parseRSS(url: string) {
|
|||||||
throw new Error(intl.get("log.parseError"))
|
throw new Error(intl.get("log.parseError"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.statusText)
|
throw new Error(result.status + " " + result.statusText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,9 +182,9 @@ export function calculateItemSize(): Promise<number> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateRegex(regex: string): RegExp {
|
export function validateRegex(regex: string, flags = ""): RegExp {
|
||||||
try {
|
try {
|
||||||
return new RegExp(regex)
|
return new RegExp(regex, flags)
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user