list view

This commit is contained in:
刘浩远 2020-06-08 16:54:53 +08:00
parent 165597a454
commit 53a280cd72
20 changed files with 343 additions and 32 deletions

View File

@ -17,6 +17,10 @@ a:hover, a:active {
text-decoration: underline;
}
#main {
max-width: 700px;
margin: 0 auto;
}
#main > p.title {
font-size: 1.25rem;
line-height: 1.75rem;

BIN
dist/icons/fabric-icons-15-3807251b.woff vendored Normal file

Binary file not shown.

121
dist/styles.css vendored
View File

@ -59,6 +59,10 @@ i.ms-Nav-chevron {
user-select: none;
overflow: hidden;
}
#root > nav .btn, #root > nav span {
z-index: 1;
position: relative;
}
nav .progress {
position: fixed;
top: 0;
@ -113,7 +117,7 @@ nav .progress {
font-size: 14px;
vertical-align: top;
}
.btn-group .btn.system {
#root > nav .btn-group .btn.system {
position: relative;
z-index: 10;
}
@ -301,11 +305,11 @@ img.favicon {
height: 120%;
box-shadow: inset 5px 0 20px #0004;
}
.main.menu-on {
.main.menu-on, .list-main.menu-on {
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;
}
.btn-group .btn.inline-block-wide {
@ -335,9 +339,12 @@ img.favicon {
.article-container .btn-group.next {
right: calc(50% - 486px);
}
.article {
height: 100%;
}
.article webview {
width: 100%;
height: calc(100vh - 86px);
height: calc(100% - 36px);
border: none;
}
.article i.ms-Icon {
@ -358,6 +365,63 @@ img.favicon {
white-space: nowrap;
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 {
display: inline-flex;
@ -496,4 +560,51 @@ img.favicon {
}
.card p.snippet.show {
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;
}

View File

@ -36,6 +36,7 @@
"redux-devtools": "^3.5.0",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"simplebar-react": "^2.2.0",
"ts-loader": "^7.0.4",
"typescript": "^3.9.2",
"webpack": "^4.43.0",

View File

@ -152,7 +152,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
<CommandBarButton
title="关闭"
iconProps={{iconName: "Cancel"}}
iconProps={{iconName: "BackToWindow"}}
onClick={this.props.dismiss} />
</Stack>
</Stack>

View 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

View File

@ -6,17 +6,20 @@ import { ContextMenuType } from "../scripts/models/app"
import { RSSItem } from "../scripts/models/item"
import { ContextReduxProps } from "../containers/context-menu-container"
import { FeedIdType } from "../scripts/models/feed"
import { ViewType } from "../scripts/models/page"
export type ContextMenuProps = ContextReduxProps & {
type: ContextMenuType
event?: MouseEvent
event?: MouseEvent | string
position?: [number, number]
item?: RSSItem
feedId?: FeedIdType
text?: string
viewType: ViewType
showItem: (feedId: FeedIdType, item: RSSItem) => void
markRead: (item: RSSItem) => void
markUnread: (item: RSSItem) => void
switchView: (viewType: ViewType) => void
close: () => void
}
@ -88,6 +91,24 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
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 []
}
}

View File

@ -1,9 +1,9 @@
import * as React from "react"
import { Feed } from "./feed"
import { Feed, FeedProps } from "./feed"
import DefaultCard from "../cards/default-card"
import { PrimaryButton } from 'office-ui-fabric-react';
class CardsFeed extends Feed {
class CardsFeed extends React.Component<FeedProps> {
state = { width: window.innerWidth - 12 }
updateWidth = () => {

View File

@ -2,9 +2,13 @@ import * as React from "react"
import { RSSItem } from "../../scripts/models/item"
import { FeedReduxProps } from "../../containers/feed-container"
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
viewType: ViewType
items: RSSItem[]
sourceMap: Object
markRead: (item: RSSItem) => void
@ -13,4 +17,15 @@ type FeedProps = FeedReduxProps & {
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} />
)
}
}
}

View 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

View File

@ -10,6 +10,7 @@ type NavProps = {
fetch: () => void,
menu: () => void,
logs: () => void,
views: () => void,
settings: () => void
}
@ -59,6 +60,12 @@ class Nav extends React.Component<NavProps, NavState> {
if (this.canFetch()) this.props.fetch()
}
views = () => {
if (this.props.state.contextMenu.event !== "#view-toggle") {
this.props.views()
}
}
getProgress = () => {
return this.props.state.fetchingTotal > 0
? 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}>
{this.props.state.logMenu.notify ? <Icon iconName="RingerSolid" /> : <Icon iconName="Ringer" />}
</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>
<span className="seperator"></span>
<a className="btn system" title="最小化" onClick={this.minimize} style={{fontSize: 12}}><Icon iconName="Remove" /></a>

View File

@ -3,12 +3,14 @@ import { FeedIdType } from "../scripts/models/feed"
import { FeedContainer } from "../containers/feed-container"
import { AnimationClassNames, Icon } from "@fluentui/react"
import ArticleContainer from "../containers/article-container"
import { ViewType } from "../scripts/models/page"
type PageProps = {
menuOn: boolean
settingsOn: boolean
feeds: FeedIdType[]
itemId: number
viewType: ViewType
dismissItem: () => void
offsetItem: (offset: number) => void
}
@ -23,12 +25,13 @@ class Page extends React.Component<PageProps> {
this.props.offsetItem(1)
}
render = () => (
render = () => this.props.viewType == ViewType.Cards
? (
<>
{this.props.settingsOn ? null :
<div className={"main" + (this.props.menuOn ? " menu-on" : "")}>
{this.props.feeds.map(fid => (
<FeedContainer feedId={fid} key={fid} />
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
))}
</div>}
{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

View File

@ -272,6 +272,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
selectionMode={SelectionMode.multiple} />
</MarqueeSelection>
<span className="settings-hint"></span>
</>}
{(!this.state.manageGroup || !this.state.selectedGroup)
?<>

View File

@ -4,14 +4,16 @@ import { RootState } from "../scripts/reducer"
import { ContextMenuType, closeContextMenu } from "../scripts/models/app"
import { ContextMenu } from "../components/context-menu"
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 { setDefaultView } from "../scripts/utils"
const getContext = (state: RootState) => state.app.contextMenu
const getViewType = (state: RootState) => state.page.viewType
const mapStateToProps = createSelector(
[getContext],
(context) => {
[getContext, getViewType],
(context, viewType) => {
switch (context.type) {
case ContextMenuType.Item: return {
type: context.type,
@ -24,6 +26,11 @@ const mapStateToProps = createSelector(
position: context.position,
text: context.target as string
}
case ContextMenuType.View: return {
type: context.type,
event: context.event,
viewType: viewType
}
default: return { type: ContextMenuType.Hidden }
}
}
@ -34,6 +41,10 @@ const mapDispatchToProps = dispatch => {
showItem: (feedId: FeedIdType, item: RSSItem) => dispatch(showItem(feedId, item)),
markRead: (item: RSSItem) => dispatch(markRead(item)),
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
switchView: (viewType: ViewType) => {
setDefaultView(viewType)
dispatch(switchView(viewType))
},
close: () => dispatch(closeContextMenu())
}
}

View File

@ -1,27 +1,30 @@
import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import CardsFeed from "../components/feeds/cards-feed"
import { markRead, RSSItem } from "../scripts/models/item"
import { openItemMenu } from "../scripts/models/app"
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 {
feedId: FeedIdType
viewType: ViewType
}
const getSources = (state: RootState) => state.sources
const getItems = (state: RootState) => state.items
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
const getView = (_, props: FeedContainerProps) => props.viewType
const makeMapStateToProps = () => {
return createSelector(
[getSources, getItems, getFeed],
(sources, items, feed) => ({
[getSources, getItems, getFeed, getView],
(sources, items, feed, viewType) => ({
feed: feed,
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)
export type FeedReduxProps = typeof connector
export const FeedContainer = connector(CardsFeed)
export const FeedContainer = connector(Feed)

View File

@ -2,11 +2,12 @@ import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
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"
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(
[getState, getItemShown],
@ -20,6 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
fetch: () => dispatch(fetchItems()),
menu: () => dispatch(toggleMenu()),
logs: () => dispatch(toggleLogMenu()),
views: () => dispatch(openViewMenu()),
settings: () => dispatch(toggleSettings())
})

View File

@ -15,7 +15,8 @@ const mapStateToProps = createSelector(
feeds: [page.feedId],
settingsOn: settingsOn,
menuOn: menuOn,
itemId: page.itemId
itemId: page.itemId,
viewType: page.viewType
})
)

View File

@ -6,7 +6,7 @@ import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELET
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
export enum ContextMenuType {
Hidden, Item, Text
Hidden, Item, Text, View
}
export enum AppLogType {
@ -64,6 +64,7 @@ export class AppState {
export const CLOSE_CONTEXT_MENU = "CLOSE_CONTEXT_MENU"
export const OPEN_ITEM_MENU = "OPEN_ITEM_MENU"
export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU"
export const OPEN_VIEW_MENU = "OPEN_VIEW_MENU"
interface CloseContextMenuAction {
type: typeof CLOSE_CONTEXT_MENU
@ -82,7 +83,11 @@ interface OpenTextMenuAction {
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 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 toggleLogMenu = () => ({ type: TOGGLE_LOGS })
export const toggleSettings = () => ({ type: TOGGLE_SETTINGS })
@ -274,6 +281,13 @@ export function appReducer(
target: action.item
}
}
case OPEN_VIEW_MENU: return {
...state,
contextMenu: {
type: ContextMenuType.View,
event: "#view-toggle"
}
}
case TOGGLE_MENU: return {
...state,
menu: !state.menu

View File

@ -1,8 +1,10 @@
import { ALL, SOURCE, FeedIdType, loadMore } from "./feed"
import { getWindowBreakpoint, AppThunk } from "../utils"
import { RSSItem, ItemActionTypes, MARK_READ, MARK_UNREAD, markRead } from "./item"
import { getWindowBreakpoint, AppThunk, getDefaultView } from "../utils"
import { RSSItem, markRead } from "./item"
import { SourceActionTypes, DELETE_SOURCE } from "./source"
export const SELECT_PAGE = "SELECT_PAGE"
export const SWITCH_VIEW = "SWITCH_VIEW"
export const SHOW_ITEM = "SHOW_ITEM"
export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
export const DISMISS_ITEM = "DISMISS_ITEM"
@ -11,6 +13,10 @@ export enum PageType {
AllArticles, Sources, Page
}
export enum ViewType {
Cards, List, Customized
}
interface SelectPageAction {
type: typeof SELECT_PAGE
pageType: PageType
@ -21,6 +27,11 @@ interface SelectPageAction {
title?: string
}
interface SwitchViewAction {
type: typeof SWITCH_VIEW
viewType: ViewType
}
interface ShowItemAction {
type: typeof SHOW_ITEM
feedId: FeedIdType
@ -29,7 +40,7 @@ interface ShowItemAction {
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 {
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 {
return {
type: SHOW_ITEM,
@ -90,13 +108,14 @@ export function showOffsetItem(offset: number): AppThunk {
}
export class PageState {
viewType = getDefaultView()
feedId = ALL as FeedIdType
itemId = -1
}
export function pageReducer(
state = new PageState(),
action: PageActionTypes
action: PageActionTypes | SourceActionTypes
): PageState {
switch (action.type) {
case SELECT_PAGE:
@ -111,10 +130,16 @@ export function pageReducer(
}
default: return state
}
case SWITCH_VIEW: return {
...state,
viewType: action.viewType,
itemId: action.viewType === ViewType.List ? state.itemId : -1
}
case SHOW_ITEM: return {
...state,
itemId: action.item.id
}
case DELETE_SOURCE:
case DISMISS_ITEM: return {
...state,
itemId: -1

View File

@ -48,6 +48,7 @@ export function setProxy(address = null) {
}
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")
import { ViewType } from "./models/page"
let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session)
export const rssParser = new Parser({
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))
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))
}