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.
v1.18.x
Julian Lam 9 years ago
parent ae856395c3
commit ebed9d641c

@ -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;

@ -40,6 +40,16 @@
return '<meta ' + name + property + content + '/>';
};
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 '<link ' + link + rel + type + sizes + href + '/>';
};
helpers.stringify = function(obj) {
// Turns the incoming object into a JSON string
return JSON.stringify(obj).replace(/&/gm,"&amp;").replace(/</gm,"&lt;").replace(/>/gm,"&gt;").replace(/"/g, '&quot;');

@ -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);
};

@ -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 = {

@ -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);
}

@ -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);

@ -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) {

@ -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);
}

@ -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);
};

@ -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) {

@ -74,12 +74,33 @@
<input id="faviconUrl" type="text" class="form-control" placeholder="favicon.ico" data-field="brand:favicon" data-action="upload" data-target="faviconUrl" data-route="{config.relative_path}/api/admin/uploadfavicon" readonly />
<span class="input-group-btn">
<input data-action="upload" data-target="faviconUrl" data-route="{config.relative_path}/api/admin/uploadfavicon" type="button" class="btn btn-default" value="Upload"></input>
<button data-action="removeFavicon" type="button" class="btn btn-default btn-danger"><i class="fa fa-times"></i></button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">
Homescreen/Touch Icon
</div>
<div class="col-sm-10 col-xs-12">
<div class="form-group">
<div class="input-group">
<input id="touchIconUrl" type="text" class="form-control" data-field="brand:touchIcon" data-action="upload" data-target="touchIconUrl" data-route="{config.relative_path}/api/admin/uploadTouchIcon" readonly />
<span class="input-group-btn">
<input data-action="upload" data-target="touchIconUrl" data-route="{config.relative_path}/api/admin/uploadTouchIcon" type="button" class="btn btn-default" value="Upload"></input>
<button data-action="removeTouchIcon" type="button" class="btn btn-default btn-danger"><i class="fa fa-times"></i></button>
</span>
</div>
<p class="help-block">
Recommended size and format: 192x192, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">Miscellaneous</div>
<div class="col-sm-10 col-xs-12">

Loading…
Cancel
Save