diff --git a/src/App.js b/src/App.js
index ec74f68c..14ec30f6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -12,7 +12,7 @@ import * as Actions from "./redux/actionCreators";
import { ABOUT_PAGE } from "./constants";
import { useWindowSize, useScrollbarWidth, useActionCreator } from "./hooks";
import { SCREENSHOT_WIDTH, SKIN_RATIO } from "./constants";
-import UploadGrid from "./UploadGrid";
+import UploadGrid from "./upload/UploadGrid";
import Metadata from "./components/Metadata";
import SkinReadme from "./SkinReadme";
import { useDropzone } from "react-dropzone";
diff --git a/src/DropTarget.js b/src/DropTarget.js
new file mode 100644
index 00000000..b305a8b2
--- /dev/null
+++ b/src/DropTarget.js
@@ -0,0 +1,39 @@
+import React from "react";
+
+function DropTarget({ getInputProps }) {
+ return (
+
+
Drop Skins Here
+
+ We'll analyze them in your browser to find any that are missing from the
+ museum
+
+
+
+ );
+}
+
+export default DropTarget;
diff --git a/src/UploadGrid.js b/src/UploadGrid.js
deleted file mode 100644
index 10dca643..00000000
--- a/src/UploadGrid.js
+++ /dev/null
@@ -1,173 +0,0 @@
-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 Utils from "./utils";
-
-function DropTarget({ getInputProps }) {
- return (
-
-
Drop Skins Here
-
- We'll analyze them in your browser to find any that are missing from the
- museum
-
-
-
- );
-}
-
-function Section({ files, filter, title, render }) {
- const matches = useMemo(() => files.filter(filter), [files, filter]);
- if (matches.length === 0) {
- return null;
- }
- return (
- <>
-
- {matches.length.toLocaleString()} {title}
-
-
- {matches.map((match, i) => {
- return - {render(match)}
;
- })}
-
- >
- );
-}
-
-function UploadGrid({ getInputProps, isDragActive }) {
- const files = useSelector((state) => state.fileUploads);
- const tryToUploadAllFiles = useActionCreator(Actions.tryToUploadAllFiles);
- const filesArr = Object.values(files);
-
- return (
-
- {isDragActive || Object.keys(files).length === 0 ? (
-
- ) : (
-
-
-
- {`You've dragged in ${filesArr.length.toLocaleString()} files`}
- {filesArr.some((file) => file.status === "NEW") &&
- " (Analyzing...)"}
-
-
- are new skins!
- {filesArr.some((file) => file.status === "MISSING") && (
-
-
-
- )}
- >
- }
- 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 (
- <>
-
- {file.file.name}
- {" "}
- (✅ Added!)
- >
- );
- default:
- console.error(`Unexpected file status: ${file.status}`);
- return null;
- }
- }}
- />
- file.status === "INVALID_FILE_EXTENSION"}
- render={(file) => file.file.name}
- />
- file.status === "NOT_CLASSIC_SKIN"}
- render={(file) => file.file.name}
- />
-
- )}
-
- );
-}
-export default UploadGrid;
diff --git a/src/components/Metadata.js b/src/components/Metadata.js
index dfc26651..2a6252a1 100644
--- a/src/components/Metadata.js
+++ b/src/components/Metadata.js
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import DownloadLink from "./DownloadLink";
import * as Utils from "../utils";
import LinkInput from "./LinkInput";
-import { API_URL } from "../constants";
+// import { API_URL } from "../constants";
// import * as Actions from "../redux/actionCreators";
import * as Selectors from "../redux/selectors";
import * as Actions from "../redux/actionCreators";
diff --git a/src/redux/actionCreators.js b/src/redux/actionCreators.js
index 1bd1d3d1..44e94fd3 100644
--- a/src/redux/actionCreators.js
+++ b/src/redux/actionCreators.js
@@ -67,8 +67,12 @@ export function invalidFileExtension(id) {
return { type: "INVALID_FILE_EXTENSION", id };
}
-export function notClassicSkin(id) {
- return { type: "NOT_CLASSIC_SKIN", id };
+export function invalidArchive(id) {
+ return { type: "INVALID_ARCHIVE", id };
+}
+
+export function gotSkinType(id, skinType) {
+ return { type: "GOT_SKIN_TYPE", id, skinType };
}
export function gotFileMd5(id, md5) {
diff --git a/src/redux/epics.js b/src/redux/epics.js
index c3f4494d..d43bf57e 100644
--- a/src/redux/epics.js
+++ b/src/redux/epics.js
@@ -18,7 +18,7 @@ import {
import { search } from "../algolia";
import queryParser from "../queryParser";
import { API_URL, CHUNK_SIZE } from "../constants";
-import * as UploadUtils from "../uploadUtils";
+import * as UploadUtils from "../upload/uploadUtils";
const urlChangedEpic = (actions) =>
actions.pipe(
@@ -213,16 +213,17 @@ const uploadSingleFileEpic = (actions) =>
if (!UploadUtils.isValidSkinFilename(file.name)) {
return of(Actions.invalidFileExtension(id));
}
- return from(UploadUtils.isClassicSkin(file))
+ return from(UploadUtils.getSkinType(file))
.pipe(
- mergeMap((isClassic) => {
- if (!isClassic) {
- return of(Actions.notClassicSkin(id));
+ mergeMap((skinType) => {
+ if (skinType === null) {
+ return of(Actions.invalidArchive(id));
}
- return from(UploadUtils.hashFile(file)).pipe(
- map((md5) => {
- return Actions.gotFileMd5(id, md5);
- })
+ return concat(
+ of(Actions.gotSkinType(id, skinType)),
+ from(UploadUtils.hashFile(file)).pipe(
+ map((md5) => Actions.gotFileMd5(id, md5))
+ )
);
})
)
@@ -235,8 +236,8 @@ const checkIfUploadsAreMissingEpic = (actions, state) =>
filter((action) => {
return (
(action.type === "GOT_FILE_MD5" ||
- action.type === "NOT_CLASSIC_SKIN" ||
- action.type === "INVALID_FILE_EXTENSION") &&
+ action.type === "INVALID_FILE_EXTENSION" ||
+ action.type === "INVALID_ARCHIVE") &&
Selectors.getAreReadyToCheckMissingUploads(state.value)
);
}),
@@ -323,7 +324,6 @@ const loggingEpic = (actions, state) =>
case "TRY_TO_UPLOAD_ALL_FILES":
case "INVALID_FILE_EXTENSION":
case "GOT_FILE":
- case "NOT_CLASSIC_SKIN":
case "GOT_MISSING_AND_FOUND_MD5S":
window.ga("send", "event", "redux", action.type);
break;
diff --git a/src/redux/reducer.js b/src/redux/reducer.js
index a74a29e7..ef569fe5 100644
--- a/src/redux/reducer.js
+++ b/src/redux/reducer.js
@@ -66,8 +66,18 @@ export default function reducer(state = defaultState, action) {
true
);
}
- case "NOT_CLASSIC_SKIN": {
- return setUploadFileStatus(state, action.id, "NOT_CLASSIC_SKIN", true);
+ case "GOT_SKIN_TYPE": {
+ const previousFile = state.fileUploads[action.id];
+ return {
+ ...state,
+ fileUploads: {
+ ...state.fileUploads,
+ [action.id]: {
+ ...previousFile,
+ skinType: action.skinType,
+ },
+ },
+ };
}
case "STARTING_FILE_UPLOAD": {
return setUploadFileStatus(state, action.id, "UPLOADING");
@@ -78,6 +88,8 @@ export default function reducer(state = defaultState, action) {
case "ARCHIVED_SKIN": {
return setUploadFileStatus(state, action.id, "ARCHIVED");
}
+ case "INVALID_ARCHIVE":
+ return setUploadFileStatus(state, action.id, "INVALID_ARCHIVE");
case "GOT_FILE_MD5": {
return {
...state,
@@ -232,7 +244,6 @@ export default function reducer(state = defaultState, action) {
},
};
case "REQUESTED_PAGE":
- console.log(action);
return {
...state,
activeContentPage: action.page,
diff --git a/src/upload/UploadGrid.js b/src/upload/UploadGrid.js
new file mode 100644
index 00000000..513a7630
--- /dev/null
+++ b/src/upload/UploadGrid.js
@@ -0,0 +1,128 @@
+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 DropTarget from "../DropTarget";
+import UploadSection from "./UploadSection";
+
+function useBucketed(filesArr) {
+ return useMemo(() => {
+ const missing = [];
+ const notSkins = [];
+ const foundSkins = [];
+ filesArr.forEach((file) => {
+ switch (file.status) {
+ case "MISSING":
+ case "UPLOADING":
+ case "UPLOAD_FAILED":
+ case "ARCHIVED":
+ missing.push(file);
+ break;
+ case "INVALID_FILE_EXTENSION":
+ case "INVALID_ARCHIVE":
+ notSkins.push(file);
+ break;
+ case "FOUND":
+ foundSkins.push(file);
+ break;
+ default:
+ }
+ });
+ return { missing, notSkins, foundSkins };
+ }, [filesArr]);
+}
+
+function Plural({ count, single, plural }) {
+ return count === 1 ? single : plural;
+}
+
+function Inner({ files }) {
+ const tryToUploadAllFiles = useActionCreator(Actions.tryToUploadAllFiles);
+ const filesArr = Object.values(files);
+ const { missing, notSkins, foundSkins } = useBucketed(filesArr);
+
+ const analyzing = filesArr.some((file) => file.status === "NEW");
+
+ if (analyzing) {
+ return (
+
+ Analyzing {filesArr.length.toLocaleString()}{" "}
+
+ ...
+
+ );
+ }
+
+ const stillHaveFilesToUpload = filesArr.some(
+ (file) => file.status === "MISSING" || file.status === "UPLOADING"
+ );
+
+ const filesToUpload = missing.length;
+
+ const getTitle = () => {
+ if (filesToUpload > 0) {
+ if (stillHaveFilesToUpload) {
+ return (
+ <>
+ Found {filesToUpload.toLocaleString()}{" "}
+ to
+ upload
+ >
+ );
+ } else {
+ return `Thanks for your contribution!`;
+ }
+ }
+ return `No missing skins found`;
+ };
+
+ return (
+
+
{getTitle()}
+
+ Upload All
+
+ )
+ }
+ files={missing}
+ />
+
+
+
+ );
+}
+
+function UploadGrid({ getInputProps, isDragActive, ...props }) {
+ const files = useSelector((state) => state.fileUploads);
+ return (
+
+ {isDragActive || Object.keys(files).length === 0 ? (
+
+ ) : (
+
+
+
+ )}
+
+ );
+}
+export default UploadGrid;
diff --git a/src/upload/UploadRow.js b/src/upload/UploadRow.js
new file mode 100644
index 00000000..aab6e526
--- /dev/null
+++ b/src/upload/UploadRow.js
@@ -0,0 +1,117 @@
+import React from "react";
+import * as Utils from "../utils";
+
+function Row({ name, loading, right, complete }) {
+ return (
+
+ {(loading != null || complete) && (
+
+ )}
+
+ {name}
+ {right && (
+
+ {right}
+
+ )}
+
+
+ );
+}
+
+function SkinLink({ md5, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+// TODO: This is a component
+function getRight(file) {
+ switch (file.status) {
+ case "ARCHIVED":
+ switch (file.skinType) {
+ case "MODERN":
+ return "archived";
+ case "CLASSIC":
+ return added;
+ default:
+ throw new Error(`Unknown skinType "${file.skinType}"`);
+ }
+ case "FOUND":
+ switch (file.skinType) {
+ case "MODERN":
+ return "archived";
+ case "CLASSIC":
+ return view;
+ default:
+ throw new Error(`Unknown skinType "${file.skinType}"`);
+ }
+ case "MISSING":
+ return "missing";
+ case "UPLOADING":
+ return "uploading...";
+ case "UPLOAD_FAILED":
+ return "upload failed";
+ case "INVALID_ARCHIVE":
+ return "corrupt";
+ case "INVALID_FILE_EXTENSION":
+ return "not skin";
+ default:
+ return file.status;
+ }
+}
+
+function UploadRow({ file }) {
+ return (
+
+ );
+}
+
+export default UploadRow;
diff --git a/src/upload/UploadSection.js b/src/upload/UploadSection.js
new file mode 100644
index 00000000..feffb179
--- /dev/null
+++ b/src/upload/UploadSection.js
@@ -0,0 +1,34 @@
+import React from "react";
+import UploadRow from "./UploadRow";
+
+function UploadSection({ files, title, extra }) {
+ if (files.length === 0) {
+ return null;
+ }
+ return (
+
+
+
+ {title} ({files.length.toLocaleString()})
+
+
{extra}
+
+ {files.map((file) => (
+
+ ))}
+
+ );
+}
+
+export default UploadSection;
diff --git a/src/uploadUtils.js b/src/upload/uploadUtils.js
similarity index 75%
rename from src/uploadUtils.js
rename to src/upload/uploadUtils.js
index 2b5bbb0a..c06187ef 100644
--- a/src/uploadUtils.js
+++ b/src/upload/uploadUtils.js
@@ -1,4 +1,4 @@
-import { API_URL } from "./constants";
+import { API_URL } from "../constants";
export async function upload(file) {
const formData = new FormData();
formData.append("skin", file, file.name);
@@ -22,7 +22,7 @@ export async function checkMd5sAreMissing(md5s) {
}
export async function hashFile(file) {
- const { hashFile: hasher } = await import("./hashFile");
+ const { hashFile: hasher } = await import("../hashFile");
return hasher(file);
}
@@ -33,14 +33,19 @@ export function isValidSkinFilename(filename) {
return validSkinFilename.test(filename);
}
-export async function isClassicSkin(file) {
+export async function getSkinType(file) {
const JSZip = await import("jszip");
try {
const zip = await JSZip.loadAsync(file);
- return zip.file(/main\.bmp$/i).length > 0;
+ if (zip.file(/main\.bmp$/i).length > 0) {
+ return "CLASSIC";
+ } else if (zip.file(/skin\.xml$/i).length > 0) {
+ return "MODERN";
+ }
+ return null;
} catch (e) {
// TODO: We could give a better message here.
console.error(e);
- return false;
+ return null;
}
}