diff --git a/dist/article/article.css b/dist/article/article.css index 1351e00..ac8a255 100644 --- a/dist/article/article.css +++ b/dist/article/article.css @@ -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; diff --git a/dist/icons/fabric-icons-15-3807251b.woff b/dist/icons/fabric-icons-15-3807251b.woff new file mode 100644 index 0000000..7ddc486 Binary files /dev/null and b/dist/icons/fabric-icons-15-3807251b.woff differ diff --git a/dist/styles.css b/dist/styles.css index dd10423..2d3b238 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -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; -} \ No newline at end of file +} + +.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; +} diff --git a/package.json b/package.json index 5d6d14d..331f46e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/article.tsx b/src/components/article.tsx index d031f78..a9fc52e 100644 --- a/src/components/article.tsx +++ b/src/components/article.tsx @@ -152,7 +152,7 @@ class Article extends React.Component { diff --git a/src/components/cards/list-card.tsx b/src/components/cards/list-card.tsx new file mode 100644 index 0000000..80580f1 --- /dev/null +++ b/src/components/cards/list-card.tsx @@ -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 ( +
+ {this.props.item.thumb ? ( +
+ ) : null} +
+

+ {this.props.source.iconurl ? : null} + {this.props.source.name} +

+

{this.props.item.title}

+
+
+ ) + } +} + +export default ListCard \ No newline at end of file diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index f6072f1..bd9031d 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -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 { 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 [] } } diff --git a/src/components/feeds/cards-feed.tsx b/src/components/feeds/cards-feed.tsx index b5ddb71..854ba11 100644 --- a/src/components/feeds/cards-feed.tsx +++ b/src/components/feeds/cards-feed.tsx @@ -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 { state = { width: window.innerWidth - 12 } updateWidth = () => { diff --git a/src/components/feeds/feed.tsx b/src/components/feeds/feed.tsx index 024697d..8251805 100644 --- a/src/components/feeds/feed.tsx +++ b/src/components/feeds/feed.tsx @@ -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 { } \ No newline at end of file +export class Feed extends React.Component { + render() { + switch (this.props.viewType) { + case (ViewType.Cards): return ( + + ) + case (ViewType.List): return ( + + ) + } + } +} \ No newline at end of file diff --git a/src/components/feeds/list-feed.tsx b/src/components/feeds/list-feed.tsx new file mode 100644 index 0000000..b1b5ecb --- /dev/null +++ b/src/components/feeds/list-feed.tsx @@ -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 { + render() { + return this.props.feed.loaded && ( +
+ { + this.props.items.map((item) => ( + + )) + } + { + (this.props.feed.loaded && !this.props.feed.allLoaded) + ?
this.props.loadMore(this.props.feed)} />
+ : null + } +
+ ) + } +} + +export default ListFeed \ No newline at end of file diff --git a/src/components/nav.tsx b/src/components/nav.tsx index 92223e5..0a1cabb 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -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 { 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 { {this.props.state.logMenu.notify ? : } - + {if (this.props.state.contextMenu.event === "#view-toggle") e.stopPropagation()}}> + diff --git a/src/components/page.tsx b/src/components/page.tsx index 13ad4ce..02bb92e 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -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 { this.props.offsetItem(1) } - render = () => ( + render = () => this.props.viewType == ViewType.Cards + ? ( <> {this.props.settingsOn ? null :
{this.props.feeds.map(fid => ( - + ))}
} {this.props.itemId >= 0 && ( @@ -42,6 +45,23 @@ class Page extends React.Component { )} ) + : ( + <> + {this.props.settingsOn ? null : +
+
+ {this.props.feeds.map(fid => ( + + ))} +
+ {this.props.itemId >= 0 && ( +
+ +
+ )} +
} + + ) } export default Page \ No newline at end of file diff --git a/src/components/settings/groups.tsx b/src/components/settings/groups.tsx index df9c80c..7090697 100644 --- a/src/components/settings/groups.tsx +++ b/src/components/settings/groups.tsx @@ -272,6 +272,7 @@ class GroupsTab extends React.Component { selectionMode={SelectionMode.multiple} /> + 拖拽订阅源以排序 } {(!this.state.manageGroup || !this.state.selectedGroup) ?<> diff --git a/src/containers/context-menu-container.tsx b/src/containers/context-menu-container.tsx index 1a1bbba..206fbdc 100644 --- a/src/containers/context-menu-container.tsx +++ b/src/containers/context-menu-container.tsx @@ -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()) } } diff --git a/src/containers/feed-container.tsx b/src/containers/feed-container.tsx index cef66d4..755801f 100644 --- a/src/containers/feed-container.tsx +++ b/src/containers/feed-container.tsx @@ -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) \ No newline at end of file +export const FeedContainer = connector(Feed) \ No newline at end of file diff --git a/src/containers/nav-container.tsx b/src/containers/nav-container.tsx index c2b7a2f..2515526 100644 --- a/src/containers/nav-container.tsx +++ b/src/containers/nav-container.tsx @@ -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()) }) diff --git a/src/containers/page-container.tsx b/src/containers/page-container.tsx index e411201..15cfeed 100644 --- a/src/containers/page-container.tsx +++ b/src/containers/page-container.tsx @@ -15,7 +15,8 @@ const mapStateToProps = createSelector( feeds: [page.feedId], settingsOn: settingsOn, menuOn: menuOn, - itemId: page.itemId + itemId: page.itemId, + viewType: page.viewType }) ) diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index f9276c8..6d56200 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -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 diff --git a/src/scripts/models/page.ts b/src/scripts/models/page.ts index 654d5ed..c01cbc8 100644 --- a/src/scripts/models/page.ts +++ b/src/scripts/models/page.ts @@ -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 diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index 6c69d7d..152d71e 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -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)) +} \ No newline at end of file