- <%= ctx.makeCheckbox({
- text: 'Upload anonymously',
- name: 'anonymous',
- checked: ctx.uploadable.anonymous,
- }) %>
+
+
+ <% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
+ <%= ctx.makeRadio({
+ name: 'safety-' + ctx.uploadable.key,
+ value: safety,
+ text: safety[0].toUpperCase() + safety.substr(1),
+ selectedValue: ctx.uploadable.safety,
+ }) %>
+ <% } %>
- <% } %>
- <% if (['video'].includes(ctx.uploadable.type)) { %>
-
- <%= ctx.makeCheckbox({
- text: 'Loop video',
- name: 'loop-video',
- checked: ctx.uploadable.flags.includes('loop'),
- }) %>
+
+ <% if (ctx.canUploadAnonymously) { %>
+
+ <%= ctx.makeCheckbox({
+ text: 'Upload anonymously',
+ name: 'anonymous',
+ checked: ctx.uploadable.anonymous,
+ }) %>
+
+ <% } %>
+
+ <% if (['video'].includes(ctx.uploadable.type)) { %>
+
+ <%= ctx.makeCheckbox({
+ text: 'Loop video',
+ name: 'loop-video',
+ checked: ctx.uploadable.flags.includes('loop'),
+ }) %>
+
+ <% } %>
- <% } %>
+
+
+
diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js
index 02b45cf..9cf3d4b 100644
--- a/client/js/controllers/post_upload_controller.js
+++ b/client/js/controllers/post_upload_controller.js
@@ -8,9 +8,13 @@ const Post = require('../models/post.js');
const PostUploadView = require('../views/post_upload_view.js');
const EmptyView = require('../views/empty_view.js');
+const genericErrorMessage =
+ 'One of the posts needs your attention; ' +
+ 'click "resume upload" when you\'re ready.';
+
class PostUploadController {
constructor() {
- this._lastPromise = null;
+ this._lastCancellablePromise = null;
if (!api.hasPrivilege('posts:create')) {
this._view = new EmptyView();
@@ -22,6 +26,7 @@ class PostUploadController {
topNavigation.setTitle('Upload');
this._view = new PostUploadView({
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'),
+ canViewPosts: api.hasPrivilege('posts:view'),
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtSubmit(e));
@@ -33,13 +38,13 @@ class PostUploadController {
misc.enableExitConfirmation();
} else {
misc.disableExitConfirmation();
+ this._view.clearMessages();
}
- this._view.clearMessages();
}
_evtCancel(e) {
- if (this._lastPromise) {
- this._lastPromise.abort();
+ if (this._lastCancellablePromise) {
+ this._lastCancellablePromise.abort();
}
}
@@ -47,46 +52,57 @@ class PostUploadController {
this._view.disableForm();
this._view.clearMessages();
- e.detail.uploadables.reduce((promise, uploadable) => {
- return promise.then(() => {
- let post = new Post();
- post.safety = uploadable.safety;
- post.flags = uploadable.flags;
- if (uploadable.url) {
- post.newContentUrl = uploadable.url;
- } else {
- post.newContent = uploadable.file;
- }
+ e.detail.uploadables.reduce(
+ (promise, uploadable) =>
+ promise.then(() =>
+ this._uploadSinglePost(
+ uploadable, e.detail.skipDuplicates)),
+ Promise.resolve())
+ .then(() => {
+ this._view.clearMessages();
+ misc.disableExitConfirmation();
+ const ctx = router.show('/posts');
+ ctx.controller.showSuccess('Posts uploaded.');
+ }, errorContext => {
+ if (errorContext.constructor === Array) {
+ const [errorMessage, uploadable] = errorContext;
+ this._view.showError(genericErrorMessage);
+ this._view.showError(errorMessage, uploadable);
+ } else {
+ this._view.showError(errorContext);
+ }
+ this._view.enableForm();
+ return Promise.reject();
+ });
+ }
- let modelPromise = post.save(uploadable.anonymous);
- this._lastPromise = modelPromise;
+ _uploadSinglePost(uploadable, skipDuplicates) {
+ let post = new Post();
+ post.safety = uploadable.safety;
+ post.flags = uploadable.flags;
- return modelPromise
- .then(() => {
- this._view.removeUploadable(uploadable);
- return Promise.resolve();
- }).catch(errorMessage => {
- // XXX:
- // lame, API eats error codes so we need to match
- // messages instead
- if (e.detail.skipDuplicates &&
- errorMessage.match(/already uploaded/)) {
- return Promise.resolve();
- }
- return Promise.reject(errorMessage);
- });
- });
- }, Promise.resolve())
+ if (uploadable.url) {
+ post.newContentUrl = uploadable.url;
+ } else {
+ post.newContent = uploadable.file;
+ }
+ let savePromise = post.save(uploadable.anonymous)
.then(() => {
- misc.disableExitConfirmation();
- const ctx = router.show('/posts');
- ctx.controller.showSuccess('Posts uploaded.');
+ this._view.removeUploadable(uploadable);
+ return Promise.resolve();
}, errorMessage => {
- this._view.showError(errorMessage);
- this._view.enableForm();
- return Promise.reject();
+ // XXX:
+ // lame, API eats error codes so we need to match
+ // messages instead
+ if (skipDuplicates &&
+ errorMessage.match(/already uploaded/)) {
+ return Promise.resolve();
+ }
+ return Promise.reject([errorMessage, uploadable, null]);
});
+ this._lastCancellablePromise = savePromise;
+ return savePromise;
}
}
diff --git a/client/js/util/views.js b/client/js/util/views.js
index 9febc7f..a70ec69 100644
--- a/client/js/util/views.js
+++ b/client/js/util/views.js
@@ -267,25 +267,26 @@ function showMessage(target, message, className) {
if (!message) {
message = 'Unknown message';
}
- const messagesHolder = target.querySelector('.messages');
- if (!messagesHolder) {
+ const messagesHolderNode = target.querySelector('.messages');
+ if (!messagesHolderNode) {
return false;
}
- /* TODO: animate this */
- const node = document.createElement('div');
- node.innerHTML = message.replace(/\n/g, '
');
- node.classList.add('message');
- node.classList.add(className);
- const wrapper = document.createElement('div');
- wrapper.classList.add('message-wrapper');
- wrapper.appendChild(node);
- messagesHolder.appendChild(wrapper);
+ const textNode = document.createElement('div');
+ textNode.innerHTML = message.replace(/\n/g, '
');
+ textNode.classList.add('message');
+ textNode.classList.add(className);
+ const wrapperNode = document.createElement('div');
+ wrapperNode.classList.add('message-wrapper');
+ wrapperNode.appendChild(textNode);
+ messagesHolderNode.appendChild(wrapperNode);
return true;
}
function showError(target, message) {
- document.oldTitle = document.title;
- document.title = `! ${document.title}`;
+ if (!document.title.startsWith('!')) {
+ document.oldTitle = document.title;
+ document.title = `! ${document.title}`;
+ }
return showMessage(target, misc.formatInlineMarkdown(message), 'error');
}
@@ -302,9 +303,9 @@ function clearMessages(target) {
document.title = document.oldTitle;
document.oldTitle = null;
}
- const messagesHolder = target.querySelector('.messages');
- /* TODO: animate that */
- emptyContent(messagesHolder);
+ for (let messagesHolderNode of target.querySelectorAll('.messages')) {
+ emptyContent(messagesHolderNode);
+ }
}
function htmlToDom(html) {
diff --git a/client/js/views/post_upload_view.js b/client/js/views/post_upload_view.js
index 51a035c..185f3dd 100644
--- a/client/js/views/post_upload_view.js
+++ b/client/js/views/post_upload_view.js
@@ -20,12 +20,6 @@ function _mimeTypeToPostType(mimeType) {
}[mimeType] || 'unknown';
}
-function _listen(rootNode, selector, eventType, handler) {
- for (let node of rootNode.querySelectorAll(selector)) {
- node.addEventListener(eventType, e => handler(e));
- }
-}
-
class Uploadable extends events.EventTarget {
constructor() {
super();
@@ -193,8 +187,16 @@ class PostUploadView extends events.EventTarget {
views.showSuccess(this._hostNode, message);
}
- showError(message) {
- views.showError(this._hostNode, message);
+ showError(message, uploadable) {
+ this._showMessage(views.showError, message, uploadable);
+ }
+
+ showInfo(message, uploadable) {
+ this._showMessage(views.showInfo, message, uploadable);
+ }
+
+ _showMessage(functor, message, uploadable) {
+ functor(uploadable ? uploadable.rowNode : this._hostNode, message);
}
addUploadables(uploadables) {
@@ -207,9 +209,9 @@ class PostUploadView extends events.EventTarget {
}
this._uploadables.set(uploadable.key, uploadable);
this._emit('change');
- this._createRowNode(uploadable);
+ this._renderRowNode(uploadable);
uploadable.addEventListener(
- 'finish', e => this._updateRowNode(e.detail.uploadable));
+ 'finish', e => this._updateThumbnailNode(e.detail.uploadable));
}
if (duplicatesFound) {
let message = null;
@@ -236,6 +238,7 @@ class PostUploadView extends events.EventTarget {
this._emit('change');
if (!this._uploadables.size) {
this._formNode.classList.add('inactive');
+ this._submitButtonNode.value = 'Upload all';
}
}
@@ -254,9 +257,24 @@ class PostUploadView extends events.EventTarget {
_evtFormSubmit(e) {
e.preventDefault();
+ for (let uploadable of this._uploadables.values()) {
+ this._updateUploadableFromDom(uploadable);
+ }
+ this._submitButtonNode.value = 'Resume upload';
this._emit('submit');
}
+ _updateUploadableFromDom(uploadable) {
+ const rowNode = uploadable.rowNode;
+ uploadable.safety =
+ rowNode.querySelector('.safety input:checked').value;
+ uploadable.anonymous =
+ rowNode.querySelector('.anonymous input').checked;
+ if (rowNode.querySelector('.loop-video input:checked')) {
+ uploadable.flags.push('loop');
+ }
+ }
+
_evtRemoveClick(e, uploadable) {
e.preventDefault();
if (this._uploading) {
@@ -265,48 +283,23 @@ class PostUploadView extends events.EventTarget {
this.removeUploadable(uploadable);
}
- _evtMoveUpClick(e, uploadable) {
+ _evtMoveClick(e, uploadable, delta) {
e.preventDefault();
if (this._uploading) {
return;
}
let sortedUploadables = this._getSortedUploadables();
- if (uploadable.order > 0) {
- uploadable.order--;
- const prevUploadable = sortedUploadables[uploadable.order];
- prevUploadable.order++;
- uploadable.rowNode.parentNode.insertBefore(
- uploadable.rowNode, prevUploadable.rowNode);
- }
- }
-
- _evtMoveDownClick(e, uploadable) {
- e.preventDefault();
- if (this._uploading) {
- return;
- }
- let sortedUploadables = this._getSortedUploadables();
- if (uploadable.order + 1 < sortedUploadables.length) {
- uploadable.order++;
- const nextUploadable = sortedUploadables[uploadable.order];
- nextUploadable.order--;
- uploadable.rowNode.parentNode.insertBefore(
- nextUploadable.rowNode, uploadable.rowNode);
- }
- }
-
- _evtSafetyRadioboxChange(e, uploadable) {
- uploadable.safety = e.target.value;
- }
-
- _evtAnonymityCheckboxChange(e, uploadable) {
- uploadable.anonymous = e.target.checked;
- }
-
- _evtLoopVideoCheckboxChange(e, uploadable) {
- uploadable.flags = uploadable.flags.filter(f => f !== 'loop');
- if (e.target.checked) {
- uploadable.flags.push('loop');
+ if ((uploadable.order + delta).between(-1, sortedUploadables.length)) {
+ uploadable.order += delta;
+ const otherUploadable = sortedUploadables[uploadable.order];
+ otherUploadable.order -= delta;
+ if (delta === 1) {
+ uploadable.rowNode.parentNode.insertBefore(
+ otherUploadable.rowNode, uploadable.rowNode);
+ } else {
+ uploadable.rowNode.parentNode.insertBefore(
+ uploadable.rowNode, otherUploadable.rowNode);
+ }
}
}
@@ -333,28 +326,27 @@ class PostUploadView extends events.EventTarget {
}}));
}
- _createRowNode(uploadable) {
+ _renderRowNode(uploadable) {
const rowNode = rowTemplate(Object.assign(
{}, this._ctx, {uploadable: uploadable}));
- this._listNode.appendChild(rowNode);
+ if (uploadable.rowNode) {
+ uploadable.rowNode.parentNode.replaceChild(
+ rowNode, uploadable.rowNode);
+ } else {
+ this._listNode.appendChild(rowNode);
+ }
- _listen(rowNode, '.safety input', 'change',
- e => this._evtSafetyRadioboxChange(e, uploadable));
- _listen(rowNode, '.anonymous input', 'change',
- e => this._evtAnonymityCheckboxChange(e, uploadable));
- _listen(rowNode, '.loop-video input', 'change',
- e => this._evtLoopVideoCheckboxChange(e, uploadable));
-
- _listen(rowNode, 'a.remove', 'click',
- e => this._evtRemoveClick(e, uploadable));
- _listen(rowNode, 'a.move-up', 'click',
- e => this._evtMoveUpClick(e, uploadable));
- _listen(rowNode, 'a.move-down', 'click',
- e => this._evtMoveDownClick(e, uploadable));
uploadable.rowNode = rowNode;
+
+ rowNode.querySelector('a.remove').addEventListener('click',
+ e => this._evtRemoveClick(e, uploadable));
+ rowNode.querySelector('a.move-up').addEventListener('click',
+ e => this._evtMoveClick(e, uploadable, -1));
+ rowNode.querySelector('a.move-down').addEventListener('click',
+ e => this._evtMoveClick(e, uploadable, 1));
}
- _updateRowNode(uploadable) {
+ _updateThumbnailNode(uploadable) {
const rowNode = rowTemplate(Object.assign(
{}, this._ctx, {uploadable: uploadable}));
views.replaceContent(