Merge pull request #7734 from Fensterbank/client-side-picture-resize

Resize images on client-side before uploading
This commit is contained in:
Dennis Schubert 2018-04-02 10:37:45 +02:00
commit ae17248b2e
No known key found for this signature in database
GPG key ID: 5A0304BEA7966D7E
11 changed files with 234 additions and 163 deletions

View file

@ -1,12 +1,13 @@
# 0.8.0.0
## Refactor
* Add bootstrapping for using ECMAScript 6 with automatic transpiling for compatiblilty [#7581](https://github.com/diaspora/diaspora/pull/7581)
* Add bootstrapping for using ECMAScript 6 with automatic transpiling for compatibility [#7581](https://github.com/diaspora/diaspora/pull/7581)
## Bug fixes
## Features
* Add client-side cropping of profile image uploads [#7581](https://github.com/diaspora/diaspora/pull/7581)
* Add client-site rescaling of post images if they exceed the maximum possible size [#7734](https://github.com/diaspora/diaspora/pull/7734)
# 0.7.5.0

View file

@ -113,6 +113,7 @@ source "https://rails-assets.org" do
gem "rails-assets-corejs-typeahead", "1.1.1"
gem "rails-assets-cropperjs", "1.2.1"
gem "rails-assets-fine-uploader", "5.13.0"
gem "rails-assets-pica", "4.0.1"
# jQuery plugins

View file

@ -558,6 +558,7 @@ GEM
rails-assets-markdown-it-sub (1.0.0)
rails-assets-markdown-it-sup (1.0.0)
rails-assets-perfect-scrollbar (0.6.16)
rails-assets-pica (4.0.1)
rails-assets-underscore (1.8.3)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@ -878,6 +879,7 @@ DEPENDENCIES
rails-assets-markdown-it-sub (= 1.0.0)!
rails-assets-markdown-it-sup (= 1.0.0)!
rails-assets-perfect-scrollbar (= 0.6.16)!
rails-assets-pica (= 4.0.1)!
rails-controller-testing (= 1.0.2)
rails-i18n (= 5.1.1)
rails-timeago (= 2.16.0)

View file

@ -5,65 +5,19 @@
// progress. Attaches previews of finished uploads to the publisher.
app.views.PublisherUploader = Backbone.View.extend({
allowedExtensions: ["jpg", "jpeg", "png", "gif"],
sizeLimit: 4194304, // bytes
initialize: function(opts) {
this.publisher = opts.publisher;
this.uploader = new qq.FineUploaderBasic({
element: this.el,
button: this.el,
text: {
fileInputTitle: Diaspora.I18n.t("photo_uploader.upload_photos")
},
request: {
endpoint: Routes.photos(),
params: {
/* eslint-disable camelcase */
authenticity_token: $("meta[name='csrf-token']").attr("content"),
/* eslint-enable camelcase */
photo: {
pending: true
}
}
},
validation: {
allowedExtensions: this.allowedExtensions,
sizeLimit: this.sizeLimit
},
messages: {
typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"),
sizeError: Diaspora.I18n.t("photo_uploader.size_error"),
emptyError: Diaspora.I18n.t("photo_uploader.empty")
},
callbacks: {
onProgress: _.bind(this.progressHandler, this),
onSubmit: _.bind(this.submitHandler, this),
onComplete: _.bind(this.uploadCompleteHandler, this),
onError: function(id, name, errorReason) {
if (app.flashMessages) { app.flashMessages.error(errorReason); }
}
}
});
this.info = $("<div id=\"fileInfo\" />");
this.publisher.wrapperEl.before(this.info);
this.publisher.photozoneEl.on("click", ".x", _.bind(this._removePhoto, this));
},
progressHandler: function(id, fileName, loaded, total) {
var progress = Math.round(loaded / total * 100);
this.info.text(fileName + " " + progress + "%").fadeTo(200, 1);
this.publisher.photozoneEl
.find("li.loading").first().find(".progress-bar")
.width(progress + "%");
},
// Initialize the PostPhotoUploader and subscribe its events
this.uploader = new Diaspora.PostPhotoUploader(this.el);
submitHandler: function() {
this.$el.addClass("loading");
this._addPhotoPlaceholder();
this.uploader.onUploadStarted = _.bind(this.uploadStartedHandler, this);
this.uploader.onProgress = _.bind(this.progressHandler, this);
this.uploader.onUploadCompleted = _.bind(this.uploadCompleteHandler, this);
},
// add photo placeholders to the publisher to indicate an upload in progress
@ -82,14 +36,26 @@ app.views.PublisherUploader = Backbone.View.extend({
);
},
uploadStartedHandler: function() {
this.$el.addClass("loading");
this._addPhotoPlaceholder();
},
progressHandler: function(fileName, progress) {
this.info.text(fileName + " " + progress + "%").fadeTo(200, 1);
this.publisher.photozoneEl
.find("li.loading").first().find(".progress-bar")
.width(progress + "%");
},
uploadCompleteHandler: function(_id, fileName, response) {
if (response.success){
this.info.text(Diaspora.I18n.t("photo_uploader.completed", {file: fileName})).fadeTo(2000, 0);
var id = response.data.photo.id,
url = response.data.photo.unprocessed_image.url;
image = response.data.photo.unprocessed_image;
this._addFinishedPhoto(id, url);
this._addFinishedPhoto(id, image);
this.trigger("change");
} else {
this._cancelPhotoUpload();
@ -105,14 +71,13 @@ app.views.PublisherUploader = Backbone.View.extend({
// replace the first photo placeholder with the finished uploaded image and
// add the id to the publishers form
_addFinishedPhoto: function(id, url) {
_addFinishedPhoto: function(id, image) {
var publisher = this.publisher;
// add form input element
publisher.$(".content_creation form").append(
"<input type=\"hidden\", value=\""+id+"\" name=\"photos[]\" />"
);
// replace placeholder
var placeholder = publisher.photozoneEl.find("li.loading").first();
placeholder
@ -121,7 +86,13 @@ app.views.PublisherUploader = Backbone.View.extend({
"<div class=\"x\"></div>"+
"<div class=\"circle\"></div>"
)
.find("img").attr({"src": url, "data-id": id}).removeClass("ajax-loader");
.find("img").attr(
{
"src": image.thumb_medium.url,
"data-small": image.thumb_small.url,
"data-scaled": image.scaled_full.url,
"data-id": id
}).removeClass("ajax-loader");
placeholder
.find("div.progress").remove();

View file

@ -284,13 +284,13 @@ app.views.Publisher = Backbone.View.extend({
getUploadedPhotos: function() {
var photos = [];
$("li.publisher_photo img").each(function() {
var file = $(this).attr("src").substring("/uploads/images/".length);
var photo = $(this);
photos.push(
{
"sizes": {
"small" : "/uploads/images/thumb_small_" + file,
"medium" : "/uploads/images/thumb_medium_" + file,
"large" : "/uploads/images/scaled_full_" + file
"small": photo.data("small"),
"medium": photo.attr("src"),
"large": photo.data("scaled")
}
}
);

View file

@ -0,0 +1,103 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
Diaspora.PostPhotoUploader = class {
/**
* Initializes a new instance of PostPhotoUploader
* This class handles uploading photos and provides client side scaling
*/
constructor(el, aspectIds) {
this.element = el;
this.sizeLimit = 4194304;
this.aspectIds = aspectIds;
this.onProgress = null;
this.onUploadStarted = null;
this.onUploadCompleted = null;
/**
* Shows a message using flash messages or alert for mobile.
* @param {string} type - The type of the message, e.g. "error" or "success".
* @param text - The text to display.
*/
this.showMessage = (type, text) => (app.flashMessages ? app.flashMessages[type](text) : alert(text));
/**
* Returns true if the given parameter is a function
* @param {param} - The object to check
* @returns {boolean}
*/
this.func = param => (typeof param === "function");
this.initFineUploader();
}
/**
* Initializes the fine uploader component
*/
initFineUploader() {
this.fineUploader = new qq.FineUploaderBasic({
element: this.element,
button: this.element,
text: {
fileInputTitle: Diaspora.I18n.t("photo_uploader.upload_photos")
},
request: {
endpoint: Routes.photos(),
params: {
/* eslint-disable camelcase */
authenticity_token: $("meta[name='csrf-token']").attr("content"),
photo: {
pending: true,
aspect_ids: this.aspectIds
}
/* eslint-enable camelcase */
}
},
validation: {
allowedExtensions: ["jpg", "jpeg", "png", "gif"],
sizeLimit: (window.Promise && qq.supportedFeatures.scaling ? null : this.sizeLimit)
},
messages: {
typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"),
sizeError: Diaspora.I18n.t("photo_uploader.size_error"),
emptyError: Diaspora.I18n.t("photo_uploader.empty")
},
callbacks: {
onSubmit: (id, name) => this.onPictureSelected(id, name),
onUpload: (id, name) => (this.func(this.onUploadStarted) && this.onUploadStarted(id, name)),
onProgress: (id, fileName, loaded, total) =>
(this.func(this.onProgress) && this.onProgress(fileName, Math.round(loaded / total * 100))),
onComplete: (id, name, json) => (this.func(this.onUploadCompleted) && this.onUploadCompleted(id, name, json)),
onError: (id, name, errorReason) => this.showMessage("error", errorReason)
}
});
}
/**
* Called when a picture from user's device has been selected.
* Scales the images using Pica if the image exceeds the file size limit
* @param {number} id - The current file's id.
* @param {string} name - The current file's name.
*/
onPictureSelected(id, name) {
// scale image because it's bigger than the size limit and the browser supports it
if (this.fineUploader.getSize(id) > this.sizeLimit && window.Promise && qq.supportedFeatures.scaling) {
this.fineUploader.scaleImage(id, {
maxSize: 3072,
customResizer: !qq.ios() && (i => window.pica().resize(i.sourceCanvas, i.targetCanvas))
}).then(scaledImage => {
this.fineUploader.addFiles({
blob: scaledImage,
name: name
});
});
// since we are adding the smaller scaled image afterwards, we return false
return false;
}
// return true to upload the image without scaling
return true;
}
};
// @license-end

View file

@ -47,3 +47,4 @@
//= require helpers/markdown_editor
//= require jquery.are-you-sure
//= require cropperjs/dist/cropper.js
//= require pica

View file

@ -14,11 +14,13 @@
//= require jquery.timeago
//= require underscore
//= require bootstrap
//= require pica
//= require diaspora
//= require helpers/i18n
//= require helpers/tags_autocomplete
//= require bootstrap-markdown/bootstrap-markdown
//= require helpers/markdown_editor
//= require helpers/post_photo_uploader
//= require widgets/timeago
//= require mobile/mobile_application
//= require mobile/mobile_file_uploader

View file

@ -2,97 +2,74 @@
//= require js_image_paths
function createUploader(){
var aspectIds = gon.preloads.aspect_ids;
var aspectIds = gon.preloads.aspect_ids;
var fileInfo = $("#fileInfo-publisher");
new qq.FineUploaderBasic({
element: document.getElementById("file-upload-publisher"),
request: {
endpoint: Routes.photos(),
params: {
/* eslint-disable camelcase */
authenticity_token: $("meta[name='csrf-token']").attr("content"),
photo: {
aspect_ids: aspectIds,
/* eslint-enable camelcase */
pending: true
}
}
},
validation: {
allowedExtensions: ["jpg", "jpeg", "png", "gif"],
sizeLimit: 4194304
},
button: document.getElementById("file-upload-publisher"),
text: {
fileInputTitle: Diaspora.I18n.t("photo_uploader.upload_photos")
},
// Initialize the PostPhotoUploader and subscribe its events
this.uploader = new Diaspora.PostPhotoUploader(document.getElementById("file-upload-publisher"), aspectIds);
callbacks: {
onProgress: function(id, fileName, loaded, total) {
var progress = Math.round(loaded / total * 100);
$("#fileInfo-publisher").text(fileName + " " + progress + "%");
},
onSubmit: function() {
$("#publisher-textarea-wrapper").addClass("with_attachments");
$("#photodropzone").append(
"<li class='publisher_photo loading' style='position:relative;'>" +
"<img alt='Ajax-loader2' src='" + ImagePaths.get("ajax-loader2.gif") + "' />" +
"</li>"
);
},
onComplete: function(_id, fileName, responseJSON) {
if (responseJSON.data === undefined) {
return;
}
this.uploader.onUploadStarted = _.bind(uploadStartedHandler, this);
this.uploader.onProgress = _.bind(progressHandler, this);
this.uploader.onUploadCompleted = _.bind(uploadCompletedHandler, this);
$("#fileInfo-publisher").text(Diaspora.I18n.t("photo_uploader.completed", {"file": fileName}));
var id = responseJSON.data.photo.id,
url = responseJSON.data.photo.unprocessed_image.url,
currentPlaceholder = $("li.loading").first();
function progressHandler(fileName, progress) {
fileInfo.text(fileName + " " + progress + "%");
}
$("#publisher-textarea-wrapper").addClass("with_attachments");
$("#new_status_message").append("<input type='hidden' value='" + id + "' name='photos[]' />");
function uploadStartedHandler() {
$("#publisher-textarea-wrapper").addClass("with_attachments");
$("#photodropzone").append(
"<li class='publisher_photo loading' style='position:relative;'>" +
"<img alt='Ajax-loader2' src='" + ImagePaths.get("ajax-loader2.gif") + "' />" +
"</li>"
);
}
// replace image placeholders
var img = currentPlaceholder.find("img");
img.attr("src", url);
img.attr("data-id", id);
currentPlaceholder.removeClass("loading");
currentPlaceholder.append("<div class='x'>X</div>" +
"<div class='circle'></div>");
function uploadCompletedHandler(_id, fileName, responseJSON) {
if (responseJSON.data === undefined) {
return;
}
var publisher = $("#publisher");
fileInfo.text(Diaspora.I18n.t("photo_uploader.completed", {"file": fileName}));
var id = responseJSON.data.photo.id,
image = responseJSON.data.photo.unprocessed_image,
currentPlaceholder = $("li.loading").first();
publisher.find("input[type='submit']").removeAttr("disabled");
$("#publisher-textarea-wrapper").addClass("with_attachments");
$("#new_status_message").append("<input type='hidden' value='" + id + "' name='photos[]' />");
$(".x").bind("click", function() {
var photo = $(this).closest(".publisher_photo");
photo.addClass("dim");
$.ajax({
url: "/photos/" + photo.children("img").attr("data-id"),
dataType: "json",
type: "DELETE",
success: function() {
photo.fadeOut(400, function() {
photo.remove();
if ($(".publisher_photo").length === 0) {
$("#publisher-textarea-wrapper").removeClass("with_attachments");
}
});
// replace image placeholders
var img = currentPlaceholder.find("img");
img.attr("src", image.thumb_medium.url);
img.attr("data-small", image.thumb_small.url);
img.attr("data-scaled", image.scaled_full.url);
img.attr("data-id", id);
currentPlaceholder.removeClass("loading");
currentPlaceholder.append("<div class='x'>X</div>" +
"<div class='circle'></div>");
var publisher = $("#publisher");
publisher.find("input[type='submit']").removeAttr("disabled");
$(".x").bind("click", function() {
var photo = $(this).closest(".publisher_photo");
photo.addClass("dim");
$.ajax({
url: "/photos/" + photo.children("img").attr("data-id"),
dataType: "json",
type: "DELETE",
success: function() {
photo.fadeOut(400, function() {
photo.remove();
if ($(".publisher_photo").length === 0) {
$("#publisher-textarea-wrapper").removeClass("with_attachments");
}
});
});
},
onError: function(id, name, errorReason) {
alert(errorReason);
}
},
messages: {
typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"),
sizeError: Diaspora.I18n.t("photo_uploader.size_error"),
emptyError: Diaspora.I18n.t("photo_uploader.empty")
}
});
}
});
});
}
}
window.addEventListener("load", function() {
createUploader();

View file

@ -6,16 +6,16 @@ SecureHeaders::Configuration.default do |config|
# rubocop:disable Lint/PercentStringArray
csp = {
default_src: %w['none'],
child_src: %w['self' www.youtube.com w.soundcloud.com twitter.com platform.twitter.com syndication.twitter.com
player.vimeo.com www.mixcloud.com www.dailymotion.com media.ccc.de bandcamp.com
www.instagram.com],
child_src: %w['self' blob: www.youtube.com w.soundcloud.com twitter.com platform.twitter.com
syndication.twitter.com player.vimeo.com www.mixcloud.com www.dailymotion.com media.ccc.de
bandcamp.com www.instagram.com],
connect_src: %w['self' embedr.flickr.com geo.query.yahoo.com nominatim.openstreetmap.org api.github.com],
font_src: %w['self'],
form_action: %w['self' platform.twitter.com syndication.twitter.com],
frame_ancestors: %w['self'],
img_src: %w['self' data: blob: *],
media_src: %w[https:],
script_src: %w['self' 'unsafe-eval' platform.twitter.com cdn.syndication.twimg.com widgets.flickr.com
script_src: %w['self' blob: 'unsafe-eval' platform.twitter.com cdn.syndication.twimg.com widgets.flickr.com
embedr.flickr.com platform.instagram.com 'unsafe-inline'],
style_src: %w['self' 'unsafe-inline' platform.twitter.com *.twimg.com]
}

View file

@ -539,36 +539,36 @@ describe("app.views.Publisher", function() {
this.view = new app.views.Publisher();
// replace the uploader plugin with a dummy object
var upload_view = this.view.viewUploader;
var uploadView = this.view.viewUploader;
this.uploader = {
onProgress: _.bind(upload_view.progressHandler, upload_view),
onSubmit: _.bind(upload_view.submitHandler, upload_view),
onComplete: _.bind(upload_view.uploadCompleteHandler, upload_view)
onProgress: _.bind(uploadView.progressHandler, uploadView),
onUpload: _.bind(uploadView.uploadStartedHandler, uploadView),
onComplete: _.bind(uploadView.uploadCompleteHandler, uploadView)
};
upload_view.uploader = this.uploader;
uploadView.uploader = this.uploader;
});
context('progress', function() {
it('shows progress in percent', function() {
this.uploader.onProgress(null, 'test.jpg', 20, 100);
context("progress", function() {
it("shows progress in percent", function() {
this.uploader.onProgress("test.jpg", 20);
var info = this.view.viewUploader.info;
expect(info.text()).toContain('test.jpg');
expect(info.text()).toContain('20%');
expect(info.text()).toContain("test.jpg");
expect(info.text()).toContain("20%");
});
});
context('submitting', function() {
context("submitting", function() {
beforeEach(function() {
this.uploader.onSubmit(null, 'test.jpg');
this.uploader.onUpload(null, "test.jpg");
});
it('adds a placeholder', function() {
it("adds a placeholder", function() {
expect(this.view.wrapperEl.attr("class")).toContain("with_attachments");
expect(this.view.photozoneEl.find("li").length).toBe(1);
});
it('disables the publisher buttons', function() {
it("disables the publisher buttons", function() {
expect(this.view.submitEl.prop("disabled")).toBeTruthy();
});
});
@ -577,13 +577,20 @@ describe("app.views.Publisher", function() {
beforeEach(function() {
$('#photodropzone').html('<li class="publisher_photo loading"><img src="" /></li>');
/* eslint-disable camelcase */
this.uploader.onComplete(null, 'test.jpg', {
data: { photo: {
id: '987',
unprocessed_image: { url: 'test.jpg' }
unprocessed_image: {
thumb_small: {url: "test.jpg"},
thumb_medium: {url: "test.jpg"},
thumb_large: {url: "test.jpg"},
scaled_full: {url: "test.jpg"}
}
}},
success: true });
});
/* eslint-enable camelcase */
it('shows it in text form', function() {
var info = this.view.viewUploader.info;
@ -613,14 +620,20 @@ describe("app.views.Publisher", function() {
beforeEach(function() {
$('#photodropzone').html('<li class="publisher_photo loading"><img src="" /></li>');
/* eslint-disable camelcase */
this.uploader.onComplete(null, 'test.jpg', {
data: { photo: {
id: '987',
unprocessed_image: { url: 'test.jpg' }
unprocessed_image: {
thumb_small: {url: "test.jpg"},
thumb_medium: {url: "test.jpg"},
thumb_large: {url: "test.jpg"},
scaled_full: {url: "test.jpg"}
}
}},
success: false });
});
/* eslint-enable camelcase */
it('shows error message', function() {
var info = this.view.viewUploader.info;
expect(info.text()).toBe(Diaspora.I18n.t('photo_uploader.error', {file: 'test.jpg'}));