mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-06 04:13:45 +01:00
export to OPML & better import
This commit is contained in:
parent
9878386085
commit
d649bde776
@ -13,6 +13,7 @@ type SourcesTabProps = {
|
||||
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => void
|
||||
deleteSource: (source: RSSSource) => void
|
||||
importOPML: () => void
|
||||
exportOPML: () => void
|
||||
}
|
||||
|
||||
type SourcesTabState = {
|
||||
@ -103,7 +104,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
||||
<PrimaryButton onClick={this.props.importOPML} text={intl.get("sources.import")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton text={intl.get("sources.export")} />
|
||||
<DefaultButton onClick={this.props.exportOPML} text={intl.get("sources.export")} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
|
@ -5,7 +5,7 @@ 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 { importOPML } from "../../scripts/models/group"
|
||||
import { importOPML, exportOPML } from "../../scripts/models/group"
|
||||
import { AppDispatch } from "../../scripts/utils"
|
||||
|
||||
const getSources = (state: RootState) => state.sources
|
||||
@ -36,6 +36,16 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||
}
|
||||
)
|
||||
if (path && path.length > 0) dispatch(importOPML(path[0]))
|
||||
},
|
||||
exportOPML: () => {
|
||||
remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
filters: [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
|
||||
}
|
||||
).then(result => {
|
||||
if (!result.canceled) dispatch(exportOPML(result.filePath))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
"sources": {
|
||||
"errorImport": "Error importing {count, plural, =1 {# source} other {# sources}}.",
|
||||
"opmlFile": "OPML File",
|
||||
"name": "Source name",
|
||||
"editName": "Edit name",
|
||||
|
@ -72,6 +72,7 @@
|
||||
"feedback": "反馈"
|
||||
},
|
||||
"sources": {
|
||||
"errorImport": "导入{count}项订阅源时出错",
|
||||
"opmlFile": "OPML文件",
|
||||
"name": "订阅源名称",
|
||||
"editName": "修改名称",
|
||||
|
@ -185,7 +185,7 @@ export function initIntl(): AppThunk<Promise<void>> {
|
||||
return intl.init({
|
||||
currentLocale: locale,
|
||||
locales: locales,
|
||||
fallbackLocale: "zh-CN"
|
||||
fallbackLocale: "en-US"
|
||||
}).then(() => { dispatch(initIntlDone(locale)) })
|
||||
}
|
||||
}
|
||||
@ -234,7 +234,7 @@ export function appReducer(
|
||||
}
|
||||
default: return {
|
||||
...state,
|
||||
fetchingItems: false,
|
||||
fetchingItems: state.fetchingTotal !== 0,
|
||||
settings: {
|
||||
...state.settings,
|
||||
saving: action.batch
|
||||
|
@ -1,9 +1,12 @@
|
||||
import fs = require("fs")
|
||||
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource } from "./source"
|
||||
import intl = require("react-intl-universal")
|
||||
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource, RSSSource } from "./source"
|
||||
|
||||
import { ActionStatus, AppThunk, domParser, AppDispatch } from "../utils"
|
||||
import { saveSettings } from "./app"
|
||||
import { store } from "../settings"
|
||||
import { fetchItemsIntermediate, fetchItemsRequest, fetchItemsSuccess } from "./item"
|
||||
import { remote } from "electron"
|
||||
|
||||
const GROUPS_STORE_KEY = "sourceGroups"
|
||||
|
||||
@ -21,6 +24,7 @@ export class SourceGroup {
|
||||
} else {
|
||||
this.isMultiple = true
|
||||
this.name = name
|
||||
this.expanded = true
|
||||
}
|
||||
this.sids = sids
|
||||
}
|
||||
@ -184,15 +188,11 @@ export function toggleGroupExpansion(groupIndex: number): AppThunk {
|
||||
}
|
||||
}
|
||||
|
||||
async function outlineToSource(dispatch: AppDispatch, outline: Element): Promise<number> {
|
||||
function outlineToSource(outline: Element): [ReturnType<typeof addSource>, string] {
|
||||
let url = outline.getAttribute("xmlUrl")
|
||||
let name = outline.getAttribute("text") || outline.getAttribute("name")
|
||||
if (url) {
|
||||
try {
|
||||
return await dispatch(addSource(url.trim(), name, true))
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
return [addSource(url.trim(), name, true), url]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@ -205,38 +205,91 @@ export function importOPML(path: string): AppThunk {
|
||||
console.log(err)
|
||||
} else {
|
||||
dispatch(saveSettings())
|
||||
let successes: number = 0, failures: number = 0
|
||||
let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body")
|
||||
if (doc.length == 0) {
|
||||
dispatch(saveSettings())
|
||||
return
|
||||
}
|
||||
let sources: [ReturnType<typeof addSource>, number, string][] = []
|
||||
let errors: [string, any][] = []
|
||||
for (let el of doc[0].children) {
|
||||
if (el.getAttribute("type") === "rss") {
|
||||
let sid = await outlineToSource(dispatch, el)
|
||||
if (sid === null) failures += 1
|
||||
else successes += 1
|
||||
let source = outlineToSource(el)
|
||||
if (source) sources.push([source[0], -1, source[1]])
|
||||
} else if (el.hasAttribute("text") || el.hasAttribute("title")) {
|
||||
let groupName = el.getAttribute("text") || el.getAttribute("title")
|
||||
let gid = dispatch(createSourceGroup(groupName))
|
||||
for (let child of el.children) {
|
||||
let sid = await outlineToSource(dispatch, child)
|
||||
if (sid === null) {
|
||||
failures += 1
|
||||
} else {
|
||||
successes += 1
|
||||
dispatch(addSourceToGroup(gid, sid))
|
||||
}
|
||||
let source = outlineToSource(child)
|
||||
if (source) sources.push([source[0], gid, source[1]])
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(failures, successes)
|
||||
dispatch(saveSettings())
|
||||
dispatch(fetchItemsRequest(sources.length))
|
||||
let promises = sources.map(([s, gid, url]) => {
|
||||
return dispatch(s).then(sid => {
|
||||
if (sid !== null) dispatch(addSourceToGroup(gid, sid))
|
||||
}).catch(err => {
|
||||
errors.push([url, err])
|
||||
}).finally(() => {
|
||||
dispatch(fetchItemsIntermediate())
|
||||
})
|
||||
})
|
||||
Promise.allSettled(promises).then(() => {
|
||||
dispatch(fetchItemsSuccess([]))
|
||||
dispatch(saveSettings())
|
||||
if (errors.length > 0) {
|
||||
remote.dialog.showErrorBox(
|
||||
intl.get("sources.errorImport", { count: errors.length }),
|
||||
errors.map(e => {
|
||||
return e[0] + "\n" + String(e[1])
|
||||
}).join("\n")
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function sourceToOutline(source: RSSSource, xml: Document) {
|
||||
let outline = xml.createElement("outline")
|
||||
outline.setAttribute("text", source.name)
|
||||
outline.setAttribute("name", source.name)
|
||||
outline.setAttribute("type", "rss")
|
||||
outline.setAttribute("xmlUrl", source.url)
|
||||
return outline
|
||||
}
|
||||
|
||||
export function exportOPML(path: string): AppThunk {
|
||||
return (_, getState) => {
|
||||
let state = getState()
|
||||
let xml = domParser.parseFromString(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><opml version=\"1.0\"><head><title>Fluent Reader Export</title></head><body></body></opml>",
|
||||
"text/xml"
|
||||
)
|
||||
let body = xml.getElementsByTagName("body")[0]
|
||||
for (let group of state.groups) {
|
||||
if (group.isMultiple) {
|
||||
let outline = xml.createElement("outline")
|
||||
outline.setAttribute("text", group.name)
|
||||
outline.setAttribute("name", group.name)
|
||||
for (let sid of group.sids) {
|
||||
outline.appendChild(sourceToOutline(state.sources[sid], xml))
|
||||
}
|
||||
body.appendChild(outline)
|
||||
} else {
|
||||
body.appendChild(sourceToOutline(state.sources[group.sids[0]], xml))
|
||||
}
|
||||
}
|
||||
let serializer = new XMLSerializer()
|
||||
fs.writeFile(path, serializer.serializeToString(xml), (err) => {
|
||||
if (err) console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type GroupState = SourceGroup[]
|
||||
|
||||
export function groupReducer(
|
||||
|
@ -204,7 +204,7 @@ export function addSourceFailure(err, batch: boolean): SourceActionTypes {
|
||||
export function addSource(url: string, name: string = null, batch = false): AppThunk<Promise<number>> {
|
||||
return (dispatch, getState) => {
|
||||
let app = getState().app
|
||||
if (app.sourceInit && !app.fetchingItems) {
|
||||
if (app.sourceInit) {
|
||||
dispatch(addSourceRequest(batch))
|
||||
let source = new RSSSource(url, name)
|
||||
return source.fetchMetaData(rssParser)
|
||||
@ -235,7 +235,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
||||
return new Promise((_, reject) => { reject(e) })
|
||||
})
|
||||
}
|
||||
return new Promise((_, reject) => { reject("Sources not initialized or fetching items.") })
|
||||
return new Promise((_, reject) => { reject("Sources not initialized.") })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ const customFields = {
|
||||
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")
|
||||
import { ViewType } from "./models/page"
|
||||
import { IPartialTheme } from "@fluentui/react"
|
||||
import { SourceGroup } from "./models/group"
|
||||
let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session)
|
||||
export const rssParser = new Parser({
|
||||
customFields: customFields,
|
||||
|
Loading…
x
Reference in New Issue
Block a user