mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-01 03:26:04 +01:00
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.
This commit is contained in:
@@ -8,6 +8,12 @@ define('admin/settings/general', ['admin/settings'], function(Settings) {
|
|||||||
$('button[data-action="removeLogo"]').on('click', function() {
|
$('button[data-action="removeLogo"]').on('click', function() {
|
||||||
$('input[data-field="brand:logo"]').val('');
|
$('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;
|
return Module;
|
||||||
|
|||||||
@@ -40,6 +40,16 @@
|
|||||||
return '<meta ' + name + property + content + '/>';
|
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) {
|
helpers.stringify = function(obj) {
|
||||||
// Turns the incoming object into a JSON string
|
// Turns the incoming object into a JSON string
|
||||||
return JSON.stringify(obj).replace(/&/gm,"&").replace(/</gm,"<").replace(/>/gm,">").replace(/"/g, '"');
|
return JSON.stringify(obj).replace(/&/gm,"&").replace(/</gm,"<").replace(/>/gm,">").replace(/"/g, '"');
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
var fs = require('fs'),
|
var fs = require('fs'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
|
async = require('async'),
|
||||||
nconf = require('nconf'),
|
nconf = require('nconf'),
|
||||||
winston = require('winston'),
|
winston = require('winston'),
|
||||||
file = require('../../file'),
|
file = require('../../file'),
|
||||||
|
image = require('../../image'),
|
||||||
plugins = require('../../plugins');
|
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) {
|
uploadsController.uploadLogo = function(req, res, next) {
|
||||||
upload('site-logo', 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) {
|
Controllers.outgoing = function(req, res, next) {
|
||||||
var url = req.query.url,
|
var url = req.query.url,
|
||||||
data = {
|
data = {
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ uploadsController.uploadThumb = function(req, res, next) {
|
|||||||
|
|
||||||
if (uploadedFile.type.match(/image./)) {
|
if (uploadedFile.type.match(/image./)) {
|
||||||
var size = meta.config.topicThumbSize || 120;
|
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) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/image.js
18
src/image.js
@@ -7,18 +7,18 @@ var fs = require('fs'),
|
|||||||
|
|
||||||
var image = {};
|
var image = {};
|
||||||
|
|
||||||
image.resizeImage = function(path, extension, width, height, callback) {
|
image.resizeImage = function(data, callback) {
|
||||||
if (plugins.hasListeners('filter:image.resize')) {
|
if (plugins.hasListeners('filter:image.resize')) {
|
||||||
plugins.fireHook('filter:image.resize', {
|
plugins.fireHook('filter:image.resize', {
|
||||||
path: path,
|
path: data.path,
|
||||||
extension: extension,
|
extension: data.extension,
|
||||||
width: width,
|
width: data.width,
|
||||||
height: height
|
height: data.height
|
||||||
}, function(err, data) {
|
}, function(err, data) {
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
new Jimp(path, function(err, image) {
|
new Jimp(data.path, function(err, image) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ image.resizeImage = function(path, extension, width, height, callback) {
|
|||||||
var w = image.bitmap.width,
|
var w = image.bitmap.width,
|
||||||
h = image.bitmap.height,
|
h = image.bitmap.height,
|
||||||
origRatio = w/h,
|
origRatio = w/h,
|
||||||
desiredRatio = width/height,
|
desiredRatio = data.width/data.height,
|
||||||
x = 0,
|
x = 0,
|
||||||
y = 0,
|
y = 0,
|
||||||
crop;
|
crop;
|
||||||
@@ -47,10 +47,10 @@ image.resizeImage = function(path, extension, width, height, callback) {
|
|||||||
async.waterfall([
|
async.waterfall([
|
||||||
crop,
|
crop,
|
||||||
function(image, next) {
|
function(image, next) {
|
||||||
image.resize(width, height, next);
|
image.resize(data.width, data.height, next);
|
||||||
},
|
},
|
||||||
function(image, next) {
|
function(image, next) {
|
||||||
image.write(path, next);
|
image.write(data.target || data.path, next);
|
||||||
}
|
}
|
||||||
], function(err) {
|
], function(err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ module.exports = function(Meta) {
|
|||||||
}, {
|
}, {
|
||||||
name: 'apple-mobile-web-app-capable',
|
name: 'apple-mobile-web-app-capable',
|
||||||
content: 'yes'
|
content: 'yes'
|
||||||
|
}, {
|
||||||
|
name: 'mobile-web-app-capable',
|
||||||
|
content: 'yes'
|
||||||
}, {
|
}, {
|
||||||
property: 'og:site_name',
|
property: 'og:site_name',
|
||||||
content: Meta.config.title || 'NodeBB'
|
content: Meta.config.title || 'NodeBB'
|
||||||
@@ -42,9 +45,41 @@ module.exports = function(Meta) {
|
|||||||
type: "image/x-icon",
|
type: "image/x-icon",
|
||||||
href: nconf.get('relative_path') + '/favicon.ico'
|
href: nconf.get('relative_path') + '/favicon.ico'
|
||||||
}, {
|
}, {
|
||||||
rel: 'apple-touch-icon',
|
rel: "manifest",
|
||||||
href: nconf.get('relative_path') + '/apple-touch-icon'
|
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);
|
plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next);
|
||||||
}
|
}
|
||||||
}, function(err, results) {
|
}, function(err, results) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function apiRoutes(router, middleware, controllers) {
|
|||||||
|
|
||||||
router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture);
|
router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture);
|
||||||
router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
|
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('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
|
||||||
router.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault);
|
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('/nodebb.min.js.map', middleware.addExpiresHeaders, sendJSSourceMap);
|
||||||
app.get('/sitemap.xml', controllers.sitemap);
|
app.get('/sitemap.xml', controllers.sitemap);
|
||||||
app.get('/robots.txt', controllers.robots);
|
app.get('/robots.txt', controllers.robots);
|
||||||
|
app.get('/manifest.json', controllers.manifest);
|
||||||
app.get('/css/previews/:theme', controllers.admin.themes.get);
|
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);
|
file.isFileTypeAllowed(picture.path, ['png', 'jpeg', 'jpg', 'gif'], next);
|
||||||
},
|
},
|
||||||
function(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) {
|
function(next) {
|
||||||
if (convertToPNG) {
|
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 />
|
<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">
|
<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>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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="row">
|
||||||
<div class="col-sm-2 col-xs-12 settings-header">Miscellaneous</div>
|
<div class="col-sm-2 col-xs-12 settings-header">Miscellaneous</div>
|
||||||
<div class="col-sm-10 col-xs-12">
|
<div class="col-sm-10 col-xs-12">
|
||||||
|
|||||||
Reference in New Issue
Block a user