From 05d70ff73a0c182f876a3060dd2dfabb965111e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9=E8=BF=9C?= Date: Fri, 17 Jul 2020 13:01:29 +0800 Subject: [PATCH] add magazine and compact views --- dist/styles/cards.css | 288 +++++++++++++++++-------- dist/styles/dark.css | 20 +- dist/styles/feeds.css | 21 +- src/components/cards/compact-card.tsx | 29 +++ src/components/cards/default-card.tsx | 2 +- src/components/cards/info.tsx | 3 +- src/components/cards/list-card.tsx | 2 +- src/components/cards/magazine-card.tsx | 33 +++ src/components/context-menu.tsx | 16 ++ src/components/feeds/feed.tsx | 2 + src/components/feeds/list-feed.tsx | 52 +++-- src/components/page.tsx | 4 +- src/schema-types.ts | 6 +- src/scripts/i18n/en-US.json | 2 + src/scripts/i18n/es.json | 2 + src/scripts/i18n/fr-FR.json | 4 +- src/scripts/i18n/zh-CN.json | 2 + 17 files changed, 370 insertions(+), 118 deletions(-) create mode 100644 src/components/cards/compact-card.tsx create mode 100644 src/components/cards/magazine-card.tsx diff --git a/dist/styles/cards.css b/dist/styles/cards.css index 3226a0e..5d5b5f6 100644 --- a/dist/styles/cards.css +++ b/dist/styles/cards.css @@ -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; @@ -202,4 +200,118 @@ overflow: hidden; 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; } \ No newline at end of file diff --git a/dist/styles/dark.css b/dist/styles/dark.css index cccf47b..10526d5 100644 --- a/dist/styles/dark.css +++ b/dist/styles/dark.css @@ -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; + } } \ No newline at end of file diff --git a/dist/styles/feeds.css b/dist/styles/feeds.css index 074b3df..d63ad06 100644 --- a/dist/styles/feeds.css +++ b/dist/styles/feeds.css @@ -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; diff --git a/src/components/cards/compact-card.tsx b/src/components/cards/compact-card.tsx new file mode 100644 index 0000000..e3ca668 --- /dev/null +++ b/src/components/cards/compact-card.tsx @@ -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 = (props) => ( +
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.title} + {props.item.snippet.slice(0, 325)} +
+
+) + +export default CompactCard \ No newline at end of file diff --git a/src/components/cards/default-card.tsx b/src/components/cards/default-card.tsx index 9242420..5f25e6a 100644 --- a/src/components/cards/default-card.tsx +++ b/src/components/cards/default-card.tsx @@ -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(" ") diff --git a/src/components/cards/info.tsx b/src/components/cards/info.tsx index 0ba4008..c69ca27 100644 --- a/src/components/cards/info.tsx +++ b/src/components/cards/info.tsx @@ -6,13 +6,14 @@ import { RSSItem } from "../../scripts/models/item" type CardInfoProps = { source: RSSSource item: RSSItem + hideTime?: boolean } const CardInfo: React.FunctionComponent = (props) => (

{props.source.iconurl ? : null} {props.source.name} -

diff --git a/src/components/cards/list-card.tsx b/src/components/cards/list-card.tsx index 8473b40..ad6c697 100644 --- a/src/components/cards/list-card.tsx +++ b/src/components/cards/list-card.tsx @@ -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(" ") } diff --git a/src/components/cards/magazine-card.tsx b/src/components/cards/magazine-card.tsx new file mode 100644 index 0000000..ac7ea5d --- /dev/null +++ b/src/components/cards/magazine-card.tsx @@ -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 = (props) => ( +
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 ? ( +
+ ) : null} +
+
+

{props.item.title}

+

{props.item.snippet.slice(0, 325)}

+
+ +
+
+) + +export default MagazineCard \ No newline at end of file diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index c021151..f1115ed 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -168,6 +168,22 @@ export class ContextMenu extends React.Component { 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) + }, ] } }, diff --git a/src/components/feeds/feed.tsx b/src/components/feeds/feed.tsx index a5233b5..ae6ed6e 100644 --- a/src/components/feeds/feed.tsx +++ b/src/components/feeds/feed.tsx @@ -24,6 +24,8 @@ export class Feed extends React.Component { case (ViewType.Cards): return ( ) + case (ViewType.Magazine): + case (ViewType.Compact): case (ViewType.List): return ( ) diff --git a/src/components/feeds/list-feed.tsx b/src/components/feeds/list-feed.tsx index 882227c..74a8629 100644 --- a/src/components/feeds/list-feed.tsx +++ b/src/components/feeds/list-feed.tsx @@ -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 { - onRenderItem = (item: RSSItem) => ( - - ) + 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 + case (ViewType.Compact): return + default: return + } + } + + 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 && ( - + { usePageCache /> { (this.props.feed.loaded && !this.props.feed.allLoaded) - ?
this.props.loadMore(this.props.feed)} />
diff --git a/src/components/page.tsx b/src/components/page.tsx index 94e1563..f60cc8f 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -25,14 +25,14 @@ class Page extends React.Component { 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 :
{this.props.feeds.map(fid => ( - + ))}
} {this.props.itemId && ( diff --git a/src/schema-types.ts b/src/schema-types.ts index 4298cea..0aaad8d 100644 --- a/src/schema-types.ts +++ b/src/schema-types.ts @@ -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" diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index e805bd8..28d6026 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -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", diff --git a/src/scripts/i18n/es.json b/src/scripts/i18n/es.json index 69e485a..6b465dc 100644 --- a/src/scripts/i18n/es.json +++ b/src/scripts/i18n/es.json @@ -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", diff --git a/src/scripts/i18n/fr-FR.json b/src/scripts/i18n/fr-FR.json index 0ed0e92..f43ad1e 100644 --- a/src/scripts/i18n/fr-FR.json +++ b/src/scripts/i18n/fr-FR.json @@ -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", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index d6c43d2..ee7f203 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -74,6 +74,8 @@ "view": "视图", "cardView": "卡片视图", "listView": "列表视图", + "magazineView": "杂志视图", + "compactView": "紧凑视图", "filter": "筛选", "unreadOnly": "仅未读文章", "starredOnly": "仅星标文章",