mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-15 02:42:02 +02:00
384 lines
14 KiB
TypeScript
384 lines
14 KiB
TypeScript
import * as React from "react"
|
|
import intl from "react-intl-universal"
|
|
import { SourceGroup } from "../../schema-types"
|
|
import { SourceState, RSSSource } from "../../scripts/models/source"
|
|
import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack,
|
|
TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, CommandBarButton, MarqueeSelection, IDragDropEvents, MessageBar, MessageBarType } from "@fluentui/react"
|
|
import DangerButton from "../utils/danger-button"
|
|
|
|
type GroupsTabProps = {
|
|
sources: SourceState,
|
|
groups: SourceGroup[],
|
|
serviceOn: boolean,
|
|
createGroup: (name: string) => void,
|
|
updateGroup: (group: SourceGroup) => void,
|
|
addToGroup: (groupIndex: number, sid: number) => void,
|
|
deleteGroup: (groupIndex: number) => void,
|
|
removeFromGroup: (groupIndex: number, sids: number[]) => void,
|
|
reorderGroups: (groups: SourceGroup[]) => void
|
|
}
|
|
|
|
type GroupsTabState = {
|
|
[formName: string]: any,
|
|
selectedGroup: SourceGroup,
|
|
selectedSources: RSSSource[],
|
|
dropdownIndex: number,
|
|
manageGroup: boolean
|
|
}
|
|
|
|
class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|
groupSelection: Selection
|
|
groupDragDropEvents: IDragDropEvents
|
|
groupDraggedItem: SourceGroup
|
|
groupDraggedIndex = -1
|
|
sourcesSelection: Selection
|
|
sourcesDragDropEvents: IDragDropEvents
|
|
sourcesDraggedItem: RSSSource
|
|
sourcesDraggedIndex = -1
|
|
|
|
constructor(props) {
|
|
super(props)
|
|
this.state = {
|
|
editGroupName: "",
|
|
newGroupName: "",
|
|
selectedGroup: null,
|
|
selectedSources: null,
|
|
dropdownIndex: null,
|
|
manageGroup: false
|
|
}
|
|
this.groupDragDropEvents = this.getGroupDragDropEvents()
|
|
this.sourcesDragDropEvents = this.getSourcesDragDropEvents()
|
|
this.groupSelection = new Selection({
|
|
getKey: g => (g as SourceGroup).index,
|
|
onSelectionChanged: () => {
|
|
let g = this.groupSelection.getSelectedCount()
|
|
? this.groupSelection.getSelection()[0] as SourceGroup : null
|
|
this.setState({
|
|
selectedGroup: g,
|
|
editGroupName: g && g.isMultiple ? g.name : ""
|
|
})
|
|
}
|
|
})
|
|
this.sourcesSelection = new Selection({
|
|
getKey: s => (s as RSSSource).sid,
|
|
onSelectionChanged: () => {
|
|
let sources = this.sourcesSelection.getSelectedCount()
|
|
? this.sourcesSelection.getSelection() as RSSSource[] : null
|
|
this.setState({
|
|
selectedSources: sources
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
groupColumns = (): IColumn[] => [
|
|
{
|
|
key: "type",
|
|
name: intl.get("groups.type"),
|
|
minWidth: 40,
|
|
maxWidth: 40,
|
|
data: "string",
|
|
onRender: (g: SourceGroup) => <>
|
|
{g.isMultiple ? intl.get("groups.group") : intl.get("groups.source")}
|
|
</>
|
|
},
|
|
{
|
|
key: "capacity",
|
|
name: intl.get("groups.capacity"),
|
|
minWidth: 40,
|
|
maxWidth: 60,
|
|
data: "string",
|
|
onRender: (g: SourceGroup) => <>
|
|
{g.isMultiple ? g.sids.length : ""}
|
|
</>
|
|
},
|
|
{
|
|
key: "name",
|
|
name: intl.get("name"),
|
|
minWidth: 200,
|
|
data: "string",
|
|
isRowHeader: true,
|
|
onRender: (g: SourceGroup) => <>
|
|
{g.isMultiple ? g.name : this.props.sources[g.sids[0]].name}
|
|
</>
|
|
}
|
|
]
|
|
|
|
sourceColumns: 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'
|
|
}
|
|
]
|
|
|
|
getGroupDragDropEvents = (): IDragDropEvents => ({
|
|
canDrop: () => true,
|
|
canDrag: () => true,
|
|
onDrop: (item?: SourceGroup) => {
|
|
if (this.groupDraggedItem) {
|
|
this.reorderGroups(item)
|
|
}
|
|
},
|
|
onDragStart: (item?: SourceGroup, itemIndex?: number) => {
|
|
this.groupDraggedItem = item
|
|
this.groupDraggedIndex = itemIndex!
|
|
},
|
|
onDragEnd: () => {
|
|
this.groupDraggedItem = undefined
|
|
this.groupDraggedIndex = -1
|
|
},
|
|
})
|
|
|
|
reorderGroups = (item: SourceGroup) => {
|
|
let draggedItem = this.groupSelection.isIndexSelected(this.groupDraggedIndex)
|
|
? this.groupSelection.getSelection()[0] as SourceGroup
|
|
: this.groupDraggedItem!
|
|
|
|
let insertIndex = item.index
|
|
let groups = this.props.groups.filter(g => g.index != draggedItem.index)
|
|
|
|
groups.splice(insertIndex, 0, draggedItem)
|
|
this.groupSelection.setAllSelected(false)
|
|
this.props.reorderGroups(groups)
|
|
}
|
|
|
|
getSourcesDragDropEvents = (): IDragDropEvents => ({
|
|
canDrop: () => true,
|
|
canDrag: () => true,
|
|
onDrop: (item?: RSSSource) => {
|
|
if (this.sourcesDraggedItem) {
|
|
this.reorderSources(item)
|
|
}
|
|
},
|
|
onDragStart: (item?: RSSSource, itemIndex?: number) => {
|
|
this.sourcesDraggedItem = item
|
|
this.sourcesDraggedIndex = itemIndex!
|
|
},
|
|
onDragEnd: () => {
|
|
this.sourcesDraggedItem = undefined
|
|
this.sourcesDraggedIndex = -1
|
|
},
|
|
})
|
|
|
|
reorderSources = (item: RSSSource) => {
|
|
let draggedItems = this.sourcesSelection.isIndexSelected(this.sourcesDraggedIndex)
|
|
? (this.sourcesSelection.getSelection() as RSSSource[]).map(s => s.sid)
|
|
: [this.sourcesDraggedItem!.sid]
|
|
|
|
let insertIndex = this.state.selectedGroup.sids.indexOf(item.sid)
|
|
let items = this.state.selectedGroup.sids.filter(sid => !draggedItems.includes(sid))
|
|
|
|
items.splice(insertIndex, 0, ...draggedItems)
|
|
|
|
let group = { ...this.state.selectedGroup, sids: items }
|
|
this.props.updateGroup(group)
|
|
this.setState({ selectedGroup: group })
|
|
}
|
|
|
|
manageGroup = (g: SourceGroup) => {
|
|
if (g.isMultiple) {
|
|
this.setState({
|
|
selectedGroup: g,
|
|
editGroupName: g && g.isMultiple ? g.name : "",
|
|
manageGroup: true
|
|
})
|
|
}
|
|
}
|
|
|
|
dropdownOptions = () => this.props.groups.filter(g => g.isMultiple).map(g => ({
|
|
key: g.index,
|
|
text: g.name
|
|
}))
|
|
|
|
handleInputChange = (event) => {
|
|
const name: string = event.target.name
|
|
this.setState({[name]: event.target.value})
|
|
}
|
|
|
|
validateNewGroupName = (v: string) => {
|
|
const name = v.trim()
|
|
if (name.length == 0) {
|
|
return intl.get("emptyName")
|
|
}
|
|
for (let group of this.props.groups) {
|
|
if (group.isMultiple && group.name === name) {
|
|
return intl.get("groups.exist")
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
createGroup = (event: React.FormEvent) => {
|
|
event.preventDefault()
|
|
let trimmed = this.state.newGroupName.trim()
|
|
if (this.validateNewGroupName(trimmed) === "") this.props.createGroup(trimmed)
|
|
}
|
|
|
|
addToGroup = () => {
|
|
this.props.addToGroup(this.state.dropdownIndex, this.state.selectedGroup.sids[0])
|
|
}
|
|
|
|
removeFromGroup = () => {
|
|
this.props.removeFromGroup(this.state.selectedGroup.index, this.state.selectedSources.map(s => s.sid))
|
|
this.setState({ selectedSources: null })
|
|
}
|
|
|
|
deleteGroup = () => {
|
|
this.props.deleteGroup(this.state.selectedGroup.index)
|
|
this.groupSelection.setIndexSelected(this.state.selectedGroup.index, false, false)
|
|
this.setState({ selectedGroup: null })
|
|
}
|
|
|
|
updateGroupName = () => {
|
|
let group = this.state.selectedGroup
|
|
group = { ...group, name: this.state.editGroupName.trim() }
|
|
this.props.updateGroup(group)
|
|
}
|
|
|
|
dropdownChange = (_, item: IDropdownOption) => {
|
|
this.setState({ dropdownIndex: item ? Number(item.key) : null })
|
|
}
|
|
|
|
render = () => (
|
|
<div className="tab-body">
|
|
{this.props.serviceOn && (
|
|
<MessageBar messageBarType={MessageBarType.info}>{intl.get("service.groupsWarning")}</MessageBar>
|
|
)}
|
|
{this.state.manageGroup && this.state.selectedGroup &&
|
|
<>
|
|
<Stack horizontal horizontalAlign="space-between" style={{height: 40}}>
|
|
<CommandBarButton
|
|
text={intl.get("groups.exitGroup")}
|
|
iconProps={{iconName: "BackToWindow"}}
|
|
onClick={() => this.setState({manageGroup: false})} />
|
|
{this.state.selectedSources != null && <CommandBarButton
|
|
text={intl.get("groups.deleteSource")}
|
|
onClick={this.removeFromGroup}
|
|
iconProps={{iconName: "RemoveFromShoppingList", style: {color: "#d13438"}}} />}
|
|
</Stack>
|
|
|
|
<MarqueeSelection selection={this.sourcesSelection} isDraggingConstrainedToRoot={true}>
|
|
<DetailsList
|
|
compact={true}
|
|
items={this.state.selectedGroup.sids.map(sid => this.props.sources[sid])}
|
|
columns={this.sourceColumns}
|
|
dragDropEvents={this.sourcesDragDropEvents}
|
|
setKey="multiple"
|
|
selection={this.sourcesSelection}
|
|
selectionMode={SelectionMode.multiple} />
|
|
</MarqueeSelection>
|
|
|
|
<span className="settings-hint">{intl.get("groups.sourceHint")}</span>
|
|
</>}
|
|
{(!this.state.manageGroup || !this.state.selectedGroup)
|
|
?<>
|
|
<form onSubmit={this.createGroup}>
|
|
<Label htmlFor="newGroupName">{intl.get("groups.create")}</Label>
|
|
<Stack horizontal>
|
|
<Stack.Item grow>
|
|
<TextField
|
|
onGetErrorMessage={this.validateNewGroupName}
|
|
validateOnLoad={false}
|
|
placeholder={intl.get("groups.enterName")}
|
|
value={this.state.newGroupName}
|
|
id="newGroupName"
|
|
name="newGroupName"
|
|
onChange={this.handleInputChange} />
|
|
</Stack.Item>
|
|
<Stack.Item>
|
|
<PrimaryButton
|
|
disabled={this.validateNewGroupName(this.state.newGroupName) !== ""}
|
|
type="sumbit"
|
|
text={intl.get("create")} />
|
|
</Stack.Item>
|
|
</Stack>
|
|
</form>
|
|
|
|
<DetailsList
|
|
compact={true}
|
|
items={this.props.groups}
|
|
columns={this.groupColumns()}
|
|
setKey="selected"
|
|
onItemInvoked={this.manageGroup}
|
|
dragDropEvents={this.groupDragDropEvents}
|
|
selection={this.groupSelection}
|
|
selectionMode={SelectionMode.single} />
|
|
|
|
{this.state.selectedGroup
|
|
? ( this.state.selectedGroup.isMultiple
|
|
?<>
|
|
<Label>{intl.get("groups.selectedGroup")}</Label>
|
|
<Stack horizontal>
|
|
<Stack.Item grow>
|
|
<TextField
|
|
onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
|
|
validateOnLoad={false}
|
|
placeholder={intl.get("groups.enterName")}
|
|
value={this.state.editGroupName}
|
|
name="editGroupName"
|
|
onChange={this.handleInputChange} />
|
|
</Stack.Item>
|
|
<Stack.Item>
|
|
<DefaultButton
|
|
disabled={this.state.editGroupName.trim().length == 0}
|
|
onClick={this.updateGroupName}
|
|
text={intl.get("groups.editName")} />
|
|
</Stack.Item>
|
|
<Stack.Item>
|
|
<DangerButton
|
|
key={this.state.selectedGroup.index}
|
|
onClick={this.deleteGroup}
|
|
text={intl.get("groups.deleteGroup")} />
|
|
</Stack.Item>
|
|
</Stack>
|
|
</>
|
|
:<>
|
|
<Label>{intl.get("groups.selectedSource")}</Label>
|
|
<Stack horizontal>
|
|
<Stack.Item grow>
|
|
<Dropdown
|
|
placeholder={intl.get("groups.chooseGroup")}
|
|
selectedKey={this.state.dropdownIndex}
|
|
options={this.dropdownOptions()}
|
|
onChange={this.dropdownChange} />
|
|
</Stack.Item>
|
|
<Stack.Item>
|
|
<DefaultButton
|
|
disabled={this.state.dropdownIndex === null}
|
|
onClick={this.addToGroup}
|
|
text={intl.get("groups.addToGroup")} />
|
|
</Stack.Item>
|
|
</Stack>
|
|
</>
|
|
)
|
|
: <span className="settings-hint">{intl.get("groups.groupHint")}</span>
|
|
}
|
|
</> : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default GroupsTab |