Download readme

This commit is contained in:
Jordan Eldredge 2020-09-11 22:18:47 -07:00
parent f595ff7016
commit 5892334acc
15 changed files with 259 additions and 123 deletions

View file

@ -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;
}

View file

@ -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(
<div
style={{
zIndex: 1000,
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor:
!shouldAnimate || mounted
? "rgba(0, 0, 0, 0.95)"
: "rgba(0, 0, 0, 0)",
transition: "background-color 400ms ease-out",
}}
>
<div
style={{
width: "100%",
height: "100%",
// This one werid hack to work around margin collapse which was making
// children with top margins cause the top protion of the overlay to
// be unclickable: https://stackoverflow.com/a/47351270/1263117
display: "flex",
flexDirection: "column",
}}
onClick={handleClick}
onScroll={handleTouchMove}
>
<a
id="close-modal"
href="/"
onClick={(e) => {
closeModal();
e.preventDefault();
}}
aria-label="Close Modal"
style={{
color: "#a7a394",
position: "fixed",
top: 10,
right: 10,
padding: 0,
fontSize: 50,
lineHeight: "25px",
textDecoration: "none",
}}
>
&times;
</a>
{children}
</div>
</div>,
window.document.body
);
}
export default Overlay;

View file

@ -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(
<div
style={{
position: "fixed",
display: "flex",
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 1002,
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
position: "fixed",
maxWidth: 400,
height: "60%",
backgroundColor: "white",
overflow: "scroll",
...this.props.style,
padding: 30,
}}
>
<h2>{fileName}</h2>
<div>
<div
className={"readme"}
style={{
width: "100%",
height: "300px",
}}
>
<pre>{content}</pre>
</div>
<div className={"readme"} style={{ overflow: "scroll" }}>
<pre>{content}</pre>
</div>
</div>
);
}
</div>,
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;

View file

@ -159,7 +159,6 @@ class BaseFocusedSkin extends React.Component {
closeModal={this.props.closeModal}
/>
</div>
{this.props.fileExplorerOpen && <SkinReadme />}
</>
)}
<div
@ -198,6 +197,7 @@ class BaseFocusedSkin extends React.Component {
)}
</div>
</div>
{this.props.fileExplorerOpen && <SkinReadme />}
<Metadata
permalink={this.props.absolutePermalink}
openFileExplorer={this.props.openFileExplorer}

View file

@ -13,7 +13,6 @@ const hash = "48bbdbbeb03d347e59b1eebda4d352d0";
absolutePermalink={
"https://skins.webamp.org/skin/48bbdbbeb03d347e59b1eebda4d352d0/Zelda_Amp_3.wsz/"
}
openFileExplorer={() => {}}
initialPosition={{ top: 100, left: 100 }}
/>
)}

View file

@ -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",
}}
>
<div
@ -79,7 +79,7 @@ function Overlay({ shouldAnimate, closeModal, children }) {
// children with top margins cause the top protion of the overlay to
// be unclickable: https://stackoverflow.com/a/47351270/1263117
display: "flex",
flexDirection: "column"
flexDirection: "column",
}}
onClick={handleClick}
onScroll={handleTouchMove}
@ -87,7 +87,7 @@ function Overlay({ shouldAnimate, closeModal, children }) {
<a
id="close-modal"
href="/"
onClick={e => {
onClick={(e) => {
closeModal();
e.preventDefault();
}}
@ -100,7 +100,7 @@ function Overlay({ shouldAnimate, closeModal, children }) {
padding: 0,
fontSize: 50,
lineHeight: "25px",
textDecoration: "none"
textDecoration: "none",
}}
>
&times;

View file

@ -1,27 +0,0 @@
BaseOverlay example:
```js
const [open, setOpen] = React.useState(false);
const [shouldAnimate, setShouldAnimate] = React.useState(false);
return (
<>
<button onClick={() => setOpen(o => !o)}>Toggle Overlay</button>
{open && <BaseOverlay closeModal={() => setOpen(false)} />}
</>
);
```
BaseOverlay example (animated):
```js
const [open, setOpen] = React.useState(false);
const [shouldAnimate, setShouldAnimate] = React.useState(false);
return (
<>
<button onClick={() => setOpen(o => !o)}>Toggle Overlay</button>
{open && (
<BaseOverlay closeModal={() => setOpen(false)} shouldAnimate={true} />
)}
</>
);
```

View file

@ -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 (
<a {...restProps} href={url}>
{/* We have to explicitly set the children to make ESLint happy */}
{children}
</a>
);
}
export default DownloadText;

View file

@ -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 = (
<DownloadText
text={focusedSkinFile.content}
download={focusedSkinFile.fileName}
>
Readme
</DownloadText>
);
}
const elements = [
<DownloadLink href={Utils.skinUrlFromHash(hash)} download={fileName}>
Download
</DownloadLink>,
readmeLink,
/*
<a
href={"#"}
<button
onClick={(e) => {
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
</a>,
</button>,
*/
<a
href={permalink}
@ -63,7 +96,7 @@ function Metadata({ permalink, openFileExplorer, fileName, hash }) {
>
Report as NSFW
</button>,
];
].filter(Boolean);
return (
<div className="metadata">
{showLink && (

View file

@ -7,7 +7,6 @@ const hash = "48bbdbbeb03d347e59b1eebda4d352d0";
<Metadata
href={`${SCREENSHOT_CDN}/skins/48bbdbbeb03d347e59b1eebda4d352d0.wsz`}
permalink={`${SKIN_CDN}/skin/48bbdbbeb03d347e59b1eebda4d352d0/Zelda_Amp_3.wsz/`}
openFileExplorer={() => {}}
fileName={"fake_filename.wsz"}
hash={hash}
/>;

View file

@ -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" };
}

View file

@ -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
);

View file

@ -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,

View file

@ -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) {

View file

@ -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;
}