diff --git a/install/package.json b/install/package.json index bf7270935f..2aa43d9ba3 100644 --- a/install/package.json +++ b/install/package.json @@ -32,6 +32,7 @@ "benchpressjs": "^1.2.5", "body-parser": "^1.18.2", "bootstrap": "^3.3.7", + "bootswatch": "^3", "chart.js": "^2.7.1", "cli-graph": "^3.2.2", "clipboard": "^2.0.1", diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js index 2acd578dd9..4ebfc2e0b9 100644 --- a/public/src/client/account/settings.js +++ b/public/src/client/account/settings.js @@ -48,26 +48,20 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds' }; function changePageSkin(skinName) { - var css = $('#bootswatchCSS'); - if (skinName === 'noskin' || (skinName === 'default' && config.defaultBootswatchSkin === 'noskin')) { - css.remove(); - } else { - if (skinName === 'default') { - skinName = config.defaultBootswatchSkin; - } - var cssSource = '//maxcdn.bootstrapcdn.com/bootswatch/3.3.7/' + skinName + '/bootstrap.min.css'; - if (css.length) { - css.attr('href', cssSource); - } else { - css = $(''); - $('head').append(css); - } - } + var clientEl = Array.prototype.filter.call(document.querySelectorAll('link[rel="stylesheet"]'), function (el) { + return el.href.indexOf(config.relative_path + '/assets/client') !== -1; + })[0] || null; + + // Update client.css link element to point to selected skin variant + clientEl.href = config.relative_path + '/assets/client' + (skinName ? '-' + skinName : '') + '.css'; var currentSkinClassName = $('body').attr('class').split(/\s+/).filter(function (className) { return className.startsWith('skin-'); }); - $('body').removeClass(currentSkinClassName.join(' ')).addClass('skin-' + skinName); + $('body').removeClass(currentSkinClassName.join(' ')); + if (skinName) { + $('body').addClass('skin-' + skinName); + } } function loadSettings() { diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index 02253c3f23..55181e2a25 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -112,8 +112,7 @@ settingsController.get = function (req, res, callback) { ]; userData.bootswatchSkinOptions = [ - { name: 'No skin', value: 'noskin' }, - { name: 'Default', value: 'default' }, + { name: 'Default', value: '' }, { name: 'Cerulean', value: 'cerulean' }, { name: 'Cosmo', value: 'cosmo' }, { name: 'Cyborg', value: 'cyborg' }, diff --git a/src/controllers/api.js b/src/controllers/api.js index b74a0d1b0f..28390f1c6f 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -58,8 +58,7 @@ apiController.loadConfig = function (req, callback) { config.categoryTopicSort = meta.config.categoryTopicSort || 'newest_to_oldest'; config.csrf_token = req.csrfToken && req.csrfToken(); config.searchEnabled = plugins.hasListeners('filter:search.query'); - config.bootswatchSkin = meta.config.bootswatchSkin || 'noskin'; - config.defaultBootswatchSkin = meta.config.bootswatchSkin || 'noskin'; + config.bootswatchSkin = meta.config.bootswatchSkin || ''; config.enablePostHistory = (meta.config.enablePostHistory || 1) === 1; config.notificationAlertTimeout = meta.config.notificationAlertTimeout || 5000; @@ -85,6 +84,10 @@ apiController.loadConfig = function (req, callback) { user.getSettings(req.uid, next); }, function (settings, next) { + // Handle old skin configs + const oldSkins = ['noskin', 'default']; + settings.bootswatchSkin = oldSkins.includes(settings.bootswatchSkin) ? '' : settings.bootswatchSkin; + config.usePagination = settings.usePagination; config.topicsPerPage = settings.topicsPerPage; config.postsPerPage = settings.postsPerPage; @@ -95,7 +98,7 @@ apiController.loadConfig = function (req, callback) { config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; config.topicSearchEnabled = settings.topicSearchEnabled || false; config.delayImageLoading = settings.delayImageLoading !== undefined ? settings.delayImageLoading : true; - config.bootswatchSkin = (meta.config.disableCustomUserSkins !== 1 && settings.bootswatchSkin && settings.bootswatchSkin !== 'default') ? settings.bootswatchSkin : config.bootswatchSkin; + config.bootswatchSkin = (meta.config.disableCustomUserSkins !== 1 && settings.bootswatchSkin && settings.bootswatchSkin !== '') ? settings.bootswatchSkin : ''; plugins.fireHook('filter:config.get', config, next); }, ], callback); diff --git a/src/meta/css.js b/src/meta/css.js index 5762c2cf1c..a1a29cb731 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -5,6 +5,7 @@ var nconf = require('nconf'); var fs = require('fs'); var path = require('path'); var async = require('async'); +var rimraf = require('rimraf'); var plugins = require('../plugins'); var db = require('../database'); @@ -13,6 +14,12 @@ var minifier = require('./minifier'); var CSS = module.exports; +CSS.supportedSkins = [ + 'cerulean', 'cyborg', 'flatly', 'journal', 'lumen', 'paper', 'simplex', + 'spacelab', 'united', 'cosmo', 'darkly', 'readable', 'sandstone', + 'slate', 'superhero', 'yeti', +]; + var buildImports = { client: function (source) { return '@import "./theme";\n' + source + '\n' + [ @@ -93,21 +100,34 @@ function getBundleMetadata(target, callback) { path.join(__dirname, '../../public/vendor/fontawesome/less'), ]; + // Skin support + let skin; + if (target.startsWith('client-')) { + skin = target.split('-')[1]; + + if (CSS.supportedSkins.includes(skin)) { + target = 'client'; + } + } + async.waterfall([ function (next) { if (target !== 'client') { return next(null, null); } - db.getObjectFields('config', ['theme:type', 'theme:id'], next); + db.getObjectFields('config', ['theme:type', 'theme:id', 'bootswatchSkin'], next); }, function (themeData, next) { if (target === 'client') { var themeId = (themeData['theme:id'] || 'nodebb-theme-persona'); var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')); paths.unshift(baseThemePath); + + themeData.bootswatchSkin = skin || themeData.bootswatchSkin; } + async.parallel({ less: function (cb) { async.waterfall([ @@ -143,14 +163,24 @@ function getBundleMetadata(target, callback) { }, ], cb); }, + skin: function (cb) { + const skinImport = []; + if (themeData && themeData.bootswatchSkin) { + skinImport.push('\n@import "./bootswatch/' + themeData.bootswatchSkin + '/variables.less";'); + skinImport.push('\n@import "./bootswatch/' + themeData.bootswatchSkin + '/bootswatch.less";'); + } + + cb(null, skinImport.join('')); + }, }, next); }, function (result, next) { + var skinImport = result.skin; var cssImports = result.css; var lessImports = result.less; var acpLessImports = result.acpLess; - var imports = cssImports + '\n' + lessImports + '\n' + acpLessImports; + var imports = skinImport + '\n' + cssImports + '\n' + lessImports + '\n' + acpLessImports; imports = buildImports[target](imports); next(null, { paths: paths, imports: imports }); @@ -160,6 +190,13 @@ function getBundleMetadata(target, callback) { CSS.buildBundle = function (target, fork, callback) { async.waterfall([ + function (next) { + if (target === 'client') { + rimraf(path.join(__dirname, '../../build/public/client*'), next); + } else { + setImmediate(next); + } + }, function (next) { getBundleMetadata(target, next); }, @@ -168,7 +205,7 @@ CSS.buildBundle = function (target, fork, callback) { minifier.css.bundle(data.imports, data.paths, minify, fork, next); }, function (bundle, next) { - var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css'; + var filename = target + '.css'; fs.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code, next); }, diff --git a/src/meta/themes.js b/src/meta/themes.js index 2c3f5147e7..44841ee997 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -158,6 +158,7 @@ Themes.set = function (data, callback) { themeData['theme:staticDir'] = config.staticDir ? config.staticDir : ''; themeData['theme:templates'] = config.templates ? config.templates : ''; themeData['theme:src'] = ''; + themeData.bootswatchSkin = ''; Meta.configs.setMultiple(themeData, next); diff --git a/src/middleware/header.js b/src/middleware/header.js index 54f5022a31..f8a84ffed5 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -141,7 +141,7 @@ module.exports = function (middleware) { results.user['email:confirmed'] = results.user['email:confirmed'] === 1; results.user.isEmailConfirmSent = !!results.isEmailConfirmSent; - setBootswatchCSS(templateValues, res.locals.config); + templateValues.bootswatchSkin = parseInt(meta.config.disableCustomUserSkins, 10) !== 1 ? res.locals.config.bootswatchSkin || '' : ''; var unreadCount = { topic: results.unreadCounts[''] || 0, @@ -272,21 +272,5 @@ module.exports = function (middleware) { return title; } - - function setBootswatchCSS(obj, config) { - if (config && config.bootswatchSkin !== 'noskin') { - var skinToUse = ''; - - if (!meta.config.disableCustomUserSkins) { - skinToUse = config.bootswatchSkin; - } else if (meta.config.bootswatchSkin) { - skinToUse = meta.config.bootswatchSkin; - } - - if (skinToUse) { - obj.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/3.3.7/' + skinToUse + '/bootstrap.min.css'; - } - } - } }; diff --git a/src/middleware/index.js b/src/middleware/index.js index 2768fe644a..ff58331799 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -205,3 +205,15 @@ middleware.delayLoading = function (req, res, next) { setTimeout(next, 1000); }; + +middleware.buildSkinAsset = function (req, res, next) { + // If this middleware is reached, a skin was requested, so it is built on-demand + var target = path.basename(req.originalUrl).match(/(client-[a-z]+)/); + if (target) { + meta.css.buildBundle(target[0], true, function () { + next(); + }); + } else { + setImmediate(next); + } +}; diff --git a/src/routes/index.js b/src/routes/index.js index 4cacdac32f..cf7fb86002 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -153,6 +153,11 @@ module.exports = function (app, middleware, callback) { statics.unshift({ route: '/assets/uploads', path: nconf.get('upload_path') }); } + // Skins + meta.css.supportedSkins.forEach(function (skin) { + app.use(relativePath + '/assets/client-' + skin + '.css', middleware.buildSkinAsset); + }); + statics.forEach(function (obj) { app.use(relativePath + obj.route, express.static(obj.path, staticOptions)); }); @@ -160,6 +165,19 @@ module.exports = function (app, middleware, callback) { res.redirect(relativePath + '/assets/uploads' + req.path + '?' + meta.config['cache-buster']); }); + // only warn once + var warned = new Set(); + + // DEPRECATED (v1.12.0) + app.use(relativePath + '/assets/stylesheet.css', function (req, res) { + if (!warned.has(req.path)) { + winston.warn('[deprecated] Accessing `/assets/stylesheet.css` is deprecated to be REMOVED in NodeBB v1.12.0. ' + + 'Use `/assets/client.css` to access this file'); + warned.add(req.path); + } + res.redirect(relativePath + '/assets/client.css?' + meta.config['cache-buster']); + }); + app.use(relativePath + '/assets/vendor/jquery/timeago/locales', middleware.processTimeagoLocales); app.use(controllers['404'].handle404); app.use(controllers.errors.handleURIErrors); diff --git a/src/user/settings.js b/src/user/settings.js index ec4ecb46d4..215d8c7d5b 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -78,7 +78,7 @@ module.exports = function (User) { settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; settings.delayImageLoading = parseInt(getSetting(settings, 'delayImageLoading', 1), 10) === 1; - settings.bootswatchSkin = settings.bootswatchSkin || meta.config.bootswatchSkin || 'default'; + settings.bootswatchSkin = settings.bootswatchSkin || ''; settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; notifications.getAllNotificationTypes(next); @@ -138,12 +138,9 @@ module.exports = function (User) { incomingChatSound: data.incomingChatSound, outgoingChatSound: data.outgoingChatSound, upvoteNotifFreq: data.upvoteNotifFreq, + bootswatchSkin: data.bootswatchSkin, }; - if (data.bootswatchSkin) { - settings.bootswatchSkin = data.bootswatchSkin; - } - async.waterfall([ function (next) { notifications.getAllNotificationTypes(next); diff --git a/test/build.js b/test/build.js index 55dbae861a..0b40b28aa4 100644 --- a/test/build.js +++ b/test/build.js @@ -170,7 +170,7 @@ describe('Build', function (done) { it('should build client side styles', function (done) { build.build(['client side styles'], function (err) { assert.ifError(err); - var filename = path.join(__dirname, '../build/public/stylesheet.css'); + var filename = path.join(__dirname, '../build/public/client.css'); assert(file.existsSync(filename)); assert(fs.readFileSync(filename).toString().startsWith('/*! normalize.css')); done();