mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-22 06:07:31 +02:00
list view
This commit is contained in:
parent
165597a454
commit
53a280cd72
4
dist/article/article.css
vendored
4
dist/article/article.css
vendored
@ -17,6 +17,10 @@ a:hover, a:active {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
#main > p.title {
|
#main > p.title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
|
BIN
dist/icons/fabric-icons-15-3807251b.woff
vendored
Normal file
BIN
dist/icons/fabric-icons-15-3807251b.woff
vendored
Normal file
Binary file not shown.
119
dist/styles.css
vendored
119
dist/styles.css
vendored
@ -59,6 +59,10 @@ i.ms-Nav-chevron {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
#root > nav .btn, #root > nav span {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
nav .progress {
|
nav .progress {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -113,7 +117,7 @@ nav .progress {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
.btn-group .btn.system {
|
#root > nav .btn-group .btn.system {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
@ -301,11 +305,11 @@ img.favicon {
|
|||||||
height: 120%;
|
height: 120%;
|
||||||
box-shadow: inset 5px 0 20px #0004;
|
box-shadow: inset 5px 0 20px #0004;
|
||||||
}
|
}
|
||||||
.main.menu-on {
|
.main.menu-on, .list-main.menu-on {
|
||||||
padding-left: 280px;
|
padding-left: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.menu-on .btn-group .btn.hide-wide, .menu .btn-group .btn.hide-wide {
|
nav.hide-btns .btn-group .btn, nav.menu-on .btn-group .btn.hide-wide, .menu .btn-group .btn.hide-wide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.btn-group .btn.inline-block-wide {
|
.btn-group .btn.inline-block-wide {
|
||||||
@ -335,9 +339,12 @@ img.favicon {
|
|||||||
.article-container .btn-group.next {
|
.article-container .btn-group.next {
|
||||||
right: calc(50% - 486px);
|
right: calc(50% - 486px);
|
||||||
}
|
}
|
||||||
|
.article {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
.article webview {
|
.article webview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 86px);
|
height: calc(100% - 36px);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.article i.ms-Icon {
|
.article i.ms-Icon {
|
||||||
@ -358,6 +365,63 @@ img.favicon {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.side-article-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-top: 32px;
|
||||||
|
height: calc(100% - 32px);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.side-article-wrapper .article {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
.side-article-wrapper .article .actions {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.side-article-wrapper .article > .ms-Stack {
|
||||||
|
border-top: 1px solid #e1dfdd;
|
||||||
|
}
|
||||||
|
.list-feed-container::before, .side-article-wrapper::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid #e1dfdd;
|
||||||
|
position: absolute;
|
||||||
|
top: 31px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-main {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
top: -32px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.list-feed-container {
|
||||||
|
width: 350px;
|
||||||
|
background-color: #faf9f8;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.list-feed-container::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: -10%;
|
||||||
|
right: 0;
|
||||||
|
width: 120%;
|
||||||
|
height: 120%;
|
||||||
|
box-shadow: inset 5px 0 20px #0004;
|
||||||
|
}
|
||||||
|
.list-feed {
|
||||||
|
margin-top: 32px;
|
||||||
|
height: calc(100% - 32px);
|
||||||
|
overflow: hidden scroll;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.cards-feed-container {
|
.cards-feed-container {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -497,3 +561,50 @@ img.favicon {
|
|||||||
.card p.snippet.show {
|
.card p.snippet.show {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #161514;
|
||||||
|
user-select: none;
|
||||||
|
transition: box-shadow linear .08s;
|
||||||
|
border-bottom: 1px solid #e1dfdd;
|
||||||
|
transform: scale(1);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: #0000 0px 5px 15px;
|
||||||
|
}
|
||||||
|
.list-card:hover {
|
||||||
|
box-shadow: #0004 0px 5px 15px;
|
||||||
|
}
|
||||||
|
.list-card:active {
|
||||||
|
box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px;
|
||||||
|
}
|
||||||
|
.list-card div.head {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 8px 0 8px 10px;
|
||||||
|
}
|
||||||
|
.list-card div.head img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
.list-card .data {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.list-card .info {
|
||||||
|
margin: 8px 10px;
|
||||||
|
}
|
||||||
|
.list-card h3.title {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 8px 10px;
|
||||||
|
position: relative;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"redux-devtools": "^3.5.0",
|
"redux-devtools": "^3.5.0",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
|
"simplebar-react": "^2.2.0",
|
||||||
"ts-loader": "^7.0.4",
|
"ts-loader": "^7.0.4",
|
||||||
"typescript": "^3.9.2",
|
"typescript": "^3.9.2",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.43.0",
|
||||||
|
@ -152,7 +152,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
|
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
title="关闭"
|
title="关闭"
|
||||||
iconProps={{iconName: "Cancel"}}
|
iconProps={{iconName: "BackToWindow"}}
|
||||||
onClick={this.props.dismiss} />
|
onClick={this.props.dismiss} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
28
src/components/cards/list-card.tsx
Normal file
28
src/components/cards/list-card.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Card } from "./card"
|
||||||
|
import Time from "../utils/time"
|
||||||
|
import { AnimationClassNames } from "@fluentui/react"
|
||||||
|
|
||||||
|
class ListCard extends Card {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={"list-card "+AnimationClassNames.slideUpIn10+(this.props.item.snippet&&this.props.item.thumb?" transform":"")}
|
||||||
|
onClick={this.onClick} onMouseUp={this.onMouseUp} >
|
||||||
|
{this.props.item.thumb ? (
|
||||||
|
<div className="head"><img src={this.props.item.thumb} /></div>
|
||||||
|
) : null}
|
||||||
|
<div className="data">
|
||||||
|
<p className="info">
|
||||||
|
{this.props.source.iconurl ? <img src={this.props.source.iconurl} /> : null}
|
||||||
|
<span className="name">{this.props.source.name}</span>
|
||||||
|
<Time date={this.props.item.date} />
|
||||||
|
{this.props.item.hasRead ? null : <span className="read-indicator"></span>}
|
||||||
|
</p>
|
||||||
|
<h3 className="title">{this.props.item.title}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListCard
|
@ -6,17 +6,20 @@ import { ContextMenuType } from "../scripts/models/app"
|
|||||||
import { RSSItem } from "../scripts/models/item"
|
import { RSSItem } from "../scripts/models/item"
|
||||||
import { ContextReduxProps } from "../containers/context-menu-container"
|
import { ContextReduxProps } from "../containers/context-menu-container"
|
||||||
import { FeedIdType } from "../scripts/models/feed"
|
import { FeedIdType } from "../scripts/models/feed"
|
||||||
|
import { ViewType } from "../scripts/models/page"
|
||||||
|
|
||||||
export type ContextMenuProps = ContextReduxProps & {
|
export type ContextMenuProps = ContextReduxProps & {
|
||||||
type: ContextMenuType
|
type: ContextMenuType
|
||||||
event?: MouseEvent
|
event?: MouseEvent | string
|
||||||
position?: [number, number]
|
position?: [number, number]
|
||||||
item?: RSSItem
|
item?: RSSItem
|
||||||
feedId?: FeedIdType
|
feedId?: FeedIdType
|
||||||
text?: string
|
text?: string
|
||||||
|
viewType: ViewType
|
||||||
showItem: (feedId: FeedIdType, item: RSSItem) => void
|
showItem: (feedId: FeedIdType, item: RSSItem) => void
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
markUnread: (item: RSSItem) => void
|
markUnread: (item: RSSItem) => void
|
||||||
|
switchView: (viewType: ViewType) => void
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,6 +91,24 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||||||
onClick: () => { googleSearch(this.props.text) }
|
onClick: () => { googleSearch(this.props.text) }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
case ContextMenuType.View: return [
|
||||||
|
{
|
||||||
|
key: "cardView",
|
||||||
|
text: "卡片视图",
|
||||||
|
iconProps: { iconName: "GridViewMedium" },
|
||||||
|
canCheck: true,
|
||||||
|
checked: this.props.viewType === ViewType.Cards,
|
||||||
|
onClick: () => this.props.switchView(ViewType.Cards)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "listView",
|
||||||
|
text: "列表视图",
|
||||||
|
iconProps: { iconName: "BacklogList" },
|
||||||
|
canCheck: true,
|
||||||
|
checked: this.props.viewType === ViewType.List,
|
||||||
|
onClick: () => this.props.switchView(ViewType.List)
|
||||||
|
}
|
||||||
|
]
|
||||||
default: return []
|
default: return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Feed } from "./feed"
|
import { Feed, FeedProps } from "./feed"
|
||||||
import DefaultCard from "../cards/default-card"
|
import DefaultCard from "../cards/default-card"
|
||||||
import { PrimaryButton } from 'office-ui-fabric-react';
|
import { PrimaryButton } from 'office-ui-fabric-react';
|
||||||
|
|
||||||
class CardsFeed extends Feed {
|
class CardsFeed extends React.Component<FeedProps> {
|
||||||
state = { width: window.innerWidth - 12 }
|
state = { width: window.innerWidth - 12 }
|
||||||
|
|
||||||
updateWidth = () => {
|
updateWidth = () => {
|
||||||
|
@ -2,9 +2,13 @@ import * as React from "react"
|
|||||||
import { RSSItem } from "../../scripts/models/item"
|
import { RSSItem } from "../../scripts/models/item"
|
||||||
import { FeedReduxProps } from "../../containers/feed-container"
|
import { FeedReduxProps } from "../../containers/feed-container"
|
||||||
import { RSSFeed, FeedIdType } from "../../scripts/models/feed"
|
import { RSSFeed, FeedIdType } from "../../scripts/models/feed"
|
||||||
|
import { ViewType } from "../../scripts/models/page"
|
||||||
|
import CardsFeed from "./cards-feed"
|
||||||
|
import ListFeed from "./list-feed"
|
||||||
|
|
||||||
type FeedProps = FeedReduxProps & {
|
export type FeedProps = FeedReduxProps & {
|
||||||
feed: RSSFeed
|
feed: RSSFeed
|
||||||
|
viewType: ViewType
|
||||||
items: RSSItem[]
|
items: RSSItem[]
|
||||||
sourceMap: Object
|
sourceMap: Object
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
@ -13,4 +17,15 @@ type FeedProps = FeedReduxProps & {
|
|||||||
showItem: (fid: FeedIdType, item: RSSItem) => void
|
showItem: (fid: FeedIdType, item: RSSItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Feed extends React.Component<FeedProps> { }
|
export class Feed extends React.Component<FeedProps> {
|
||||||
|
render() {
|
||||||
|
switch (this.props.viewType) {
|
||||||
|
case (ViewType.Cards): return (
|
||||||
|
<CardsFeed {...this.props} />
|
||||||
|
)
|
||||||
|
case (ViewType.List): return (
|
||||||
|
<ListFeed {...this.props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
src/components/feeds/list-feed.tsx
Normal file
35
src/components/feeds/list-feed.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { FeedProps } from "./feed"
|
||||||
|
import { PrimaryButton } from 'office-ui-fabric-react';
|
||||||
|
import ListCard from "../cards/list-card";
|
||||||
|
|
||||||
|
class ListFeed extends React.Component<FeedProps> {
|
||||||
|
render() {
|
||||||
|
return this.props.feed.loaded && (
|
||||||
|
<div className="list-feed">
|
||||||
|
{
|
||||||
|
this.props.items.map((item) => (
|
||||||
|
<ListCard
|
||||||
|
feedId={this.props.feed.id}
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
source={this.props.sourceMap[item.source]}
|
||||||
|
markRead={this.props.markRead}
|
||||||
|
contextMenu={this.props.contextMenu}
|
||||||
|
showItem={this.props.showItem} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(this.props.feed.loaded && !this.props.feed.allLoaded)
|
||||||
|
? <div className="load-more-wrapper"><PrimaryButton
|
||||||
|
text="加载更多"
|
||||||
|
disabled={this.props.feed.loading}
|
||||||
|
onClick={() => this.props.loadMore(this.props.feed)} /></div>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListFeed
|
@ -10,6 +10,7 @@ type NavProps = {
|
|||||||
fetch: () => void,
|
fetch: () => void,
|
||||||
menu: () => void,
|
menu: () => void,
|
||||||
logs: () => void,
|
logs: () => void,
|
||||||
|
views: () => void,
|
||||||
settings: () => void
|
settings: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +60,12 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||||||
if (this.canFetch()) this.props.fetch()
|
if (this.canFetch()) this.props.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views = () => {
|
||||||
|
if (this.props.state.contextMenu.event !== "#view-toggle") {
|
||||||
|
this.props.views()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getProgress = () => {
|
getProgress = () => {
|
||||||
return this.props.state.fetchingTotal > 0
|
return this.props.state.fetchingTotal > 0
|
||||||
? this.props.state.fetchingProgress / this.props.state.fetchingTotal
|
? this.props.state.fetchingProgress / this.props.state.fetchingTotal
|
||||||
@ -78,7 +85,9 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||||||
<a className="btn" id="log-toggle" title="消息" onClick={this.props.logs}>
|
<a className="btn" id="log-toggle" title="消息" onClick={this.props.logs}>
|
||||||
{this.props.state.logMenu.notify ? <Icon iconName="RingerSolid" /> : <Icon iconName="Ringer" />}
|
{this.props.state.logMenu.notify ? <Icon iconName="RingerSolid" /> : <Icon iconName="Ringer" />}
|
||||||
</a>
|
</a>
|
||||||
<a className="btn" title="视图"><Icon iconName="View" /></a>
|
<a className="btn" id="view-toggle" title="视图" onClick={this.props.views}
|
||||||
|
onMouseDown={e => {if (this.props.state.contextMenu.event === "#view-toggle") e.stopPropagation()}}>
|
||||||
|
<Icon iconName="View" /></a>
|
||||||
<a className="btn" title="选项" onClick={this.props.settings}><Icon iconName="Settings" /></a>
|
<a className="btn" title="选项" onClick={this.props.settings}><Icon iconName="Settings" /></a>
|
||||||
<span className="seperator"></span>
|
<span className="seperator"></span>
|
||||||
<a className="btn system" title="最小化" onClick={this.minimize} style={{fontSize: 12}}><Icon iconName="Remove" /></a>
|
<a className="btn system" title="最小化" onClick={this.minimize} style={{fontSize: 12}}><Icon iconName="Remove" /></a>
|
||||||
|
@ -3,12 +3,14 @@ import { FeedIdType } from "../scripts/models/feed"
|
|||||||
import { FeedContainer } from "../containers/feed-container"
|
import { FeedContainer } from "../containers/feed-container"
|
||||||
import { AnimationClassNames, Icon } from "@fluentui/react"
|
import { AnimationClassNames, Icon } from "@fluentui/react"
|
||||||
import ArticleContainer from "../containers/article-container"
|
import ArticleContainer from "../containers/article-container"
|
||||||
|
import { ViewType } from "../scripts/models/page"
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
menuOn: boolean
|
menuOn: boolean
|
||||||
settingsOn: boolean
|
settingsOn: boolean
|
||||||
feeds: FeedIdType[]
|
feeds: FeedIdType[]
|
||||||
itemId: number
|
itemId: number
|
||||||
|
viewType: ViewType
|
||||||
dismissItem: () => void
|
dismissItem: () => void
|
||||||
offsetItem: (offset: number) => void
|
offsetItem: (offset: number) => void
|
||||||
}
|
}
|
||||||
@ -23,12 +25,13 @@ class Page extends React.Component<PageProps> {
|
|||||||
this.props.offsetItem(1)
|
this.props.offsetItem(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
render = () => (
|
render = () => this.props.viewType == ViewType.Cards
|
||||||
|
? (
|
||||||
<>
|
<>
|
||||||
{this.props.settingsOn ? null :
|
{this.props.settingsOn ? null :
|
||||||
<div className={"main" + (this.props.menuOn ? " menu-on" : "")}>
|
<div className={"main" + (this.props.menuOn ? " menu-on" : "")}>
|
||||||
{this.props.feeds.map(fid => (
|
{this.props.feeds.map(fid => (
|
||||||
<FeedContainer feedId={fid} key={fid} />
|
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||||
))}
|
))}
|
||||||
</div>}
|
</div>}
|
||||||
{this.props.itemId >= 0 && (
|
{this.props.itemId >= 0 && (
|
||||||
@ -42,6 +45,23 @@ class Page extends React.Component<PageProps> {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
{this.props.settingsOn ? null :
|
||||||
|
<div className={"list-main" + (this.props.menuOn ? " menu-on" : "")}>
|
||||||
|
<div className="list-feed-container">
|
||||||
|
{this.props.feeds.map(fid => (
|
||||||
|
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{this.props.itemId >= 0 && (
|
||||||
|
<div className="side-article-wrapper">
|
||||||
|
<ArticleContainer itemId={this.props.itemId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page
|
export default Page
|
@ -272,6 +272,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
selectionMode={SelectionMode.multiple} />
|
selectionMode={SelectionMode.multiple} />
|
||||||
</MarqueeSelection>
|
</MarqueeSelection>
|
||||||
|
|
||||||
|
<span className="settings-hint">拖拽订阅源以排序</span>
|
||||||
</>}
|
</>}
|
||||||
{(!this.state.manageGroup || !this.state.selectedGroup)
|
{(!this.state.manageGroup || !this.state.selectedGroup)
|
||||||
?<>
|
?<>
|
||||||
|
@ -4,14 +4,16 @@ import { RootState } from "../scripts/reducer"
|
|||||||
import { ContextMenuType, closeContextMenu } from "../scripts/models/app"
|
import { ContextMenuType, closeContextMenu } from "../scripts/models/app"
|
||||||
import { ContextMenu } from "../components/context-menu"
|
import { ContextMenu } from "../components/context-menu"
|
||||||
import { RSSItem, markRead, markUnread } from "../scripts/models/item"
|
import { RSSItem, markRead, markUnread } from "../scripts/models/item"
|
||||||
import { showItem } from "../scripts/models/page"
|
import { showItem, switchView, ViewType } from "../scripts/models/page"
|
||||||
import { FeedIdType } from "../scripts/models/feed"
|
import { FeedIdType } from "../scripts/models/feed"
|
||||||
|
import { setDefaultView } from "../scripts/utils"
|
||||||
|
|
||||||
const getContext = (state: RootState) => state.app.contextMenu
|
const getContext = (state: RootState) => state.app.contextMenu
|
||||||
|
const getViewType = (state: RootState) => state.page.viewType
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
[getContext],
|
[getContext, getViewType],
|
||||||
(context) => {
|
(context, viewType) => {
|
||||||
switch (context.type) {
|
switch (context.type) {
|
||||||
case ContextMenuType.Item: return {
|
case ContextMenuType.Item: return {
|
||||||
type: context.type,
|
type: context.type,
|
||||||
@ -24,6 +26,11 @@ const mapStateToProps = createSelector(
|
|||||||
position: context.position,
|
position: context.position,
|
||||||
text: context.target as string
|
text: context.target as string
|
||||||
}
|
}
|
||||||
|
case ContextMenuType.View: return {
|
||||||
|
type: context.type,
|
||||||
|
event: context.event,
|
||||||
|
viewType: viewType
|
||||||
|
}
|
||||||
default: return { type: ContextMenuType.Hidden }
|
default: return { type: ContextMenuType.Hidden }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,6 +41,10 @@ const mapDispatchToProps = dispatch => {
|
|||||||
showItem: (feedId: FeedIdType, item: RSSItem) => dispatch(showItem(feedId, item)),
|
showItem: (feedId: FeedIdType, item: RSSItem) => dispatch(showItem(feedId, item)),
|
||||||
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
||||||
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
||||||
|
switchView: (viewType: ViewType) => {
|
||||||
|
setDefaultView(viewType)
|
||||||
|
dispatch(switchView(viewType))
|
||||||
|
},
|
||||||
close: () => dispatch(closeContextMenu())
|
close: () => dispatch(closeContextMenu())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,30 @@
|
|||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
import { RootState } from "../scripts/reducer"
|
||||||
import CardsFeed from "../components/feeds/cards-feed"
|
|
||||||
import { markRead, RSSItem } from "../scripts/models/item"
|
import { markRead, RSSItem } from "../scripts/models/item"
|
||||||
import { openItemMenu } from "../scripts/models/app"
|
import { openItemMenu } from "../scripts/models/app"
|
||||||
import { FeedIdType, loadMore, RSSFeed } from "../scripts/models/feed"
|
import { FeedIdType, loadMore, RSSFeed } from "../scripts/models/feed"
|
||||||
import { showItem } from "../scripts/models/page"
|
import { showItem, ViewType } from "../scripts/models/page"
|
||||||
|
import { Feed } from "../components/feeds/feed"
|
||||||
|
|
||||||
interface FeedContainerProps {
|
interface FeedContainerProps {
|
||||||
feedId: FeedIdType
|
feedId: FeedIdType
|
||||||
|
viewType: ViewType
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSources = (state: RootState) => state.sources
|
const getSources = (state: RootState) => state.sources
|
||||||
const getItems = (state: RootState) => state.items
|
const getItems = (state: RootState) => state.items
|
||||||
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
|
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
|
||||||
|
const getView = (_, props: FeedContainerProps) => props.viewType
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[getSources, getItems, getFeed],
|
[getSources, getItems, getFeed, getView],
|
||||||
(sources, items, feed) => ({
|
(sources, items, feed, viewType) => ({
|
||||||
feed: feed,
|
feed: feed,
|
||||||
items: feed.iids.map(iid => items[iid]),
|
items: feed.iids.map(iid => items[iid]),
|
||||||
sourceMap: sources
|
sourceMap: sources,
|
||||||
|
viewType: viewType
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -36,4 +39,4 @@ const mapDispatchToProps = dispatch => {
|
|||||||
|
|
||||||
const connector = connect(makeMapStateToProps, mapDispatchToProps)
|
const connector = connect(makeMapStateToProps, mapDispatchToProps)
|
||||||
export type FeedReduxProps = typeof connector
|
export type FeedReduxProps = typeof connector
|
||||||
export const FeedContainer = connector(CardsFeed)
|
export const FeedContainer = connector(Feed)
|
@ -2,11 +2,12 @@ import { connect } from "react-redux"
|
|||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
import { RootState } from "../scripts/reducer"
|
||||||
import { fetchItems } from "../scripts/models/item"
|
import { fetchItems } from "../scripts/models/item"
|
||||||
import { toggleMenu, toggleLogMenu, toggleSettings } from "../scripts/models/app"
|
import { toggleMenu, toggleLogMenu, toggleSettings, openViewMenu } from "../scripts/models/app"
|
||||||
|
import { ViewType } from "../scripts/models/page"
|
||||||
import Nav from "../components/nav"
|
import Nav from "../components/nav"
|
||||||
|
|
||||||
const getState = (state: RootState) => state.app
|
const getState = (state: RootState) => state.app
|
||||||
const getItemShown = (state: RootState) => state.page.itemId >= 0
|
const getItemShown = (state: RootState) => (state.page.itemId >= 0) && state.page.viewType !== ViewType.List
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
[getState, getItemShown],
|
[getState, getItemShown],
|
||||||
@ -20,6 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
fetch: () => dispatch(fetchItems()),
|
fetch: () => dispatch(fetchItems()),
|
||||||
menu: () => dispatch(toggleMenu()),
|
menu: () => dispatch(toggleMenu()),
|
||||||
logs: () => dispatch(toggleLogMenu()),
|
logs: () => dispatch(toggleLogMenu()),
|
||||||
|
views: () => dispatch(openViewMenu()),
|
||||||
settings: () => dispatch(toggleSettings())
|
settings: () => dispatch(toggleSettings())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@ const mapStateToProps = createSelector(
|
|||||||
feeds: [page.feedId],
|
feeds: [page.feedId],
|
||||||
settingsOn: settingsOn,
|
settingsOn: settingsOn,
|
||||||
menuOn: menuOn,
|
menuOn: menuOn,
|
||||||
itemId: page.itemId
|
itemId: page.itemId,
|
||||||
|
viewType: page.viewType
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELET
|
|||||||
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
|
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
|
||||||
|
|
||||||
export enum ContextMenuType {
|
export enum ContextMenuType {
|
||||||
Hidden, Item, Text
|
Hidden, Item, Text, View
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppLogType {
|
export enum AppLogType {
|
||||||
@ -64,6 +64,7 @@ export class AppState {
|
|||||||
export const CLOSE_CONTEXT_MENU = "CLOSE_CONTEXT_MENU"
|
export const CLOSE_CONTEXT_MENU = "CLOSE_CONTEXT_MENU"
|
||||||
export const OPEN_ITEM_MENU = "OPEN_ITEM_MENU"
|
export const OPEN_ITEM_MENU = "OPEN_ITEM_MENU"
|
||||||
export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU"
|
export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU"
|
||||||
|
export const OPEN_VIEW_MENU = "OPEN_VIEW_MENU"
|
||||||
|
|
||||||
interface CloseContextMenuAction {
|
interface CloseContextMenuAction {
|
||||||
type: typeof CLOSE_CONTEXT_MENU
|
type: typeof CLOSE_CONTEXT_MENU
|
||||||
@ -82,7 +83,11 @@ interface OpenTextMenuAction {
|
|||||||
item: string
|
item: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction | OpenTextMenuAction
|
interface OpenViewMenuAction {
|
||||||
|
type: typeof OPEN_VIEW_MENU
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction | OpenTextMenuAction | OpenViewMenuAction
|
||||||
|
|
||||||
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
||||||
export interface LogMenuActionType { type: typeof TOGGLE_LOGS }
|
export interface LogMenuActionType { type: typeof TOGGLE_LOGS }
|
||||||
@ -121,6 +126,8 @@ export function openTextMenu(text: string, position: [number, number]): ContextM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const openViewMenu = (): ContextMenuActionTypes => ({ type: OPEN_VIEW_MENU })
|
||||||
|
|
||||||
export const toggleMenu = () => ({ type: TOGGLE_MENU })
|
export const toggleMenu = () => ({ type: TOGGLE_MENU })
|
||||||
export const toggleLogMenu = () => ({ type: TOGGLE_LOGS })
|
export const toggleLogMenu = () => ({ type: TOGGLE_LOGS })
|
||||||
export const toggleSettings = () => ({ type: TOGGLE_SETTINGS })
|
export const toggleSettings = () => ({ type: TOGGLE_SETTINGS })
|
||||||
@ -274,6 +281,13 @@ export function appReducer(
|
|||||||
target: action.item
|
target: action.item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case OPEN_VIEW_MENU: return {
|
||||||
|
...state,
|
||||||
|
contextMenu: {
|
||||||
|
type: ContextMenuType.View,
|
||||||
|
event: "#view-toggle"
|
||||||
|
}
|
||||||
|
}
|
||||||
case TOGGLE_MENU: return {
|
case TOGGLE_MENU: return {
|
||||||
...state,
|
...state,
|
||||||
menu: !state.menu
|
menu: !state.menu
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ALL, SOURCE, FeedIdType, loadMore } from "./feed"
|
import { ALL, SOURCE, FeedIdType, loadMore } from "./feed"
|
||||||
import { getWindowBreakpoint, AppThunk } from "../utils"
|
import { getWindowBreakpoint, AppThunk, getDefaultView } from "../utils"
|
||||||
import { RSSItem, ItemActionTypes, MARK_READ, MARK_UNREAD, markRead } from "./item"
|
import { RSSItem, markRead } from "./item"
|
||||||
|
import { SourceActionTypes, DELETE_SOURCE } from "./source"
|
||||||
|
|
||||||
export const SELECT_PAGE = "SELECT_PAGE"
|
export const SELECT_PAGE = "SELECT_PAGE"
|
||||||
|
export const SWITCH_VIEW = "SWITCH_VIEW"
|
||||||
export const SHOW_ITEM = "SHOW_ITEM"
|
export const SHOW_ITEM = "SHOW_ITEM"
|
||||||
export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
|
export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
|
||||||
export const DISMISS_ITEM = "DISMISS_ITEM"
|
export const DISMISS_ITEM = "DISMISS_ITEM"
|
||||||
@ -11,6 +13,10 @@ export enum PageType {
|
|||||||
AllArticles, Sources, Page
|
AllArticles, Sources, Page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ViewType {
|
||||||
|
Cards, List, Customized
|
||||||
|
}
|
||||||
|
|
||||||
interface SelectPageAction {
|
interface SelectPageAction {
|
||||||
type: typeof SELECT_PAGE
|
type: typeof SELECT_PAGE
|
||||||
pageType: PageType
|
pageType: PageType
|
||||||
@ -21,6 +27,11 @@ interface SelectPageAction {
|
|||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SwitchViewAction {
|
||||||
|
type: typeof SWITCH_VIEW
|
||||||
|
viewType: ViewType
|
||||||
|
}
|
||||||
|
|
||||||
interface ShowItemAction {
|
interface ShowItemAction {
|
||||||
type: typeof SHOW_ITEM
|
type: typeof SHOW_ITEM
|
||||||
feedId: FeedIdType
|
feedId: FeedIdType
|
||||||
@ -29,7 +40,7 @@ interface ShowItemAction {
|
|||||||
|
|
||||||
interface DismissItemAction { type: typeof DISMISS_ITEM }
|
interface DismissItemAction { type: typeof DISMISS_ITEM }
|
||||||
|
|
||||||
export type PageActionTypes = SelectPageAction | ShowItemAction | DismissItemAction
|
export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction | DismissItemAction
|
||||||
|
|
||||||
export function selectAllArticles(init = false): PageActionTypes {
|
export function selectAllArticles(init = false): PageActionTypes {
|
||||||
return {
|
return {
|
||||||
@ -52,6 +63,13 @@ export function selectSources(sids: number[], menuKey: string, title: string): P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function switchView(viewType: ViewType): PageActionTypes {
|
||||||
|
return {
|
||||||
|
type: SWITCH_VIEW,
|
||||||
|
viewType: viewType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function showItem(feedId: FeedIdType, item: RSSItem): PageActionTypes {
|
export function showItem(feedId: FeedIdType, item: RSSItem): PageActionTypes {
|
||||||
return {
|
return {
|
||||||
type: SHOW_ITEM,
|
type: SHOW_ITEM,
|
||||||
@ -90,13 +108,14 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PageState {
|
export class PageState {
|
||||||
|
viewType = getDefaultView()
|
||||||
feedId = ALL as FeedIdType
|
feedId = ALL as FeedIdType
|
||||||
itemId = -1
|
itemId = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pageReducer(
|
export function pageReducer(
|
||||||
state = new PageState(),
|
state = new PageState(),
|
||||||
action: PageActionTypes
|
action: PageActionTypes | SourceActionTypes
|
||||||
): PageState {
|
): PageState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SELECT_PAGE:
|
case SELECT_PAGE:
|
||||||
@ -111,10 +130,16 @@ export function pageReducer(
|
|||||||
}
|
}
|
||||||
default: return state
|
default: return state
|
||||||
}
|
}
|
||||||
|
case SWITCH_VIEW: return {
|
||||||
|
...state,
|
||||||
|
viewType: action.viewType,
|
||||||
|
itemId: action.viewType === ViewType.List ? state.itemId : -1
|
||||||
|
}
|
||||||
case SHOW_ITEM: return {
|
case SHOW_ITEM: return {
|
||||||
...state,
|
...state,
|
||||||
itemId: action.item.id
|
itemId: action.item.id
|
||||||
}
|
}
|
||||||
|
case DELETE_SOURCE:
|
||||||
case DISMISS_ITEM: return {
|
case DISMISS_ITEM: return {
|
||||||
...state,
|
...state,
|
||||||
itemId: -1
|
itemId: -1
|
||||||
|
@ -48,6 +48,7 @@ export function setProxy(address = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")
|
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")
|
||||||
|
import { ViewType } from "./models/page"
|
||||||
let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session)
|
let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session)
|
||||||
export const rssParser = new Parser({
|
export const rssParser = new Parser({
|
||||||
customFields: customFields,
|
customFields: customFields,
|
||||||
@ -92,3 +93,12 @@ export const cutText = (s: string, length: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const googleSearch = (text: string) => openExternal("https://www.google.com/search?q=" + encodeURIComponent(text))
|
export const googleSearch = (text: string) => openExternal("https://www.google.com/search?q=" + encodeURIComponent(text))
|
||||||
|
|
||||||
|
const VIEW_STORE_KEY = "view"
|
||||||
|
export const getDefaultView = () => {
|
||||||
|
let view = localStorage.getItem(VIEW_STORE_KEY)
|
||||||
|
return view ? parseInt(view) as ViewType : ViewType.Cards
|
||||||
|
}
|
||||||
|
export const setDefaultView = (viewType: ViewType) => {
|
||||||
|
localStorage.setItem(VIEW_STORE_KEY, String(viewType))
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user