Files
fluent-reader/src/components/settings/rules.tsx
T
2020-06-29 14:01:21 +08:00

391 lines
15 KiB
TypeScript

import * as React from "react"
import intl from "react-intl-universal"
import { SourceState, RSSSource } from "../../scripts/models/source"
import { Stack, Label, Dropdown, IDropdownOption, TextField, PrimaryButton, Icon, DropdownMenuItemType,
DefaultButton, DetailsList, IColumn, CommandBar, ICommandBarItemProps, Selection, SelectionMode, MarqueeSelection, IDragDropEvents } from "@fluentui/react"
import { SourceRule, RuleActions } from "../../scripts/models/rule"
import { FilterType } from "../../scripts/models/feed"
import { validateRegex } from "../../scripts/utils"
import { RSSItem } from "../../scripts/models/item"
import Parser = require("@yang991178/rss-parser")
const actionKeyMap = {
"r-true": "article.markRead",
"r-false": "article.markUnread",
"s-true": "article.star",
"s-false": "article.unstar",
"h-true": "article.hide",
"h-false": "article.unhide",
}
type RulesTabProps = {
sources: SourceState
updateSourceRules: (source: RSSSource, rules: SourceRule[]) => void
}
type RulesTabState = {
sid: string
selectedRules: number[]
editIndex: number
regex: string
fullSearch: boolean
match: boolean
actionKeys: string[]
mockTitle: string
mockContent: string
mockResult: string
}
class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
rulesSelection: Selection
rulesDragDropEvents: IDragDropEvents
rulesDraggedItem: SourceRule
rulesDraggedIndex = -1
constructor(props) {
super(props)
this.state = {
sid: null,
selectedRules: [],
editIndex: -1,
regex: "",
fullSearch: false,
match: true,
actionKeys: [],
mockTitle: "",
mockContent: "",
mockResult: ""
}
this.rulesSelection = new Selection({
getKey: (_, i) => i,
onSelectionChanged: () => {
this.setState({selectedRules: this.rulesSelection.getSelectedIndices()})
}
})
this.rulesDragDropEvents = this.getRulesDragDropEvents()
}
getRulesDragDropEvents = (): IDragDropEvents => ({
canDrop: () => true,
canDrag: () => true,
onDrop: (item?: SourceRule) => {
if (this.rulesDraggedItem) {
this.reorderRules(item)
}
},
onDragStart: (item?: SourceRule, itemIndex?: number) => {
this.rulesDraggedItem = item
this.rulesDraggedIndex = itemIndex!
},
onDragEnd: () => {
this.rulesDraggedItem = undefined
this.rulesDraggedIndex = -1
},
})
reorderRules = (item: SourceRule) => {
let rules = this.getSourceRules()
let draggedItems = this.rulesSelection.isIndexSelected(this.rulesDraggedIndex)
? this.rulesSelection.getSelection() as SourceRule[]
: [this.rulesDraggedItem]
let insertIndex = rules.indexOf(item)
let items = rules.filter(r => !draggedItems.includes(r))
items.splice(insertIndex, 0, ...draggedItems)
this.rulesSelection.setAllSelected(false)
let source = this.props.sources[parseInt(this.state.sid)]
this.props.updateSourceRules(source, items)
}
initRuleEdit = (rule: SourceRule = null) => {
this.setState({
regex: rule ? rule.filter.search : "",
fullSearch: rule ? Boolean(rule.filter.type & FilterType.FullSearch) : false,
match: rule ? rule.match : true,
actionKeys: rule ? RuleActions.toKeys(rule.actions) : []
})
}
getSourceRules = () => this.props.sources[parseInt(this.state.sid)].rules
ruleColumns = (): IColumn[] => [
{
isRowHeader: true,
key: "regex",
name: intl.get("rules.regex"),
minWidth: 100,
maxWidth: 200,
onRender: (rule: SourceRule) => rule.filter.search
},
{
key: "actions",
name: intl.get("rules.action"),
minWidth: 100,
onRender: (rule: SourceRule) => RuleActions.toKeys(rule.actions).map(k => intl.get(actionKeyMap[k])).join(", ")
}
]
handleInputChange = (event) => {
const name = event.target.name as "regex"
this.setState({[name]: event.target.value})
}
sourceOptions = (): IDropdownOption[] => Object.entries(this.props.sources).map(([sid, s]) => ({
key: sid,
text: s.name,
data: { icon: s.iconurl }
}))
onRenderSourceOption = (option: IDropdownOption) => (
<div>
{option.data && option.data.icon && (
<img src={option.data.icon} className="favicon dropdown"/>
)}
<span>{option.text}</span>
</div>
)
onRenderSourceTitle = (options: IDropdownOption[]) => {
return this.onRenderSourceOption(options[0])
}
onSourceOptionChange = (_, item: IDropdownOption) => {
this.initRuleEdit()
this.rulesSelection.setAllSelected(false)
this.setState({
sid: item.key as string, selectedRules: [], editIndex: -1,
mockTitle: "", mockContent: "", mockResult: ""
})
}
searchOptions = (): IDropdownOption[] => [
{ key: 0, text: intl.get("rules.title") },
{ key: 1, text: intl.get("rules.fullSearch") }
]
onSearchOptionChange = (_, item: IDropdownOption) => {
this.setState({ fullSearch: Boolean(item.key) })
}
matchOptions = (): IDropdownOption[] => [
{ key: 1, text: intl.get("rules.match") },
{ key: 0, text: intl.get("rules.notMatch") }
]
onMatchOptionChange = (_, item: IDropdownOption) => {
this.setState({ match: Boolean(item.key) })
}
actionOptions = (): IDropdownOption[] => [
...Object.entries(actionKeyMap).map(([k, t], i) => {
if (k.includes("-false")) {
return [{ key: k, text: intl.get(t) }, { key: i, text: "-", itemType: DropdownMenuItemType.Divider }]
} else {
return [{ key: k, text: intl.get(t) }]
}
})
].flat(1)
onActionOptionChange = (_, item: IDropdownOption) => {
if (item.selected) {
this.setState(prevState => {
let [a, f] = (item.key as string).split("-")
let keys = prevState.actionKeys.filter(k => !k.startsWith(`${a}-`))
keys.push(item.key as string)
return { actionKeys: keys }
})
} else {
this.setState(prevState => ({ actionKeys: prevState.actionKeys.filter(k => k !== item.key) }))
}
}
validateRegexField = (value: string) => {
if (value.length === 0) return intl.get("emptyField")
else if (validateRegex(value) === null) return intl.get("rules.badRegex")
else return ""
}
saveRule = () => {
let rule = new SourceRule(this.state.regex, this.state.actionKeys, this.state.fullSearch, this.state.match)
let source = this.props.sources[parseInt(this.state.sid)]
let rules = source.rules ? [ ...source.rules ] : []
if (this.state.editIndex === -1) {
rules.push(rule)
} else {
rules.splice(this.state.editIndex, 1, rule)
}
this.props.updateSourceRules(source, rules)
this.setState({ editIndex: -1 })
this.initRuleEdit()
}
newRule = () => {
this.initRuleEdit()
this.setState({ editIndex: this.getSourceRules().length })
}
editRule = (rule: SourceRule, index: number) => {
this.initRuleEdit(rule)
this.setState({ editIndex: index })
}
deleteRules = () => {
let rules = this.getSourceRules()
for (let i of this.state.selectedRules) rules[i] = null
let source = this.props.sources[parseInt(this.state.sid)]
this.props.updateSourceRules(source, rules.filter(r => r !== null))
this.initRuleEdit()
}
commandBarItems = (): ICommandBarItemProps[] => [{
key: "new", text: intl.get("rules.new"), iconProps: { iconName: "Add" },
onClick: this.newRule
}]
commandBarFarItems = (): ICommandBarItemProps[] => {
let items = []
if (this.state.selectedRules.length === 1) {
let index = this.state.selectedRules[0]
items.push({
key: "edit", text: intl.get("edit"), iconProps: { iconName: "Edit" },
onClick: () => this.editRule(this.getSourceRules()[index], index)
})
}
if (this.state.selectedRules.length > 0) {
items.push({
key: "del", text: intl.get("delete"),
iconProps: { iconName: "Delete", style: { color: "#d13438" } },
onClick: this.deleteRules
})
}
return items
}
testMockItem = () => {
let parsed = { title: this.state.mockTitle }
let source = this.props.sources[parseInt(this.state.sid)]
let item = new RSSItem(parsed as Parser.Item, source)
item.snippet = this.state.mockContent
SourceRule.applyAll(this.getSourceRules(), item)
let result = []
result.push(intl.get(item.hasRead ? "article.markRead" : "article.markUnread"))
if (item.starred) result.push(intl.get("article.star"))
if (item.hidden) result.push(intl.get("article.hide"))
this.setState({ mockResult: result.join(", ") })
}
render = () => (
<div className="tab-body">
<Stack horizontal tokens={{childrenGap: 16}}>
<Stack.Item>
<Label>{intl.get("rules.source")}</Label>
</Stack.Item>
<Stack.Item grow>
<Dropdown
placeholder={intl.get("rules.selectSource")}
options={this.sourceOptions()}
onRenderOption={this.onRenderSourceOption}
onRenderTitle={this.onRenderSourceTitle}
selectedKey={this.state.sid}
onChange={this.onSourceOptionChange} />
</Stack.Item>
</Stack>
{this.state.sid && (
this.state.editIndex > -1 || !this.getSourceRules() || this.getSourceRules().length === 0
? <>
<Label>
{intl.get((this.state.editIndex >= 0 && this.state.editIndex < this.getSourceRules().length) ? "edit" : "rules.new")}
</Label>
<Stack horizontal>
<Stack.Item>
<Label>{intl.get("rules.if")}</Label>
</Stack.Item>
<Stack.Item>
<Dropdown
options={this.searchOptions()}
selectedKey={this.state.fullSearch ? 1 : 0}
onChange={this.onSearchOptionChange}
style={{width: 140}} />
</Stack.Item>
<Stack.Item>
<Dropdown
options={this.matchOptions()}
selectedKey={this.state.match ? 1 : 0}
onChange={this.onMatchOptionChange}
style={{width: 130}} />
</Stack.Item>
<Stack.Item grow>
<TextField
name="regex"
placeholder={intl.get("rules.regex")}
value={this.state.regex}
onGetErrorMessage={this.validateRegexField}
validateOnLoad={false}
onChange={this.handleInputChange} />
</Stack.Item>
</Stack>
<Stack horizontal>
<Stack.Item>
<Label>{intl.get("rules.then")}</Label>
</Stack.Item>
<Stack.Item grow>
<Dropdown multiSelect
placeholder={intl.get("rules.selectAction")}
options={this.actionOptions()}
selectedKeys={this.state.actionKeys}
onChange={this.onActionOptionChange}
onRenderCaretDown={() => <Icon iconName="CirclePlus" />} />
</Stack.Item>
</Stack>
<Stack horizontal>
<Stack.Item>
<PrimaryButton
disabled={this.state.regex.length == 0 || validateRegex(this.state.regex) === null || this.state.actionKeys.length == 0}
text={intl.get("confirm")}
onClick={this.saveRule} />
</Stack.Item>
{this.state.editIndex > -1 && <Stack.Item>
<DefaultButton
text={intl.get("cancel")}
onClick={() => this.setState({ editIndex: -1 })} />
</Stack.Item>}
</Stack>
</>
: <>
<CommandBar
items={this.commandBarItems()}
farItems={this.commandBarFarItems()} />
<MarqueeSelection selection={this.rulesSelection} isDraggingConstrainedToRoot>
<DetailsList compact
columns={this.ruleColumns()}
items={this.getSourceRules()}
onItemInvoked={this.editRule}
dragDropEvents={this.rulesDragDropEvents}
setKey="selected"
selection={this.rulesSelection}
selectionMode={SelectionMode.multiple} />
</MarqueeSelection>
<span className="settings-hint up">{intl.get("rules.hint")}</span>
<Label>{intl.get("rules.test")}</Label>
<Stack horizontal>
<Stack.Item grow>
<TextField
name="mockTitle"
placeholder={intl.get("rules.title")}
value={this.state.mockTitle}
onChange={this.handleInputChange} />
</Stack.Item>
<Stack.Item grow>
<TextField
name="mockContent"
placeholder={intl.get("rules.content")}
value={this.state.mockContent}
onChange={this.handleInputChange} />
</Stack.Item>
<Stack.Item>
<PrimaryButton
text={intl.get("confirm")}
onClick={this.testMockItem} />
</Stack.Item>
</Stack>
<span className="settings-hint up">{this.state.mockResult}</span>
</>)}
</div>
)
}
export default RulesTab