From 5892334acc88a292c527185db46cf2b79ae403e8 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 11 Sep 2020 22:18:47 -0700 Subject: [PATCH] Download readme --- src/App.css | 8 -- src/Overlay.js | 126 +++++++++++++++++++++++++++--- src/SkinReadme.js | 78 +++++++++--------- src/components/BaseFocusedSkin.js | 2 +- src/components/BaseFocusedSkin.md | 1 - src/components/BaseOverlay.js | 14 ++-- src/components/BaseOverlay.md | 27 ------- src/components/DownloadText.js | 24 ++++++ src/components/Metadata.js | 45 +++++++++-- src/components/Metadata.md | 1 - src/redux/actionCreators.js | 4 + src/redux/epics.js | 25 +++++- src/redux/reducer.js | 7 ++ src/redux/selectors.js | 6 +- src/redux/store.js | 14 ---- 15 files changed, 259 insertions(+), 123 deletions(-) delete mode 100644 src/components/BaseOverlay.md create mode 100644 src/components/DownloadText.js diff --git a/src/App.css b/src/App.css index f8b30cab..8028e7c3 100644 --- a/src/App.css +++ b/src/App.css @@ -188,16 +188,8 @@ body.webamp-loaded #webamp { line-height: 1.4em; } -.readme { - background: white; -} - .readme pre { - padding: 10px; - width: 100%; - height: 100%; white-space: pre-wrap; margin: 0; font-size: 12px; - box-sizing: border-box; } diff --git a/src/Overlay.js b/src/Overlay.js index fce23034..ac54e200 100644 --- a/src/Overlay.js +++ b/src/Overlay.js @@ -1,10 +1,118 @@ -import { connect } from "react-redux"; -import * as ActionCreators from "./redux/actionCreators"; -import BaseOverlay from "./components/BaseOverlay"; +import * as Actions from "./redux/actionCreators"; +import React, { + useEffect, + useCallback, + useLayoutEffect, + useState, +} from "react"; +import ReactDOM from "react-dom"; +import { useActionCreator } from "./hooks"; -const mapDispatchToProps = dispatch => ({ - closeModal() { - dispatch(ActionCreators.closeModal()); - } -}); -export default connect(null, mapDispatchToProps)(BaseOverlay); +function handleTouchMove(e) { + e.preventDefault(); +} + +function Overlay({ shouldAnimate, children }) { + const closeModal = useActionCreator(Actions.closeModal); + const [mounted, setMounted] = useState(); + + useLayoutEffect(() => { + const timeout = setTimeout(() => { + setMounted(true); + }); + const bodyOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = bodyOverflow; + if (timeout != null) { + clearTimeout(timeout); + } + }; + }, []); + + const handleClick = useCallback( + (e) => { + if (e.target === e.currentTarget) { + closeModal(); + } + }, + [closeModal] + ); + + const handleKeyDown = useCallback( + (e) => { + // Esc + if (e.keyCode === 27) { + closeModal(); + } + }, + [closeModal] + ); + + useEffect(() => { + window.document.addEventListener("keydown", handleKeyDown); + window.document.addEventListener("touchmove", handleTouchMove); + return () => { + window.document.removeEventListener("keydown", handleKeyDown); + window.document.removeEventListener("touchmove", handleTouchMove); + }; + }, [handleKeyDown]); + + return ReactDOM.createPortal( +
+
+ { + closeModal(); + e.preventDefault(); + }} + aria-label="Close Modal" + style={{ + color: "#a7a394", + position: "fixed", + top: 10, + right: 10, + padding: 0, + fontSize: 50, + lineHeight: "25px", + textDecoration: "none", + }} + > + × + + {children} +
+
, + window.document.body + ); +} + +export default Overlay; diff --git a/src/SkinReadme.js b/src/SkinReadme.js index f42d967f..cdf7c050 100644 --- a/src/SkinReadme.js +++ b/src/SkinReadme.js @@ -1,55 +1,47 @@ import React from "react"; -import { connect } from "react-redux"; -import * as Actions from "./redux/actionCreators"; +import { createPortal } from "react-dom"; +import { useSelector } from "react-redux"; -class SkinReadme extends React.Component { - render() { - if (this.props.focusedFile == null) { - return null; - } +function SkinReadme() { + const focusedFile = useSelector((state) => state.focusedSkinFile); + if (focusedFile == null) { + return null; + } - const { fileName, content } = this.props.focusedFile; - if (content == null) { - return null; - } - return ( + const { content } = focusedFile; + if (content == null) { + return null; + } + return createPortal( +
-

{fileName}

-
-
-
{content}
-
+
+
{content}
- ); - } +
, + + window.document.body + ); } -function mapStateToProps(state) { - return { - zip: state.skinZip, - focusedFile: state.focusedSkinFile, - }; -} - -function mapDispatchToProps(dispatch) { - return { - selectSkinFile(fileName) { - dispatch(Actions.selectSkinFile(fileName)); - }, - }; -} -export default connect(mapStateToProps, mapDispatchToProps)(SkinReadme); +export default SkinReadme; diff --git a/src/components/BaseFocusedSkin.js b/src/components/BaseFocusedSkin.js index 48357ff9..e0a18597 100644 --- a/src/components/BaseFocusedSkin.js +++ b/src/components/BaseFocusedSkin.js @@ -159,7 +159,6 @@ class BaseFocusedSkin extends React.Component { closeModal={this.props.closeModal} />
- {this.props.fileExplorerOpen && } )}
+ {this.props.fileExplorerOpen && } {}} initialPosition={{ top: 100, left: 100 }} /> )} diff --git a/src/components/BaseOverlay.js b/src/components/BaseOverlay.js index 6e9a023a..cfbc34c9 100644 --- a/src/components/BaseOverlay.js +++ b/src/components/BaseOverlay.js @@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useLayoutEffect, - useState + useState, } from "react"; import ReactDOM from "react-dom"; @@ -28,7 +28,7 @@ function Overlay({ shouldAnimate, closeModal, children }) { }, []); const handleClick = useCallback( - e => { + (e) => { if (e.target === e.currentTarget) { closeModal(); } @@ -37,7 +37,7 @@ function Overlay({ shouldAnimate, closeModal, children }) { ); const handleKeyDown = useCallback( - e => { + (e) => { // Esc if (e.keyCode === 27) { closeModal(); @@ -68,7 +68,7 @@ function Overlay({ shouldAnimate, closeModal, children }) { !shouldAnimate || mounted ? "rgba(0, 0, 0, 0.95)" : "rgba(0, 0, 0, 0)", - transition: "background-color 400ms ease-out" + transition: "background-color 400ms ease-out", }} >
{ + onClick={(e) => { closeModal(); e.preventDefault(); }} @@ -100,7 +100,7 @@ function Overlay({ shouldAnimate, closeModal, children }) { padding: 0, fontSize: 50, lineHeight: "25px", - textDecoration: "none" + textDecoration: "none", }} > × diff --git a/src/components/BaseOverlay.md b/src/components/BaseOverlay.md deleted file mode 100644 index 4cfd5259..00000000 --- a/src/components/BaseOverlay.md +++ /dev/null @@ -1,27 +0,0 @@ -BaseOverlay example: - -```js -const [open, setOpen] = React.useState(false); -const [shouldAnimate, setShouldAnimate] = React.useState(false); -return ( - <> - - {open && setOpen(false)} />} - -); -``` - -BaseOverlay example (animated): - -```js -const [open, setOpen] = React.useState(false); -const [shouldAnimate, setShouldAnimate] = React.useState(false); -return ( - <> - - {open && ( - setOpen(false)} shouldAnimate={true} /> - )} - -); -``` diff --git a/src/components/DownloadText.js b/src/components/DownloadText.js new file mode 100644 index 00000000..36dd4766 --- /dev/null +++ b/src/components/DownloadText.js @@ -0,0 +1,24 @@ +import React, { useLayoutEffect, useState } from "react"; + +function DownloadText({ text, children, ...restProps }) { + const [url, setUrl] = useState(null); + useLayoutEffect(() => { + var blob = new Blob([text], { + type: "text/plain;charset=utf-8", + }); + const url = URL.createObjectURL(blob); + setUrl(url); + return () => { + URL.revokeObjectURL(url); + }; + }, [text]); + + return ( + + {/* We have to explicitly set the children to make ESLint happy */} + {children} + + ); +} + +export default DownloadText; diff --git a/src/components/Metadata.js b/src/components/Metadata.js index b0468a7e..b29a5fe4 100644 --- a/src/components/Metadata.js +++ b/src/components/Metadata.js @@ -3,9 +3,17 @@ import DownloadLink from "./DownloadLink"; import * as Utils from "../utils"; import LinkInput from "./LinkInput"; import { API_URL } from "../constants"; +import * as Actions from "../redux/actionCreators"; +import * as Selectors from "../redux/selectors"; +import { useSelector } from "react-redux"; +import { useActionCreator } from "../hooks"; +import DownloadText from "./DownloadText"; -function Metadata({ permalink, openFileExplorer, fileName, hash }) { +function Metadata({ permalink, fileName, hash }) { + const toggleFileExplorer = useActionCreator(Actions.toggleFileExplorer); + const focusedSkinFile = useSelector(Selectors.getFocusedSkinFile); const [showLink, setShowLink] = useState(false); + // TODO: Move to Epic async function report(e) { e.preventDefault(); try { @@ -20,20 +28,45 @@ function Metadata({ permalink, openFileExplorer, fileName, hash }) { alert("Thanks for reporting. We'll review this skin."); } + let readmeLink = null; + if ( + focusedSkinFile != null && + focusedSkinFile.content != null && + focusedSkinFile.fileName != null + ) { + readmeLink = ( + + Readme + + ); + } + const elements = [ Download , + readmeLink, /* - { - openFileExplorer(); + // The UI for this is not good yet + toggleFileExplorer(); e.preventDefault(); }} + style={{ + border: "none", + background: "none", + padding: 0, + textDecoration: "underline", + cursor: "pointer", + margin: 0, + }} > Readme - , + , */ Report as NSFW , - ]; + ].filter(Boolean); return (
{showLink && ( diff --git a/src/components/Metadata.md b/src/components/Metadata.md index 472e0450..e0d8a696 100644 --- a/src/components/Metadata.md +++ b/src/components/Metadata.md @@ -7,7 +7,6 @@ const hash = "48bbdbbeb03d347e59b1eebda4d352d0"; {}} fileName={"fake_filename.wsz"} hash={hash} />; diff --git a/src/redux/actionCreators.js b/src/redux/actionCreators.js index da8cd21c..f8ead861 100644 --- a/src/redux/actionCreators.js +++ b/src/redux/actionCreators.js @@ -112,6 +112,10 @@ export function openFileExplorer() { return { type: "OPEN_FILE_EXPLORER" }; } +export function toggleFileExplorer() { + return { type: "TOGGLE_FILE_EXPLORER" }; +} + export function closeFileExlporer() { return { type: "CLOSE_FILE_EXPLORER" }; } diff --git a/src/redux/epics.js b/src/redux/epics.js index 0d7e4cdb..11dc3309 100644 --- a/src/redux/epics.js +++ b/src/redux/epics.js @@ -12,7 +12,8 @@ import { takeUntil, catchError, ignoreElements, - delay, + distinctUntilChanged, + startWith, } from "rxjs/operators"; import { search } from "../algolia"; import queryParser from "../queryParser"; @@ -32,8 +33,12 @@ const urlChangedEpic = (actions) => if (action.location.pathname.startsWith("/skin/")) { const segments = action.location.pathname.split("/"); const actions = [Actions.selectedSkin(segments[2])]; - if (segments[3] === "files") { - actions.push(Actions.selectSkinFile(segments[4])); + if (segments[4] === "files") { + actions.push( + // For now this is always the readme, so we don't need it. + // Actions.selectSkinFile(segments[5]), + Actions.openFileExplorer() + ); } return of(...actions); } @@ -328,6 +333,19 @@ const loggingEpic = (actions, state) => ignoreElements() ); +const urlEpic = (actions, state) => { + return actions.pipe( + map(() => Selectors.getUrl(state.value)), + distinctUntilChanged(), + startWith(window.location.pathname), + tap((url) => { + window.ga("set", "page", url); + window.history.replaceState({}, Selectors.getPageTitle(state), url); + }), + ignoreElements() + ); +}; + export default combineEpics( searchEpic, urlChangedEpic, @@ -343,5 +361,6 @@ export default combineEpics( uploadAllFilesEpic, uploadSingleFileEpic, checkIfUploadsAreMissingEpic, + urlEpic, loggingEpic ); diff --git a/src/redux/reducer.js b/src/redux/reducer.js index d5eae5ae..4a86af50 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -192,6 +192,8 @@ export default function reducer(state = defaultState, action) { selectedSkinPosition: null, skinZip: null, activeContentPage: null, + focusedSkinFile: null, + fileExplorerOpen: false, }; case "SEARCH_QUERY_CHANGED": return { @@ -250,6 +252,11 @@ export default function reducer(state = defaultState, action) { ...state, fileExplorerOpen: true, }; + case "TOGGLE_FILE_EXPLORER": + return { + ...state, + fileExplorerOpen: !state.fileExplorerOpen, + }; case "CLOSE_FILE_EXPLORER": return { ...state, diff --git a/src/redux/selectors.js b/src/redux/selectors.js index 2ba2d758..431a1581 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -131,7 +131,6 @@ export const getUrl = createSelector( getSearchQuery, getFileExplorerOpen, getFocusedSkinFile, - getSkins, getPermalinkUrlFromHashGetter, ( activeContentPage, @@ -139,7 +138,6 @@ export const getUrl = createSelector( query, fileExplorerOpen, focusedSkinFile, - skins, getPermalinkUrlFromHash ) => { if (activeContentPage === ABOUT_PAGE) { @@ -180,7 +178,9 @@ export function getSkins(state) { } export function getFileExplorerOpen(state) { - return state.fileExplorerOpen; + // The UI for this is not done yet. + // return state.fileExplorerOpen; + return false; } export function getFocusedSkinFile(state) { diff --git a/src/redux/store.js b/src/redux/store.js index 8ed0b82b..8364da71 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,6 +1,5 @@ import { createStore as createReduxStore, applyMiddleware } from "redux"; import { createEpicMiddleware } from "redux-observable"; -import * as Selectors from "./selectors"; import * as Actions from "./actionCreators"; import rootEpic from "./epics"; import reducer from "./reducer"; @@ -10,24 +9,11 @@ export function createStore() { const store = createReduxStore(reducer, applyMiddleware(epicMiddleware)); epicMiddleware.run(rootEpic); - let lastUrl = window.location.pathname; window.onpopstate = function () { store.dispatch({ type: "URL_CHANGED", location: document.location }); }; store.dispatch({ type: "URL_CHANGED", location: document.location }); // TODO: We could maybe get this going eaven before JS starts parsing... store.dispatch(Actions.requestUnloadedSkin(0)); - store.subscribe(() => { - const state = store.getState(); - const url = Selectors.getUrl(state); - if (url !== lastUrl) { - window.ga("set", "page", url); - if (lastUrl != null) { - window.ga("send", "pageview"); - } - window.history.replaceState({}, Selectors.getPageTitle(state), url); - lastUrl = url; - } - }); return store; }