mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 12:05:57 +01:00
Ability to crop profile images before uploading
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
"connect-redis": "~3.1.0",
|
"connect-redis": "~3.1.0",
|
||||||
"cookie-parser": "^1.3.3",
|
"cookie-parser": "^1.3.3",
|
||||||
"cron": "^1.0.5",
|
"cron": "^1.0.5",
|
||||||
|
"cropperjs": "^0.8.1",
|
||||||
"csurf": "^1.6.1",
|
"csurf": "^1.6.1",
|
||||||
"daemon": "~1.1.0",
|
"daemon": "~1.1.0",
|
||||||
"express": "^4.14.0",
|
"express": "^4.14.0",
|
||||||
|
|||||||
@@ -68,6 +68,8 @@
|
|||||||
"remove_uploaded_picture" : "Remove Uploaded Picture",
|
"remove_uploaded_picture" : "Remove Uploaded Picture",
|
||||||
"upload_cover_picture": "Upload cover picture",
|
"upload_cover_picture": "Upload cover picture",
|
||||||
"remove_cover_picture_confirm": "Are you sure you want to remove the 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",
|
"settings": "Settings",
|
||||||
"show_email": "Show My Email",
|
"show_email": "Show My Email",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/* globals define, ajaxify, socket, app, config, templates, bootbox */
|
/* 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 = {};
|
var AccountEdit = {};
|
||||||
|
|
||||||
AccountEdit.init = function () {
|
AccountEdit.init = function () {
|
||||||
@@ -211,6 +211,38 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator',
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleImageCrop(data) {
|
||||||
|
templates.parse('partials/modals/crop_uploaded_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, {});
|
||||||
|
|
||||||
|
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.find('[data-action="upload"]').on('click', function () {
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
|
|
||||||
@@ -221,8 +253,8 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator',
|
|||||||
title: '[[user:upload_picture]]',
|
title: '[[user:upload_picture]]',
|
||||||
description: '[[user:upload_a_picture]]',
|
description: '[[user:upload_a_picture]]',
|
||||||
accept: '.png,.jpg,.bmp'
|
accept: '.png,.jpg,.bmp'
|
||||||
}, function (imageUrlOnServer) {
|
}, function (data) {
|
||||||
onUploadComplete(imageUrlOnServer);
|
handleImageCrop(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -240,14 +272,9 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator',
|
|||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
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;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use strict';
|
'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 = {};
|
var module = {};
|
||||||
|
|
||||||
@@ -61,46 +61,27 @@ define('uploader', ['translator'], function (translator) {
|
|||||||
uploadModal.find('#alert-' + type).translateText(message).removeClass('hide');
|
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');
|
var fileInput = uploadModal.find('#fileInput');
|
||||||
if (!fileInput.val()) {
|
if (!fileInput.val()) {
|
||||||
return showAlert('error', '[[uploads:select-file-to-upload]]');
|
return showAlert('error', '[[uploads:select-file-to-upload]]');
|
||||||
}
|
}
|
||||||
if (!hasValidFileSize(fileInput[0], fileSize)) {
|
|
||||||
return showAlert('error', '[[error:file-too-big, ' + fileSize + ']]');
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadModal.find('#uploadForm').ajaxSubmit({
|
var file = fileInput[0].files[0];
|
||||||
headers: {
|
var reader = new FileReader();
|
||||||
'x-csrf-token': config.csrf_token
|
var imageUrl;
|
||||||
},
|
var imageType = file.type;
|
||||||
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) {
|
reader.addEventListener("load", function () {
|
||||||
return showAlert('error', response.error);
|
imageUrl = reader.result;
|
||||||
}
|
|
||||||
|
|
||||||
callback(response[0].url);
|
|
||||||
|
|
||||||
showAlert('success', '[[uploads:upload-success]]');
|
|
||||||
setTimeout(function () {
|
|
||||||
module.hideAlerts(uploadModal);
|
|
||||||
uploadModal.modal('hide');
|
uploadModal.modal('hide');
|
||||||
}, 750);
|
|
||||||
|
callback({url: imageUrl, imageType: imageType});
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseModal(tplVals, callback) {
|
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;
|
return module;
|
||||||
});
|
});
|
||||||
|
|||||||
9
public/vendor/mdl/material.min.css
vendored
Normal file
9
public/vendor/mdl/material.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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/generics.less";';
|
||||||
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.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 "..' + 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;
|
source = '@import "./theme";\n' + source;
|
||||||
|
|
||||||
minify(source, paths, 'cache', callback);
|
minify(source, paths, 'cache', callback);
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ module.exports = function (Meta) {
|
|||||||
"Chart.js": './node_modules/chart.js/dist/Chart.min.js',
|
"Chart.js": './node_modules/chart.js/dist/Chart.min.js',
|
||||||
"mousetrap.js": './node_modules/mousetrap/mousetrap.min.js',
|
"mousetrap.js": './node_modules/mousetrap/mousetrap.min.js',
|
||||||
"jqueryui.js": 'public/vendor/jquery/js/jquery-ui.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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,20 @@ module.exports = function (SocketUser) {
|
|||||||
], callback);
|
], 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) {
|
SocketUser.removeCover = function (socket, data, callback) {
|
||||||
if (!socket.uid) {
|
if (!socket.uid) {
|
||||||
return callback(new Error('[[error:no-privileges]]'));
|
return callback(new Error('[[error:no-privileges]]'));
|
||||||
|
|||||||
@@ -221,6 +221,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) {
|
User.removeCoverPicture = function (data, callback) {
|
||||||
db.deleteObjectFields('user:' + data.uid, ['cover:url', 'cover:position'], callback);
|
db.deleteObjectFields('user:' + data.uid, ['cover:url', 'cover:position'], callback);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user