mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 10:07:35 +00:00
commit
c154b6083e
16 changed files with 360 additions and 299 deletions
|
|
@ -14,6 +14,7 @@
|
|||
"md5-file": "^4.0.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-dropzone": "^11.1.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-scripts": "^3.4.1",
|
||||
|
|
|
|||
53
src/App.js
53
src/App.js
|
|
@ -10,17 +10,12 @@ import { useSelector } from "react-redux";
|
|||
import * as Selectors from "./redux/selectors";
|
||||
import * as Actions from "./redux/actionCreators";
|
||||
import { ABOUT_PAGE } from "./constants";
|
||||
import {
|
||||
useWindowSize,
|
||||
useScrollbarWidth,
|
||||
useDropFiles,
|
||||
useActionCreator,
|
||||
} from "./hooks";
|
||||
import { useWindowSize, useScrollbarWidth, useActionCreator } from "./hooks";
|
||||
import { SCREENSHOT_WIDTH, SKIN_RATIO } from "./constants";
|
||||
import UploadGrid from "./UploadGrid";
|
||||
import DropTarget from "./DropTarget";
|
||||
import Metadata from "./components/Metadata";
|
||||
import SkinReadme from "./SkinReadme";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
|
||||
const getTableDimensions = (windowWidth, scale) => {
|
||||
const columnCount = Math.floor(windowWidth / (SCREENSHOT_WIDTH * scale));
|
||||
|
|
@ -29,21 +24,6 @@ const getTableDimensions = (windowWidth, scale) => {
|
|||
return { columnWidth, rowHeight, columnCount };
|
||||
};
|
||||
|
||||
function useDropTarget() {
|
||||
const gotFiles = useActionCreator(Actions.gotFiles);
|
||||
const areDragging = useSelector(Selectors.getAreDragging);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(e) => {
|
||||
gotFiles(Array.from(e.dataTransfer.files));
|
||||
},
|
||||
[gotFiles]
|
||||
);
|
||||
const setDragging = useActionCreator(Actions.setDragging);
|
||||
useDropFiles({ onDrop, setDragging });
|
||||
return areDragging;
|
||||
}
|
||||
|
||||
function App(props) {
|
||||
const scrollbarWidth = useScrollbarWidth();
|
||||
const {
|
||||
|
|
@ -55,19 +35,28 @@ function App(props) {
|
|||
windowWidthWithScrollabar - scrollbarWidth,
|
||||
props.scale
|
||||
);
|
||||
const gotFiles = useActionCreator(Actions.gotFiles);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles) => {
|
||||
gotFiles(acceptedFiles);
|
||||
// Do something with the files
|
||||
},
|
||||
[gotFiles]
|
||||
);
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
});
|
||||
|
||||
const fileExplorerOpen = useSelector(Selectors.getFileExplorerOpen);
|
||||
|
||||
const areDragging = useDropTarget();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div {...getRootProps()}>
|
||||
<Head />
|
||||
<Header />
|
||||
{areDragging ? (
|
||||
<DropTarget />
|
||||
) : props.uploadViewOpen ? (
|
||||
<UploadGrid />
|
||||
{props.uploadViewOpen || isDragActive ? (
|
||||
<UploadGrid isDragActive={isDragActive} getInputProps={getInputProps} />
|
||||
) : (
|
||||
<SkinTable
|
||||
columnCount={columnCount}
|
||||
|
|
@ -77,7 +66,7 @@ function App(props) {
|
|||
windowWidth={windowWidthWithScrollabar}
|
||||
/>
|
||||
)}
|
||||
{props.aboutPage ? (
|
||||
{props.page === ABOUT_PAGE ? (
|
||||
<Overlay>
|
||||
<About />
|
||||
</Overlay>
|
||||
|
|
@ -102,9 +91,9 @@ function App(props) {
|
|||
const mapStateToProps = (state) => ({
|
||||
selectedSkinHash: Selectors.getSelectedSkinHash(state),
|
||||
overlayShouldAnimate: Selectors.overlayShouldAnimate(state),
|
||||
aboutPage: Selectors.getActiveContentPage(state) === ABOUT_PAGE,
|
||||
page: Selectors.getActiveContentPage(state),
|
||||
scale: state.scale,
|
||||
uploadViewOpen: Selectors.getHaveUploadFiles(state),
|
||||
uploadViewOpen: Selectors.getUploadViewOpen(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import React from "react";
|
||||
import { HEADING_HEIGHT } from "./constants";
|
||||
|
||||
function DropTarget() {
|
||||
return (
|
||||
<div style={{ color: "white", marginTop: HEADING_HEIGHT }}>
|
||||
<h1>Drop</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default DropTarget;
|
||||
184
src/Header.js
184
src/Header.js
|
|
@ -1,10 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import * as Utils from "./utils";
|
||||
import * as Selectors from "./redux/selectors";
|
||||
import * as Actions from "./redux/actionCreators";
|
||||
import Disposable from "./Disposable";
|
||||
import { useWindowSize } from "./hooks";
|
||||
import { useActionCreator, useWindowSize } from "./hooks";
|
||||
import { ReactComponent as AlgoliaLogo } from "./searchByAlgoliaDarkbBackground.svg";
|
||||
import algoliaLogoSmallUrl from "./searchByAlgoliaSmall.png";
|
||||
import UploadButton from "./UploadButton";
|
||||
|
|
@ -23,67 +22,73 @@ function SearchLogo() {
|
|||
);
|
||||
}
|
||||
|
||||
class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super();
|
||||
this._disposable = new Disposable();
|
||||
this._inputRef = null;
|
||||
}
|
||||
function useFocusOnSlash() {
|
||||
const [input, setInput] = useState(null);
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
if (input == null) {
|
||||
return;
|
||||
}
|
||||
const handler = (e) => {
|
||||
// slash
|
||||
if (e.keyCode === 191) {
|
||||
if (this._inputRef == null) {
|
||||
return;
|
||||
}
|
||||
if (this._inputRef !== document.activeElement) {
|
||||
this._inputRef.focus();
|
||||
if (input !== document.activeElement) {
|
||||
input.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.document.addEventListener("keydown", handler);
|
||||
this._disposable.add(() => {
|
||||
return () => {
|
||||
window.document.removeEventListener("keydown", handler);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [input]);
|
||||
|
||||
componentWillUnmount() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div id="search">
|
||||
<h1>
|
||||
return setInput;
|
||||
}
|
||||
|
||||
function Header() {
|
||||
const searchQuery = useSelector(Selectors.getSearchQuery);
|
||||
// const scale = useSelector((state) => state.scale);
|
||||
const uploadViewOpen = useSelector(Selectors.getUploadViewOpen);
|
||||
|
||||
const setSearchQuery = useActionCreator(Actions.searchQueryChanged);
|
||||
const requestRandomSkin = useActionCreator(Actions.requestedRandomSkin);
|
||||
const requestedAboutPage = useActionCreator(Actions.requestedAboutPage);
|
||||
// const setScale = useActionCreator((scale) => ({ type: "SET_SCALE", scale }));
|
||||
const setInput = useFocusOnSlash();
|
||||
|
||||
return (
|
||||
<div id="search">
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
if (Utils.eventIsLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
setSearchQuery(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span id="logo">{"🌩️"}</span>
|
||||
<span className="name">Winamp Skin Museum</span>
|
||||
</a>
|
||||
</h1>
|
||||
<span style={{ flexGrow: 1 }} />
|
||||
{uploadViewOpen || (
|
||||
<>
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
if (Utils.eventIsLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
this.props.setSearchQuery(null);
|
||||
}
|
||||
href="https://www.algolia.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
opacity: searchQuery ? 0.5 : 0,
|
||||
transition: "opacity ease-in 300ms",
|
||||
}}
|
||||
>
|
||||
<span id="logo">{"🌩️"}</span>
|
||||
<span className="name">Winamp Skin Museum</span>
|
||||
<SearchLogo />
|
||||
</a>
|
||||
</h1>
|
||||
<span style={{ flexGrow: 1 }} />
|
||||
{this.props.uploadViewOpen || (
|
||||
<>
|
||||
<a
|
||||
href="https://www.algolia.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
opacity: this.props.searchQuery ? 0.5 : 0,
|
||||
transition: "opacity ease-in 300ms",
|
||||
}}
|
||||
>
|
||||
<SearchLogo />
|
||||
</a>
|
||||
{/*
|
||||
{/*
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.setScale(this.props.scale + 0.1);
|
||||
|
|
@ -99,56 +104,33 @@ class Header extends React.Component {
|
|||
-
|
||||
</button>
|
||||
*/}
|
||||
<input
|
||||
type="search"
|
||||
style={{ marginLeft: 10 }}
|
||||
onChange={(e) => this.props.setSearchQuery(e.target.value)}
|
||||
value={this.props.searchQuery || ""}
|
||||
placeholder={"Search..."}
|
||||
ref={(node) => {
|
||||
this._inputRef = node;
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.requestRandomSkin();
|
||||
}}
|
||||
>
|
||||
Random
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.requestedAboutPage();
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<UploadButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<input
|
||||
type="search"
|
||||
style={{ marginLeft: 10 }}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery || ""}
|
||||
placeholder={"Search..."}
|
||||
ref={setInput}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
requestRandomSkin();
|
||||
}}
|
||||
>
|
||||
Random
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
requestedAboutPage();
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<UploadButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
searchQuery: Selectors.getSearchQuery(state),
|
||||
scale: state.scale,
|
||||
uploadViewOpen: Selectors.getHaveUploadFiles(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSearchQuery(query) {
|
||||
dispatch(Actions.searchQueryChanged(query));
|
||||
},
|
||||
requestRandomSkin() {
|
||||
dispatch(Actions.requestedRandomSkin());
|
||||
},
|
||||
requestedAboutPage() {
|
||||
dispatch(Actions.requestedAboutPage());
|
||||
},
|
||||
setScale(scale) {
|
||||
dispatch({ type: "SET_SCALE", scale });
|
||||
},
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Header);
|
||||
export default Header;
|
||||
|
|
|
|||
|
|
@ -5,27 +5,28 @@ import * as Actions from "./redux/actionCreators";
|
|||
import { SHOW_UPLOAD } from "./constants";
|
||||
import UploadIcon from "./components/icons/UploadIcon";
|
||||
import CloseIcon from "./components/icons/CloseIcon";
|
||||
import { promptForFileReferences } from "./utils";
|
||||
import * as Selectors from "./redux/selectors";
|
||||
|
||||
function UploadButton() {
|
||||
const uploadViewOpen = useSelector(Selectors.getHaveUploadFiles);
|
||||
const gotFiles = useActionCreator(Actions.gotFiles);
|
||||
const uploadViewOpen = useSelector(Selectors.getUploadViewOpen);
|
||||
const closeUploadFiles = useActionCreator(Actions.closeUploadFiles);
|
||||
const requestedUploadPage = useActionCreator(Actions.requestedUploadPage);
|
||||
|
||||
if (!SHOW_UPLOAD) {
|
||||
return null;
|
||||
// return null;
|
||||
}
|
||||
|
||||
const style = { paddingLeft: "0.2rem", paddingRight: "0.2rem" };
|
||||
const style = {
|
||||
paddingLeft: "0.2rem",
|
||||
paddingRight: "0.2rem",
|
||||
};
|
||||
|
||||
// TODO: Make these buttons links.
|
||||
if (uploadViewOpen) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const areSure = window.confirm("Are you sure you're done uploading?");
|
||||
if (areSure) {
|
||||
closeUploadFiles();
|
||||
}
|
||||
closeUploadFiles();
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
|
|
@ -35,11 +36,9 @@ function UploadButton() {
|
|||
} else {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
const fileList = await promptForFileReferences({
|
||||
accept: ".wsz,.zip",
|
||||
});
|
||||
gotFiles(Array.from(fileList));
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
requestedUploadPage();
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,84 +1,166 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { HEADING_HEIGHT } from "./constants";
|
||||
import { useActionCreator } from "./hooks";
|
||||
import * as Actions from "./redux/actionCreators";
|
||||
import * as Selectors from "./redux/selectors";
|
||||
import * as Utils from "./utils";
|
||||
|
||||
function UploadRow({ file }) {
|
||||
const tryToUploadFile = useActionCreator(Actions.tryToUploadFile);
|
||||
function getStatus() {
|
||||
switch (file.status) {
|
||||
case "MISSING":
|
||||
return <button onClick={() => tryToUploadFile(file.id)}>Upload</button>;
|
||||
case "ARCHIVED":
|
||||
return (
|
||||
<a
|
||||
href={`https://skins.webamp.org/skin/${file.md5}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Added!
|
||||
</a>
|
||||
);
|
||||
case "FOUND":
|
||||
return (
|
||||
<a
|
||||
href={`https://skins.webamp.org/skin/${file.md5}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
);
|
||||
case "UPLOADING":
|
||||
return "Uploading...";
|
||||
case "NEW":
|
||||
return "Parsing...";
|
||||
default:
|
||||
return file.status;
|
||||
}
|
||||
}
|
||||
function DropTarget({ getInputProps }) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{getStatus()}</td>
|
||||
<td>{file.file.name}</td>
|
||||
<td>{file.status}</td>
|
||||
<td>
|
||||
{(file.status === "FOUND" || file.status === "ARCHIVED") && (
|
||||
<img
|
||||
src={Utils.screenshotUrlFromHash(file.md5)}
|
||||
alt={file.file.name}
|
||||
style={{ height: 100 }}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<div
|
||||
style={{
|
||||
margin: 20,
|
||||
flexGrow: 1,
|
||||
border: "8px dashed #FFF",
|
||||
borderRadius: 20,
|
||||
color: "grey",
|
||||
textAlign: "center",
|
||||
vericalAlign: "middle",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
fontSize: 30,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 30 }}>Drop Skins Here</div>
|
||||
<br />
|
||||
<div style={{ fontSize: 20, lineHeight: 1.3 }}>
|
||||
We'll analyze them in your browser to find <br />
|
||||
any that are missing from the museum
|
||||
</div>
|
||||
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadGrid() {
|
||||
function Section({ files, filter, title, render }) {
|
||||
const matches = useMemo(() => files.filter(filter), [files, filter]);
|
||||
if (matches.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
{matches.length.toLocaleString()} {title}
|
||||
</h2>
|
||||
<ul>
|
||||
{matches.map((match, i) => {
|
||||
return <li key={i}>{render(match)}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadGrid({ getInputProps, isDragActive }) {
|
||||
const files = useSelector((state) => state.fileUploads);
|
||||
const tryToUploadAllFiles = useActionCreator(Actions.tryToUploadAllFiles);
|
||||
const canUpload = useSelector(Selectors.getFileToUpload) != null;
|
||||
const filesArr = Object.values(files);
|
||||
|
||||
return (
|
||||
<div style={{ color: "white", marginTop: HEADING_HEIGHT }}>
|
||||
{canUpload && <button onClick={tryToUploadAllFiles}>Upload All</button>}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Filename</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.values(files).map((file, i) => {
|
||||
return <UploadRow key={i} file={file} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
top: HEADING_HEIGHT,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{isDragActive || Object.keys(files).length === 0 ? (
|
||||
<DropTarget getInputProps={getInputProps} />
|
||||
) : (
|
||||
<div style={{ color: "white", padding: 15 }}>
|
||||
<div>
|
||||
This feature is still in beta. If you have any issues, please reach
|
||||
out to{" "}
|
||||
<a href="mailto:jordan@jordaneldredge.com">
|
||||
jordan@jordaneldredge.com
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
<h1>
|
||||
{`You've dragged in ${filesArr.length.toLocaleString()} files`}
|
||||
{filesArr.some((file) => file.status === "NEW") &&
|
||||
" (Analyzing...)"}
|
||||
</h1>
|
||||
<Section
|
||||
files={filesArr}
|
||||
title={
|
||||
<>
|
||||
are new skins!
|
||||
{filesArr.some((file) => file.status === "MISSING") && (
|
||||
<div>
|
||||
<button onClick={tryToUploadAllFiles}>Upload All</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
filter={(file) =>
|
||||
file.status === "MISSING" ||
|
||||
file.status === "UPLOADING" ||
|
||||
file.status === "UPLOAD_FAILED" ||
|
||||
file.status === "ARCHIVED"
|
||||
}
|
||||
render={(file) => {
|
||||
switch (file.status) {
|
||||
case "MISSING":
|
||||
return file.file.name;
|
||||
case "UPLOADING":
|
||||
return <>{file.file.name} (🚀 Uploading...)</>;
|
||||
case "UPLOAD_FAILED":
|
||||
return <>{file.file.name} (❌ Upload Failed)</>;
|
||||
case "ARCHIVED":
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href={Utils.museumUrlFromHash(file.md5)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{file.file.name}
|
||||
</a>{" "}
|
||||
(✅ Added!)
|
||||
</>
|
||||
);
|
||||
default:
|
||||
console.error(`Unexpected file status: ${file.status}`);
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Section
|
||||
files={filesArr}
|
||||
title="are not skins"
|
||||
filter={(file) => file.status === "INVALID_FILE_EXTENSION"}
|
||||
render={(file) => file.file.name}
|
||||
/>
|
||||
<Section
|
||||
files={filesArr}
|
||||
title="Modern Skins (we're not accepting these yet)"
|
||||
filter={(file) => file.status === "NOT_CLASSIC_SKIN"}
|
||||
render={(file) => file.file.name}
|
||||
/>
|
||||
<Section
|
||||
files={filesArr}
|
||||
title="are already in the museum"
|
||||
filter={(file) => file.status === "FOUND"}
|
||||
render={(file) => (
|
||||
<a
|
||||
href={Utils.museumUrlFromHash(file.md5)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{file.file.name}
|
||||
</a>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ 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 Actions from "../redux/actionCreators";
|
||||
import * as Selectors from "../redux/selectors";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useActionCreator } from "../hooks";
|
||||
// import { useActionCreator } from "../hooks";
|
||||
import DownloadText from "./DownloadText";
|
||||
|
||||
function Metadata() {
|
||||
|
|
@ -17,7 +17,7 @@ function Metadata() {
|
|||
const permalink = useSelector(
|
||||
Selectors.getAbsolutePermalinkUrlFromHashGetter
|
||||
)(hash);
|
||||
const toggleFileExplorer = useActionCreator(Actions.toggleFileExplorer);
|
||||
// const toggleFileExplorer = useActionCreator(Actions.toggleFileExplorer);
|
||||
const focusedSkinFile = useSelector(Selectors.getFocusedSkinFile);
|
||||
const [showLink, setShowLink] = useState(false);
|
||||
// TODO: Move to Epic
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ export const SCREENSHOT_WIDTH = 275;
|
|||
export const SCREENSHOT_HEIGHT = 348;
|
||||
export const SKIN_RATIO = SCREENSHOT_HEIGHT / SCREENSHOT_WIDTH;
|
||||
export const ABOUT_PAGE = "ABOUT_PAGE";
|
||||
export const UPLOAD_PAGE = "UPLOAD_PAGE";
|
||||
// export const CDN = "https://s3.amazonaws.com/webamp-uploaded-skins";
|
||||
// export const CDN = "https://s3.amazonaws.com/cdn.webampskins.org";
|
||||
export const S3_SCREENSHOT_CDN = "https://s3.amazonaws.com/cdn.webampskins.org";
|
||||
export const SCREENSHOT_CDN = "https://cdn.webampskins.org";
|
||||
export const SKIN_CDN = "https://cdn.webampskins.org";
|
||||
export const API_URL = "https://api.webampskins.org";
|
||||
|
|
|
|||
53
src/hooks.js
53
src/hooks.js
|
|
@ -1,5 +1,5 @@
|
|||
import * as Utils from "./utils";
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useMemo, useState, useEffect, useCallback } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { delay } from "rxjs/operators";
|
||||
|
|
@ -100,54 +100,3 @@ export function useWebampAnimation({ initialPosition }) {
|
|||
handleWebampLoaded: () => webampLoadedEvents.next(null),
|
||||
};
|
||||
}
|
||||
|
||||
export function useDropFiles({ onDrop, setDragging }) {
|
||||
const onDragEnterCallback = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
},
|
||||
[setDragging]
|
||||
);
|
||||
|
||||
const onDragLeaveCallback = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
if (e.target === window.document.documentElement) {
|
||||
setDragging(false);
|
||||
}
|
||||
},
|
||||
[setDragging]
|
||||
);
|
||||
|
||||
const onDropCallback = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
onDrop(e);
|
||||
},
|
||||
[onDrop, setDragging]
|
||||
);
|
||||
|
||||
const onDragoverCallback = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("drop", onDropCallback);
|
||||
window.addEventListener("dragover", onDragoverCallback);
|
||||
window.addEventListener("dragenter", onDragEnterCallback);
|
||||
window.addEventListener("dragleave", onDragLeaveCallback);
|
||||
return () => {
|
||||
window.removeEventListener("drop", onDropCallback);
|
||||
window.removeEventListener("dragover", onDragoverCallback);
|
||||
window.removeEventListener("dragenter", onDragEnterCallback);
|
||||
window.removeEventListener("dragleave", onDragLeaveCallback);
|
||||
};
|
||||
}, [
|
||||
onDropCallback,
|
||||
onDragoverCallback,
|
||||
onDragEnterCallback,
|
||||
onDragLeaveCallback,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ABOUT_PAGE, UPLOAD_PAGE } from "../constants";
|
||||
export function closeModal() {
|
||||
return { type: "CLOSE_MODAL" };
|
||||
}
|
||||
|
|
@ -109,7 +110,11 @@ export function gotFocusedSkinFile(content) {
|
|||
}
|
||||
|
||||
export function requestedAboutPage() {
|
||||
return { type: "REQUESTED_ABOUT_PAGE" };
|
||||
return { type: "REQUESTED_PAGE", page: ABOUT_PAGE };
|
||||
}
|
||||
|
||||
export function requestedUploadPage() {
|
||||
return { type: "REQUESTED_PAGE", page: UPLOAD_PAGE };
|
||||
}
|
||||
|
||||
export function selectRelativeSkin(offset) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ const urlChangedEpic = (actions) =>
|
|||
if (action.location.pathname === "/about/") {
|
||||
return of(Actions.requestedAboutPage());
|
||||
}
|
||||
if (action.location.pathname === "/upload/") {
|
||||
return of(Actions.requestedUploadPage());
|
||||
}
|
||||
const params = new URLSearchParams(action.location.search);
|
||||
const query = params != null && params.get("query");
|
||||
|
||||
|
|
@ -231,7 +234,9 @@ const checkIfUploadsAreMissingEpic = (actions, state) =>
|
|||
actions.pipe(
|
||||
filter((action) => {
|
||||
return (
|
||||
action.type === "GOT_FILE_MD5" &&
|
||||
(action.type === "GOT_FILE_MD5" ||
|
||||
action.type === "NOT_CLASSIC_SKIN" ||
|
||||
action.type === "INVALID_FILE_EXTENSION") &&
|
||||
Selectors.getAreReadyToCheckMissingUploads(state.value)
|
||||
);
|
||||
}),
|
||||
|
|
@ -241,7 +246,15 @@ const checkIfUploadsAreMissingEpic = (actions, state) =>
|
|||
.pipe(
|
||||
map(({ missing, found }) =>
|
||||
Actions.gotMissingAndFoundMd5s({ missing, found })
|
||||
)
|
||||
),
|
||||
catchError((e) => {
|
||||
console.error("Failed fo check missing skins", e);
|
||||
// TODO: A real error here.
|
||||
alert(
|
||||
"Sorry. We had a problem checking which files are missing. Please contact jordan@jordaneldredge.com for help."
|
||||
);
|
||||
return of(Actions.closeUploadFiles());
|
||||
})
|
||||
)
|
||||
.pipe(takeUntilAction(actions, "CLOSE_UPLOAD_FILES"));
|
||||
})
|
||||
|
|
@ -251,7 +264,16 @@ function uploadActions(file) {
|
|||
return concat(
|
||||
of(Actions.startingFileUpload(file.id)),
|
||||
from(UploadUtils.upload(file.file)).pipe(
|
||||
map((response) => Actions.archivedSkin(file.id, response)),
|
||||
map((response) => {
|
||||
if (response.status === "ADDED") {
|
||||
return Actions.archivedSkin(file.id, response);
|
||||
}
|
||||
if (response.status === "FOUND") {
|
||||
// Maybe we could do something better here?
|
||||
}
|
||||
console.error(response);
|
||||
return Actions.uploadFailed(file.id);
|
||||
}),
|
||||
catchError(() => of(Actions.uploadFailed(file.id)))
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ABOUT_PAGE, CHUNK_SIZE } from "../constants";
|
||||
import { CHUNK_SIZE, UPLOAD_PAGE } from "../constants";
|
||||
|
||||
const defaultState = {
|
||||
searchQuery: null,
|
||||
|
|
@ -26,6 +26,9 @@ export default function reducer(state = defaultState, action) {
|
|||
case "SET_SCALE": {
|
||||
return { ...state, scale: action.scale };
|
||||
}
|
||||
case "GOT_FILES": {
|
||||
return { ...state, activeContentPage: UPLOAD_PAGE };
|
||||
}
|
||||
case "GOT_FILE": {
|
||||
return {
|
||||
...state,
|
||||
|
|
@ -126,8 +129,8 @@ export default function reducer(state = defaultState, action) {
|
|||
} else if (foundSet.has(file.md5)) {
|
||||
return { ...file, status: "FOUND" };
|
||||
}
|
||||
return file;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
const newFileUploads = {};
|
||||
Object.entries(state.fileUploads).forEach(([key, file]) => {
|
||||
|
|
@ -141,6 +144,7 @@ export default function reducer(state = defaultState, action) {
|
|||
case "CLOSE_UPLOAD_FILES": {
|
||||
return {
|
||||
...state,
|
||||
activeContentPage: null,
|
||||
fileUploads: {},
|
||||
};
|
||||
}
|
||||
|
|
@ -248,10 +252,11 @@ export default function reducer(state = defaultState, action) {
|
|||
content: action.content,
|
||||
},
|
||||
};
|
||||
case "REQUESTED_ABOUT_PAGE":
|
||||
case "REQUESTED_PAGE":
|
||||
console.log(action);
|
||||
return {
|
||||
...state,
|
||||
activeContentPage: ABOUT_PAGE,
|
||||
activeContentPage: action.page,
|
||||
};
|
||||
case "OPEN_FILE_EXPLORER":
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createSelector } from "reselect";
|
||||
import * as Utils from "../utils";
|
||||
import { ABOUT_PAGE } from "../constants";
|
||||
import { ABOUT_PAGE, UPLOAD_PAGE } from "../constants";
|
||||
|
||||
export function getSelectedSkinHash(state) {
|
||||
return state.selectedSkinHash;
|
||||
|
|
@ -42,13 +42,18 @@ export const getCurrentSkinCount = createSelector(
|
|||
);
|
||||
|
||||
export const getFileToUpload = (state) => {
|
||||
return Object.values(state.fileUploads).find(
|
||||
(file) => file.status === "MISSING"
|
||||
);
|
||||
return Object.values(state.fileUploads).find((file) => {
|
||||
if (file == null) {
|
||||
console.warn("Got a nullish file");
|
||||
return false;
|
||||
}
|
||||
return file.status === "MISSING";
|
||||
});
|
||||
};
|
||||
|
||||
export const getHaveUploadFiles = (state) =>
|
||||
Object.keys(state.fileUploads).length > 0;
|
||||
export const getUploadViewOpen = (state) => {
|
||||
return state.activeContentPage === UPLOAD_PAGE;
|
||||
};
|
||||
|
||||
export const getAreDragging = (state) => state.areDragging;
|
||||
|
||||
|
|
@ -145,6 +150,9 @@ export const getUrl = createSelector(
|
|||
if (activeContentPage === ABOUT_PAGE) {
|
||||
return "/about/";
|
||||
}
|
||||
if (activeContentPage === UPLOAD_PAGE) {
|
||||
return "/upload/";
|
||||
}
|
||||
if (hash) {
|
||||
const skinUrl = getPermalinkUrlFromHash(hash);
|
||||
if (fileExplorerOpen && focusedSkinFile) {
|
||||
|
|
@ -196,7 +204,5 @@ export function getAreReadyToCheckMissingUploads(state) {
|
|||
}
|
||||
|
||||
export function getUploadedFilesMd5s(state) {
|
||||
return Object.values(state.fileUploads)
|
||||
.map((file) => file.md5)
|
||||
.filter(Boolean);
|
||||
return Object.values(state.fileUploads).map((file) => file.md5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export async function hashFile(file) {
|
|||
}
|
||||
|
||||
/* SKIN UPLOAD UTILS */
|
||||
const validSkinFilename = /(\.wsz)|(\.zip)$/i;
|
||||
const validSkinFilename = /(\.wsz)|(\.zip)|(.wal)$/i;
|
||||
|
||||
export function isValidSkinFilename(filename) {
|
||||
return validSkinFilename.test(filename);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ export function skinUrlFromHash(hash) {
|
|||
return `${SKIN_CDN}/skins/${hash}.wsz`;
|
||||
}
|
||||
|
||||
export function museumUrlFromHash(hash) {
|
||||
return `/skin/${hash}`;
|
||||
}
|
||||
|
||||
export function getWindowSize() {
|
||||
var w = window,
|
||||
d = document,
|
||||
|
|
|
|||
26
yarn.lock
26
yarn.lock
|
|
@ -2618,6 +2618,11 @@ atob@^2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
attr-accept@^2.0.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
|
||||
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
|
||||
|
||||
autoprefixer@^9.6.1:
|
||||
version "9.7.5"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.5.tgz#8df10b9ff9b5814a8d411a5cfbab9c793c392376"
|
||||
|
|
@ -6015,6 +6020,13 @@ file-loader@4.3.0:
|
|||
loader-utils "^1.2.3"
|
||||
schema-utils "^2.5.0"
|
||||
|
||||
file-selector@^0.1.12:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.13.tgz#5efd977ca2bca1700992df1b10e254f4e73d2df4"
|
||||
integrity sha512-T2efCBY6Ps+jLIWdNQsmzt/UnAjKOEAlsZVdnQztg/BtAZGNL4uX1Jet9cMM8gify/x4CSudreji2HssGBNVIQ==
|
||||
dependencies:
|
||||
tslib "^2.0.1"
|
||||
|
||||
file-size@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/file-size/-/file-size-0.0.5.tgz#057d43c3a3ed735da3f90d6052ab380f1e6d5e3b"
|
||||
|
|
@ -11962,6 +11974,15 @@ react-dom@^16.8.6:
|
|||
prop-types "^15.6.2"
|
||||
scheduler "^0.19.1"
|
||||
|
||||
react-dropzone@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.1.0.tgz#c225f3c53450c80fbd80954361dc039090bfc14c"
|
||||
integrity sha512-gJT6iJadyTbevrigm6KZFaei/yNWfokzs1idumO7fXtRNPiGFDUpsQ+trHWwUO3yWOtJibpbo5tLZggjm+KV5w==
|
||||
dependencies:
|
||||
attr-accept "^2.0.0"
|
||||
file-selector "^0.1.12"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-error-overlay@^6.0.3, react-error-overlay@^6.0.7:
|
||||
version "6.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
|
||||
|
|
@ -14119,6 +14140,11 @@ tslib@^1, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
|
||||
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
|
||||
|
||||
tslib@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
|
||||
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
|
||||
|
||||
tsutils@^3.17.1:
|
||||
version "3.17.1"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue