delete multiple sources
This commit is contained in:
parent
5fda3cac60
commit
72275241c2
|
@ -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<SourcesTabProps, SourcesTabState> {
|
||||
|
@ -31,15 +33,18 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
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<SourcesTabProps, SourcesTabState> {
|
|||
{ 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<SourcesTabProps, SourcesTabState> {
|
|||
</form>
|
||||
|
||||
<DetailsList
|
||||
compact={Object.keys(this.props.sources).length >= 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 && <>
|
||||
<Label>{intl.get("sources.selected")}</Label>
|
||||
|
@ -173,7 +185,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={this.state.newSourceName.trim().length == 0}
|
||||
onClick={() => this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName.trim())}
|
||||
onClick={this.updateSourceName}
|
||||
text={intl.get("sources.editName")} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
@ -204,6 +216,19 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
</Stack.Item>
|
||||
</Stack>
|
||||
</>}
|
||||
{this.state.selectedSources && <>
|
||||
<Label>{intl.get("sources.selectedMulti")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
onClick={() => this.props.deleteSources(this.state.selectedSources)}
|
||||
text={intl.get("sources.delete")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<span className="settings-hint">{intl.get("sources.deleteWarning")}</span>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "类型",
|
||||
|
|
|
@ -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<typeof addSource>, number, string][] = []
|
||||
let errors: [string, any][] = []
|
||||
for (let el of doc[0].children) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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<Promise<void>> {
|
||||
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<Promise<void>> {
|
||||
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
|
||||
|
|
|
@ -19,7 +19,10 @@ export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
|||
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[]
|
||||
}
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue