mirror of
https://github.com/captbaritone/webamp.git
synced 2026-08-03 15:34:25 +00:00
commit
2925acb115
11 changed files with 362 additions and 197 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";
|
||||
|
|
|
|||
39
src/DropTarget.js
Normal file
39
src/DropTarget.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
|
||||
function DropTarget({ getInputProps }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: 20,
|
||||
flexGrow: 1,
|
||||
border: "8px dashed grey",
|
||||
borderRadius: 20,
|
||||
color: "grey",
|
||||
textAlign: "center",
|
||||
vericalAlign: "middle",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 30, color: "white" }}>Drop Skins Here</div>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 30,
|
||||
fontSize: 20,
|
||||
paddingLeft: 25,
|
||||
paddingRight: 25,
|
||||
lineHeight: 1.3,
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
We'll analyze them in your browser to find any that are missing from the
|
||||
museum
|
||||
</div>
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropTarget;
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
margin: 20,
|
||||
flexGrow: 1,
|
||||
border: "8px dashed grey",
|
||||
borderRadius: 20,
|
||||
color: "grey",
|
||||
textAlign: "center",
|
||||
vericalAlign: "middle",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 30, color: "white" }}>Drop Skins Here</div>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 30,
|
||||
fontSize: 20,
|
||||
paddingLeft: 25,
|
||||
paddingRight: 25,
|
||||
lineHeight: 1.3,
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
We'll analyze them in your browser to find any that are missing from the
|
||||
museum
|
||||
</div>
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 filesArr = Object.values(files);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
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>
|
||||
);
|
||||
}
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue