add magazine and compact views

This commit is contained in:
刘浩远 2020-07-17 13:01:29 +08:00
parent 378c74c98b
commit 05d70ff73a
17 changed files with 370 additions and 118 deletions

288
dist/styles/cards.css vendored
View File

@ -50,29 +50,17 @@
} }
.card { .card {
display: inline-block;
position: relative; position: relative;
width: 256px;
height: 264px;
border-radius: 4px;
background-color: var(--white);
overflow: hidden;
box-shadow: #0004 0px 5px 20px;
margin: 18px 12px;
color: var(--neutralDarker); color: var(--neutralDarker);
user-select: none; user-select: none;
transition: box-shadow linear .08s;
transform: scale(1); transform: scale(1);
cursor: pointer; cursor: pointer;
animation-fill-mode: none; overflow: hidden;
} }
.card:focus, .list-card:focus { .card:focus {
outline: none; outline: none;
} }
.card:hover, .ms-Fabric--isFocusVisible .card:focus { .ms-Fabric--isFocusVisible .card:focus::after {
box-shadow: #0006 0px 5px 40px;
}
.ms-Fabric--isFocusVisible .card:focus::after, .ms-Fabric--isFocusVisible .list-card:focus::after {
content: ""; content: "";
position: absolute; position: absolute;
top: 2px; top: 2px;
@ -82,70 +70,7 @@
border: 1px solid var(--white); border: 1px solid var(--white);
outline: 2px solid #0078d4; outline: 2px solid #0078d4;
} }
.card:active { .card.hidden::after {
transform: scale(.97);
}
.card .bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.card img.bg {
object-fit: cover;
filter: saturate(150%) blur(20px);
}
.card div.bg {
background-color: #fffb;
}
.card img.head {
display: block;
object-fit: cover;
position: relative;
width: 100%;
height: 144px;
-webkit-user-drag: none;
}
.card img.head, .card p, .card h3 {
transition: transform ease-out .12s;
}
.card.transform:hover img.head, .card.transform:hover p, .card.transform:hover h3,
.ms-Fabric--isFocusVisible .card.transform:focus img.head,
.ms-Fabric--isFocusVisible .card.transform:focus p,
.ms-Fabric--isFocusVisible .card.transform:focus h3 {
transform: translateY(-144px);
}
.card h3.title {
font-size: 16px;
line-height: 22px;
font-weight: 600;
margin: 10px 12px;
position: relative;
-webkit-line-clamp: 3;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.card p.snippet {
font-size: 14px;
line-height: 20px;
margin: 10px 12px;
display: -webkit-box;
position: relative;
-webkit-line-clamp: 7;
-webkit-box-orient: vertical;
overflow: hidden;
transform: translateY(64px);
}
.card:hover p.snippet {
transform: translateY(-144px);
}
.card p.snippet.show {
transform: none;
}
.card.hidden::after, .list-card.hidden::after {
content: ""; content: "";
display: block; display: block;
width: 100%; width: 100%;
@ -156,23 +81,96 @@
background: #0004; background: #0004;
} }
.default-card {
display: inline-block;
width: 256px;
height: 264px;
border-radius: 4px;
background-color: var(--white);
box-shadow: #0004 0 5px 20px;
margin: 18px 12px;
transition: box-shadow linear .08s, transform linear .08s;
animation-fill-mode: none;
}
.default-card:hover, .ms-Fabric--isFocusVisible .default-card:focus {
box-shadow: #0006 0 5px 40px;
}
.default-card:active {
transform: scale(.97);
box-shadow: #0004 0 5px 20px;
}
.default-card .bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.default-card img.bg {
object-fit: cover;
filter: saturate(150%) blur(20px);
}
.default-card div.bg {
background-color: #fffb;
}
.default-card img.head {
display: block;
object-fit: cover;
position: relative;
width: 100%;
height: 144px;
-webkit-user-drag: none;
}
.default-card img.head, .default-card p, .default-card h3 {
transition: transform ease-out .12s;
}
.default-card.transform:hover img.head, .default-card.transform:hover p, .default-card.transform:hover h3,
.ms-Fabric--isFocusVisible .default-card.transform:focus img.head,
.ms-Fabric--isFocusVisible .default-card.transform:focus p,
.ms-Fabric--isFocusVisible .default-card.transform:focus h3 {
transform: translateY(-144px);
}
.default-card h3.title {
font-size: 16px;
line-height: 22px;
font-weight: 600;
margin: 10px 12px;
position: relative;
-webkit-line-clamp: 3;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.default-card p.snippet {
font-size: 14px;
line-height: 20px;
margin: 10px 12px;
display: -webkit-box;
position: relative;
-webkit-line-clamp: 7;
-webkit-box-orient: vertical;
overflow: hidden;
transform: translateY(64px);
}
.default-card:hover p.snippet {
transform: translateY(-144px);
}
.default-card p.snippet.show {
transform: none;
}
.list-card { .list-card {
display: flex; display: flex;
position: relative;
overflow: hidden;
color: var(--neutralDarker);
user-select: none;
transition: box-shadow linear .08s; transition: box-shadow linear .08s;
border-bottom: 1px solid var(--neutralQuaternaryAlt); border-bottom: 1px solid var(--neutralQuaternaryAlt);
transform: scale(1); box-shadow: #0000 0 5px 15px;
cursor: pointer;
box-shadow: #0000 0px 5px 15px;
} }
.list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus { .list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus {
box-shadow: #0004 0px 5px 15px; box-shadow: #0004 0 5px 15px;
} }
.list-card:active { .list-card:active {
box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px; box-shadow: #0000 0 5px 15px, inset #0004 0 0 15px;
} }
.list-card div.head { .list-card div.head {
width: 80px; width: 80px;
@ -203,3 +201,117 @@
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.magazine-card {
width: 700px;
padding: 24px;
max-height: 160px;
display: flex;
transition: box-shadow linear .08s, background-color linear .08s, transform linear .08s;
border-bottom: 1px solid var(--neutralQuaternaryAlt);
box-shadow: #0000 0 5px 20px;
}
.magazine-card.read {
color: var(--neutralSecondaryAlt);
}
.magazine-card:hover, .ms-Fabric--isFocusVisible .magazine-card:focus {
box-shadow: #0004 0 5px 20px;
background-color: var(--white);
}
.magazine-card:active {
box-shadow: #0000 0 5px 20px;
transform: scale(.97);
background-color: unset;
}
.magazine-card div.head {
width: 200px;
height: 160px;
margin-right: 25px;
}
.magazine-card div.head img {
width: 200px;
height: 160px;
object-fit: cover;
-webkit-user-drag: none;
}
.magazine-card .data {
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: space-between;
}
.magazine-card .data > *:first-child {
flex-grow: 1;
}
.magazine-card .info {
height: 16px;
margin: 0;
}
.magazine-card h3.title, .magazine-card p.snippet {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
margin: 0 0 12px;
}
.magazine-card h3.title {
font-size: 18px;
line-height: 27px;
font-weight: 600;
-webkit-line-clamp: 2;
}
.magazine-card p.snippet {
font-size: 14px;
line-height: 21px;
-webkit-line-clamp: 3;
}
.compact-card {
height: 31px;
display: flex;
border-bottom: 1px solid var(--neutralQuaternaryAlt);
font-size: 14px;
line-height: 31px;
padding: 0 9px;
transition: box-shadow linear .08s, background-color linear .08s;
}
.compact-card:hover, .ms-Fabric--isFocusVisible .compact-card:focus {
box-shadow: #0004 0 0 10px;
background-color: var(--white);
}
.compact-card:active {
box-shadow: #0000 0 0 10px;
}
.compact-card > * {
margin: 0 3px;
flex-shrink: 0;
}
.compact-card .info {
display: flex;
line-height: 31px;
width: 140px;
}
.compact-card .info .name {
flex-grow: 1;
}
.compact-card .info img,
.compact-card .info .read-indicator,
.compact-card .info .starred-indicator {
margin: 7.5px 5px 7.5px 0;
}
.compact-card .data {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.compact-card .data .title {
font-weight: 600;
margin-right: 6px;
}
.compact-card .data .snippet {
color: var(--neutralSecondaryAlt);
}
.compact-card .time {
font-size: 12px;
}

20
dist/styles/dark.css vendored
View File

@ -11,19 +11,31 @@
.settings .loading { .settings .loading {
background-color: #000a; background-color: #000a;
} }
.card { .default-card {
box-shadow: #0006 0px 5px 20px; box-shadow: #0006 0px 5px 20px;
} }
.card:hover { .default-card:hover, .ms-Fabric--isFocusVisible .default-card:focus {
box-shadow: #0008 0px 5px 40px; box-shadow: #0008 0px 5px 40px;
} }
.card div.bg { .default-card div.bg {
background-color: #000b; background-color: #000b;
} }
.list-card:hover { .list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus {
box-shadow: #0006 0px 5px 15px; box-shadow: #0006 0px 5px 15px;
} }
.list-card:active { .list-card:active {
box-shadow: #0000 0px 5px 15px, inset #0006 0px 0px 15px; box-shadow: #0000 0px 5px 15px, inset #0006 0px 0px 15px;
} }
.magazine-card:hover, .ms-Fabric--isFocusVisible .magazine-card:focus {
box-shadow: #0006 0px 5px 20px;
}
.magazine-card:active {
box-shadow: #0000 0px 5px 20px;
}
.compact-card:hover, .ms-Fabric--isFocusVisible .compact-card:focus {
box-shadow: #0008 0 0 10px;
}
.compact-card:active {
box-shadow: #0000 0 0 10px;
}
} }

21
dist/styles/feeds.css vendored
View File

@ -126,11 +126,25 @@
overflow: hidden scroll; overflow: hidden scroll;
position: relative; position: relative;
} }
.list-feed > div.load-more-wrapper { .list-feed > div.load-more-wrapper,
.magazine-feed > div.load-more-wrapper,
.compact-feed > div.load-more-wrapper {
text-align: center; text-align: center;
padding: 16px 0; padding: 16px 0;
} }
.magazine-feed, .compact-feed {
padding-top: 28px;
height: calc(100% - 60px);
overflow: hidden scroll;
margin-top: var(--navHeight);
}
.magazine-feed .ms-List-page {
display: flex;
flex-direction: column;
align-items: center;
}
.cards-feed-container { .cards-feed-container {
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -155,7 +169,10 @@
.flex-fix { .flex-fix {
min-width: 280px; min-width: 280px;
} }
.cards-feed-container > .empty, .list-feed > .empty { .cards-feed-container > .empty,
.list-feed > .empty,
.magazine-feed > .empty,
.compact-feed > .empty {
width: 100%; width: 100%;
height: calc(100vh - 64px); height: calc(100vh - 64px);
display: flex; display: flex;

View File

@ -0,0 +1,29 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Time from "../utils/time"
const className = (props: Card.Props) => {
let cn = ["card", "compact-card"]
if (props.item.hasRead) cn.push("read")
return cn.join(" ")
}
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
data-iid={props.item._id}
data-is-focusable>
<CardInfo source={props.source} item={props.item} hideTime />
<div className="data">
<span className="title">{props.item.title}</span>
<span className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</span>
</div>
<Time date={props.item.date} />
</div>
)
export default CompactCard

View File

@ -3,7 +3,7 @@ import { Card } from "./card"
import CardInfo from "./info" import CardInfo from "./info"
const className = (props: Card.Props) => { const className = (props: Card.Props) => {
let cn = ["card"] let cn = ["card", "default-card"]
if (props.item.snippet && props.item.thumb) cn.push("transform") if (props.item.snippet && props.item.thumb) cn.push("transform")
if (props.item.hidden) cn.push("hidden") if (props.item.hidden) cn.push("hidden")
return cn.join(" ") return cn.join(" ")

View File

@ -6,13 +6,14 @@ import { RSSItem } from "../../scripts/models/item"
type CardInfoProps = { type CardInfoProps = {
source: RSSSource source: RSSSource
item: RSSItem item: RSSItem
hideTime?: boolean
} }
const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => ( const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
<p className="info"> <p className="info">
{props.source.iconurl ? <img src={props.source.iconurl} /> : null} {props.source.iconurl ? <img src={props.source.iconurl} /> : null}
<span className="name">{props.source.name}</span> <span className="name">{props.source.name}</span>
<Time date={props.item.date} /> {props.hideTime ? null : <Time date={props.item.date} />}
{props.item.hasRead ? null : <span className="read-indicator"></span>} {props.item.hasRead ? null : <span className="read-indicator"></span>}
{props.item.starred ? <span className="starred-indicator"></span> : null} {props.item.starred ? <span className="starred-indicator"></span> : null}
</p> </p>

View File

@ -3,7 +3,7 @@ import { Card } from "./card"
import CardInfo from "./info" import CardInfo from "./info"
const className = (props: Card.Props) => { const className = (props: Card.Props) => {
let cn = ["list-card"] let cn = ["card", "list-card"]
if (props.item.hidden) cn.push("hidden") if (props.item.hidden) cn.push("hidden")
return cn.join(" ") return cn.join(" ")
} }

View File

@ -0,0 +1,33 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
const className = (props: Card.Props) => {
let cn = ["card", "magazine-card"]
if (props.item.hasRead) cn.push("read")
if (props.item.hidden) cn.push("hidden")
return cn.join(" ")
}
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
<div className="head"><img src={props.item.thumb} /></div>
) : null}
<div className="data">
<div>
<h3 className="title">{props.item.title}</h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
</div>
<CardInfo source={props.source} item={props.item} />
</div>
</div>
)
export default MagazineCard

View File

@ -168,6 +168,22 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
checked: this.props.viewType === ViewType.List, checked: this.props.viewType === ViewType.List,
onClick: () => this.props.switchView(ViewType.List) onClick: () => this.props.switchView(ViewType.List)
}, },
{
key: "magazineView",
text: intl.get("context.magazineView"),
iconProps: { iconName: "Articles" },
canCheck: true,
checked: this.props.viewType === ViewType.Magazine,
onClick: () => this.props.switchView(ViewType.Magazine)
},
{
key: "compactView",
text: intl.get("context.compactView"),
iconProps: { iconName: "BulletedList" },
canCheck: true,
checked: this.props.viewType === ViewType.Compact,
onClick: () => this.props.switchView(ViewType.Compact)
},
] ]
} }
}, },

View File

@ -24,6 +24,8 @@ export class Feed extends React.Component<FeedProps> {
case (ViewType.Cards): return ( case (ViewType.Cards): return (
<CardsFeed {...this.props} /> <CardsFeed {...this.props} />
) )
case (ViewType.Magazine):
case (ViewType.Compact):
case (ViewType.List): return ( case (ViewType.List): return (
<ListFeed {...this.props} /> <ListFeed {...this.props} />
) )

View File

@ -1,27 +1,49 @@
import * as React from "react" import * as React from "react"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { FeedProps } from "./feed" import { FeedProps } from "./feed"
import { DefaultButton, FocusZone, FocusZoneDirection, List } from 'office-ui-fabric-react'; import { PrimaryButton, FocusZone, FocusZoneDirection, List } from 'office-ui-fabric-react';
import ListCard from "../cards/list-card";
import { RSSItem } from "../../scripts/models/item"; import { RSSItem } from "../../scripts/models/item";
import { AnimationClassNames } from "@fluentui/react"; import { AnimationClassNames } from "@fluentui/react";
import { ViewType } from "../../schema-types";
import ListCard from "../cards/list-card";
import MagazineCard from "../cards/magazine-card";
import CompactCard from "../cards/compact-card";
class ListFeed extends React.Component<FeedProps> { class ListFeed extends React.Component<FeedProps> {
onRenderItem = (item: RSSItem) => ( onRenderItem = (item: RSSItem) => {
<ListCard const props = {
feedId={this.props.feed._id} feedId: this.props.feed._id,
key={item._id} key: item._id,
item={item} item: item,
source={this.props.sourceMap[item.source]} source: this.props.sourceMap[item.source],
shortcuts={this.props.shortcuts} shortcuts: this.props.shortcuts,
markRead={this.props.markRead} markRead: this.props.markRead,
contextMenu={this.props.contextMenu} contextMenu: this.props.contextMenu,
showItem={this.props.showItem} /> showItem: this.props.showItem,
) }
switch (this.props.viewType) {
case (ViewType.Magazine): return <MagazineCard {...props} />
case (ViewType.Compact): return <CompactCard {...props} />
default: return <ListCard {...props} />
}
}
getClassName = () => {
switch (this.props.viewType) {
case (ViewType.Magazine): return "magazine-feed"
case (ViewType.Compact): return "compact-feed"
default: return "list-feed"
}
}
render() { render() {
return this.props.feed.loaded && ( return this.props.feed.loaded && (
<FocusZone as="div" id="refocus" direction={FocusZoneDirection.vertical} className="list-feed" data-is-scrollable> <FocusZone as="div"
id="refocus"
direction={FocusZoneDirection.vertical}
className={this.getClassName()}
data-is-scrollable>
<List <List
className={AnimationClassNames.slideUpIn10} className={AnimationClassNames.slideUpIn10}
items={this.props.items} items={this.props.items}
@ -29,7 +51,7 @@ class ListFeed extends React.Component<FeedProps> {
usePageCache /> usePageCache />
{ {
(this.props.feed.loaded && !this.props.feed.allLoaded) (this.props.feed.loaded && !this.props.feed.allLoaded)
? <div className="load-more-wrapper"><DefaultButton ? <div className="load-more-wrapper"><PrimaryButton
text={intl.get("loadMore")} text={intl.get("loadMore")}
disabled={this.props.feed.loading} disabled={this.props.feed.loading}
onClick={() => this.props.loadMore(this.props.feed)} /></div> onClick={() => this.props.loadMore(this.props.feed)} /></div>

View File

@ -25,14 +25,14 @@ class Page extends React.Component<PageProps> {
prevItem = (event: React.MouseEvent) => this.offsetItem(event, -1) prevItem = (event: React.MouseEvent) => this.offsetItem(event, -1)
nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1) nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1)
render = () => this.props.viewType == ViewType.Cards render = () => this.props.viewType !== ViewType.List
? ( ? (
<> <>
{this.props.settingsOn ? null : {this.props.settingsOn ? null :
<div key="card" className={"main" + (this.props.menuOn ? " menu-on" : "")}> <div key="card" className={"main" + (this.props.menuOn ? " menu-on" : "")}>
<ArticleSearch /> <ArticleSearch />
{this.props.feeds.map(fid => ( {this.props.feeds.map(fid => (
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} /> <FeedContainer viewType={this.props.viewType} feedId={fid} key={fid + this.props.viewType} />
))} ))}
</div>} </div>}
{this.props.itemId && ( {this.props.itemId && (

View File

@ -18,11 +18,11 @@ export class SourceGroup {
} }
} }
export enum ViewType { export const enum ViewType {
Cards, List, Customized Cards, List, Magazine, Compact, Customized
} }
export enum ThemeSettings { export const enum ThemeSettings {
Default = "system", Default = "system",
Light = "light", Light = "light",
Dark = "dark" Dark = "dark"

View File

@ -74,6 +74,8 @@
"view": "View", "view": "View",
"cardView": "Card view", "cardView": "Card view",
"listView": "List view", "listView": "List view",
"magazineView": "Magazine view",
"compactView": "Compact view",
"filter": "Filtering", "filter": "Filtering",
"unreadOnly": "Unread only", "unreadOnly": "Unread only",
"starredOnly": "Starred only", "starredOnly": "Starred only",

View File

@ -68,6 +68,8 @@
"view": "Ver", "view": "Ver",
"cardView": "Vista en modo tarjeta", "cardView": "Vista en modo tarjeta",
"listView": "Vista en modo listado", "listView": "Vista en modo listado",
"magazineView": "Vista en modo revista",
"compactView": "Vista en modo compacta",
"filter": "Filtrando", "filter": "Filtrando",
"unreadOnly": "Solo no leídos", "unreadOnly": "Solo no leídos",
"starredOnly": "Solo destacados", "starredOnly": "Solo destacados",

View File

@ -67,7 +67,9 @@
"search": "Rechercher \"{text}\" sur Google", "search": "Rechercher \"{text}\" sur Google",
"view": "Affichage", "view": "Affichage",
"cardView": "Vue par carte", "cardView": "Vue par carte",
"listView": "vue par liste", "listView": "Vue par liste",
"magazineView": "Vue par magazine",
"compactView": "Vue par compact",
"filter": "Filtrer", "filter": "Filtrer",
"unreadOnly": "Non lu uniquement", "unreadOnly": "Non lu uniquement",
"starredOnly": "Favoris uniquement", "starredOnly": "Favoris uniquement",

View File

@ -74,6 +74,8 @@
"view": "视图", "view": "视图",
"cardView": "卡片视图", "cardView": "卡片视图",
"listView": "列表视图", "listView": "列表视图",
"magazineView": "杂志视图",
"compactView": "紧凑视图",
"filter": "筛选", "filter": "筛选",
"unreadOnly": "仅未读文章", "unreadOnly": "仅未读文章",
"starredOnly": "仅星标文章", "starredOnly": "仅星标文章",