From ebed9d641ccdf20f160eece9207a267c7df741ac Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 24 Sep 2015 12:04:24 -0400 Subject: [PATCH] Add new ACP option to upload Touch Icon, #3668 Also added a number of fixes for mobile enhancements, such as serving a manifest.json file for Android devices, and serving proper link tags for all uploaded touch icons. This commit also creates a new template helper for link tags. --- public/src/admin/settings/general.js | 6 ++++ public/src/modules/helpers.js | 10 ++++++ src/controllers/admin/uploads.js | 37 ++++++++++++++++++++++ src/controllers/index.js | 46 ++++++++++++++++++++++++++++ src/controllers/uploads.js | 7 ++++- src/image.js | 18 +++++------ src/meta/tags.js | 39 +++++++++++++++++++++-- src/routes/admin.js | 1 + src/routes/meta.js | 1 + src/user/picture.js | 7 ++++- src/views/admin/settings/general.tpl | 21 +++++++++++++ 11 files changed, 180 insertions(+), 13 deletions(-) diff --git a/public/src/admin/settings/general.js b/public/src/admin/settings/general.js index 7b22e95042..83ecff7907 100644 --- a/public/src/admin/settings/general.js +++ b/public/src/admin/settings/general.js @@ -8,6 +8,12 @@ define('admin/settings/general', ['admin/settings'], function(Settings) { $('button[data-action="removeLogo"]').on('click', function() { $('input[data-field="brand:logo"]').val(''); }); + $('button[data-action="removeFavicon"]').on('click', function() { + $('input[data-field="brand:favicon"]').val(''); + }); + $('button[data-action="removeTouchIcon"]').on('click', function() { + $('input[data-field="brand:touchIcon"]').val(''); + }); }; return Module; diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index ac6e37ff3c..2700aab137 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -40,6 +40,16 @@ return ''; }; + helpers.buildLinkTag = function(tag) { + var link = tag.link ? 'link="' + tag.link + '" ' : '', + rel = tag.rel ? 'rel="' + tag.rel + '" ' : '', + type = tag.type ? 'type="' + tag.type + '" ' : '', + href = tag.href ? 'href="' + tag.href + '" ' : '', + sizes = tag.sizes ? 'sizes="' + tag.sizes + '" ' : ''; + + return ''; + }; + helpers.stringify = function(obj) { // Turns the incoming object into a JSON string return JSON.stringify(obj).replace(/&/gm,"&").replace(//gm,">").replace(/"/g, '"'); diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index cbf6e7f650..c299eb5e5c 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -2,9 +2,11 @@ var fs = require('fs'), path = require('path'), + async = require('async'), nconf = require('nconf'), winston = require('winston'), file = require('../../file'), + image = require('../../image'), plugins = require('../../plugins'); @@ -52,6 +54,41 @@ uploadsController.uploadFavicon = function(req, res, next) { } }; +uploadsController.uploadTouchIcon = function(req, res, next) { + var uploadedFile = req.files.files[0], + allowedTypes = ['image/png'], + sizes = [36, 48, 72, 96, 144, 192]; + + if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { + file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function(err, imageObj) { + // Resize the image into squares for use as touch icons at various DPIs + async.each(sizes, function(size, next) { + async.series([ + async.apply(file.saveFileToLocal, 'touchicon-' + size + '.png', 'system', uploadedFile.path), + async.apply(image.resizeImage, { + path: path.join(nconf.get('base_dir'), nconf.get('upload_path'), 'system', 'touchicon-' + size + '.png'), + extension: 'png', + width: size, + height: size + }) + ], next); + }, function(err) { + fs.unlink(uploadedFile.path, function(err) { + if (err) { + winston.error(err); + } + }); + + if (err) { + return next(err); + } + + res.json([{name: uploadedFile.name, url: imageObj.url}]); + }); + }); + } +}; + uploadsController.uploadLogo = function(req, res, next) { upload('site-logo', req, res, next); }; diff --git a/src/controllers/index.js b/src/controllers/index.js index 88d906e22c..3b364ebf9e 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -186,6 +186,52 @@ Controllers.robots = function (req, res) { } }; +Controllers.manifest = function(req, res) { + var manifest = { + name: meta.config.title || 'NodeBB', + start_url: nconf.get('relative_path') + '/', + display: 'standalone', + orientation: 'portrait', + icons: [] + }; + + if (meta.config['brand:touchIcon']) { + manifest.icons.push({ + src: nconf.get('relative_path') + '/uploads/system/touchicon-36.png', + sizes: '36x36', + type: 'image/png', + density: 0.75 + }, { + src: nconf.get('relative_path') + '/uploads/system/touchicon-48.png', + sizes: '48x48', + type: 'image/png', + density: 1.0 + }, { + src: nconf.get('relative_path') + '/uploads/system/touchicon-72.png', + sizes: '72x72', + type: 'image/png', + density: 1.5 + }, { + src: nconf.get('relative_path') + '/uploads/system/touchicon-96.png', + sizes: '96x96', + type: 'image/png', + density: 2.0 + }, { + src: nconf.get('relative_path') + '/uploads/system/touchicon-144.png', + sizes: '144x144', + type: 'image/png', + density: 3.0 + }, { + src: nconf.get('relative_path') + '/uploads/system/touchicon-192.png', + sizes: '192x192', + type: 'image/png', + density: 4.0 + }) + } + + res.status(200).json(manifest); +}; + Controllers.outgoing = function(req, res, next) { var url = req.query.url, data = { diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index be5d80bf23..491553fb24 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -74,7 +74,12 @@ uploadsController.uploadThumb = function(req, res, next) { if (uploadedFile.type.match(/image./)) { var size = meta.config.topicThumbSize || 120; - image.resizeImage(uploadedFile.path, path.extname(uploadedFile.name), size, size, function(err) { + image.resizeImage({ + path: uploadedFile.path, + extension: path.extname(uploadedFile.name), + width: size, + height: size + }, function(err) { if (err) { return next(err); } diff --git a/src/image.js b/src/image.js index 923131b4d2..2c5c5ceb8c 100644 --- a/src/image.js +++ b/src/image.js @@ -7,18 +7,18 @@ var fs = require('fs'), var image = {}; -image.resizeImage = function(path, extension, width, height, callback) { +image.resizeImage = function(data, callback) { if (plugins.hasListeners('filter:image.resize')) { plugins.fireHook('filter:image.resize', { - path: path, - extension: extension, - width: width, - height: height + path: data.path, + extension: data.extension, + width: data.width, + height: data.height }, function(err, data) { callback(err); }); } else { - new Jimp(path, function(err, image) { + new Jimp(data.path, function(err, image) { if (err) { return callback(err); } @@ -26,7 +26,7 @@ image.resizeImage = function(path, extension, width, height, callback) { var w = image.bitmap.width, h = image.bitmap.height, origRatio = w/h, - desiredRatio = width/height, + desiredRatio = data.width/data.height, x = 0, y = 0, crop; @@ -47,10 +47,10 @@ image.resizeImage = function(path, extension, width, height, callback) { async.waterfall([ crop, function(image, next) { - image.resize(width, height, next); + image.resize(data.width, data.height, next); }, function(image, next) { - image.write(path, next); + image.write(data.target || data.path, next); } ], function(err) { callback(err); diff --git a/src/meta/tags.js b/src/meta/tags.js index 64e183e43e..d571038cc4 100644 --- a/src/meta/tags.js +++ b/src/meta/tags.js @@ -21,6 +21,9 @@ module.exports = function(Meta) { }, { name: 'apple-mobile-web-app-capable', content: 'yes' + }, { + name: 'mobile-web-app-capable', + content: 'yes' }, { property: 'og:site_name', content: Meta.config.title || 'NodeBB' @@ -42,9 +45,41 @@ module.exports = function(Meta) { type: "image/x-icon", href: nconf.get('relative_path') + '/favicon.ico' }, { - rel: 'apple-touch-icon', - href: nconf.get('relative_path') + '/apple-touch-icon' + rel: "manifest", + href: nconf.get('relative_path') + '/manifest.json' }]; + + // Touch icons for mobile-devices + if (Meta.config['brand:touchIcon']) { + defaultLinks.push({ + rel: 'apple-touch-icon', + href: nconf.get('relative_path') + '/apple-touch-icon' + }, { + rel: 'icon', + sizes: '36x36', + href: nconf.get('relative_path') + '/uploads/system/touchicon-36.png' + }, { + rel: 'icon', + sizes: '48x48', + href: nconf.get('relative_path') + '/uploads/system/touchicon-48.png' + }, { + rel: 'icon', + sizes: '72x72', + href: nconf.get('relative_path') + '/uploads/system/touchicon-72.png' + }, { + rel: 'icon', + sizes: '96x96', + href: nconf.get('relative_path') + '/uploads/system/touchicon-96.png' + }, { + rel: 'icon', + sizes: '144x144', + href: nconf.get('relative_path') + '/uploads/system/touchicon-144.png' + }, { + rel: 'icon', + sizes: '192x192', + href: nconf.get('relative_path') + '/uploads/system/touchicon-192.png' + }); + } plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next); } }, function(err, results) { diff --git a/src/routes/admin.js b/src/routes/admin.js index 63cee2ceb4..eb261b58be 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -13,6 +13,7 @@ function apiRoutes(router, middleware, controllers) { router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture); router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon); + router.post('/uploadTouchIcon', middlewares, controllers.admin.uploads.uploadTouchIcon); router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo); router.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault); } diff --git a/src/routes/meta.js b/src/routes/meta.js index 03cf1cde24..04968ce89e 100644 --- a/src/routes/meta.js +++ b/src/routes/meta.js @@ -33,5 +33,6 @@ module.exports = function(app, middleware, controllers) { // app.get('/nodebb.min.js.map', middleware.addExpiresHeaders, sendJSSourceMap); app.get('/sitemap.xml', controllers.sitemap); app.get('/robots.txt', controllers.robots); + app.get('/manifest.json', controllers.manifest); app.get('/css/previews/:theme', controllers.admin.themes.get); }; diff --git a/src/user/picture.js b/src/user/picture.js index 2a934bfc7e..cf89f34399 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -37,7 +37,12 @@ module.exports = function(User) { file.isFileTypeAllowed(picture.path, ['png', 'jpeg', 'jpg', 'gif'], next); }, function(next) { - image.resizeImage(picture.path, extension, imageDimension, imageDimension, next); + image.resizeImage({ + path: picture.path, + extension: extension, + width: imageDimension, + height: imageDimension + }, next); }, function(next) { if (convertToPNG) { diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index ecad320279..e45f1093eb 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -74,12 +74,33 @@ + +
+
+ Homescreen/Touch Icon +
+
+
+
+ + + + + +
+

+ Recommended size and format: 192x192, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon. +

+
+
+
+
Miscellaneous