Merge pull request #46 from yang991178/0.6.3

Version 0.6.3
This commit is contained in:
Haoyuan Liu 2020-07-27 13:04:53 +08:00 committed by GitHub
commit 5927bf3441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 7654 additions and 102 deletions

View File

@ -25,6 +25,8 @@ jobs:
- name: Get release
id: get_release
uses: bruceadams/get-release@v1.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AppImage to release assets
uses: actions/upload-release-asset@v1

1
.gitignore vendored
View File

@ -3,6 +3,5 @@ dist/*.js
dist/*.js.map
dist/*.html
bin/*
package-lock.json
.DS_Store
*.provisionprofile

16
dist/styles/cards.css vendored
View File

@ -1,4 +1,5 @@
.info {
display: flex;
position: relative;
margin: 10px 12px;
line-height: 16px;
@ -12,13 +13,20 @@
font-size: 12px;
vertical-align: top;
display: inline-block;
max-width: 144px;
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info span.creator {
color: var(--neutralSecondaryAlt);
}
.info span.creator::before {
display: inline-block;
content: "/";
margin: 0 5px;
}
.info span.time {
float: right;
font-size: 12px;
}
@ -26,7 +34,6 @@
display: block;
width: 16px;
height: 16px;
float: right;
text-align: center;
}
.read-indicator::after {
@ -80,6 +87,9 @@
left: 0;
background: #0003;
}
.card span.h {
background: #fce10080;
}
.default-card {
display: inline-block;

7368
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,5 @@
import { ipcRenderer } from "electron"
import { ImageCallbackTypes } from "../schema-types"
const utilsBridge = {
platform: process.platform,
@ -7,8 +8,8 @@ const utilsBridge = {
return ipcRenderer.sendSync("get-version")
},
openExternal: (url: string) => {
ipcRenderer.invoke("open-external", url)
openExternal: (url: string, background=false) => {
ipcRenderer.invoke("open-external", url, background)
},
showErrorBox: (title: string, content: string) => {
@ -54,6 +55,9 @@ const utilsBridge = {
callback(pos, text)
})
},
imageCallback: (type: ImageCallbackTypes) => {
ipcRenderer.invoke("image-callback", type)
},
addWebviewKeydownListener: (callback: (event: Electron.Input) => any) => {
ipcRenderer.removeAllListeners("webview-keydown")

View File

@ -19,6 +19,7 @@ type ArticleProps = {
toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void
textMenu: (text: string, position: [number, number]) => void
imageMenu: (position: [number, number]) => void
dismissContextMenu: () => void
}
@ -71,7 +72,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
key: "openInBrowser",
text: intl.get("openExternal"),
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",
@ -95,7 +96,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
contextMenuHandler = (pos: [number, number], text: string) => {
if (pos) {
this.props.textMenu(text, pos)
if (text) this.props.textMenu(text, pos)
else this.props.imageMenu(pos)
} else {
this.props.dismissContextMenu()
}
@ -169,10 +171,6 @@ class Article extends React.Component<ArticleProps, ArticleState> {
if (refocus) refocus.focus()
}
openInBrowser = () => {
window.utils.openExternal(this.props.item.link)
}
toggleWebpage = () => {
if (this.state.loadWebpage) {
this.setState({loadWebpage: false})

View File

@ -7,18 +7,25 @@ export namespace Card {
feedId: string
item: RSSItem
source: RSSSource
keyword: string
shortcuts: (item: RSSItem, key: string) => void
markRead: (item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void
showItem: (fid: string, item: RSSItem) => void
}
export const openInBrowser = (props: Props) => {
const openInBrowser = (props: Props, e: React.MouseEvent) => {
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.stopPropagation()
switch (props.source.openTarget) {
@ -29,25 +36,25 @@ export namespace Card {
break
}
case SourceOpenTarget.External: {
openInBrowser(props)
openInBrowser(props, e)
break
}
}
}
export const onMouseUp = (props: Props, e: React.MouseEvent) => {
const onMouseUp = (props: Props, e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
switch (e.button) {
case 1:
openInBrowser(props)
openInBrowser(props, e)
break
case 2:
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)
}
}

View File

@ -2,6 +2,7 @@ import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Time from "../utils/time"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "compact-card"]
@ -12,15 +13,13 @@ const className = (props: Card.Props) => {
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
<CardInfo source={props.source} item={props.item} hideTime />
<div className="data">
<span className="title">{props.item.title}</span>
<span className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</span>
<span className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></span>
<span className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></span>
</div>
<Time date={props.item.date} />
</div>

View File

@ -1,6 +1,7 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "default-card"]
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
@ -25,8 +24,10 @@ const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
<img className="head" src={props.item.thumb} />
) : null}
<CardInfo source={props.source} item={props.item} />
<h3 className="title">{props.item.title}</h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}>
<Highlights text={props.item.snippet} keyword={props.keyword} />
</p>
</div>
)

View 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

View File

@ -7,15 +7,21 @@ type CardInfoProps = {
source: RSSSource
item: RSSItem
hideTime?: boolean
showCreator?: boolean
}
const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
<p className="info">
{props.source.iconurl ? <img src={props.source.iconurl} /> : null}
<span className="name">{props.source.name}</span>
{props.hideTime ? null : <Time date={props.item.date} />}
{props.item.hasRead ? null : <span className="read-indicator"></span>}
<span className="name">
{props.source.name}
{props.showCreator && props.item.creator && (
<span className="creator">{props.item.creator}</span>
)}
</span>
{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>
)

View File

@ -1,6 +1,7 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "list-card"]
@ -11,9 +12,7 @@ const className = (props: Card.Props) => {
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
@ -21,7 +20,7 @@ const ListCard: React.FunctionComponent<Card.Props> = (props) => (
) : null}
<div className="data">
<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>
)

View File

@ -1,6 +1,7 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "magazine-card"]
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
@ -22,10 +21,10 @@ const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
) : null}
<div className="data">
<div>
<h3 className="title">{props.item.title}</h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
<p className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></p>
</div>
<CardInfo source={props.source} item={props.item} />
<CardInfo source={props.source} item={props.item} showCreator />
</div>
</div>
)

View File

@ -6,7 +6,7 @@ import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, Directiona
import { ContextMenuType } from "../scripts/models/app"
import { RSSItem } from "../scripts/models/item"
import { ContextReduxProps } from "../containers/context-menu-container"
import { ViewType } from "../schema-types"
import { ViewType, ImageCallbackTypes } from "../schema-types"
import { FilterType } from "../scripts/models/feed"
export type ContextMenuProps = ContextReduxProps & {
@ -75,9 +75,9 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
key: "openInBrowser",
text: intl.get("openExternal"),
iconProps: { iconName: "NavigateExternalInline" },
onClick: () => {
onClick: (e) => {
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)
]
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 [
{
key: "section_1",

View File

@ -47,6 +47,7 @@ class CardsFeed extends React.Component<FeedProps> {
key={item._id}
item={item}
source={this.props.sourceMap[item.source]}
keyword={this.props.keyword}
shortcuts={this.props.shortcuts}
markRead={this.props.markRead}
contextMenu={this.props.contextMenu}

View File

@ -11,6 +11,7 @@ export type FeedProps = FeedReduxProps & {
viewType: ViewType
items: RSSItem[]
sourceMap: Object
keyword: string
shortcuts: (item: RSSItem, key: string) => void
markRead: (item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void

View File

@ -16,6 +16,7 @@ class ListFeed extends React.Component<FeedProps> {
key: item._id,
item: item,
source: this.props.sourceMap[item.source],
keyword: this.props.keyword,
shortcuts: this.props.shortcuts,
markRead: this.props.markRead,
contextMenu: this.props.contextMenu,

View File

@ -5,7 +5,7 @@ import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden, itemShortcu
import { AppDispatch } from "../scripts/utils"
import { dismissItem, showOffsetItem } from "../scripts/models/page"
import Article from "../components/article"
import { openTextMenu, closeContextMenu } from "../scripts/models/app"
import { openTextMenu, closeContextMenu, openImageMenu } from "../scripts/models/app"
type ArticleContainerProps = {
itemId: string
@ -35,6 +35,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)),
imageMenu: (position: [number, number]) => dispatch(openImageMenu(position)),
dismissContextMenu: () => dispatch(closeContextMenu())
}
}

View File

@ -38,6 +38,10 @@ const mapStateToProps = createSelector(
event: context.event,
sids: context.target
}
case ContextMenuType.Image: return {
type: context.type,
position: context.position
}
default: return { type: ContextMenuType.Hidden }
}
}

View File

@ -16,15 +16,17 @@ interface FeedContainerProps {
const getSources = (state: RootState) => state.sources
const getItems = (state: RootState) => state.items
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 makeMapStateToProps = () => {
return createSelector(
[getSources, getItems, getFeed, getView],
(sources, items, feed, viewType) => ({
[getSources, getItems, getFeed, getView, getKeyword],
(sources, items, feed, viewType, keyword) => ({
feed: feed,
items: feed.iids.map(iid => items[iid]),
sourceMap: sources,
keyword: keyword,
viewType: viewType
})
)

View File

@ -3,7 +3,6 @@ import { ThemeSettings, SchemaTypes } from "./schema-types"
import { store } from "./main/settings"
import performUpdate from "./main/update-scripts"
import { WindowManager } from "./main/window"
import { openExternal } from "./main/utils"
if (!process.mas) {
const locked = app.requestSingleInstanceLock()
@ -85,14 +84,3 @@ ipcMain.handle("import-all-settings", (_, configs: SchemaTypes) => {
winManager.mainWindow.close()
}, 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)
})
})

View File

@ -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 fs = require("fs")
export function openExternal(url: string) {
if (url.startsWith("https://") || url.startsWith("http://"))
shell.openExternal(url)
}
import { ImageCallbackTypes } from "../schema-types"
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) => {
event.returnValue = app.getVersion()
})
ipcMain.handle("open-external", (_, url: string) => {
openExternal(url)
ipcMain.handle("open-external", (_, url: string, background: boolean) => {
openExternal(url, background)
})
ipcMain.handle("show-error-box", (_, title, content) => {
@ -88,8 +110,30 @@ export function setUtilsListeners(manager: WindowManager) {
}
})
contents.on("context-menu", (_, params) => {
if (params.selectionText && manager.hasWindow()) {
manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y], params.selectionText)
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)
}
contents.executeJavaScript(`new Promise(resolve => {
const dismiss = () => {
document.removeEventListener("mousedown", dismiss)
@ -142,6 +186,10 @@ export function setUtilsListeners(manager: WindowManager) {
if (manager.hasWindow()) {
const win = manager.mainWindow
if (win.isMinimized()) win.restore()
if (process.platform === "win32") {
win.setAlwaysOnTop(true)
win.setAlwaysOnTop(false)
}
win.focus()
}
})

View File

@ -32,6 +32,10 @@ export const enum SearchEngines {
Google, Bing, Baidu, DuckDuckGo
}
export const enum ImageCallbackTypes {
OpenExternal, OpenExternalBg, SaveAs, Copy, CopyLink
}
export type SchemaTypes = {
version: string
theme: ThemeSettings

View File

@ -83,7 +83,10 @@
"starredOnly": "Starred only",
"fullSearch": "Search in full text",
"showHidden": "Show hidden articles",
"manageSources": "Manage sources"
"manageSources": "Manage sources",
"saveImageAs": "Save image as …",
"copyImage": "Copy image",
"copyImageURL": "Copy image link"
},
"searchEngine": {
"name": "Search engine",
@ -113,6 +116,7 @@
"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.",
"errorImport": "Error importing {count, plural, =1 {# source} other {# sources}}.",
"exist": "This source already exists.",
"opmlFile": "OPML File",
"name": "Source name",
"editName": "Edit name",

View File

@ -83,7 +83,10 @@
"starredOnly": "仅星标文章",
"fullSearch": "在正文中搜索",
"showHidden": "显示隐藏文章",
"manageSources": "管理订阅源"
"manageSources": "管理订阅源",
"saveImageAs": "将图像另存为",
"copyImage": "复制图像",
"copyImageURL": "复制图像链接"
},
"searchEngine": {
"name": "搜索引擎",
@ -111,6 +114,7 @@
"errorParse": "解析OPML文件时出错",
"errorParseHint": "请确保OPML文件完整且使用UTF-8编码。",
"errorImport": "导入{count}项订阅源时出错",
"exist": "该订阅源已存在",
"opmlFile": "OPML文件",
"name": "订阅源名称",
"editName": "修改名称",

View File

@ -1,5 +1,5 @@
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 { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils"
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
@ -10,7 +10,7 @@ import locales from "../i18n/_locales"
import * as db from "../db"
export const enum ContextMenuType {
Hidden, Item, Text, View, Group
Hidden, Item, Text, View, Group, Image
}
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_VIEW_MENU = "OPEN_VIEW_MENU"
export const OPEN_GROUP_MENU = "OPEN_GROUP_MENU"
export const OPEN_IMAGE_MENU = "OPEN_IMAGE_MENU"
interface CloseContextMenuAction {
type: typeof CLOSE_CONTEXT_MENU
@ -102,8 +103,13 @@ interface OpenGroupMenuAction {
sids: number[]
}
interface OpenImageMenuAction {
type: typeof OPEN_IMAGE_MENU
position: [number, number]
}
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction
| OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction
| OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction | OpenImageMenuAction
export const TOGGLE_LOGS = "TOGGLE_LOGS"
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 {
return (dispatch, getState) => {
dispatch({ type: TOGGLE_MENU })
@ -213,14 +226,16 @@ export function setupAutoFetch(): AppThunk {
export function pushNotification(item: RSSItem): AppThunk {
return (dispatch, getState) => {
const state = getState()
const sourceName = state.sources[item.source].name
const sourceName = getState().sources[item.source].name
if (!window.utils.isFocused()) {
const options = { body: sourceName } as any
if (item.thumb) options.icon = item.thumb
const notification = new Notification(item.title, options)
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()
dispatch(showItemFromId(item._id))
}
@ -422,6 +437,13 @@ export function appReducer(
target: action.sids
}
}
case OPEN_IMAGE_MENU: return {
...state,
contextMenu: {
type: ContextMenuType.Image,
position: action.position
}
}
case TOGGLE_MENU: return {
...state,
menu: !state.menu

View File

@ -204,29 +204,26 @@ export function addSourceFailure(err, batch: boolean): SourceActionTypes {
}
}
function insertSource(source: RSSSource, trials = 0): AppThunk<Promise<RSSSource>> {
return (dispatch, getState) => {
let insertPromises = Promise.resolve()
function insertSource(source: RSSSource): AppThunk<Promise<RSSSource>> {
return (_, getState) => {
return new Promise((resolve, reject) => {
if (trials >= 25) {
reject("Failed to insert the source into NeDB.")
return
}
let sids = Object.values(getState().sources).map(s => s.sid)
source.sid = Math.max(...sids, -1) + 1
db.sdb.insert(source, (err, inserted) => {
if (err) {
if (/^Can't insert key [0-9]+,/.test(err.message)) {
console.log("sid conflict")
dispatch(insertSource(source, trials + 1))
.then(inserted => resolve(inserted))
.catch(err => reject(err))
insertPromises = insertPromises.then(() => new Promise(innerResolve => {
let sids = Object.values(getState().sources).map(s => s.sid)
source.sid = Math.max(...sids, -1) + 1
db.sdb.insert(source, (err, inserted) => {
if (err) {
if ((new RegExp(`^Can't insert key ${source.url},`)).test(err.message)) {
reject(intl.get("sources.exist"))
} else {
reject(err)
}
} else {
reject(err)
resolve(inserted)
}
} else {
resolve(inserted)
}
})
innerResolve()
})
}))
})
}

View File

@ -47,7 +47,7 @@ export async function parseRSS(url: string) {
throw new Error(intl.get("log.parseError"))
}
} 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 {
return new RegExp(regex)
return new RegExp(regex, flags)
} catch {
return null
}