diff --git a/dist/article/article.css b/dist/article/article.css new file mode 100644 index 0000000..06faa55 --- /dev/null +++ b/dist/article/article.css @@ -0,0 +1,36 @@ +html, body { + font-family: "Segoe UI Regular", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif; + margin: 16px 48px; + overflow-x: hidden; +} + +a { + color: #0078d4; + text-decoration: none; +} +a:hover, a:active { + color: #004578; + text-decoration: underline; +} + +#main > p.title { + font-size: 1.25rem; + line-height: 1.75rem; + font-weight: 600; +} + +article { + line-height: 1.6; +} +article img { + max-width: 100%; + height: auto; +} +article figure { + margin: 16px 0; + text-align: center; +} +article figure figcaption { + font-size: .875rem; + color: #484644; +} \ No newline at end of file diff --git a/dist/article/article.html b/dist/article/article.html new file mode 100644 index 0000000..8314b32 --- /dev/null +++ b/dist/article/article.html @@ -0,0 +1,14 @@ + + + + + + Hello World! + + + +
+ + + \ No newline at end of file diff --git a/dist/article/article.js b/dist/article/article.js new file mode 100644 index 0000000..86cd60f --- /dev/null +++ b/dist/article/article.js @@ -0,0 +1,10 @@ +function get(name) { + if (name = (new RegExp('[?&]' + encodeURIComponent(name) + '=([^&]*)')).exec(location.search)) + return decodeURIComponent(name[1]); +} +let main = document.getElementById("main") +main.innerHTML = decodeURIComponent(window.atob(get("h"))) +document.addEventListener("click", event => { + event.preventDefault() + if (event.target.href) ipcRenderer.sendToHost("request-navigation", event.target.href) +}) \ No newline at end of file diff --git a/dist/article/preload.js b/dist/article/preload.js new file mode 100644 index 0000000..82734e1 --- /dev/null +++ b/dist/article/preload.js @@ -0,0 +1 @@ +global.ipcRenderer = require("electron").ipcRenderer \ No newline at end of file diff --git a/dist/styles.css b/dist/styles.css index d491afd..6d69d31 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -117,7 +117,7 @@ nav.menu-on .btn-group .btn, nav.hide-btns .btn-group .btn { nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system { display: inline-block; } -.btn-group .btn.system.menu-on { +nav.menu-on .btn-group .btn.system { color: #fff; } .btn-group .btn:hover { @@ -139,17 +139,17 @@ nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system { } .btn-group .btn.close:hover { background-color: #e81123; - color: #fff; + color: #fff !important; } .btn-group .btn.close:active { background-color: #f1707a; - color: #fff; + color: #fff !important; } .btn-group .btn.inline-block-wide { display: none; } -.menu-container { +.menu-container, .article-container { position: fixed; z-index: 5; left: 0; @@ -159,6 +159,10 @@ nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system { background-color: #0008; backdrop-filter: saturate(150%) blur(20px); } +.article-container { + z-index: 6; + background-color: #fff6; +} .menu-container .menu { position: absolute; left: 0; @@ -266,7 +270,7 @@ img.favicon { nav.menu-on .btn-group .btn { display: inline-block; } - .btn-group .btn.system.menu-on { + nav.menu-on .btn-group .btn.system { color: #000; } .menu-container { @@ -298,16 +302,29 @@ img.favicon { } } +.article-wrapper { + margin: 32px auto 0; + width: 860px; + height: calc(100% - 50px); + background-color: #fff; + box-shadow: 0 6.4px 14.4px 0 rgba(0,0,0,.132), 0 1.2px 3.6px 0 rgba(0,0,0,.108); + border-radius: 5px; + overflow: hidden; +} +.article webview { + width: 100%; + height: calc(100vh - 50px); + border: none; +} + .cards-feed-container { display: inline-flex; flex-wrap: wrap; - justify-content: center; + justify-content: space-around; padding: 12px; } -.cards-feed-container > div { - flex-grow: 1; - display: inline-flex; - justify-content: center; +.cards-feed-container > div.load-more-wrapper, .flex-fix { + text-align: center; } .cards-feed-container > div.load-more-wrapper { width: 100%; diff --git a/src/components/article.tsx b/src/components/article.tsx new file mode 100644 index 0000000..f27a8e5 --- /dev/null +++ b/src/components/article.tsx @@ -0,0 +1,50 @@ +import * as React from "react" +import { renderToString } from "react-dom/server" +import { RSSItem } from "../scripts/models/item" +import { openExternal } from "../scripts/utils" + +type ArticleProps = { + item: RSSItem + dismiss: () => void +} + +class Article extends React.Component { + webview: HTMLWebViewElement + + constructor(props) { + super(props) + } + + ipcHandler = event => { + if (event.channel === "request-navigation") { + openExternal(event.args[0]) + } + } + + componentDidMount = () => { + this.webview = document.getElementById("article") + this.webview.addEventListener("ipc-message", this.ipcHandler) + this.webview.addEventListener("will-navigate", this.props.dismiss) + } + + componentWillUnmount = () => { + this.webview.removeEventListener("ipc-message", this.ipcHandler) + this.webview.removeEventListener("will-navigate", this.props.dismiss) + } + + articleView = () => "article/article.html?h=" + window.btoa(encodeURIComponent(renderToString(<> +

{this.props.item.title}

+
+ ))) + + render = () => ( +
+ +
+ ) +} + +export default Article \ No newline at end of file diff --git a/src/components/cards/card.tsx b/src/components/cards/card.tsx index 1ddad6c..23dac05 100644 --- a/src/components/cards/card.tsx +++ b/src/components/cards/card.tsx @@ -2,12 +2,16 @@ import * as React from "react" import { openExternal } from "../../scripts/utils" import { RSSSource } from "../../scripts/models/source" import { RSSItem } from "../../scripts/models/item" +import { FeedIdType } from "../../scripts/models/feed" export interface CardProps { + feedId: FeedIdType + index: number item: RSSItem source: RSSSource - markRead: Function - contextMenu: Function + markRead: (item: RSSItem) => void + contextMenu: (item: RSSItem, e) => void + showItem: (fid: FeedIdType, index: number) => void } export class Card extends React.Component { @@ -19,7 +23,8 @@ export class Card extends React.Component { onClick = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() - this.openInBrowser() + this.props.markRead(this.props.item) + this.props.showItem(this.props.feedId, this.props.index) } onMouseUp = (e: React.MouseEvent) => { diff --git a/src/components/feeds/cards-feed.tsx b/src/components/feeds/cards-feed.tsx index bad3186..f5f4cf1 100644 --- a/src/components/feeds/cards-feed.tsx +++ b/src/components/feeds/cards-feed.tsx @@ -32,14 +32,16 @@ class CardsFeed extends Feed { return this.props.feed.loaded && (
{ - this.props.items.map(item => ( -
- -
+ this.props.items.map((item, index) => ( + )) } { this.flexFix() } diff --git a/src/components/feeds/feed.tsx b/src/components/feeds/feed.tsx index aa01ef9..d920168 100644 --- a/src/components/feeds/feed.tsx +++ b/src/components/feeds/feed.tsx @@ -1,15 +1,16 @@ import * as React from "react" import { RSSItem } from "../../scripts/models/item" import { FeedReduxProps } from "../../containers/feed-container" -import { RSSFeed } from "../../scripts/models/feed" +import { RSSFeed, FeedIdType } from "../../scripts/models/feed" type FeedProps = FeedReduxProps & { feed: RSSFeed items: RSSItem[] sourceMap: Object - markRead: Function - contextMenu: Function - loadMore: Function + markRead: (item: RSSItem) => void + contextMenu: (item: RSSItem, e) => void + loadMore: (feed: RSSFeed) => void + showItem: (fid: FeedIdType, index: number) => void } export class Feed extends React.Component { } \ No newline at end of file diff --git a/src/components/menu.tsx b/src/components/menu.tsx index be6e08d..2d58b8a 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -8,6 +8,7 @@ import { AnimationClassNames } from "@fluentui/react" export type MenuProps = { status: boolean, + display: boolean, selected: string, sources: SourceState, groups: SourceGroup[], @@ -73,8 +74,8 @@ export class Menu extends React.Component { }) render() { - return this.props.status ? ( -
+ return this.props.status && ( +
e.stopPropagation()}>
@@ -91,6 +92,6 @@ export class Menu extends React.Component {
- ) : null + ) } } \ No newline at end of file diff --git a/src/components/nav.tsx b/src/components/nav.tsx index 52a6c7d..2d18731 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -79,11 +79,11 @@ class Nav extends React.Component { - - + + {this.state.maximized ? :} - +
{!this.canFetch() && void } class Page extends React.Component { @@ -17,6 +23,13 @@ class Page extends React.Component { ))}
} + {this.props.item && ( +
+
e.stopPropagation()}> +
+
+
+ )} ) } diff --git a/src/containers/feed-container.tsx b/src/containers/feed-container.tsx index 8678cc1..5209cad 100644 --- a/src/containers/feed-container.tsx +++ b/src/containers/feed-container.tsx @@ -2,9 +2,10 @@ import { connect } from "react-redux" import { createSelector } from "reselect" import { RootState } from "../scripts/reducer" import CardsFeed from "../components/feeds/cards-feed" -import { markRead } from "../scripts/models/item" +import { markRead, RSSItem } from "../scripts/models/item" import { openItemMenu } from "../scripts/models/app" -import { FeedIdType, loadMore } from "../scripts/models/feed" +import { FeedIdType, loadMore, RSSFeed } from "../scripts/models/feed" +import { showItem } from "../scripts/models/page" interface FeedContainerProps { feedId: FeedIdType @@ -26,9 +27,10 @@ const makeMapStateToProps = () => { } const mapDispatchToProps = dispatch => { return { - markRead: item => dispatch(markRead(item)), - contextMenu: (item, e) => dispatch(openItemMenu(item, e)), - loadMore: feed => dispatch(loadMore(feed)) + markRead: (item: RSSItem) => dispatch(markRead(item)), + contextMenu: (item: RSSItem, e) => dispatch(openItemMenu(item, e)), + loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)), + showItem: (fid: FeedIdType, index: number) => dispatch(showItem(fid, index)) } } diff --git a/src/containers/menu-container.tsx b/src/containers/menu-container.tsx index bf2ffe0..dabe502 100644 --- a/src/containers/menu-container.tsx +++ b/src/containers/menu-container.tsx @@ -8,16 +8,16 @@ import { selectAllArticles, selectSources } from "../scripts/models/page" import { initFeeds } from "../scripts/models/feed" import { RSSSource } from "../scripts/models/source" -const getStatus = (state: RootState) => state.app.menu && state.app.sourceInit -const getKey = (state: RootState) => state.app.menuKey +const getApp = (state: RootState) => state.app const getSources = (state: RootState) => state.sources const getGroups = (state: RootState) => state.groups const mapStateToProps = createSelector( - [getStatus, getKey, getSources, getGroups], - (status, key, sources, groups) => ({ - status: status, - selected: key, + [getApp, getSources, getGroups], + (app, sources, groups) => ({ + status: app.sourceInit, + display: app.menu, + selected: app.menuKey, sources: sources, groups: groups }) diff --git a/src/containers/page-container.tsx b/src/containers/page-container.tsx index 5ab442a..3368aa3 100644 --- a/src/containers/page-container.tsx +++ b/src/containers/page-container.tsx @@ -2,19 +2,30 @@ import { connect } from "react-redux" import { createSelector } from "reselect" import { RootState } from "../scripts/reducer" import Page from "../components/page" +import { AppDispatch } from "../scripts/utils" +import { dismissItem } from "../scripts/models/page" -const getFeeds = (state: RootState) => state.page.feedId +const getPage = (state: RootState) => state.page const getSettings = (state: RootState) => state.app.settings.display const getMenu = (state: RootState) => state.app.menu +const getItems = (state: RootState) => state.items +const getFeeds = (state: RootState) => state.feeds const mapStateToProps = createSelector( - [getFeeds, getSettings, getMenu], - (feeds, settingsOn, menuOn) => ({ - feeds: [feeds], + [getPage, getSettings, getMenu, getItems, getFeeds], + (page, settingsOn, menuOn, items, feeds) => ({ + feeds: [page.feedId], settingsOn: settingsOn, - menuOn: menuOn + menuOn: menuOn, + item: page.itemIndex >= 0 // && page.itemIndex < feeds[page.feedId].iids.length + ? items[feeds[page.feedId].iids[page.itemIndex]] + : null }) ) -const PageContainer = connect(mapStateToProps)(Page) +const mapDispatchToProps = (dispatch: AppDispatch) => ({ + dismissItem: () => dispatch(dismissItem()) +}) + +const PageContainer = connect(mapStateToProps, mapDispatchToProps)(Page) export default PageContainer \ No newline at end of file diff --git a/src/containers/settings/sources-container.tsx b/src/containers/settings/sources-container.tsx index a6e1ad3..db2efb6 100644 --- a/src/containers/settings/sources-container.tsx +++ b/src/containers/settings/sources-container.tsx @@ -5,6 +5,7 @@ import { RootState } from "../../scripts/reducer" import SourcesTab from "../../components/settings/sources" import { addSource, RSSSource, updateSource, deleteSource } from "../../scripts/models/source" import { importOPML } from "../../scripts/models/group" +import { AppDispatch } from "../../scripts/utils" const getSources = (state: RootState) => state.sources @@ -15,7 +16,7 @@ const mapStateToProps = createSelector( }) ) -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch: AppDispatch) => { return { addSource: (url: string) => dispatch(addSource(url)), updateSourceName: (source: RSSSource, name: string) => { @@ -30,7 +31,7 @@ const mapDispatchToProps = dispatch => { properties: ["openFile"] } ) - if (path.length > 0) dispatch(importOPML(path[0])) + if (path && path.length > 0) dispatch(importOPML(path[0])) } } } diff --git a/src/electron.ts b/src/electron.ts index da74fe2..da62e3b 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -19,7 +19,8 @@ function createWindow() { minHeight: 600, frame: false, webPreferences: { - nodeIntegration: true + nodeIntegration: true, + webviewTag: true } }) mainWindowState.manage(mainWindow) diff --git a/src/index.html b/src/index.html index 0b72c16..0753f9e 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - + Hello World! diff --git a/src/scripts/models/page.ts b/src/scripts/models/page.ts index ea49ff3..5a7fddc 100644 --- a/src/scripts/models/page.ts +++ b/src/scripts/models/page.ts @@ -2,6 +2,8 @@ import { ALL, SOURCE, FeedIdType } from "./feed" import { getWindowBreakpoint } from "../utils" export const SELECT_PAGE = "SELECT_PAGE" +export const SHOW_ITEM = "SHOW_ITEM" +export const DISMISS_ITEM = "DISMISS_ITEM" export enum PageType { AllArticles, Sources, Page @@ -17,9 +19,17 @@ interface SelectPageAction { title?: string } -export type PageActionTypes = SelectPageAction +interface ShowItemAction { + type: typeof SHOW_ITEM + feedId: FeedIdType + itemIndex: number +} -export function selectAllArticles(init = false): SelectPageAction { +interface DismissItemAction { type: typeof DISMISS_ITEM } + +export type PageActionTypes = SelectPageAction | ShowItemAction | DismissItemAction + +export function selectAllArticles(init = false): PageActionTypes { return { type: SELECT_PAGE, keepMenu: getWindowBreakpoint(), @@ -28,7 +38,7 @@ export function selectAllArticles(init = false): SelectPageAction { } } -export function selectSources(sids: number[], menuKey: string, title: string) { +export function selectSources(sids: number[], menuKey: string, title: string): PageActionTypes { return { type: SELECT_PAGE, pageType: PageType.Sources, @@ -40,8 +50,19 @@ export function selectSources(sids: number[], menuKey: string, title: string) { } } +export function showItem(feedId: FeedIdType, itemIndex: number): PageActionTypes { + return { + type: SHOW_ITEM, + feedId: feedId, + itemIndex: itemIndex + } +} + +export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM }) + export class PageState { - feedId = ALL + feedId = ALL as FeedIdType + itemIndex = -1 } export function pageReducer( @@ -61,6 +82,14 @@ export function pageReducer( } default: return state } + case SHOW_ITEM: return { + ...state, + itemIndex: action.itemIndex + } + case DISMISS_ITEM: return { + ...state, + itemIndex: -1 + } default: return state } } \ No newline at end of file