split source groups into model

This commit is contained in:
刘浩远 2020-06-06 09:33:59 +08:00
parent 454cf48a45
commit 143a295024
11 changed files with 303 additions and 306 deletions

8
dist/styles.css vendored
View File

@ -1,6 +1,6 @@
html, body {
background-color: #faf9f8;
font-family: "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
font-family: "Segoe UI Regular", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
height: 100%;
overflow: hidden;
margin: 0;
@ -236,6 +236,10 @@ img.favicon {
overflow-y: auto;
margin-bottom: 16px;
}
.settings-hint {
font-size: 12px;
color: #605e5c;
}
.main {
height: calc(100% - 32px);
@ -409,7 +413,7 @@ img.favicon {
.card h3.title {
font-size: 16px;
line-height: 22px;
font-weight: 700;
font-weight: 600;
margin: 10px 12px;
position: relative;
-webkit-line-clamp: 3;

View File

@ -1,7 +1,7 @@
import * as React from "react"
import { Icon } from "@fluentui/react/lib/Icon"
import { Nav, INavLink, INavLinkGroup } from "office-ui-fabric-react/lib/Nav"
import { SourceGroup } from "../scripts/models/page"
import { SourceGroup } from "../scripts/models/group"
import { SourceState, RSSSource } from "../scripts/models/source"
import { ALL } from "../scripts/models/feed"
import { AnimationClassNames } from "@fluentui/react"

View File

@ -1,5 +1,5 @@
import * as React from "react"
import { SourceGroup } from "../../scripts/models/page"
import { SourceGroup } from "../../scripts/models/group"
import { SourceState, RSSSource } from "../../scripts/models/source"
import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack,
TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, CommandBarButton, MarqueeSelection, IDragDropEvents, IDragDropContext } from "@fluentui/react"
@ -261,7 +261,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
iconProps={{iconName: "RemoveFromShoppingList", style: {color: "#d13438"}}} />}
</Stack>
<MarqueeSelection selection={this.sourcesSelection}>
<MarqueeSelection selection={this.sourcesSelection} isDraggingConstrainedToRoot={true}>
<DetailsList
compact={true}
items={this.state.selectedGroup.sids.map(sid => this.props.sources[sid])}
@ -307,8 +307,8 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
selection={this.groupSelection}
selectionMode={SelectionMode.single} />
{this.state.selectedGroup && (
this.state.selectedGroup.isMultiple
{this.state.selectedGroup
? ( this.state.selectedGroup.isMultiple
?<>
<Label></Label>
<Stack horizontal>
@ -353,7 +353,9 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
</Stack.Item>
</Stack>
</>
)}
)
: <p className="settings-hint"></p>
}
</> : null}
</div>
)

View File

@ -3,14 +3,15 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { Menu } from "../components/menu"
import { toggleMenu } from "../scripts/models/app"
import { selectAllArticles, selectSources, SourceGroup } from "../scripts/models/page"
import { SourceGroup } from "../scripts/models/group"
import { selectAllArticles, selectSources } from "../scripts/models/page"
import { initFeeds } from "../scripts/models/feed"
import { RSSSource } from "../scripts/models/source"
const getStatus = (state: RootState) => state.app.menu && state.app.sourceInit
const getKey = (state: RootState) => state.app.menuKey
const getSources = (state: RootState) => state.sources
const getGroups = (state: RootState) => state.page.sourceGroups
const getGroups = (state: RootState) => state.groups
const mapStateToProps = createSelector(
[getStatus, getKey, getSources, getGroups],

View File

@ -4,10 +4,10 @@ import { createSelector } from "reselect"
import { RootState } from "../../scripts/reducer"
import GroupsTab from "../../components/settings/groups"
import { createSourceGroup, SourceGroup, updateSourceGroup, addSourceToGroup,
deleteSourceGroup, removeSourceFromGroup, reorderSourceGroups } from "../../scripts/models/page"
deleteSourceGroup, removeSourceFromGroup, reorderSourceGroups } from "../../scripts/models/group"
const getSources = (state: RootState) => state.sources
const getGroups = (state: RootState) => state.page.sourceGroups
const getGroups = (state: RootState) => state.groups
const mapStateToProps = createSelector(
[getSources, getGroups],

View File

@ -4,7 +4,7 @@ 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"
import { importOPML } from "../../scripts/models/group"
const getSources = (state: RootState) => state.sources

View File

@ -2,7 +2,8 @@ import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE,
import { RSSItem, ItemActionTypes, FETCH_ITEMS } from "./item"
import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils"
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles, SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./page"
import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./group"
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
export enum ContextMenuType {
Hidden, Item

271
src/scripts/models/group.ts Normal file
View File

@ -0,0 +1,271 @@
import fs = require("fs")
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource } from "./source"
import { ActionStatus, AppThunk, domParser, AppDispatch, getWindowBreakpoint } from "../utils"
import { saveSettings } from "./app"
const GROUPS_STORE_KEY = "sourceGroups"
export class SourceGroup {
isMultiple: boolean
sids: number[]
name?: string
index?: number // available only from groups tab container
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 = sids
}
static save(groups: SourceGroup[]) {
localStorage.setItem(GROUPS_STORE_KEY, JSON.stringify(groups))
}
static load(): SourceGroup[] {
let stored = localStorage.getItem(GROUPS_STORE_KEY)
return stored ? <SourceGroup[]>JSON.parse(stored) : []
}
}
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().groups
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().groups)
}
}
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().groups)
}
}
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().groups)
}
}
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().groups)
}
}
function reorderSourceGroupsDone(groups: SourceGroup[]): SourceGroupActionTypes {
return {
type: REORDER_SOURCE_GROUPS,
groups: groups
}
}
export function reorderSourceGroups(groups: SourceGroup[]): AppThunk {
return (dispatch, getState) => {
dispatch(reorderSourceGroupsDone(groups))
SourceGroup.save(getState().groups)
}
}
async function outlineToSource(dispatch: AppDispatch, outline: Element): Promise<number> {
let url = outline.getAttribute("xmlUrl")
let name = outline.getAttribute("text") || outline.getAttribute("name")
if (url) {
try {
return await dispatch(addSource(url.trim(), name, true))
} catch (e) {
return 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 = 0, failures: number = 0
let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body")
if (doc.length == 0) {
dispatch(saveSettings())
return
}
for (let el of doc[0].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))
for (let child of el.children) {
let sid = await outlineToSource(dispatch, child)
if (sid === null) {
failures += 1
} else {
successes += 1
dispatch(addSourceToGroup(gid, sid))
}
}
}
}
console.log(failures, successes)
dispatch(saveSettings())
}
})
}
}
export type GroupState = SourceGroup[]
export function groupReducer(
state = SourceGroup.load(),
action: SourceActionTypes | SourceGroupActionTypes
): GroupState {
switch(action.type) {
case ADD_SOURCE:
switch (action.status) {
case ActionStatus.Success: return [
...state,
new SourceGroup([action.source.sid])
]
default: return state
}
case DELETE_SOURCE: return [
...state.map(group => ({
...group,
sids: group.sids.filter(sid => sid != action.source.sid)
})).filter(g => g.isMultiple || g.sids.length == 1)
]
case CREATE_SOURCE_GROUP: return [ ...state, action.group ]
case ADD_SOURCE_TO_GROUP: return state.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.slice(0, action.groupIndex),
{
...state[action.groupIndex],
sids: state[action.groupIndex].sids.filter(sid => !action.sids.includes(sid))
},
...action.sids.map(sid => new SourceGroup([sid])),
...state.slice(action.groupIndex + 1)
]
case UPDATE_SOURCE_GROUP: return [
...state.slice(0, action.groupIndex),
action.group,
...state.slice(action.groupIndex + 1)
]
case REORDER_SOURCE_GROUPS: return action.groups
case DELETE_SOURCE_GROUP: return [
...state.slice(0, action.groupIndex),
...state[action.groupIndex].sids.map(sid => new SourceGroup([sid])),
...state.slice(action.groupIndex + 1)
]
default: return state
}
}

View File

@ -1,37 +1,5 @@
import fs = require("fs")
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource } from "./source"
import { ALL, SOURCE } from "./feed"
import { ActionStatus, AppThunk, domParser, AppDispatch, getWindowBreakpoint } from "../utils"
import { saveSettings } from "./app"
const GROUPS_STORE_KEY = "sourceGroups"
export class SourceGroup {
isMultiple: boolean
sids: number[]
name?: string
index?: number // available only from groups tab container
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 = sids
}
static save(groups: SourceGroup[]) {
localStorage.setItem(GROUPS_STORE_KEY, JSON.stringify(groups))
}
static load(): SourceGroup[] {
let stored = localStorage.getItem(GROUPS_STORE_KEY)
return stored ? <SourceGroup[]>JSON.parse(stored) : []
}
}
import { ALL, SOURCE, FeedIdType } from "./feed"
import { getWindowBreakpoint } from "../utils"
export const SELECT_PAGE = "SELECT_PAGE"
@ -72,223 +40,15 @@ 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)
}
}
function reorderSourceGroupsDone(groups: SourceGroup[]): SourceGroupActionTypes {
return {
type: REORDER_SOURCE_GROUPS,
groups: groups
}
}
export function reorderSourceGroups(groups: SourceGroup[]): AppThunk {
return (dispatch, getState) => {
dispatch(reorderSourceGroupsDone(groups))
SourceGroup.save(getState().page.sourceGroups)
}
}
async function outlineToSource(dispatch: AppDispatch, outline: Element): Promise<number> {
let url = outline.getAttribute("xmlUrl")
let name = outline.getAttribute("text") || outline.getAttribute("name")
if (url) {
try {
return await dispatch(addSource(url.trim(), name, true))
} catch (e) {
return 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 = 0, failures: number = 0
let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body")
if (doc.length == 0) {
dispatch(saveSettings())
return
}
for (let el of doc[0].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))
for (let child of el.children) {
let sid = await outlineToSource(dispatch, child)
if (sid === null) {
failures += 1
} else {
successes += 1
dispatch(addSourceToGroup(gid, sid))
}
}
}
}
console.log(failures, successes)
dispatch(saveSettings())
}
})
}
}
export class PageState {
feedId = ALL
sourceGroups = SourceGroup.load()
}
export function pageReducer(
state = new PageState(),
action: PageActionTypes | SourceActionTypes | SourceGroupActionTypes
action: PageActionTypes
): PageState {
switch(action.type) {
case ADD_SOURCE:
switch (action.status) {
case ActionStatus.Success: return {
...state,
sourceGroups: [
...state.sourceGroups,
new SourceGroup([action.source.sid])
]
}
default: return state
}
case DELETE_SOURCE: return {
...state,
sourceGroups: [
...state.sourceGroups.map(group => ({
...group,
sids: group.sids.filter(sid => sid != action.source.sid)
})).filter(g => g.isMultiple || g.sids.length == 1)
]
}
switch (action.type) {
case SELECT_PAGE:
switch (action.pageType) {
case PageType.AllArticles: return {
@ -301,49 +61,6 @@ export function pageReducer(
}
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 REORDER_SOURCE_GROUPS: return {
...state,
sourceGroups: action.groups
}
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
}
}

View File

@ -1,9 +1,8 @@
import Parser = require("@yang991178/rss-parser")
import * as db from "../db"
import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils"
import { RSSItem, fetchItemsSuccess, insertItems } from "./item"
import { SourceGroup } from "./page"
import { initFeeds } from "./feed"
import { RSSItem, insertItems } from "./item"
import { SourceGroup } from "./group"
import { saveSettings } from "./app"
export class RSSSource {
@ -199,7 +198,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
.then(items => insertItems(items))
.then(items => {
//dispatch(fetchItemsSuccess(items))
SourceGroup.save(getState().page.sourceGroups)
SourceGroup.save(getState().groups)
resolve(source.sid)
})
}
@ -254,7 +253,7 @@ export function deleteSource(source: RSSSource): AppThunk {
dispatch(saveSettings())
} else {
dispatch(deleteSourceDone(source))
SourceGroup.save(getState().page.sourceGroups)
SourceGroup.save(getState().groups)
dispatch(saveSettings())
}
})

View File

@ -4,12 +4,14 @@ import { sourceReducer } from "./models/source"
import { itemReducer } from "./models/item"
import { feedReducer } from "./models/feed"
import { appReducer } from "./models/app"
import { groupReducer } from "./models/group"
import { pageReducer } from "./models/page"
export const rootReducer = combineReducers({
sources: sourceReducer,
items: itemReducer,
feeds: feedReducer,
groups: groupReducer,
page: pageReducer,
app: appReducer
})