edit icon url #29

This commit is contained in:
刘浩远 2020-07-06 17:44:57 +08:00
parent 27c81788e7
commit f7685e555a
6 changed files with 107 additions and 19 deletions

View File

@ -10,6 +10,7 @@ type SourcesTabProps = {
sources: SourceState
addSource: (url: string) => void
updateSourceName: (source: RSSSource, name: string) => void
updateSourceIcon: (source: RSSSource, iconUrl: string) => Promise<void>
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => void
updateFetchFrequency: (source: RSSSource, frequency: number) => void
deleteSource: (source: RSSSource) => void
@ -25,6 +26,12 @@ type SourcesTabState = {
selectedSources: RSSSource[]
}
const enum EditDropdownKeys {
Name = "n",
Icon = "i",
Url = "u",
}
class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
selection: Selection
@ -44,7 +51,9 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
this.setState({
selectedSource: count === 1 ? sources[0] : null,
selectedSources: count > 1 ? sources : null,
newSourceName: count === 1 ? sources[0].name : ""
newSourceName: count === 1 ? sources[0].name : "",
newSourceIcon: count === 1 ? (sources[0].iconurl || "") : "",
sourceEditOption: EditDropdownKeys.Name,
})
}
})
@ -80,6 +89,16 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
}
]
sourceEditOptions = (): IDropdownOption[] => [
{ key: EditDropdownKeys.Name, text: intl.get("name") },
{ key: EditDropdownKeys.Icon, text: intl.get("icon") },
{ key: EditDropdownKeys.Url, text: "URL" },
]
onSourceEditOptionChange = (_, option: IDropdownOption) => {
this.setState({sourceEditOption: option.key as string})
}
fetchFrequencyOptions = (): IDropdownOption[] => [
{ key: "0", text: intl.get("sources.unlimited") },
{ key: "15", text: intl.get("time.minute", { m: 15 }) },
@ -110,6 +129,12 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
this.setState({selectedSource: {...this.state.selectedSource, name: newName} as RSSSource})
}
updateSourceIcon = () => {
let newIcon = this.state.newSourceIcon.trim()
this.props.updateSourceIcon(this.state.selectedSource, newIcon)
this.setState({selectedSource: {...this.state.selectedSource, iconurl: newIcon}})
}
handleInputChange = (event) => {
const name: string = event.target.name
this.setState({[name]: event.target.value})
@ -173,21 +198,58 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
{this.state.selectedSource && <>
<Label>{intl.get("sources.selected")}</Label>
<Stack horizontal>
<Stack.Item grow>
<TextField
onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
validateOnLoad={false}
placeholder={intl.get("sources.name")}
value={this.state.newSourceName}
name="newSourceName"
onChange={this.handleInputChange} />
</Stack.Item>
<Stack.Item>
<DefaultButton
disabled={this.state.newSourceName.trim().length == 0}
onClick={this.updateSourceName}
text={intl.get("sources.editName")} />
<Dropdown
options={this.sourceEditOptions()}
selectedKey={this.state.sourceEditOption}
onChange={this.onSourceEditOptionChange}
style={{width: 120}} />
</Stack.Item>
{this.state.sourceEditOption === EditDropdownKeys.Name && <>
<Stack.Item grow>
<TextField
onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
validateOnLoad={false}
placeholder={intl.get("sources.name")}
value={this.state.newSourceName}
name="newSourceName"
onChange={this.handleInputChange} />
</Stack.Item>
<Stack.Item>
<DefaultButton
disabled={this.state.newSourceName.trim().length == 0}
onClick={this.updateSourceName}
text={intl.get("sources.editName")} />
</Stack.Item>
</>}
{this.state.sourceEditOption === EditDropdownKeys.Icon && <>
<Stack.Item grow>
<TextField
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
validateOnLoad={false}
placeholder={intl.get("sources.inputUrl")}
value={this.state.newSourceIcon}
name="newSourceIcon"
onChange={this.handleInputChange} />
</Stack.Item>
<Stack.Item>
<DefaultButton
disabled={!urlTest(this.state.newSourceIcon.trim())}
onClick={this.updateSourceIcon}
text={intl.get("edit")} />
</Stack.Item>
</>}
{this.state.sourceEditOption === EditDropdownKeys.Url && <>
<Stack.Item grow>
<TextField disabled value={this.state.selectedSource.url} />
</Stack.Item>
<Stack.Item>
<DefaultButton
onClick={() => window.utils.writeClipboard(this.state.selectedSource.url)}
text={intl.get("context.copy")} />
</Stack.Item>
</>}
</Stack>
<Label>{intl.get("sources.fetchFrequency")}</Label>
<Stack>

View File

@ -1,10 +1,12 @@
import intl from "react-intl-universal"
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, deleteSources } from "../../scripts/models/source"
import { importOPML, exportOPML } from "../../scripts/models/group"
import { AppDispatch } from "../../scripts/utils"
import { AppDispatch, validateFavicon } from "../../scripts/utils"
import { saveSettings } from "../../scripts/models/app"
const getSources = (state: RootState) => state.sources
@ -21,6 +23,15 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
updateSourceName: (source: RSSSource, name: string) => {
dispatch(updateSource({ ...source, name: name } as RSSSource))
},
updateSourceIcon: async (source: RSSSource, iconUrl: string) => {
dispatch(saveSettings())
if (await validateFavicon(iconUrl)) {
dispatch(updateSource({ ...source, iconurl: iconUrl }))
} else {
window.utils.showErrorBox(intl.get("sources.badIcon"), "")
}
dispatch(saveSettings())
},
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => {
dispatch(updateSource({ ...source, openTarget: target } as RSSSource))
},

View File

@ -60,6 +60,7 @@ export class WindowManager {
webviewTag: true,
enableRemoteModule: false,
contextIsolation: true,
spellcheck: false,
preload: path.join(app.getAppPath(), (app.isPackaged ? "dist/" : "") + "preload.js")
}
})

View File

@ -110,6 +110,7 @@
"rssText": "RSS full text",
"loadWebpage": "Load webpage",
"inputUrl": "Enter URL",
"badIcon": "Invalid icon",
"badUrl": "Invalid URL",
"deleteWarning": "The source and all saved articles will be removed.",
"selected": "Selected source",

View File

@ -110,6 +110,7 @@
"rssText": "RSS正文",
"loadWebpage": "加载网页",
"inputUrl": "输入URL",
"badIcon": "图标不存在或非图片",
"badUrl": "请正确输入URL",
"deleteWarning": "这将移除订阅源与所有已保存的文章",
"selected": "选中订阅源",

View File

@ -66,17 +66,29 @@ export async function fetchFavicon(url: string) {
}
}
url = url + "/favicon.ico"
result = await fetch(url, { credentials: "omit" })
if (result.status == 200 && result.headers.has("Content-Type")
&& result.headers.get("Content-Type").startsWith("image")) {
if (await validateFavicon(url)) {
return url
} else {
return null
}
return null
} catch {
return null
}
}
export async function validateFavicon(url: string) {
let flag = false
try {
const result = await fetch(url, { credentials: "omit" })
if (result.status == 200 && result.headers.has("Content-Type")
&& result.headers.get("Content-Type").startsWith("image")) {
flag = true
}
} finally {
return flag
}
}
export function htmlDecode(input: string) {
var doc = domParser.parseFromString(input, "text/html")
return doc.documentElement.textContent