mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-26 08:27:38 +01:00
source grouping settings
This commit is contained in:
parent
600f21b8aa
commit
22f528d3e0
BIN
dist/icons/fabric-icons-16-9cf93f3b.woff
vendored
Normal file
BIN
dist/icons/fabric-icons-16-9cf93f3b.woff
vendored
Normal file
Binary file not shown.
@ -4,8 +4,8 @@ import { AnimationClassNames } from "@fluentui/react/lib/Styling"
|
||||
import { SettingsReduxProps } from "../containers/settings-container"
|
||||
import AboutTab from "./settings/about"
|
||||
import { Pivot, PivotItem, Spinner } from "@fluentui/react"
|
||||
import { SourcesTabContainer } from "../containers/settings/sources-container"
|
||||
import GroupsTab from "./settings/groups"
|
||||
import SourcesTabContainer from "../containers/settings/sources-container"
|
||||
import GroupsTabContainer from "../containers/settings/groups-container"
|
||||
import ProxyTab from "./settings/proxy"
|
||||
|
||||
type SettingsProps = SettingsReduxProps & {
|
||||
@ -36,7 +36,7 @@ class Settings extends React.Component<SettingsProps> {
|
||||
<SourcesTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText="分组与排序" itemIcon="GroupList">
|
||||
<GroupsTab />
|
||||
<GroupsTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText="代理" itemIcon="Globe">
|
||||
<ProxyTab />
|
||||
|
@ -1,9 +1,279 @@
|
||||
import * as React from "react"
|
||||
import { SourceGroup } from "../../scripts/models/page"
|
||||
import { SourceState, RSSSource } from "../../scripts/models/source"
|
||||
import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack,
|
||||
TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, CommandBarButton } from "@fluentui/react"
|
||||
import DangerButton from "../utils/danger-button"
|
||||
|
||||
type GroupsTabProps = {
|
||||
sources: SourceState,
|
||||
groups: SourceGroup[],
|
||||
createGroup: (name: string) => void,
|
||||
updateGroup: (group: SourceGroup) => void,
|
||||
addToGroup: (groupIndex: number, sid: number) => void,
|
||||
deleteGroup: (groupIndex: number) => void,
|
||||
removeFromGroup: (groupIndex: number, sids: number[]) => void
|
||||
}
|
||||
|
||||
type GroupsTabState = {
|
||||
[formName: string]: any,
|
||||
selectedGroup: SourceGroup,
|
||||
selectedSources: RSSSource[],
|
||||
dropdownIndex: number,
|
||||
manageGroup: boolean
|
||||
}
|
||||
|
||||
class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
||||
groupSelection: Selection
|
||||
sourcesSelection: Selection
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
editGroupName: "",
|
||||
newGroupName: "",
|
||||
selectedGroup: null,
|
||||
selectedSources: null,
|
||||
dropdownIndex: null,
|
||||
manageGroup: false
|
||||
}
|
||||
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: "类型",
|
||||
minWidth: 40,
|
||||
maxWidth: 40,
|
||||
data: "string",
|
||||
onRender: (g: SourceGroup) => <>
|
||||
{g.isMultiple ? "分组" : "订阅源"}
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: "capacity",
|
||||
name: "容量",
|
||||
minWidth: 40,
|
||||
maxWidth: 40,
|
||||
data: "string",
|
||||
onRender: (g: SourceGroup) => <>
|
||||
{g.isMultiple ? g.sids.length : ""}
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
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: "图标",
|
||||
fieldName: "name",
|
||||
isIconOnly: true,
|
||||
iconName: "ImagePixel",
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
onRender: (s: RSSSource) => s.iconurl && (
|
||||
<img src={s.iconurl} className="favicon" />
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
name: "名称",
|
||||
fieldName: "name",
|
||||
minWidth: 200,
|
||||
data: 'string',
|
||||
isRowHeader: true
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
name: "URL",
|
||||
fieldName: "url",
|
||||
minWidth: 280,
|
||||
data: 'string'
|
||||
}
|
||||
]
|
||||
|
||||
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.trim()})
|
||||
}
|
||||
|
||||
createGroup = () => {
|
||||
this.props.createGroup(this.state.newGroupName)
|
||||
}
|
||||
|
||||
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 }
|
||||
this.props.updateGroup(group)
|
||||
}
|
||||
|
||||
dropdownChange = (_, item: IDropdownOption) => {
|
||||
this.setState({ dropdownIndex: item ? Number(item.key) : null })
|
||||
}
|
||||
|
||||
class GroupsTab extends React.Component {
|
||||
render = () => (
|
||||
<div className="tab-body">
|
||||
<p>Groups</p>
|
||||
{this.state.manageGroup
|
||||
?<>
|
||||
<Stack horizontal horizontalAlign="space-between" style={{height: 44}}>
|
||||
<CommandBarButton
|
||||
text="退出分组"
|
||||
iconProps={{iconName: "BackToWindow"}}
|
||||
onClick={() => this.setState({manageGroup: false})} />
|
||||
{this.state.selectedSources != null && <CommandBarButton
|
||||
text="从分组删除订阅源"
|
||||
onClick={this.removeFromGroup}
|
||||
iconProps={{iconName: "RemoveFromShoppingList", style: {color: "#d13438"}}} />}
|
||||
</Stack>
|
||||
|
||||
<DetailsList
|
||||
compact={true}
|
||||
items={this.state.selectedGroup.sids.map(sid => this.props.sources[sid])}
|
||||
columns={this.sourceColumns}
|
||||
setKey="multiple"
|
||||
selection={this.sourcesSelection}
|
||||
selectionMode={SelectionMode.multiple} />
|
||||
|
||||
</>
|
||||
:<>
|
||||
<Label>新建分组</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => v.trim().length == 0 ? "名称不得为空" : ""}
|
||||
validateOnLoad={false}
|
||||
placeholder="输入名称"
|
||||
value={this.state.newGroupName}
|
||||
name="newGroupName"
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={this.state.newGroupName.length == 0}
|
||||
onClick={this.createGroup}
|
||||
text="新建" />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
<DetailsList
|
||||
compact={true}
|
||||
items={Object.values(this.props.groups)}
|
||||
columns={this.groupColumns}
|
||||
setKey="selected"
|
||||
onItemInvoked={this.manageGroup}
|
||||
selection={this.groupSelection}
|
||||
selectionMode={SelectionMode.single} />
|
||||
|
||||
{this.state.selectedGroup && (
|
||||
this.state.selectedGroup.isMultiple
|
||||
?<>
|
||||
<Label>选中分组</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => v.trim().length == 0 ? "名称不得为空" : ""}
|
||||
validateOnLoad={false}
|
||||
placeholder="分组名称"
|
||||
value={this.state.editGroupName}
|
||||
name="editGroupName"
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={this.state.editGroupName.length == 0}
|
||||
onClick={this.updateGroupName}
|
||||
text="修改名称" />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
key={this.state.selectedGroup.index}
|
||||
onClick={this.deleteGroup}
|
||||
text={`删除分组`} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
:<>
|
||||
<Label>选中订阅源</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
placeholder="选择分组"
|
||||
selectedKey={this.state.dropdownIndex}
|
||||
options={this.dropdownOptions()}
|
||||
onChange={this.dropdownChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={this.state.dropdownIndex === null}
|
||||
onClick={this.addToGroup}
|
||||
text="添加至分组" />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</>}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import * as React from "react"
|
||||
import { Label, DefaultButton, TextField, Stack, PrimaryButton, DetailsList,
|
||||
IColumn, SelectionMode, Selection } from "@fluentui/react"
|
||||
import { SourcesTabReduxProps } from "../../containers/settings/sources-container"
|
||||
import { SourceState, RSSSource } from "../../scripts/models/source"
|
||||
import { urlTest } from "../../scripts/utils"
|
||||
import DangerButton from "../utils/danger-button"
|
||||
|
||||
type SourcesTabProps = SourcesTabReduxProps & {
|
||||
type SourcesTabProps = {
|
||||
sources: SourceState,
|
||||
addSource: (url: string) => void,
|
||||
updateSourceName: (source: RSSSource, name: string) => void,
|
||||
deleteSource: (source: RSSSource) => void
|
||||
deleteSource: (source: RSSSource) => void,
|
||||
importOPML: () => void
|
||||
}
|
||||
|
||||
type SourcesTabState = {
|
||||
@ -81,7 +81,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
||||
<Label>OPML文件</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<PrimaryButton text="导入文件" />
|
||||
<PrimaryButton onClick={this.props.importOPML} text="导入文件" />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton text="导出文件" />
|
||||
@ -115,7 +115,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
||||
selection={this.selection}
|
||||
selectionMode={SelectionMode.single} />
|
||||
|
||||
{this.state.selectedSource && (<>
|
||||
{this.state.selectedSource && <>
|
||||
<Label>选中订阅源</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
@ -140,7 +140,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
||||
text={`删除订阅源`} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>)}
|
||||
</>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
29
src/containers/settings/groups-container.tsx
Normal file
29
src/containers/settings/groups-container.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { remote } from "electron"
|
||||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../../scripts/reducer"
|
||||
import GroupsTab from "../../components/settings/groups"
|
||||
import { createSourceGroup, SourceGroup, updateSourceGroup, addSourceToGroup, deleteSourceGroup, removeSourceFromGroup } from "../../scripts/models/page"
|
||||
|
||||
const getSources = (state: RootState) => state.sources
|
||||
const getGroups = (state: RootState) => state.page.sourceGroups
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getSources, getGroups],
|
||||
(sources, groups) => ({
|
||||
sources: sources,
|
||||
groups: groups.map((g, i) => ({ ...g, index: i })),
|
||||
key: groups.length
|
||||
})
|
||||
)
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
createGroup: (name: string) => dispatch(createSourceGroup(name)),
|
||||
updateGroup: (group: SourceGroup) => dispatch(updateSourceGroup(group)),
|
||||
addToGroup: (groupIndex: number, sid: number) => dispatch(addSourceToGroup(groupIndex, sid)),
|
||||
deleteGroup: (groupIndex: number) => dispatch(deleteSourceGroup(groupIndex)),
|
||||
removeFromGroup: (groupIndex: number, sids: number[]) => dispatch(removeSourceFromGroup(groupIndex, sids))
|
||||
})
|
||||
|
||||
const GroupsTabContainer = connect(mapStateToProps, mapDispatchToProps)(GroupsTab)
|
||||
export default GroupsTabContainer
|
@ -1,8 +1,10 @@
|
||||
import { remote } from "electron"
|
||||
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 } from "../../scripts/models/source"
|
||||
import { importOPML } from "../../scripts/models/page"
|
||||
|
||||
const getSources = (state: RootState) => state.sources
|
||||
|
||||
@ -19,10 +21,19 @@ const mapDispatchToProps = dispatch => {
|
||||
updateSourceName: (source: RSSSource, name: string) => {
|
||||
dispatch(updateSource({ ...source, name: name } as RSSSource))
|
||||
},
|
||||
deleteSource: (source: RSSSource) => dispatch(deleteSource(source))
|
||||
deleteSource: (source: RSSSource) => dispatch(deleteSource(source)),
|
||||
importOPML: () => {
|
||||
let path = remote.dialog.showOpenDialogSync(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
filters: [{ name: "OPML文件", extensions: ["xml", "opml"] }],
|
||||
properties: ["openFile"]
|
||||
}
|
||||
)
|
||||
if (path.length > 0) dispatch(importOPML(path[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps)
|
||||
export type SourcesTabReduxProps = typeof connector
|
||||
export const SourcesTabContainer = connector(SourcesTab)
|
||||
const SourcesTabContainer = connect(mapStateToProps, mapDispatchToProps)(SourcesTab)
|
||||
export default SourcesTabContainer
|
@ -10,13 +10,14 @@ import { initSources, addSource } from "./scripts/models/source"
|
||||
import { fetchItems } from "./scripts/models/item"
|
||||
import Root from "./components/root"
|
||||
import { initFeeds } from "./scripts/models/feed"
|
||||
import { AppDispatch } from "./scripts/utils"
|
||||
|
||||
loadTheme({ defaultFontStyle: { fontFamily: '"Source Han Sans", sans-serif' } })
|
||||
initializeIcons("icons/")
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware<ThunkDispatch<RootState, undefined, AnyAction>, RootState>(thunkMiddleware)
|
||||
applyMiddleware<AppDispatch, RootState>(thunkMiddleware)
|
||||
)
|
||||
|
||||
store.dispatch(initSources()).then(() => store.dispatch(initFeeds())).then(() => store.dispatch(fetchItems()))
|
||||
|
@ -2,7 +2,7 @@ import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE,
|
||||
import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item"
|
||||
import { ActionStatus, AppThunk } from "../utils"
|
||||
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
|
||||
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
|
||||
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles, SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./page"
|
||||
|
||||
export enum ContextMenuType {
|
||||
Hidden, Item
|
||||
@ -130,7 +130,7 @@ export function exitSettings(): AppThunk {
|
||||
export function appReducer(
|
||||
state = new AppState(),
|
||||
action: SourceActionTypes | ItemActionTypes | ContextMenuActionTypes | SettingsActionTypes
|
||||
| MenuActionTypes | LogMenuActionType | FeedActionTypes | PageActionTypes
|
||||
| MenuActionTypes | LogMenuActionType | FeedActionTypes | PageActionTypes | SourceGroupActionTypes
|
||||
): AppState {
|
||||
switch (action.type) {
|
||||
case INIT_SOURCES:
|
||||
@ -162,7 +162,11 @@ export function appReducer(
|
||||
}
|
||||
}
|
||||
case UPDATE_SOURCE:
|
||||
case DELETE_SOURCE: return {
|
||||
case DELETE_SOURCE:
|
||||
case UPDATE_SOURCE_GROUP:
|
||||
case ADD_SOURCE_TO_GROUP:
|
||||
case REMOVE_SOURCE_FROM_GROUP:
|
||||
case DELETE_SOURCE_GROUP: return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { RSSSource, SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source"
|
||||
import { RSSSource, SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE, addSource } from "./source"
|
||||
import { ALL, SOURCE } from "./feed"
|
||||
import { ActionStatus } from "../utils"
|
||||
import { ActionStatus, AppThunk, domParser, AppDispatch } from "../utils"
|
||||
import fs = require("fs")
|
||||
import { saveSettings } from "./app"
|
||||
|
||||
const GROUPS_STORE_KEY = "sourceGroups"
|
||||
|
||||
@ -8,15 +10,17 @@ export class SourceGroup {
|
||||
isMultiple: boolean
|
||||
sids: number[]
|
||||
name?: string
|
||||
index?: number // available only from groups tab container
|
||||
|
||||
constructor(sources: RSSSource[], name: string = "订阅源组") {
|
||||
if (sources.length == 1) {
|
||||
constructor(sids: number[], name: string = null) {
|
||||
name = (name && name.trim()) || "订阅源组"
|
||||
if (sids.length == 1) {
|
||||
this.isMultiple = false
|
||||
} else {
|
||||
this.isMultiple = true
|
||||
this.name = name
|
||||
}
|
||||
this.sids = sources.map(s => s.sid)
|
||||
this.sids = sids
|
||||
}
|
||||
|
||||
static save(groups: SourceGroup[]) {
|
||||
@ -24,7 +28,8 @@ export class SourceGroup {
|
||||
}
|
||||
|
||||
static load(): SourceGroup[] {
|
||||
return <SourceGroup[]>JSON.parse(localStorage.getItem(GROUPS_STORE_KEY))
|
||||
let stored = localStorage.getItem(GROUPS_STORE_KEY)
|
||||
return stored ? <SourceGroup[]>JSON.parse(stored) : []
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,15 +69,175 @@ export function selectSources(sids: number[], menuKey: string, title: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const CREATE_SOURCE_GROUP = "CREATE_SOURCE_GROUP"
|
||||
export const ADD_SOURCE_TO_GROUP = "ADD_SOURCE_TO_GROUP"
|
||||
export const REMOVE_SOURCE_FROM_GROUP = "REMOVE_SOURCE_FROM_GROUP"
|
||||
export const UPDATE_SOURCE_GROUP = "UPDATE_SOURCE_GROUP"
|
||||
export const REORDER_SOURCE_GROUPS = "REORDER_SOURCE_GROUPS"
|
||||
export const DELETE_SOURCE_GROUP = "DELETE_SOURCE_GROUP"
|
||||
|
||||
interface CreateSourceGroupAction {
|
||||
type: typeof CREATE_SOURCE_GROUP,
|
||||
group: SourceGroup
|
||||
}
|
||||
|
||||
interface AddSourceToGroupAction {
|
||||
type: typeof ADD_SOURCE_TO_GROUP,
|
||||
groupIndex: number,
|
||||
sid: number
|
||||
}
|
||||
|
||||
interface RemoveSourceFromGroupAction {
|
||||
type: typeof REMOVE_SOURCE_FROM_GROUP,
|
||||
groupIndex: number,
|
||||
sids: number[]
|
||||
}
|
||||
|
||||
interface UpdateSourceGroupAction {
|
||||
type: typeof UPDATE_SOURCE_GROUP,
|
||||
groupIndex: number,
|
||||
group: SourceGroup
|
||||
}
|
||||
|
||||
interface ReorderSourceGroupsAction {
|
||||
type: typeof REORDER_SOURCE_GROUPS,
|
||||
groups: SourceGroup[]
|
||||
}
|
||||
|
||||
interface DeleteSourceGroupAction {
|
||||
type: typeof DELETE_SOURCE_GROUP,
|
||||
groupIndex: number
|
||||
}
|
||||
|
||||
export type SourceGroupActionTypes = CreateSourceGroupAction | AddSourceToGroupAction
|
||||
| RemoveSourceFromGroupAction | UpdateSourceGroupAction | ReorderSourceGroupsAction
|
||||
| DeleteSourceGroupAction
|
||||
|
||||
export function createSourceGroupDone(group: SourceGroup): SourceGroupActionTypes {
|
||||
return {
|
||||
type: CREATE_SOURCE_GROUP,
|
||||
group: group
|
||||
}
|
||||
}
|
||||
|
||||
export function createSourceGroup(name: string): AppThunk<number> {
|
||||
return (dispatch, getState) => {
|
||||
let group = new SourceGroup([], name)
|
||||
dispatch(createSourceGroupDone(group))
|
||||
let groups = getState().page.sourceGroups
|
||||
SourceGroup.save(groups)
|
||||
return groups.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
function addSourceToGroupDone(groupIndex: number, sid: number): SourceGroupActionTypes {
|
||||
return {
|
||||
type: ADD_SOURCE_TO_GROUP,
|
||||
groupIndex: groupIndex,
|
||||
sid: sid
|
||||
}
|
||||
}
|
||||
|
||||
export function addSourceToGroup(groupIndex: number, sid: number): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(addSourceToGroupDone(groupIndex, sid))
|
||||
SourceGroup.save(getState().page.sourceGroups)
|
||||
}
|
||||
}
|
||||
|
||||
function removeSourceFromGroupDone(groupIndex: number, sids: number[]): SourceGroupActionTypes {
|
||||
return {
|
||||
type: REMOVE_SOURCE_FROM_GROUP,
|
||||
groupIndex: groupIndex,
|
||||
sids: sids
|
||||
}
|
||||
}
|
||||
|
||||
export function removeSourceFromGroup(groupIndex: number, sids: number[]): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(removeSourceFromGroupDone(groupIndex, sids))
|
||||
SourceGroup.save(getState().page.sourceGroups)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSourceGroupDone(groupIndex: number): SourceGroupActionTypes {
|
||||
return {
|
||||
type: DELETE_SOURCE_GROUP,
|
||||
groupIndex: groupIndex
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteSourceGroup(groupIndex: number): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(deleteSourceGroupDone(groupIndex))
|
||||
SourceGroup.save(getState().page.sourceGroups)
|
||||
}
|
||||
}
|
||||
|
||||
function updateSourceGroupDone(group: SourceGroup): SourceGroupActionTypes {
|
||||
return {
|
||||
type: UPDATE_SOURCE_GROUP,
|
||||
groupIndex: group.index,
|
||||
group: group
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSourceGroup(group: SourceGroup): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(updateSourceGroupDone(group))
|
||||
SourceGroup.save(getState().page.sourceGroups)
|
||||
}
|
||||
}
|
||||
|
||||
async function outlineToSource(dispatch: AppDispatch, outline: Element): Promise<number> {
|
||||
let url = outline.getAttribute("xmlUrl").trim()
|
||||
let name = outline.getAttribute("text") || outline.getAttribute("name")
|
||||
if (url) {
|
||||
let sid = await dispatch(addSource(url, name))
|
||||
return sid || null
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function importOPML(path: string): AppThunk {
|
||||
return async (dispatch) => {
|
||||
fs.readFile(path, "utf-8", async (err, data) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
} else {
|
||||
dispatch(saveSettings())
|
||||
let successes: number, failures: number
|
||||
let doc = domParser.parseFromString(data, "text/xml")
|
||||
for (let el of doc.body.children) {
|
||||
if (el.getAttribute("type") === "rss") {
|
||||
let sid = await outlineToSource(dispatch, el)
|
||||
if (sid === null) failures += 1
|
||||
else successes += 1
|
||||
} else if (el.hasAttribute("text") || el.hasAttribute("title")) {
|
||||
let groupName = el.getAttribute("text") || el.getAttribute("title")
|
||||
let gid = dispatch(createSourceGroup(groupName))
|
||||
let sid = await outlineToSource(dispatch, el)
|
||||
if (sid === null) failures += 1
|
||||
else {
|
||||
successes += 1
|
||||
dispatch(addSourceToGroup(gid, sid))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class PageState {
|
||||
feedId = ALL
|
||||
sourceGroups = SourceGroup.load()
|
||||
}
|
||||
|
||||
|
||||
export function pageReducer(
|
||||
state = new PageState(),
|
||||
action: PageActionTypes | SourceActionTypes
|
||||
action: PageActionTypes | SourceActionTypes | SourceGroupActionTypes
|
||||
): PageState {
|
||||
switch(action.type) {
|
||||
case ADD_SOURCE:
|
||||
@ -81,7 +246,7 @@ export function pageReducer(
|
||||
...state,
|
||||
sourceGroups: [
|
||||
...state.sourceGroups,
|
||||
new SourceGroup([action.source])
|
||||
new SourceGroup([action.source.sid])
|
||||
]
|
||||
}
|
||||
default: return state
|
||||
@ -105,7 +270,47 @@ export function pageReducer(
|
||||
...state,
|
||||
feedId: SOURCE
|
||||
}
|
||||
default: return state
|
||||
}
|
||||
case CREATE_SOURCE_GROUP: return {
|
||||
...state,
|
||||
sourceGroups: [ ...state.sourceGroups, action.group ]
|
||||
}
|
||||
case ADD_SOURCE_TO_GROUP: return {
|
||||
...state,
|
||||
sourceGroups: state.sourceGroups.map((g, i) => i == action.groupIndex ? ({
|
||||
...g,
|
||||
sids: [ ...g.sids, action.sid ]
|
||||
}) : g).filter(g => g.isMultiple || !g.sids.includes(action.sid) )
|
||||
}
|
||||
case REMOVE_SOURCE_FROM_GROUP: return {
|
||||
...state,
|
||||
sourceGroups: [
|
||||
...state.sourceGroups.slice(0, action.groupIndex),
|
||||
{
|
||||
...state.sourceGroups[action.groupIndex],
|
||||
sids: state.sourceGroups[action.groupIndex].sids.filter(sid => !action.sids.includes(sid))
|
||||
},
|
||||
...action.sids.map(sid => new SourceGroup([sid])),
|
||||
...state.sourceGroups.slice(action.groupIndex + 1)
|
||||
]
|
||||
}
|
||||
case UPDATE_SOURCE_GROUP: return {
|
||||
...state,
|
||||
sourceGroups: [
|
||||
...state.sourceGroups.slice(0, action.groupIndex),
|
||||
action.group,
|
||||
...state.sourceGroups.slice(action.groupIndex + 1)
|
||||
]
|
||||
}
|
||||
case DELETE_SOURCE_GROUP: return {
|
||||
...state,
|
||||
sourceGroups: [
|
||||
...state.sourceGroups.slice(0, action.groupIndex),
|
||||
...state.sourceGroups[action.groupIndex].sids.map(sid => new SourceGroup([sid])),
|
||||
...state.sourceGroups.slice(action.groupIndex + 1)
|
||||
]
|
||||
}
|
||||
default: return state
|
||||
}
|
||||
}
|
@ -14,14 +14,15 @@ export class RSSSource {
|
||||
description: string
|
||||
useProxy: boolean
|
||||
|
||||
constructor(url: string, useProxy=false) {
|
||||
constructor(url: string, name: string = null) {
|
||||
this.url = url
|
||||
this.useProxy = useProxy
|
||||
this.name = name
|
||||
this.useProxy = false
|
||||
}
|
||||
|
||||
async fetchMetaData(parser: Parser) {
|
||||
let feed = await parser.parseURL(this.url)
|
||||
this.name = feed.title.trim()
|
||||
if (!this.name) this.name = feed.title.trim()
|
||||
this.description = feed.description
|
||||
let domain = this.url.split("/").slice(0, 3).join("/")
|
||||
let f = await faviconPromise(domain)
|
||||
@ -170,17 +171,17 @@ export function addSourceFailure(err): SourceActionTypes {
|
||||
}
|
||||
}
|
||||
|
||||
export function addSource(url: string): AppThunk<Promise<void>> {
|
||||
export function addSource(url: string, name: string = null): AppThunk<Promise<void|number>> {
|
||||
return (dispatch, getState) => {
|
||||
let app = getState().app
|
||||
if (app.sourceInit && !app.fetchingItems) {
|
||||
dispatch(addSourceRequest())
|
||||
let source = new RSSSource(url)
|
||||
let source = new RSSSource(url, name)
|
||||
return source.fetchMetaData(rssParser)
|
||||
.then(feed => {
|
||||
let sids = Object.values(getState().sources).map(s => s.sid)
|
||||
source.sid = Math.max(...sids, -1) + 1
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
db.sdb.insert(source, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
@ -191,7 +192,7 @@ export function addSource(url: string): AppThunk<Promise<void>> {
|
||||
.then(items => {
|
||||
//dispatch(fetchItemsSuccess(items))
|
||||
SourceGroup.save(getState().page.sourceGroups)
|
||||
resolve()
|
||||
resolve(source.sid)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { shell } from "electron"
|
||||
import { ThunkAction } from "redux-thunk"
|
||||
import { ThunkAction, ThunkDispatch } from "redux-thunk"
|
||||
import { AnyAction } from "redux"
|
||||
import { RootState } from "./reducer"
|
||||
|
||||
@ -14,6 +14,8 @@ export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
AnyAction
|
||||
>
|
||||
|
||||
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
||||
|
||||
import Parser = require("rss-parser")
|
||||
const customFields = {
|
||||
item: ["thumb", "image", ["content:encoded", "fullContent"]] as Parser.CustomFieldItem[]
|
||||
|
Loading…
x
Reference in New Issue
Block a user