diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index f7ccec33bc..eb9a45f994 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -5,6 +5,11 @@ "new_group": "Create New Group", "no_groups_found": "There are no groups to see", + "cover-instructions": "Drag and Drop a photo, drag to position, and hit Save", + "cover-change": "Change", + "cover-save": "Save", + "cover-saving": "Saving", + "details.title": "Group Details", "details.members": "Member List", "details.pending": "Pending Members", diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index d8d98a2edc..de9ec199db 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,14 +1,17 @@ "use strict"; -/* globals define, socket, ajaxify, app, bootbox */ +/* globals define, socket, ajaxify, app, bootbox, RELATIVE_PATH */ -define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], function(iconSelect) { - var Details = {}; +define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', 'vendor/jquery/draggable-background/backgroundDraggable'], function(iconSelect) { + var Details = { + cover: {} + }; Details.init = function() { var detailsPage = $('.groups'), settingsFormEl = detailsPage.find('form'); Details.prepareSettings(); + Details.initialiseCover(); $('.latest-posts .content img').addClass('img-responsive'); @@ -150,5 +153,87 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], }); }; + Details.initialiseCover = function() { + var coverEl = $('.group-cover'); + coverEl.find('.change').on('click', function() { + coverEl.toggleClass('active', 1); + coverEl.backgroundDraggable(); + coverEl.on('dragover', Details.cover.onDragOver); + coverEl.on('drop', Details.cover.onDrop); + }); + + coverEl.find('.save').on('click', Details.cover.save); + }; + + // Cover Photo Handling Code + + Details.cover.load = function() { + socket.emit('groups.cover.get', function(err, data) { + if (!err) { + var coverEl = $('.group-cover'); + if (data['cover:url']) { + coverEl.css('background-image', 'url(' + RELATIVE_PATH + '/theme-rocket/cover/' + data['cover:url'] + ')'); + } + + if (data['cover:position']) { + coverEl.css('background-position', data['cover:position']); + } + + delete Details.cover.newCover; + } else { + app.alertError(err.message); + } + }); + }; + + Details.cover.onDragOver = function(e) { + e.stopPropagation(); + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = 'copy'; + }; + + Details.cover.onDrop = function(e) { + var coverEl = $('.group-cover'); + e.stopPropagation(); + e.preventDefault(); + + var files = e.originalEvent.dataTransfer.files, + reader = new FileReader(); + + if (files.length && files[0].type.match('image.*')) { + reader.onload = function(e) { + coverEl.css('background-image', 'url(' + e.target.result + ')'); + coverEl.backgroundDraggable(); + Details.cover.newCover = e.target.result; + }; + + reader.readAsDataURL(files[0]); + } + }; + + Details.cover.save = function() { + var coverEl = $('.group-cover'); + + coverEl.addClass('saving'); + + socket.emit('groups.cover.update', { + groupName: ajaxify.variables.get('group_name'), + imageData: Details.cover.newCover || undefined, + position: $('.group-cover').css('background-position') + }, function(err) { + if (!err) { + coverEl.toggleClass('active', 0); + coverEl.backgroundDraggable('disable'); + coverEl.off('dragover', Details.cover.onDragOver); + coverEl.off('drop', Details.cover.onDrop); + Details.cover.load(); + } else { + app.alertError(err.message); + } + + coverEl.removeClass('saving'); + }); + }; + return Details; }); \ No newline at end of file diff --git a/public/vendor/jquery/draggable-background/backgroundDraggable.js b/public/vendor/jquery/draggable-background/backgroundDraggable.js new file mode 100644 index 0000000000..baae4d0f95 --- /dev/null +++ b/public/vendor/jquery/draggable-background/backgroundDraggable.js @@ -0,0 +1,157 @@ +/** + * Draggable Background plugin for jQuery + * + * v1.2.4 + * + * Copyright (c) 2014 Kenneth Chung + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +;(function($) { + var $window = $(window); + + // Helper function to guarantee a value between low and hi unless bool is false + var limit = function(low, hi, value, bool) { + if (arguments.length === 3 || bool) { + if (value < low) return low; + if (value > hi) return hi; + } + return value; + }; + + // Adds clientX and clientY properties to the jQuery's event object from touch + var modifyEventForTouch = function(e) { + e.clientX = e.originalEvent.touches[0].clientX; + e.clientY = e.originalEvent.touches[0].clientY; + }; + + var getBackgroundImageDimensions = function($el) { + var bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1]; + if (!bgSrc) return; + + var imageDimensions = { width: 0, height: 0 }, + image = new Image(); + + image.onload = function() { + if ($el.css('background-size') == "cover") { + var elementWidth = $el.innerWidth(), + elementHeight = $el.innerHeight(), + elementAspectRatio = elementWidth / elementHeight; + imageAspectRatio = image.width / image.height, + scale = 1; + + if (imageAspectRatio >= elementAspectRatio) { + scale = elementHeight / image.height; + } else { + scale = elementWidth / image.width; + } + + imageDimensions.width = image.width * scale; + imageDimensions.height = image.height * scale; + } else { + imageDimensions.width = image.width; + imageDimensions.height = image.height; + } + }; + + image.src = bgSrc; + + return imageDimensions; + }; + + function Plugin(element, options) { + this.element = element; + this.options = options; + this.init(); + } + + Plugin.prototype.init = function() { + var $el = $(this.element), + bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1], + options = this.options; + + if (!bgSrc) return; + + // Get the image's width and height if bound + var imageDimensions = { width: 0, height: 0 }; + if (options.bound) { + imageDimensions = getBackgroundImageDimensions($el); + } + + $el.on('mousedown.dbg touchstart.dbg', function(e) { + if (e.target !== $el[0]) { + return; + } + e.preventDefault(); + + if (e.originalEvent.touches) { + modifyEventForTouch(e); + } else if (e.which !== 1) { + return; + } + + var x0 = e.clientX, + y0 = e.clientY, + pos = $el.css('background-position').match(/(-?\d+).*?\s(-?\d+)/) || [], + xPos = parseInt(pos[1]) || 0, + yPos = parseInt(pos[2]) || 0; + + $window.on('mousemove.dbg touchmove.dbg', function(e) { + e.preventDefault(); + + if (e.originalEvent.touches) { + modifyEventForTouch(e); + } + + var x = e.clientX, + y = e.clientY; + + xPos = options.axis === 'y' ? xPos : limit($el.innerWidth()-imageDimensions.width, 0, xPos+x-x0, options.bound); + yPos = options.axis === 'x' ? yPos : limit($el.innerHeight()-imageDimensions.height, 0, yPos+y-y0, options.bound); + x0 = x; + y0 = y; + + $el.css('background-position', xPos + 'px ' + yPos + 'px'); + }); + + $window.on('mouseup.dbg touchend.dbg mouseleave.dbg', function() { + if (options.done) { + options.done(); + } + + $window.off('mousemove.dbg touchmove.dbg'); + $window.off('mouseup.dbg touchend.dbg mouseleave.dbg'); + }); + }); + }; + + Plugin.prototype.disable = function() { + var $el = $(this.element); + $el.off('mousedown.dbg touchstart.dbg'); + $window.off('mousemove.dbg touchmove.dbg mouseup.dbg touchend.dbg mouseleave.dbg'); + } + + $.fn.backgroundDraggable = function(options) { + var options = options; + var args = Array.prototype.slice.call(arguments, 1); + + return this.each(function() { + var $this = $(this); + + if (typeof options == 'undefined' || typeof options == 'object') { + options = $.extend({}, $.fn.backgroundDraggable.defaults, options); + var plugin = new Plugin(this, options); + $this.data('dbg', plugin); + } else if (typeof options == 'string' && $this.data('dbg')) { + var plugin = $this.data('dbg'); + Plugin.prototype[options].apply(plugin, args); + } + }); + }; + + $.fn.backgroundDraggable.defaults = { + bound: true, + axis: undefined + }; +}(jQuery)); \ No newline at end of file diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 237ae5c453..b7e590b6bf 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -72,6 +72,10 @@ uploadsController.uploadThumb = function(req, res, next) { }, next); }; +uploadsController.uploadGroupCover = function(data, next) { + uploadImage(0/*req.user.uid*/, data, next); +}; + function uploadImage(uid, image, callback) { if (plugins.hasListeners('filter:uploadImage')) { return plugins.fireHook('filter:uploadImage', {image: image, uid: uid}, callback); diff --git a/src/groups.js b/src/groups.js index 19560ca14c..4e4656dd3a 100644 --- a/src/groups.js +++ b/src/groups.js @@ -3,14 +3,20 @@ var async = require('async'), winston = require('winston'), _ = require('underscore'), + crypto = require('crypto'), + path = require('path'), + nconf = require('nconf'), + fs = require('fs'), + user = require('./user'), meta = require('./meta'), db = require('./database'), plugins = require('./plugins'), posts = require('./posts'), privileges = require('./privileges'), - utils = require('../public/src/utils'); + utils = require('../public/src/utils'), + uploadsController = require('./controllers/uploads'); (function(Groups) { @@ -60,7 +66,12 @@ var async = require('async'), } return groups; - } + }/*, + fixImageUrl: function(url) { + if (url) { + return url.indexOf('http') === -1 ? nconf.get('relative_path') + url : url; + } + }*/ }; Groups.list = function(options, callback) { @@ -185,6 +196,7 @@ var async = require('async'), return callback(err); } + // results.base.image = internals.fixImageUrl(results.base.image); results.base.members = results.users.filter(Boolean); results.base.pending = results.pending.filter(Boolean); results.base.count = numUsers || results.base.members.length; @@ -203,6 +215,15 @@ var async = require('async'), }); }; + Groups.getGroupFields = function(groupName, fields, callback) { + db.getObjectFields('group:' + groupName, fields, callback); + }; + + Groups.setGroupField = function(groupName, field, value, callback) { + plugins.fireHook('action:group.set', {field: field, value: value, type: 'set'}); + db.setObjectField('group:' + groupName, field, value, callback); + }; + Groups.isPrivate = function(groupName, callback) { db.getObjectField('group:' + groupName, 'private', function(err, isPrivate) { isPrivate = isPrivate || isPrivate === null; @@ -688,6 +709,63 @@ var async = require('async'), }); }; + Groups.updateCoverPosition = function(groupName, position, callback) { + Groups.setGroupField(groupName, 'cover:position', position, callback); + }; + + Groups.updateCover = function(data, callback) { + var tempPath, md5sum, url; + + // Position only? That's fine + if (!data.imageData && data.position) { + return Groups.updateCoverPosition(data.groupName, data.position, callback); + } + + async.series([ + function(next) { + // Calculate md5sum of image + // This is required because user data can be private + md5sum = crypto.createHash('md5'); + md5sum.update(data.imageData); + md5sum = md5sum.digest('hex'); + next(); + }, + function(next) { + // Save image + tempPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), md5sum); + var buffer = new Buffer(data.imageData.slice(data.imageData.indexOf('base64') + 7), 'base64'); + + fs.writeFile(tempPath, buffer, { + encoding: 'base64' + }, next); + }, + function(next) { + uploadsController.uploadGroupCover({ + path: tempPath + }, function(err, uploadData) { + if (err) { + return next(err); + } + + url = uploadData.url; + next(); + }); + }, + function(next) { + Groups.setGroupField(data.groupName, 'cover:url', url, next); + }, + function(next) { + fs.unlink(tempPath, next); // Delete temporary file + } + ], function(err) { + if (err) { + return callback(err); + } + + Groups.updateCoverPosition(data.groupName, data.position, callback); + }); + } + Groups.ownership = {}; Groups.ownership.isOwner = function(uid, groupName, callback) { diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 3f3718d97f..c4b4535e82 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -126,4 +126,26 @@ SocketGroups.delete = function(socket, data, callback) { }); }; +SocketGroups.cover = {}; + +SocketGroups.cover.get = function(socket, data, callback) { + groups.getGroupFields(data.groupName, ['cover:url', 'cover:position'], callback); +}; + +SocketGroups.cover.update = function(socket, data, callback) { + if(!data) { + return callback(new Error('[[error:invalid-data]]')); + } else if (socket.uid === 0) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.updateCover(data, callback); + }); +}; + module.exports = SocketGroups;