diff --git a/src/components/settings/sources.tsx b/src/components/settings/sources.tsx index ac60248..7d866ba 100644 --- a/src/components/settings/sources.tsx +++ b/src/components/settings/sources.tsx @@ -13,6 +13,7 @@ type SourcesTabProps = { updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => void updateFetchFrequency: (source: RSSSource, frequency: number) => void deleteSource: (source: RSSSource) => void + deleteSources: (sources: RSSSource[]) => void importOPML: () => void exportOPML: () => void } @@ -20,7 +21,8 @@ type SourcesTabProps = { type SourcesTabState = { [formName: string]: string } & { - selectedSource: RSSSource + selectedSource: RSSSource, + selectedSources: RSSSource[] } class SourcesTab extends React.Component { @@ -31,15 +33,18 @@ class SourcesTab extends React.Component { this.state = { newUrl: "", newSourceName: "", - selectedSource: null + selectedSource: null, + selectedSources: null } this.selection = new Selection({ getKey: s => (s as RSSSource).sid, onSelectionChanged: () => { - let source = this.selection.getSelectedCount() ? this.selection.getSelection()[0] as RSSSource : null + let count = this.selection.getSelectedCount() + let sources = count ? this.selection.getSelection() as RSSSource[] : null this.setState({ - selectedSource: source, - newSourceName: source ? source.name : "" + selectedSource: count === 1 ? sources[0] : null, + selectedSources: count > 1 ? sources : null, + newSourceName: count === 1 ? sources[0].name : "" }) } }) @@ -99,6 +104,12 @@ class SourcesTab extends React.Component { { key: String(SourceOpenTarget.External), text: intl.get("openExternal") } ] + updateSourceName = () => { + let newName = this.state.newSourceName.trim() + this.props.updateSourceName(this.state.selectedSource, newName) + this.setState({selectedSource: {...this.state.selectedSource, name: newName} as RSSSource}) + } + handleInputChange = (event) => { const name: string = event.target.name this.setState({[name]: event.target.value}) @@ -151,12 +162,13 @@ class SourcesTab extends React.Component { = 10} items={Object.values(this.props.sources)} columns={this.columns()} getKey={s => s.sid} setKey="selected" selection={this.selection} - selectionMode={SelectionMode.single} /> + selectionMode={SelectionMode.multiple} /> {this.state.selectedSource && <> @@ -173,7 +185,7 @@ class SourcesTab extends React.Component { this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName.trim())} + onClick={this.updateSourceName} text={intl.get("sources.editName")} /> @@ -204,6 +216,19 @@ class SourcesTab extends React.Component { } + {this.state.selectedSources && <> + + + + this.props.deleteSources(this.state.selectedSources)} + text={intl.get("sources.delete")} /> + + + {intl.get("sources.deleteWarning")} + + + } ) } diff --git a/src/containers/settings/sources-container.tsx b/src/containers/settings/sources-container.tsx index 9c297b6..9873d43 100644 --- a/src/containers/settings/sources-container.tsx +++ b/src/containers/settings/sources-container.tsx @@ -4,7 +4,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, SourceOpenTarget } from "../../scripts/models/source" +import { addSource, RSSSource, updateSource, deleteSource, SourceOpenTarget, deleteSources } from "../../scripts/models/source" import { importOPML, exportOPML } from "../../scripts/models/group" import { AppDispatch } from "../../scripts/utils" @@ -30,6 +30,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => { dispatch(updateSource({ ...source, fetchFrequency: frequency } as RSSSource)) }, deleteSource: (source: RSSSource) => dispatch(deleteSource(source)), + deleteSources: (sources: RSSSource[]) => dispatch(deleteSources(sources)), importOPML: () => { remote.dialog.showOpenDialog( remote.getCurrentWindow(), diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index c4ab21d..5eb8306 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -84,6 +84,8 @@ "sources": { "untitled": "Source", "errorAdd": "An error has occured when adding the source.", + "errorParse": "An error has occured 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}}.", "opmlFile": "OPML File", "name": "Source name", @@ -100,7 +102,8 @@ "inputUrl": "Enter URL", "badUrl": "Invalid URL", "deleteWarning": "The source and all saved articles will be removed.", - "selected": "Selected source" + "selected": "Selected source", + "selectedMulti": "Selected multiple sources" }, "groups": { "type": "Type", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index 7ab0844..a28aa03 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -84,6 +84,8 @@ "sources": { "untitled": "订阅源", "errorAdd": "添加订阅源时出错", + "errorParse": "解析OPML文件时出错", + "errorParseHint": "请确保OPML文件完整且使用UTF-8编码。", "errorImport": "导入{count}项订阅源时出错", "opmlFile": "OPML文件", "name": "订阅源名称", @@ -99,8 +101,9 @@ "loadWebpage": "加载网页", "inputUrl": "输入URL", "badUrl": "请正确输入URL", - "deleteWarning": "这将移除此订阅源与所有已保存的文章", - "selected": "选中订阅源" + "deleteWarning": "这将移除订阅源与所有已保存的文章", + "selected": "选中订阅源", + "selectedMulti": "选中多个订阅源" }, "groups": { "type": "类型", diff --git a/src/scripts/models/group.ts b/src/scripts/models/group.ts index b07946d..c213709 100644 --- a/src/scripts/models/group.ts +++ b/src/scripts/models/group.ts @@ -210,6 +210,12 @@ export function importOPML(path: string): AppThunk { dispatch(saveSettings()) return } + let parseError = doc[0].getElementsByTagName("parsererror") + if (parseError.length > 0) { + dispatch(saveSettings()) + remote.dialog.showErrorBox(intl.get("sources.errorParse"), intl.get("sources.errorParseHint")) + return + } let sources: [ReturnType, number, string][] = [] let errors: [string, any][] = [] for (let el of doc[0].children) { diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index c5f9c88..a39a2f9 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -26,26 +26,33 @@ export class RSSItem { this.link = item.link || "" this.fetchedDate = new Date() this.date = item.isoDate ? new Date(item.isoDate) : this.fetchedDate - if (item.fullContent) { - this.content = item.fullContent - this.snippet = htmlDecode(item.fullContent) - } else { - this.content = item.content || "" - this.snippet = htmlDecode(item.contentSnippet || "") - } - if (item.thumb) this.thumb = item.thumb - else if (item.image) this.thumb = item.image - else { - let dom = domParser.parseFromString(this.content, "text/html") - let baseEl = dom.createElement('base') - baseEl.setAttribute('href', this.link.split("/").slice(0, 3).join("/")) - dom.head.append(baseEl) - let img = dom.querySelector("img") - if (img && img.src) this.thumb = img.src - } this.creator = item.creator this.hasRead = false } + + static parseContent(item: RSSItem, parsed: Parser.Item) { + if (parsed.fullContent) { + item.content = parsed.fullContent + item.snippet = htmlDecode(parsed.fullContent) + } else { + item.content = parsed.content || "" + item.snippet = htmlDecode(parsed.contentSnippet || "") + } + if (parsed.thumb) item.thumb = parsed.thumb + else if (parsed.image) item.thumb = parsed.image + else if (parsed.mediaContent) { + let images = parsed.mediaContent.filter(c => c.$ && c.$.medium === "image" && c.$.url) + if (images.length > 0) item.thumb = images[0].$.url + } + if(!item.thumb) { + let dom = domParser.parseFromString(item.content, "text/html") + let baseEl = dom.createElement('base') + baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/")) + dom.head.append(baseEl) + let img = dom.querySelector("img") + if (img && img.src) item.thumb = img.src + } + } } export type ItemState = { diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index 48cea54..ae6e7a8 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -55,6 +55,7 @@ export class RSSSource { if (err) { reject(err) } else if (doc === null) { + RSSItem.parseContent(i, item) resolve(i) } else { resolve(null) @@ -289,29 +290,44 @@ export function deleteSourceDone(source: RSSSource): SourceActionTypes { } } -export function deleteSource(source: RSSSource): AppThunk { +export function deleteSource(source: RSSSource, batch = false): AppThunk> { return (dispatch, getState) => { - dispatch(saveSettings()) - db.idb.remove({ source: source.sid }, { multi: true }, (err) => { - if (err) { - console.log(err) - dispatch(saveSettings()) - } else { - db.sdb.remove({ sid: source.sid }, {}, (err) => { - if (err) { - console.log(err) - dispatch(saveSettings()) - } else { - dispatch(deleteSourceDone(source)) - SourceGroup.save(getState().groups) - dispatch(saveSettings()) - } - }) - } + return new Promise((resolve) => { + if (!batch) dispatch(saveSettings()) + db.idb.remove({ source: source.sid }, { multi: true }, (err) => { + if (err) { + console.log(err) + if (!batch) dispatch(saveSettings()) + resolve() + } else { + db.sdb.remove({ sid: source.sid }, {}, (err) => { + if (err) { + console.log(err) + if (!batch) dispatch(saveSettings()) + resolve() + } else { + dispatch(deleteSourceDone(source)) + SourceGroup.save(getState().groups) + if (!batch) dispatch(saveSettings()) + resolve() + } + }) + } + }) }) } } +export function deleteSources(sources: RSSSource[]): AppThunk> { + return async (dispatch) => { + dispatch(saveSettings()) + for (let source of sources) { + await dispatch(deleteSource(source, true)) + } + dispatch(saveSettings()) + } +} + export function sourceReducer( state: SourceState = {}, action: SourceActionTypes | ItemActionTypes diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index d697d73..3fdbb5d 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -19,7 +19,10 @@ export type AppDispatch = ThunkDispatch import Parser = require("@yang991178/rss-parser") const rssParser = new Parser({ customFields: { - item: ["thumb", "image", ["content:encoded", "fullContent"]] as Parser.CustomFieldItem[] + item: [ + "thumb", "image", ["content:encoded", "fullContent"], + ['media:content', 'mediaContent', {keepArray: true}], + ] as Parser.CustomFieldItem[] } })