mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 10:07:35 +00:00
Table based upload flow
This commit is contained in:
parent
4595d32137
commit
8cd744d3e3
8 changed files with 284 additions and 220 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -1,215 +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";
|
||||
import DropTarget from "./DropTarget";
|
||||
|
||||
function Section({ files, title, render, open = false }) {
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<details open={open}>
|
||||
<summary>
|
||||
<h2 style={{ display: "inline" }}>{title}</h2>
|
||||
</summary>
|
||||
<ul>
|
||||
{files.map((match, i) => {
|
||||
return <li key={i}>{render(match)}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (filesArr.some((file) => file.status === "NEW")) {
|
||||
return (
|
||||
<h1>
|
||||
Analyzing {filesArr.length.toLocaleString()}{" "}
|
||||
<Plural count={filesArr.length} single="file" plural="files" />
|
||||
...
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>
|
||||
You have <strong>{missing.length.toLocaleString()}</strong>{" "}
|
||||
<Plural count={missing.length} single="skin" plural="skins" /> that we
|
||||
are missing!
|
||||
</h1>
|
||||
{filesArr.some((file) => file.status === "MISSING") && (
|
||||
<div>
|
||||
<button onClick={tryToUploadAllFiles}>Upload All</button>
|
||||
</div>
|
||||
)}
|
||||
<table>
|
||||
<thead style={{ textAlign: "left" }}>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Filename</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{missing
|
||||
.map((file) => {
|
||||
const fileName = <code>{file.file.name}</code>;
|
||||
switch (file.status) {
|
||||
case "MISSING":
|
||||
return (
|
||||
<>
|
||||
<td />
|
||||
<td>{fileName}</td>
|
||||
</>
|
||||
);
|
||||
case "UPLOADING":
|
||||
return (
|
||||
<>
|
||||
<td>🚀 Uploading...</td>
|
||||
<td>{fileName}</td>
|
||||
</>
|
||||
);
|
||||
case "UPLOAD_FAILED":
|
||||
return (
|
||||
<>
|
||||
<td>❌ Upload Failed</td>
|
||||
<td>{fileName}</td>
|
||||
</>
|
||||
);
|
||||
case "ARCHIVED":
|
||||
return (
|
||||
<>
|
||||
<td>✅ Added!</td>
|
||||
<td>
|
||||
{file.skinType === "CLASSIC" ? (
|
||||
<a
|
||||
href={Utils.museumUrlFromHash(file.md5)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{fileName}
|
||||
</a>
|
||||
) : file.skinType === "MODERN" ? (
|
||||
<>
|
||||
{fileName} (Note: Modern skins are not yet visible
|
||||
in the museum)
|
||||
</>
|
||||
) : // TODO: Throw?
|
||||
null}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
console.error(`Unexpected file status: ${file.status}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.map((rows, i) => (
|
||||
<tr key={i}>{rows}</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Section
|
||||
files={notSkins}
|
||||
title={
|
||||
<>
|
||||
Files that are not valid skins ({notSkins.length.toLocaleString()})
|
||||
</>
|
||||
}
|
||||
render={(file) => <code>{file.file.name}</code>}
|
||||
/>
|
||||
|
||||
<Section
|
||||
files={foundSkins}
|
||||
title={
|
||||
<>Already in the museum ({foundSkins.length.toLocaleString()})</>
|
||||
}
|
||||
render={(file) => {
|
||||
if (file.skinType === "MODERN") {
|
||||
return (
|
||||
<>
|
||||
<code>{file.file.name}</code> (Note: Modern skins are not yet
|
||||
visible in the museum)
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={Utils.museumUrlFromHash(file.md5)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<code>{file.file.name}</code>
|
||||
</a>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadGrid({ getInputProps, isDragActive, ...props }) {
|
||||
const files = useSelector((state) => state.fileUploads);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: HEADING_HEIGHT,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
color: "lightgrey",
|
||||
}}
|
||||
>
|
||||
{isDragActive || Object.keys(files).length === 0 ? (
|
||||
<DropTarget getInputProps={getInputProps} />
|
||||
) : (
|
||||
<div style={{ padding: 15 }}>
|
||||
<Inner files={files} {...props} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default UploadGrid;
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
128
src/upload/UploadGrid.js
Normal file
128
src/upload/UploadGrid.js
Normal file
|
|
@ -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 (
|
||||
<h1>
|
||||
Analyzing {filesArr.length.toLocaleString()}{" "}
|
||||
<Plural count={filesArr.length} single="file" plural="files" />
|
||||
...
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
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()}{" "}
|
||||
<Plural single="skin" plural="skins" count={filesToUpload} /> to
|
||||
upload
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return `Thanks for your contribution!`;
|
||||
}
|
||||
}
|
||||
return `No missing skins found`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 600, minWidth: 400, lineHeight: 1.2 }}>
|
||||
<h1>{getTitle()}</h1>
|
||||
<UploadSection
|
||||
title="Missing from the museum"
|
||||
extra={
|
||||
stillHaveFilesToUpload && (
|
||||
<button
|
||||
onClick={tryToUploadAllFiles}
|
||||
style={{ marginRight: 0, height: 30, fontWeight: "bold" }}
|
||||
>
|
||||
Upload All
|
||||
</button>
|
||||
)
|
||||
}
|
||||
files={missing}
|
||||
/>
|
||||
<UploadSection title="Invalid" files={notSkins} />
|
||||
<UploadSection title="Already collected" files={foundSkins} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadGrid({ getInputProps, isDragActive, ...props }) {
|
||||
const files = useSelector((state) => state.fileUploads);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: HEADING_HEIGHT,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
color: "lightgrey",
|
||||
}}
|
||||
>
|
||||
{isDragActive || Object.keys(files).length === 0 ? (
|
||||
<DropTarget getInputProps={getInputProps} />
|
||||
) : (
|
||||
<div style={{ padding: 15, display: "flex", justifyContent: "center" }}>
|
||||
<Inner files={files} {...props} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default UploadGrid;
|
||||
117
src/upload/UploadRow.js
Normal file
117
src/upload/UploadRow.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import React from "react";
|
||||
import * as Utils from "../utils";
|
||||
|
||||
function Row({ name, loading, right, complete }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderBottom: "1px solid rgba(32, 31, 51, 1)",
|
||||
position: "relative",
|
||||
paddingTop: 4,
|
||||
paddingBottom: 2,
|
||||
}}
|
||||
>
|
||||
{(loading != null || complete) && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "rgba(51, 71, 88, 1)",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: loading ? `90%` : complete ? `100%` : `0%`,
|
||||
transitionProperty: "all",
|
||||
// TODO: Try to learn how long it really takes
|
||||
transitionDuration: complete ? "200ms" : "9s",
|
||||
height: "100%",
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<code>{name}</code>
|
||||
{right && (
|
||||
<code
|
||||
style={{
|
||||
color: "darkgray",
|
||||
paddingLeft: 10,
|
||||
// Ensure we are wide enough that text changes won't affect the layout
|
||||
minWidth: 100,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{right}
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkinLink({ md5, children }) {
|
||||
return (
|
||||
<a
|
||||
href={Utils.museumUrlFromHash(md5)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "darkgray" }}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: This is a component
|
||||
function getRight(file) {
|
||||
switch (file.status) {
|
||||
case "ARCHIVED":
|
||||
switch (file.skinType) {
|
||||
case "MODERN":
|
||||
return "archived";
|
||||
case "CLASSIC":
|
||||
return <SkinLink md5={file.md5}>added</SkinLink>;
|
||||
default:
|
||||
throw new Error(`Unknown skinType "${file.skinType}"`);
|
||||
}
|
||||
case "FOUND":
|
||||
switch (file.skinType) {
|
||||
case "MODERN":
|
||||
return "archived";
|
||||
case "CLASSIC":
|
||||
return <SkinLink md5={file.md5}>view</SkinLink>;
|
||||
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 (
|
||||
<Row
|
||||
name={file.file.name}
|
||||
loading={file.status === "UPLOADING"}
|
||||
right={getRight(file)}
|
||||
complete={file.status === "ARCHIVED"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default UploadRow;
|
||||
34
src/upload/UploadSection.js
Normal file
34
src/upload/UploadSection.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
import UploadRow from "./UploadRow";
|
||||
|
||||
function UploadSection({ files, title, extra }) {
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<h2>
|
||||
{title} ({files.length.toLocaleString()})
|
||||
</h2>
|
||||
<div>{extra}</div>
|
||||
</div>
|
||||
{files.map((file) => (
|
||||
<UploadRow key={file.id} file={file} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UploadSection;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue