613 lines
20 KiB
HTML
613 lines
20 KiB
HTML
{{ template "base.html" . }}
|
|
|
|
{{ define "extrahead" }}
|
|
{{ template "markdown_previews.html" . }}
|
|
<script src="{{ static "js/tabs.js" }}"></script>
|
|
<script src="{{ static "js/image_selector.js" }}"></script>
|
|
<script src="{{ static "js/templates.js" }}"></script>
|
|
<script src="{{ static "js/base64.js" }}"></script>
|
|
|
|
<style>
|
|
#desc-preview:empty::after {
|
|
content: 'A preview of your description will appear here.';
|
|
color: var(--dimmer-color);
|
|
font-style: italic;
|
|
}
|
|
|
|
#description.drop {
|
|
box-shadow: inset 0px 0px 5px yellow;
|
|
}
|
|
</style>
|
|
{{ end }}
|
|
|
|
{{ define "content" }}
|
|
<div class="ph3 ph0-ns">
|
|
{{ if .Editing }}
|
|
<h1>Edit {{ .ProjectSettings.Name }}</h1>
|
|
{{ else }}
|
|
<h1>Create a new project</h1>
|
|
{{ end }}
|
|
<form id="project_form" class="tabbed edit-form" method="POST" enctype="multipart/form-data">
|
|
{{ csrftoken .Session }}
|
|
<div class="tab" data-name="General" data-slug="general">
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Project name:</div>
|
|
<div>
|
|
<input required type="text" name="project_name" maxlength="255" class="textbox" value="{{ .ProjectSettings.Name }}">
|
|
<span class="note">* Required</span>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Status:</div>
|
|
<div>
|
|
<select name="lifecycle">
|
|
<option value="active" {{ if eq .ProjectSettings.Lifecycle "active" }}selected{{ end }}>Active</option>
|
|
<option value="hiatus" {{ if eq .ProjectSettings.Lifecycle "hiatus" }}selected{{ end }}>On Hiatus</option>
|
|
<option value="done" {{ if eq .ProjectSettings.Lifecycle "done" }}selected{{ end }}>Completed</option>
|
|
<option value="dead" {{ if eq .ProjectSettings.Lifecycle "dead" }}selected{{ end }}>Abandoned</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Owners:</div>
|
|
<div>
|
|
<input id="owner_name" form="" type="text" placeholder="Enter another user's username" />
|
|
<a href="javascript:;" id="owner_add" class="">Add</a>
|
|
<span id="owners_error" class="note"></span>
|
|
<div id="owner_list" class="pt1">
|
|
<template id="owner_row">
|
|
<div class="owner_row flex flex-row bg--card w5 pv1 ph2" data-tmpl="root">
|
|
<input type="hidden" name="owners" data-tmpl="input" />
|
|
<span class="flex-grow-1" data-tmpl="name"></span>
|
|
<a class="remove_owner" href="javascript:;">X</a>
|
|
</div>
|
|
</template>
|
|
{{ range .ProjectSettings.Owners }}
|
|
<div class="owner_row flex flex-row bg--card w5 pv1 ph2">
|
|
<input type="hidden" name="owners" value="{{ .Username }}" />
|
|
<span class="flex-grow-1">{{ .Username }}</span>
|
|
{{ if (or $.User.IsStaff (ne .ID $.User.ID)) }}
|
|
<a class="remove_owner" href="javascript:;">X</a>
|
|
{{ end }}
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Tag:</div>
|
|
<div>
|
|
<input
|
|
id="tag" name="tag" type="text"
|
|
pattern="^[a-z0-9]+(-[a-z0-9]+)*$" maxlength="20"
|
|
value="{{ .ProjectSettings.Tag }}"
|
|
/>
|
|
<div class="c--dim f7 mt1">e.g. "imgui" or "text-editor". Tags must be all lowercase, and can use hyphens to separate words.</div>
|
|
<div class="c--dim f7" id="tag-discord-info">If you have linked your Discord account, any #project-showcase messages with the tag "&<span id="tag-preview"></span>" will automatically be associated with this project.</div>
|
|
</div>
|
|
</div>
|
|
{{ if and .Editing .User.IsStaff }}
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Admin settings</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div>Official:</div>
|
|
<div>
|
|
<input id="official" type="checkbox" name="official" {{ if not .ProjectSettings.Personal }}checked{{ end }} />
|
|
<label for="official">Official HMN project</label>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div>Hidden:</div>
|
|
<div>
|
|
<input id="hidden" type="checkbox" name="hidden" {{ if .ProjectSettings.Hidden }}checked{{ end }} />
|
|
<label for="hidden">Hide</label>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Slug:</div>
|
|
<div>
|
|
<input type="text" name="slug" maxlength="255" class="textbox" value="{{ .ProjectSettings.Slug }}">
|
|
<div class="c--dim f7 mt1">Has no effect for personal projects. Personal projects have a slug derived from the title.</div>
|
|
<div class="c--dim f7">If you change this, make sure to change DNS too!</div>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div>Featured:</div>
|
|
<div>
|
|
<input id="featured" type="checkbox" name="featured" {{ if .ProjectSettings.Featured }}checked{{ end }} />
|
|
<label for="featured">Featured</label>
|
|
<div class="c--dim f7">Bump to the top of the project index and show in the carousel. Has no effect for personal projects.</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
<div class="edit-form-row">
|
|
<div></div>
|
|
<div>
|
|
{{ if .Editing }}
|
|
<input type="submit" value="Save" />
|
|
{{ else }}
|
|
<a class="button" href="javascript:;" onclick="gotoTab('description');">Next</a>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab" data-name="Description" data-slug="description">
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Short description:</div>
|
|
<div>
|
|
<textarea required maxlength="140" name="shortdesc">
|
|
{{- .ProjectSettings.Blurb -}}
|
|
</textarea>
|
|
<div class="c--dim f7">Plaintext only. No links or markdown.</div>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Project links:</div>
|
|
<div>
|
|
<textarea class="links" name="links" id="links" maxlength="2048" data-max-chars="2048">
|
|
{{- .ProjectSettings.LinksText -}}
|
|
</textarea>
|
|
<div class="c--dim f7">
|
|
<div>Relevant links to put on the project page.</div>
|
|
<div>Format: url [Title] (e.g. <code>http://example.com/ Example Site</code>)</div>
|
|
<div>(1 per line, 10 max)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div class="pt-input-ns">Full description:</div>
|
|
<div>
|
|
<textarea id="description" class="w-100 h5 minh-5 mono lh-copy" name="description">
|
|
{{- .ProjectSettings.Description -}}
|
|
</textarea>
|
|
<div class="flex justify-end items-center mt2">
|
|
<div class="upload_bar flex-grow-1">
|
|
<div class="instructions">
|
|
Upload files by dragging & dropping, pasting, or <label class="pointer link" for="file_input">selecting</label> them.
|
|
</div>
|
|
<div class="progress flex">
|
|
<div class="progress_text mr3"></div>
|
|
<div class="progress_bar flex-grow-1 flex-shrink-1 pa1"><div class=""></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="b mt3 mb2">Preview:</div>
|
|
<div id="desc-preview" class="w-100"></div>
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div></div>
|
|
<div>
|
|
{{ if .Editing }}
|
|
<input type="submit" value="Save" />
|
|
{{ else }}
|
|
<a class="button" href="javascript:;" onclick="gotoTab('assets');">Next</a>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab" data-name="Assets" data-slug="assets">
|
|
<div class="edit-form-row">
|
|
<div>Light theme logo:</div>
|
|
<div class="light_logo">
|
|
{{ template "image_selector.html" imageselectordata "light_logo" .ProjectSettings.LightLogo false }}
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div>Dark theme logo:</div>
|
|
<div class="dark_logo">
|
|
{{ template "image_selector.html" imageselectordata "dark_logo" .ProjectSettings.DarkLogo false }}
|
|
</div>
|
|
</div>
|
|
<div class="edit-form-row">
|
|
<div></div>
|
|
<div>
|
|
{{ if .Editing }}
|
|
<input type="submit" value="Save" />
|
|
{{ else }}
|
|
<input type="submit" value="Create project" />
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<input type="file" multiple name="file_input" id="file_input" class="dn" />{{/* NOTE(mark): copied NOTE(asaf): Placing this outside the form to avoid submitting it to the server by accident */}}
|
|
</div>
|
|
<script>
|
|
let csrf = JSON.parse({{ csrftokenjs .Session }});
|
|
|
|
let projectForm = document.querySelector("#project_form");
|
|
|
|
projectForm.addEventListener("invalid", function(ev) {
|
|
switchToTabOfElement(document.body, ev.target);
|
|
}, true);
|
|
|
|
function gotoTab(tabName) {
|
|
switchTab(document.body, tabName);
|
|
}
|
|
|
|
//////////
|
|
// Tags //
|
|
//////////
|
|
|
|
const tag = document.querySelector('#tag');
|
|
const tagPreview = document.querySelector('#tag-preview');
|
|
function updateTagPreview() {
|
|
tagPreview.innerText = tag.value;
|
|
document.querySelector('#tag-discord-info').classList.toggle('dn', tag.value.length === 0);
|
|
}
|
|
updateTagPreview();
|
|
tag.addEventListener('input', () => updateTagPreview());
|
|
|
|
////////////////////////////
|
|
// Description management //
|
|
////////////////////////////
|
|
|
|
{{ if .Editing }}
|
|
const projectName = "new-project";
|
|
{{ else }}
|
|
const projectName = "{{ .Project.Name }}";
|
|
{{ end }}
|
|
const description = document.querySelector('#description');
|
|
const descPreview = document.querySelector('#desc-preview');
|
|
const { clear: clearDescription } = autosaveContent({
|
|
inputEl: description,
|
|
storageKey: `project-description/${projectName}`,
|
|
});
|
|
projectForm.addEventListener('submit', () => clearDescription());
|
|
|
|
let doMarkdown = initLiveMarkdown({ inputEl: description, previewEl: descPreview });
|
|
|
|
//////////////////////
|
|
// Owner management //
|
|
//////////////////////
|
|
|
|
const OWNER_QUERY_STATE_IDLE = 0;
|
|
const OWNER_QUERY_STATE_QUERYING = 1;
|
|
|
|
const MAX_OWNERS = {{ .MaxOwners }};
|
|
|
|
let ownerCheckUrl = "{{ .APICheckUsernameUrl }}";
|
|
let ownerQueryState = OWNER_QUERY_STATE_IDLE;
|
|
let addOwnerInput = document.querySelector("#owner_name");
|
|
let addOwnerButton = document.querySelector("#owner_add");
|
|
let ownersError = document.querySelector("#owners_error");
|
|
let ownerList = document.querySelector("#owner_list");
|
|
let ownerTemplate = makeTemplateCloner("owner_row");
|
|
|
|
addOwnerInput.addEventListener("keypress", function(ev) {
|
|
if (ev.which == 13) {
|
|
startAddOwner();
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
}
|
|
});
|
|
|
|
addOwnerButton.addEventListener("click", function(ev) {
|
|
startAddOwner();
|
|
});
|
|
|
|
function updateAddOwnerStyles() {
|
|
const numOwnerRows = document.querySelectorAll('.owner_row').length;
|
|
addOwnerInput.disabled = numOwnerRows >= MAX_OWNERS;
|
|
}
|
|
updateAddOwnerStyles();
|
|
|
|
function startAddOwner() {
|
|
if (ownerQueryState == OWNER_QUERY_STATE_QUERYING) {
|
|
return;
|
|
}
|
|
let newOwner = addOwnerInput.value.trim().toLowerCase();
|
|
if (newOwner.length == 0) {
|
|
return;
|
|
}
|
|
let ownerEls = ownerList.querySelectorAll(".owner_row input[name='owners']");
|
|
for (let i = 0; i < ownerEls.length; ++i) {
|
|
let existingOwner = ownerEls[i].value.toLowerCase();
|
|
if (newOwner == existingOwner) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
ownersError.textContent = "";
|
|
let xhr = new XMLHttpRequest();
|
|
xhr.withCredentials = true;
|
|
xhr.open("POST", ownerCheckUrl);
|
|
xhr.responseType = "json";
|
|
xhr.addEventListener("load", function(ev) {
|
|
let result = xhr.response;
|
|
if (result) {
|
|
if (result.found) {
|
|
addOwner(result.canonical);
|
|
addOwnerInput.value = "";
|
|
} else {
|
|
ownersError.textContent = "Username not found";
|
|
}
|
|
} else {
|
|
ownersError.textContent = "There was an issue validating this username";
|
|
}
|
|
setOwnerQueryState(OWNER_QUERY_STATE_IDLE);
|
|
if (document.activeElement == addOwnerButton) {
|
|
addOwnerInput.focus();
|
|
}
|
|
});
|
|
xhr.addEventListener("error", function(ev) {
|
|
ownersError.textContent = "There was an issue validating this username";
|
|
setOwnerQueryState(OWNER_QUERY_STATE_IDLE);
|
|
});
|
|
let data = new FormData();
|
|
data.append(csrf.field, csrf.token);
|
|
data.append("username", newOwner);
|
|
xhr.send(data);
|
|
setOwnerQueryState(OWNER_QUERY_STATE_QUERYING);
|
|
}
|
|
|
|
function setOwnerQueryState(state) {
|
|
ownerQueryState = state;
|
|
querying = (ownerQueryState == OWNER_QUERY_STATE_QUERYING);
|
|
addOwnerInput.disabled = querying;
|
|
addOwnerButton.disabled = querying;
|
|
updateAddOwnerStyles();
|
|
}
|
|
|
|
function addOwner(username) {
|
|
let ownerEl = ownerTemplate();
|
|
ownerEl.input.value = username;
|
|
ownerEl.name.textContent = username;
|
|
ownerList.appendChild(ownerEl.root);
|
|
updateAddOwnerStyles();
|
|
}
|
|
|
|
ownerList.addEventListener("click", function(ev) {
|
|
if (ev.target.classList.contains("remove_owner")) {
|
|
ev.target.parentElement.remove();
|
|
}
|
|
updateAddOwnerStyles();
|
|
});
|
|
|
|
/////////////////////
|
|
// Logo management //
|
|
/////////////////////
|
|
|
|
const logoMaxFileSize = {{ .LogoMaxFileSize }};
|
|
|
|
let lightLogoSelector = new ImageSelector(
|
|
document.querySelector("#project_form"),
|
|
logoMaxFileSize,
|
|
document.querySelector(".light_logo")
|
|
);
|
|
|
|
let darkLogoSelector = new ImageSelector(
|
|
document.querySelector("#project_form"),
|
|
logoMaxFileSize,
|
|
document.querySelector(".dark_logo")
|
|
);
|
|
|
|
//////////////////
|
|
// Asset upload //
|
|
//////////////////
|
|
|
|
const maxFileSize = {{ .MaxFileSize }};
|
|
const uploadUrl = {{ .UploadUrl }};
|
|
const submitButton = document.querySelector("#project_form [data-name=Description] input[type=submit]");
|
|
const submitText = submitButton.value;
|
|
const fileInput = document.querySelector('#file_input');
|
|
const uploadBar = document.querySelector('.upload_bar');
|
|
const uploadProgress = document.querySelector('.upload_bar .progress');
|
|
const uploadProgressText = document.querySelector('.upload_bar .progress_text');
|
|
const uploadProgressBar = document.querySelector('.upload_bar .progress_bar');
|
|
const uploadProgressBarFill = document.querySelector('.upload_bar .progress_bar > div');
|
|
let fileCounter = 0;
|
|
let enterCounter = 0;
|
|
let uploadQueue = [];
|
|
let currentUpload = null;
|
|
let currentXhr = null;
|
|
let currentBatchSize = 0;
|
|
let currentBatchDone = 0;
|
|
|
|
fileInput.addEventListener("change", function(ev) {
|
|
if (fileInput.files.length > 0) {
|
|
importUserFiles(fileInput.files);
|
|
}
|
|
});
|
|
|
|
description.addEventListener("dragover", function(ev) {
|
|
let effect = "none";
|
|
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
|
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
|
effect = "copy";
|
|
break;
|
|
}
|
|
}
|
|
ev.dataTransfer.dropEffect = effect;
|
|
ev.preventDefault();
|
|
});
|
|
|
|
description.addEventListener("dragenter", function(ev) {
|
|
enterCounter++;
|
|
let droppable = false;
|
|
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
|
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
|
droppable = true;
|
|
break;
|
|
}
|
|
}
|
|
if (droppable) {
|
|
description.classList.add("drop");
|
|
}
|
|
});
|
|
|
|
description.addEventListener("dragleave", function(ev) {
|
|
enterCounter--;
|
|
if (enterCounter == 0) {
|
|
description.classList.remove("drop");
|
|
}
|
|
});
|
|
|
|
function makeUploadString(uploadNumber, filename) {
|
|
return `Uploading file #${uploadNumber}: \`${filename}\`...`;
|
|
}
|
|
|
|
description.addEventListener("drop", function(ev) {
|
|
enterCounter = 0;
|
|
description.classList.remove("drop");
|
|
|
|
if (ev.dataTransfer && ev.dataTransfer.files) {
|
|
importUserFiles(ev.dataTransfer.files)
|
|
}
|
|
|
|
ev.preventDefault();
|
|
});
|
|
|
|
description.addEventListener("paste", function(ev) {
|
|
const files = ev.clipboardData?.files ?? [];
|
|
if (files.length > 0) {
|
|
importUserFiles(files)
|
|
}
|
|
});
|
|
|
|
function importUserFiles(files) {
|
|
let items = [];
|
|
for (let i = 0; i < files.length; ++i) {
|
|
let f = files[i];
|
|
if (f.size < maxFileSize) {
|
|
items.push({ file: f, error: null });
|
|
} else {
|
|
items.push({ file: null, error: `\`${f.name}\` is too big! Max size is ${maxFileSize} but the file is ${f.size}.` });
|
|
}
|
|
}
|
|
|
|
let cursorStart = description.selectionStart;
|
|
let cursorEnd = description.selectionEnd;
|
|
|
|
let toInsert = "";
|
|
let linesToCursor = description.value.substr(0, cursorStart).split("\n");
|
|
let cursorLine = linesToCursor[linesToCursor.length-1].trim();
|
|
if (cursorLine.length > 0) {
|
|
toInsert = "\n\n";
|
|
}
|
|
for (let i = 0; i < items.length; ++i) {
|
|
if (items[i].file) {
|
|
fileCounter++;
|
|
toInsert += makeUploadString(fileCounter, items[i].file.name) + "\n\n";
|
|
queueUpload(fileCounter, items[i].file);
|
|
} else {
|
|
toInsert += `${items[i].error}\n\n`;
|
|
}
|
|
}
|
|
|
|
description.value = description.value.substring(0, cursorStart) + toInsert + description.value.substring(cursorEnd, description.value.length);
|
|
doMarkdown();
|
|
uploadNext();
|
|
}
|
|
|
|
function replaceUploadString(upload, newString) {
|
|
let cursorStart = description.selectionStart;
|
|
let cursorEnd = description.selectionEnd;
|
|
let uploadString = makeUploadString(upload.uploadNumber, upload.file.name);
|
|
let insertIndex = description.value.indexOf(uploadString)
|
|
description.value = description.value.replace(uploadString, newString);
|
|
if (cursorStart <= insertIndex + uploadString.length) {
|
|
description.selectionStart = cursorStart;
|
|
} else {
|
|
description.selectionStart = cursorStart - uploadString.length + newString.length;
|
|
}
|
|
if (cursorEnd <= insertIndex + uploadString.length) {
|
|
description.selectionEnd = cursorEnd;
|
|
} else {
|
|
description.selectionEnd = cursorEnd - uploadString.length + newString.length;
|
|
}
|
|
doMarkdown();
|
|
}
|
|
|
|
function replaceUploadStringError(upload) {
|
|
replaceUploadString(upload, `There was a problem uploading your file \`${upload.file.name}\`.`);
|
|
}
|
|
|
|
function queueUpload(uploadNumber, file) {
|
|
uploadQueue.push({
|
|
uploadNumber: uploadNumber,
|
|
file: file
|
|
});
|
|
|
|
currentBatchSize++;
|
|
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
|
|
}
|
|
|
|
function uploadDone(ev) {
|
|
try {
|
|
if (currentXhr.status == 200 && currentXhr.response) {
|
|
if (currentXhr.response.url) {
|
|
let url = currentXhr.response.url;
|
|
let newString = `[${currentUpload.file.name}](${url})`;
|
|
if (currentXhr.response.mime.startsWith("image")) {
|
|
newString = "!" + newString;
|
|
}
|
|
|
|
replaceUploadString(currentUpload, newString);
|
|
} else if (currentXhr.response.error) {
|
|
replaceUploadString(currentUpload, `Upload failed for \`${currentUpload.file.name}\`: ${currentXhr.response.error}.`);
|
|
} else {
|
|
replaceUploadStringError(currentUpload);
|
|
}
|
|
} else {
|
|
replaceUploadStringError(currentUpload);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
replaceUploadStringError(currentUpload);
|
|
}
|
|
currentUpload = null;
|
|
currentXhr = null;
|
|
currentBatchDone++;
|
|
uploadNext();
|
|
}
|
|
|
|
function updateUploadProgress(ev) {
|
|
if (ev.lengthComputable) {
|
|
let progress = ev.loaded / ev.total;
|
|
uploadProgressBarFill.style.width = Math.floor(progress * 100) + "%";
|
|
}
|
|
}
|
|
|
|
function uploadNext() {
|
|
if (currentUpload == null) {
|
|
next = uploadQueue.shift();
|
|
if (next) {
|
|
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
|
|
uploadBar.classList.add("uploading");
|
|
uploadProgressBarFill.style.width = "0%";
|
|
submitButton.disabled = true;
|
|
submitButton.value = "Uploading files...";
|
|
|
|
try {
|
|
let utf8Filename = strToUTF8Arr(next.file.name);
|
|
let base64Filename = base64EncArr(utf8Filename);
|
|
// NOTE(mark): copied from NOTE(asaf): We use XHR because fetch can't do upload progress reports. Womp womp. https://youtu.be/Pubd-spHN-0?t=2
|
|
currentXhr = new XMLHttpRequest();
|
|
currentXhr.upload.addEventListener("progress", updateUploadProgress);
|
|
currentXhr.open("POST", uploadUrl, true);
|
|
currentXhr.setRequestHeader("Hmn-Upload-Filename", base64Filename);
|
|
currentXhr.responseType = "json";
|
|
currentXhr.addEventListener("loadend", uploadDone);
|
|
currentXhr.send(next.file);
|
|
currentUpload = next;
|
|
} catch (err) {
|
|
replaceUploadStringError(next);
|
|
console.error(err);
|
|
uploadNext();
|
|
}
|
|
} else {
|
|
submitButton.disabled = false;
|
|
submitButton.value = submitText;
|
|
uploadBar.classList.remove("uploading");
|
|
currentBatchSize = 0;
|
|
currentBatchDone = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
</script>
|
|
{{ end }}
|