Merge pull request #5412 from pichalite/cropperjs

Ability to crop profile images before uploading
v1.18.x
psychobunny 8 years ago committed by GitHub
commit dbc3113940

@ -31,6 +31,7 @@
"connect-redis": "~3.1.0",
"cookie-parser": "^1.3.3",
"cron": "^1.0.5",
"cropperjs": "^0.8.1",
"csurf": "^1.6.1",
"daemon": "~1.1.0",
"express": "^4.14.0",

@ -68,6 +68,8 @@
"remove_uploaded_picture" : "Remove Uploaded Picture",
"upload_cover_picture": "Upload cover picture",
"remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?",
"crop_picture": "Crop picture",
"upload_cropped_picture": "Crop and upload",
"settings": "Settings",
"show_email": "Show My Email",

@ -129,4 +129,14 @@
.admin .ban-modal .units {
line-height: 1.846;
}
}
#crop-picture-modal {
.cropped-image {
max-width: 100%;
}
.cropper-container.cropper-bg {
max-width: 100%;
}
}

@ -2,7 +2,7 @@
/* globals define, ajaxify, socket, app, config, templates, bootbox */
define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', 'components'], function (header, uploader, translator, components) {
define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', 'components', 'cropper'], function (header, uploader, translator, components, cropper) {
var AccountEdit = {};
AccountEdit.init = function () {
@ -210,6 +210,42 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator',
updateHeader();
}
}
function handleImageCrop(data) {
$('#crop-picture-modal').remove();
templates.parse('modals/crop_picture', {url: data.url}, function (cropperHtml) {
translator.translate(cropperHtml, function (translated) {
var cropperModal = $(translated);
cropperModal.modal('show');
var img = document.getElementById('cropped-image');
var cropperTool = new cropper.default(img, {
aspectRatio: 1 / 1,
viewMode: 1
});
cropperModal.find('.crop-btn').on('click', function () {
$(this).addClass('disabled');
var imageData = data.imageType ? cropperTool.getCroppedCanvas().toDataURL(data.imageType) : cropperTool.getCroppedCanvas().toDataURL();
cropperModal.find('#upload-progress-bar').css('width', '100%');
cropperModal.find('#upload-progress-box').show().removeClass('hide');
socket.emit('user.uploadCroppedPicture', {
uid: ajaxify.data.theirid,
imageData: imageData
}, function (err, imageData) {
if (err) {
app.alertError(err.message);
}
onUploadComplete(imageData.url);
cropperModal.modal('hide');
});
});
});
});
}
modal.find('[data-action="upload"]').on('click', function () {
modal.modal('hide');
@ -221,8 +257,8 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator',
title: '[[user:upload_picture]]',
description: '[[user:upload_a_picture]]',
accept: '.png,.jpg,.bmp'
}, function (imageUrlOnServer) {
onUploadComplete(imageUrlOnServer);
}, function (data) {
handleImageCrop(data);
});
return false;
@ -240,15 +276,10 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator',
if (!url) {
return;
}
socket.emit('user.uploadProfileImageFromUrl', {url: url, uid: ajaxify.data.theirid}, function (err, imageUrlOnServer) {
if (err) {
return app.alertError(err.message);
}
onUploadComplete(imageUrlOnServer);
uploadModal.modal('hide');
});
uploadModal.modal('hide');
handleImageCrop({url: url});
return false;
});
});

@ -1,8 +1,8 @@
'use strict';
/* globals define, templates */
/* globals define, ajaxify, socket, app, templates */
define('uploader', ['translator'], function (translator) {
define('uploader', ['translator', 'cropper'], function (translator, cropper) {
var module = {};
@ -61,46 +61,27 @@ define('uploader', ['translator'], function (translator) {
uploadModal.find('#alert-' + type).translateText(message).removeClass('hide');
}
showAlert('status', '[[uploads:uploading-file]]');
uploadModal.find('#upload-progress-bar').css('width', '0%');
uploadModal.find('#upload-progress-box').show().removeClass('hide');
var fileInput = uploadModal.find('#fileInput');
if (!fileInput.val()) {
return showAlert('error', '[[uploads:select-file-to-upload]]');
}
if (!hasValidFileSize(fileInput[0], fileSize)) {
return showAlert('error', '[[error:file-too-big, ' + fileSize + ']]');
var file = fileInput[0].files[0];
var reader = new FileReader();
var imageUrl;
var imageType = file.type;
reader.addEventListener("load", function () {
imageUrl = reader.result;
uploadModal.modal('hide');
callback({url: imageUrl, imageType: imageType});
}, false);
if (file) {
reader.readAsDataURL(file);
}
uploadModal.find('#uploadForm').ajaxSubmit({
headers: {
'x-csrf-token': config.csrf_token
},
error: function (xhr) {
xhr = maybeParse(xhr);
showAlert('error', xhr.responseJSON ? (xhr.responseJSON.error || xhr.statusText) : 'error uploading, code : ' + xhr.status);
},
uploadProgress: function (event, position, total, percent) {
uploadModal.find('#upload-progress-bar').css('width', percent + '%');
},
success: function (response) {
response = maybeParse(response);
if (response.error) {
return showAlert('error', response.error);
}
callback(response[0].url);
showAlert('success', '[[uploads:upload-success]]');
setTimeout(function () {
module.hideAlerts(uploadModal);
uploadModal.modal('hide');
}, 750);
}
});
}
function parseModal(tplVals, callback) {
@ -109,23 +90,5 @@ define('uploader', ['translator'], function (translator) {
});
}
function maybeParse(response) {
if (typeof response === 'string') {
try {
return $.parseJSON(response);
} catch (e) {
return {error: '[[error:parse-error]]'};
}
}
return response;
}
function hasValidFileSize(fileElement, maxSize) {
if (window.FileReader && maxSize) {
return fileElement.files[0].size <= maxSize * 1000;
}
return true;
}
return module;
});

@ -70,6 +70,7 @@ module.exports = function (Meta) {
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";';
source += '\n@import (inline) "..' + path.sep + 'node_modules/cropperjs/dist/cropper.css";';
source = '@import "./theme";\n' + source;
minify(source, paths, 'cache', callback);

@ -81,7 +81,8 @@ module.exports = function (Meta) {
"Chart.js": './node_modules/chart.js/dist/Chart.min.js',
"mousetrap.js": './node_modules/mousetrap/mousetrap.min.js',
"jqueryui.js": 'public/vendor/jquery/js/jquery-ui.js',
"buzz.js": 'public/vendor/buzz/buzz.js'
"buzz.js": 'public/vendor/buzz/buzz.js',
"cropper.js": './node_modules/cropperjs/dist/cropper.min.js'
}
}
};

@ -37,6 +37,20 @@ module.exports = function (SocketUser) {
}
], callback);
};
SocketUser.uploadCroppedPicture = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
async.waterfall([
function (next) {
user.isAdminOrSelf(socket.uid, data.uid, next);
},
function (next) {
user.uploadCroppedPicture(data, next);
}
], callback);
};
SocketUser.removeCover = function (socket, data, callback) {
if (!socket.uid) {

@ -220,6 +220,84 @@ module.exports = function (User) {
}
});
};
User.uploadCroppedPicture = function (data, callback) {
var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1;
var url, md5sum;
if (!data.imageData) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
var size = data.file ? data.file.size : data.imageData.length;
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
if (size > uploadSize * 1024) {
return next(new Error('[[error:file-too-big, ' + meta.config.maximumProfileImageSize + ']]'));
}
md5sum = crypto.createHash('md5');
md5sum.update(data.imageData);
md5sum = md5sum.digest('hex');
data.file = {
path: path.join(os.tmpdir(), md5sum)
};
var buffer = new Buffer(data.imageData.slice(data.imageData.indexOf('base64') + 7), 'base64');
fs.writeFile(data.file.path, buffer, {
encoding: 'base64'
}, next);
},
function (next) {
var image = {
name: 'profileAvatar',
path: data.file.path,
uid: data.uid
};
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {image: image, uid: data.uid}, next);
}
var filename = data.uid + '-profileavatar' + (keepAllVersions ? '-' + Date.now() : '');
async.waterfall([
function (next) {
file.isFileTypeAllowed(data.file.path, next);
},
function (next) {
file.saveFileToLocal(filename, 'profile', image.path, next);
},
function (upload, next) {
next(null, {
url: nconf.get('relative_path') + upload.url,
name: image.name
});
}
], next);
},
function (uploadData, next) {
url = uploadData.url;
User.setUserFields(data.uid, {uploadedpicture: url, picture: url}, next);
},
function (next) {
fs.unlink(data.file.path, function (err) {
if (err) {
winston.error(err);
}
next();
});
}
], function (err) {
if (err) {
callback(err); // send back the original error
}
callback(err, {url: url});
});
};
User.removeCoverPicture = function (data, callback) {
db.deleteObjectFields('user:' + data.uid, ['cover:url', 'cover:position'], callback);

@ -0,0 +1,22 @@
<div id="crop-picture-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="crop-picture" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="crop-picture">[[user:crop_picture]]</h3>
</div>
<div class="modal-body">
<div id="upload-progress-box" class="progress hide">
<div id="upload-progress-bar" class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0">
</div>
</div>
<img id="cropped-image" src="{url}" >
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
<button class="btn btn-primary crop-btn">[[user:upload_cropped_picture]]</button>
</div>
</div>
</div>
</div>
Loading…
Cancel
Save