Merge pull request #6 from captbaritone/uploadv2

Uploadv2
This commit is contained in:
Jordan Eldredge 2020-09-14 14:20:48 -07:00 committed by GitHub
commit c154b6083e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 360 additions and 299 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
]);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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