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 {
display: inline-block;
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);
user-select: none;
transition: box-shadow linear .08s;
transform: scale(1);
cursor: pointer;
animation-fill-mode: none;
overflow: hidden;
}
.card:focus, .list-card:focus {
.card:focus {
outline: none;
}
.card:hover, .ms-Fabric--isFocusVisible .card:focus {
box-shadow: #0006 0px 5px 40px;
}
.ms-Fabric--isFocusVisible .card:focus::after, .ms-Fabric--isFocusVisible .list-card:focus::after {
.ms-Fabric--isFocusVisible .card:focus::after {
content: "";
position: absolute;
top: 2px;
@ -82,70 +70,7 @@
border: 1px solid var(--white);
outline: 2px solid #0078d4;
}
.card:active {
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 {
.card.hidden::after {
content: "";
display: block;
width: 100%;
@ -156,23 +81,96 @@
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 {
display: flex;
position: relative;
overflow: hidden;
color: var(--neutralDarker);
user-select: none;
transition: box-shadow linear .08s;
border-bottom: 1px solid var(--neutralQuaternaryAlt);
transform: scale(1);
cursor: pointer;
box-shadow: #0000 0px 5px 15px;
box-shadow: #0000 0 5px 15px;
}
.list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus {
box-shadow: #0004 0px 5px 15px;
box-shadow: #0004 0 5px 15px;
}
.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 {
width: 80px;
@ -203,3 +201,117 @@
display: -webkit-box;
-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 {
background-color: #000a;
}
.card {
.default-card {
box-shadow: #0006 0px 5px 20px;
}
.card:hover {
.default-card:hover, .ms-Fabric--isFocusVisible .default-card:focus {
box-shadow: #0008 0px 5px 40px;
}
.card div.bg {
.default-card div.bg {
background-color: #000b;
}
.list-card:hover {
.list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus {
box-shadow: #0006 0px 5px 15px;
}
.list-card:active {
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;
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;
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 {
display: inline-flex;
flex-wrap: wrap;
@ -155,7 +169,10 @@
.flex-fix {
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%;
height: calc(100vh - 64px);
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"
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.hidden) cn.push("hidden")
return cn.join(" ")

View File

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

View File

@ -3,7 +3,7 @@ import { Card } from "./card"
import CardInfo from "./info"
const className = (props: Card.Props) => {
let cn = ["list-card"]
let cn = ["card", "list-card"]
if (props.item.hidden) cn.push("hidden")
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,
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 (
<CardsFeed {...this.props} />
)
case (ViewType.Magazine):
case (ViewType.Compact):
case (ViewType.List): return (
<ListFeed {...this.props} />
)

View File

@ -1,27 +1,49 @@
import * as React from "react"
import intl from "react-intl-universal"
import { FeedProps } from "./feed"
import { DefaultButton, FocusZone, FocusZoneDirection, List } from 'office-ui-fabric-react';
import ListCard from "../cards/list-card";
import { PrimaryButton, FocusZone, FocusZoneDirection, List } from 'office-ui-fabric-react';
import { RSSItem } from "../../scripts/models/item";
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> {
onRenderItem = (item: RSSItem) => (
<ListCard
feedId={this.props.feed._id}
key={item._id}
item={item}
source={this.props.sourceMap[item.source]}
shortcuts={this.props.shortcuts}
markRead={this.props.markRead}
contextMenu={this.props.contextMenu}
showItem={this.props.showItem} />
)
onRenderItem = (item: RSSItem) => {
const props = {
feedId: this.props.feed._id,
key: item._id,
item: item,
source: this.props.sourceMap[item.source],
shortcuts: this.props.shortcuts,
markRead: this.props.markRead,
contextMenu: this.props.contextMenu,
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() {
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
className={AnimationClassNames.slideUpIn10}
items={this.props.items}
@ -29,7 +51,7 @@ class ListFeed extends React.Component<FeedProps> {
usePageCache />
{
(this.props.feed.loaded && !this.props.feed.allLoaded)
? <div className="load-more-wrapper"><DefaultButton
? <div className="load-more-wrapper"><PrimaryButton
text={intl.get("loadMore")}
disabled={this.props.feed.loading}
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)
nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1)
render = () => this.props.viewType == ViewType.Cards
render = () => this.props.viewType !== ViewType.List
? (
<>
{this.props.settingsOn ? null :
<div key="card" className={"main" + (this.props.menuOn ? " menu-on" : "")}>
<ArticleSearch />
{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>}
{this.props.itemId && (

View File

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

View File

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

View File

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

View File

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

View File

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