mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-25 11:04:00 +00:00
Download readme
This commit is contained in:
parent
f595ff7016
commit
5892334acc
15 changed files with 259 additions and 123 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
126
src/Overlay.js
126
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(
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</a>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
window.document.body
|
||||
);
|
||||
}
|
||||
|
||||
export default Overlay;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const hash = "48bbdbbeb03d347e59b1eebda4d352d0";
|
|||
absolutePermalink={
|
||||
"https://skins.webamp.org/skin/48bbdbbeb03d347e59b1eebda4d352d0/Zelda_Amp_3.wsz/"
|
||||
}
|
||||
openFileExplorer={() => {}}
|
||||
initialPosition={{ top: 100, left: 100 }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}}
|
||||
>
|
||||
×
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
```
|
||||
24
src/components/DownloadText.js
Normal file
24
src/components/DownloadText.js
Normal 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;
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>;
|
||||
|
|
|
|||
|
|
@ -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" };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue