467 lines
18 KiB
TypeScript
467 lines
18 KiB
TypeScript
import * as React from "react"
|
|
import intl from "react-intl-universal"
|
|
import {
|
|
Label,
|
|
DefaultButton,
|
|
TextField,
|
|
Stack,
|
|
PrimaryButton,
|
|
DetailsList,
|
|
IColumn,
|
|
SelectionMode,
|
|
Selection,
|
|
IChoiceGroupOption,
|
|
ChoiceGroup,
|
|
IDropdownOption,
|
|
Dropdown,
|
|
MessageBar,
|
|
MessageBarType,
|
|
} from "@fluentui/react"
|
|
import {
|
|
SourceState,
|
|
RSSSource,
|
|
SourceOpenTarget,
|
|
} from "../../scripts/models/source"
|
|
import { urlTest } from "../../scripts/utils"
|
|
import DangerButton from "../utils/danger-button"
|
|
|
|
type SourcesTabProps = {
|
|
sources: SourceState
|
|
serviceOn: boolean
|
|
sids: number[]
|
|
acknowledgeSIDs: () => void
|
|
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
|
|
deleteSources: (sources: RSSSource[]) => void
|
|
importOPML: () => void
|
|
exportOPML: () => void
|
|
}
|
|
|
|
type SourcesTabState = {
|
|
[formName: string]: string
|
|
} & {
|
|
selectedSource: RSSSource
|
|
selectedSources: RSSSource[]
|
|
}
|
|
|
|
const enum EditDropdownKeys {
|
|
Name = "n",
|
|
Icon = "i",
|
|
Url = "u",
|
|
}
|
|
|
|
class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|
selection: Selection
|
|
|
|
constructor(props) {
|
|
super(props)
|
|
this.state = {
|
|
newUrl: "",
|
|
newSourceName: "",
|
|
selectedSource: null,
|
|
selectedSources: null,
|
|
}
|
|
this.selection = new Selection({
|
|
getKey: s => (s as RSSSource).sid,
|
|
onSelectionChanged: () => {
|
|
let count = this.selection.getSelectedCount()
|
|
let sources = count
|
|
? (this.selection.getSelection() as RSSSource[])
|
|
: null
|
|
this.setState({
|
|
selectedSource: count === 1 ? sources[0] : null,
|
|
selectedSources: count > 1 ? sources : null,
|
|
newSourceName: count === 1 ? sources[0].name : "",
|
|
newSourceIcon: count === 1 ? sources[0].iconurl || "" : "",
|
|
sourceEditOption: EditDropdownKeys.Name,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
componentDidMount = () => {
|
|
if (this.props.sids.length > 0) {
|
|
for (let sid of this.props.sids) {
|
|
this.selection.setKeySelected(String(sid), true, false)
|
|
}
|
|
this.props.acknowledgeSIDs()
|
|
}
|
|
}
|
|
|
|
columns = (): IColumn[] => [
|
|
{
|
|
key: "favicon",
|
|
name: intl.get("icon"),
|
|
fieldName: "name",
|
|
isIconOnly: true,
|
|
iconName: "ImagePixel",
|
|
minWidth: 16,
|
|
maxWidth: 16,
|
|
onRender: (s: RSSSource) =>
|
|
s.iconurl && <img src={s.iconurl} className="favicon" />,
|
|
},
|
|
{
|
|
key: "name",
|
|
name: intl.get("name"),
|
|
fieldName: "name",
|
|
minWidth: 200,
|
|
data: "string",
|
|
isRowHeader: true,
|
|
},
|
|
{
|
|
key: "url",
|
|
name: "URL",
|
|
fieldName: "url",
|
|
minWidth: 280,
|
|
data: "string",
|
|
},
|
|
]
|
|
|
|
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 }) },
|
|
{ key: "30", text: intl.get("time.minute", { m: 30 }) },
|
|
{ key: "60", text: intl.get("time.hour", { h: 1 }) },
|
|
{ key: "120", text: intl.get("time.hour", { h: 2 }) },
|
|
{ key: "180", text: intl.get("time.hour", { h: 3 }) },
|
|
{ key: "360", text: intl.get("time.hour", { h: 6 }) },
|
|
{ key: "720", text: intl.get("time.hour", { h: 12 }) },
|
|
{ key: "1440", text: intl.get("time.day", { d: 1 }) },
|
|
]
|
|
|
|
onFetchFrequencyChange = (_, option: IDropdownOption) => {
|
|
let frequency = parseInt(option.key as string)
|
|
this.props.updateFetchFrequency(this.state.selectedSource, frequency)
|
|
this.setState({
|
|
selectedSource: {
|
|
...this.state.selectedSource,
|
|
fetchFrequency: frequency,
|
|
} as RSSSource,
|
|
})
|
|
}
|
|
|
|
sourceOpenTargetChoices = (): IChoiceGroupOption[] => [
|
|
{
|
|
key: String(SourceOpenTarget.Local),
|
|
text: intl.get("sources.rssText"),
|
|
},
|
|
{
|
|
key: String(SourceOpenTarget.FullContent),
|
|
text: intl.get("article.loadFull"),
|
|
},
|
|
{
|
|
key: String(SourceOpenTarget.Webpage),
|
|
text: intl.get("sources.loadWebpage"),
|
|
},
|
|
{
|
|
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,
|
|
})
|
|
}
|
|
|
|
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 })
|
|
}
|
|
|
|
addSource = (event: React.FormEvent) => {
|
|
event.preventDefault()
|
|
let trimmed = this.state.newUrl.trim()
|
|
if (urlTest(trimmed)) this.props.addSource(trimmed)
|
|
}
|
|
|
|
onOpenTargetChange = (_, option: IChoiceGroupOption) => {
|
|
let newTarget = parseInt(option.key) as SourceOpenTarget
|
|
this.props.updateSourceOpenTarget(this.state.selectedSource, newTarget)
|
|
this.setState({
|
|
selectedSource: {
|
|
...this.state.selectedSource,
|
|
openTarget: newTarget,
|
|
} as RSSSource,
|
|
})
|
|
}
|
|
|
|
render = () => (
|
|
<div className="tab-body">
|
|
{this.props.serviceOn && (
|
|
<MessageBar messageBarType={MessageBarType.info}>
|
|
{intl.get("sources.serviceWarning")}
|
|
</MessageBar>
|
|
)}
|
|
<Label>{intl.get("sources.opmlFile")}</Label>
|
|
<Stack horizontal>
|
|
<Stack.Item>
|
|
<PrimaryButton
|
|
onClick={this.props.importOPML}
|
|
text={intl.get("sources.import")}
|
|
/>
|
|
</Stack.Item>
|
|
<Stack.Item>
|
|
<DefaultButton
|
|
onClick={this.props.exportOPML}
|
|
text={intl.get("sources.export")}
|
|
/>
|
|
</Stack.Item>
|
|
</Stack>
|
|
|
|
<form onSubmit={this.addSource}>
|
|
<Label htmlFor="newUrl">{intl.get("sources.add")}</Label>
|
|
<Stack horizontal>
|
|
<Stack.Item grow>
|
|
<TextField
|
|
onGetErrorMessage={v =>
|
|
urlTest(v.trim())
|
|
? ""
|
|
: intl.get("sources.badUrl")
|
|
}
|
|
validateOnLoad={false}
|
|
placeholder={intl.get("sources.inputUrl")}
|
|
value={this.state.newUrl}
|
|
id="newUrl"
|
|
name="newUrl"
|
|
onChange={this.handleInputChange}
|
|
/>
|
|
</Stack.Item>
|
|
<Stack.Item>
|
|
<PrimaryButton
|
|
disabled={!urlTest(this.state.newUrl.trim())}
|
|
type="submit"
|
|
text={intl.get("add")}
|
|
/>
|
|
</Stack.Item>
|
|
</Stack>
|
|
</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.multiple}
|
|
/>
|
|
|
|
{this.state.selectedSource && (
|
|
<>
|
|
{this.state.selectedSource.serviceRef && (
|
|
<MessageBar messageBarType={MessageBarType.info}>
|
|
{intl.get("sources.serviceManaged")}
|
|
</MessageBar>
|
|
)}
|
|
<Label>{intl.get("sources.selected")}</Label>
|
|
<Stack horizontal>
|
|
<Stack.Item>
|
|
<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>
|
|
{!this.state.selectedSource.serviceRef && (
|
|
<>
|
|
<Label>{intl.get("sources.fetchFrequency")}</Label>
|
|
<Stack>
|
|
<Stack.Item>
|
|
<Dropdown
|
|
options={this.fetchFrequencyOptions()}
|
|
selectedKey={
|
|
this.state.selectedSource
|
|
.fetchFrequency
|
|
? String(
|
|
this.state.selectedSource
|
|
.fetchFrequency
|
|
)
|
|
: "0"
|
|
}
|
|
onChange={this.onFetchFrequencyChange}
|
|
style={{ width: 200 }}
|
|
/>
|
|
</Stack.Item>
|
|
</Stack>
|
|
</>
|
|
)}
|
|
<ChoiceGroup
|
|
label={intl.get("sources.openTarget")}
|
|
options={this.sourceOpenTargetChoices()}
|
|
selectedKey={String(
|
|
this.state.selectedSource.openTarget
|
|
)}
|
|
onChange={this.onOpenTargetChange}
|
|
/>
|
|
{!this.state.selectedSource.serviceRef && (
|
|
<Stack horizontal>
|
|
<Stack.Item>
|
|
<DangerButton
|
|
onClick={() =>
|
|
this.props.deleteSource(
|
|
this.state.selectedSource
|
|
)
|
|
}
|
|
key={this.state.selectedSource.sid}
|
|
text={intl.get("sources.delete")}
|
|
/>
|
|
</Stack.Item>
|
|
<Stack.Item>
|
|
<span className="settings-hint">
|
|
{intl.get("sources.deleteWarning")}
|
|
</span>
|
|
</Stack.Item>
|
|
</Stack>
|
|
)}
|
|
</>
|
|
)}
|
|
{this.state.selectedSources &&
|
|
(this.state.selectedSources.filter(s => s.serviceRef).length ===
|
|
0 ? (
|
|
<>
|
|
<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>
|
|
</>
|
|
) : (
|
|
<MessageBar messageBarType={MessageBarType.info}>
|
|
{intl.get("sources.serviceManaged")}
|
|
</MessageBar>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default SourcesTab
|