feat: custom skins panel in acp

isekai-main
Barış Soner Uşaklı 2 years ago
parent ae34900417
commit 556fd65d51

@ -1,5 +1,12 @@
{
"skins": "Skins",
"bootswatch-skins": "Bootswatch Skins",
"custom-skins": "Custom Skins",
"add-skin": "Add Skin",
"save-custom-skins": "Save Custom Skins",
"save-custom-skins-success": "Custom skins saved successfully",
"custom-skin-name": "Custom Skin Name",
"custom-skin-variables": "Custom Skin Variables",
"loading": "Loading Skins...",
"homepage": "Homepage",
"select-skin": "Select Skin",

@ -1,7 +1,9 @@
'use strict';
define('admin/appearance/skins', ['translator', 'alerts'], function (translator, alerts) {
define('admin/appearance/skins', [
'translator', 'alerts', 'settings',
], function (translator, alerts, settings) {
const Skins = {};
Skins.init = function () {
@ -11,6 +13,15 @@ define('admin/appearance/skins', ['translator', 'alerts'], function (translator,
url: 'https://bootswatch.com/api/5.json',
}).done(Skins.render);
settings.load('custom-skins', $('.custom-skin-settings'));
$('#save-custom-skins').on('click', function () {
settings.save('custom-skins', $('.custom-skin-settings'), function () {
alerts.success('[[admin/appearance/skins:save-custom-skins-success]]');
});
return false;
});
$('#skins').on('click', function (e) {
let target = $(e.target);

@ -14,6 +14,7 @@ const notifications = require('../../notifications');
const db = require('../../database');
const helpers = require('../helpers');
const accountHelpers = require('./helpers');
const slugify = require('../../slugify');
const settingsController = module.exports;
@ -39,13 +40,15 @@ settingsController.get = async function (req, res, next) {
uid: req.uid,
});
const [notificationSettings, routes] = await Promise.all([
const [notificationSettings, routes, bsSkinOptions] = await Promise.all([
getNotificationSettings(userData),
getHomePageRoutes(userData),
getSkinOptions(userData),
]);
userData.customSettings = data.customSettings;
userData.homePageRoutes = routes;
userData.bootswatchSkinOptions = bsSkinOptions;
userData.notificationSettings = notificationSettings;
userData.disableEmailSubscriptions = meta.config.disableEmailSubscriptions;
@ -57,8 +60,6 @@ settingsController.get = async function (req, res, next) {
{ value: 'month', name: '[[user:digest_monthly]]', selected: userData.settings.dailyDigestFreq === 'month' },
];
getSkinOptions(userData);
userData.languages.forEach((language) => {
language.selected = language.code === userData.settings.userLang;
});
@ -220,17 +221,28 @@ async function getHomePageRoutes(userData) {
return routes;
}
function getSkinOptions(userData) {
async function getSkinOptions(userData) {
const defaultSkin = _.capitalize(meta.config.bootswatchSkin) || '[[user:no-skin]]';
userData.bootswatchSkinOptions = [
const bootswatchSkinOptions = [
{ name: '[[user:no-skin]]', value: 'noskin' },
{ name: `[[user:default, ${defaultSkin}]]`, value: '' },
];
userData.bootswatchSkinOptions.push(
const customSkins = await meta.settings.get('custom-skins');
if (customSkins && Array.isArray(customSkins['custom-skin-list'])) {
customSkins['custom-skin-list'].forEach((customSkin) => {
bootswatchSkinOptions.push({
name: customSkin['custom-skin-name'],
value: slugify(customSkin['custom-skin-name']),
});
});
}
bootswatchSkinOptions.push(
...meta.css.supportedSkins.map(skin => ({ name: _.capitalize(skin), value: skin }))
);
userData.bootswatchSkinOptions.forEach((skin) => {
bootswatchSkinOptions.forEach((skin) => {
skin.selected = skin.value === userData.settings.bootswatchSkin;
});
return bootswatchSkinOptions;
}

@ -117,7 +117,7 @@ apiController.loadConfig = async function (req) {
if (!config.disableCustomUserSkins && settings.bootswatchSkin) {
if (settings.bootswatchSkin === 'noskin') {
config.bootswatchSkin = '';
} else if (settings.bootswatchSkin !== '') {
} else if (settings.bootswatchSkin !== '' && await meta.css.isSkinValid(settings.bootswatchSkin)) {
config.bootswatchSkin = settings.bootswatchSkin;
}
}

@ -1,5 +1,6 @@
'use strict';
const _ = require('lodash');
const winston = require('winston');
const nconf = require('nconf');
const fs = require('fs');
@ -48,11 +49,19 @@ const buildImports = {
function boostrapImport(themeData) {
// see https://getbootstrap.com/docs/5.0/customize/sass/#variable-defaults
// for an explanation of this order and https://bootswatch.com/help/
const { bootswatchSkin, bsVariables } = themeData;
const { bootswatchSkin, bsVariables, isCustomSkin } = themeData;
function bsvariables() {
if (bootswatchSkin) {
if (isCustomSkin) {
return themeData._variables || '';
}
return `@import "bootswatch/dist/${bootswatchSkin}/variables";`;
}
return bsVariables;
}
return [
bootswatchSkin ?
`@import "bootswatch/dist/${bootswatchSkin}/variables";` :
bsVariables,
bsvariables(),
'@import "bootstrap/scss/mixins/banner";',
'@include bsBanner("");',
// functions must be included first
@ -115,7 +124,7 @@ function boostrapImport(themeData) {
'@import "generics";',
'@import "client";', // core page styles
'@import "./theme";', // rest of the theme scss
bootswatchSkin ? `@import "bootswatch/dist/${bootswatchSkin}/bootswatch";` : '',
bootswatchSkin && !isCustomSkin ? `@import "bootswatch/dist/${bootswatchSkin}/bootswatch";` : '',
].join('\n');
}
@ -171,11 +180,14 @@ async function getBundleMetadata(target) {
// Skin support
let skin;
let isCustomSkin = false;
if (target.startsWith('client-')) {
skin = target.split('-')[1];
if (CSS.supportedSkins.includes(skin)) {
target = 'client';
skin = target.split('-').slice(1).join('-');
const isBootswatchSkin = CSS.supportedSkins.includes(skin);
isCustomSkin = !isBootswatchSkin && await CSS.isCustomSkin(skin);
target = 'client';
if (!isBootswatchSkin && !isCustomSkin) {
skin = ''; // invalid skin or deleted use default
}
}
@ -191,6 +203,9 @@ async function getBundleMetadata(target) {
paths.unshift(`${baseThemePath}/node_modules`);
themeData.bsVariables = parseInt(themeData.useBSVariables, 10) === 1 ? (themeData.bsVariables || '') : '';
themeData.bootswatchSkin = skin;
themeData.isCustomSkin = isCustomSkin;
const customSkin = isCustomSkin ? await CSS.getCustomSkin(skin) : null;
themeData._variables = customSkin && customSkin._variables;
}
const [scssImports, cssImports, acpScssImports] = await Promise.all([
@ -210,6 +225,80 @@ async function getBundleMetadata(target) {
return { paths: paths, imports: imports };
}
CSS.getSkinSwitcherOptions = async function (uid) {
const user = require('../user');
const meta = require('./index');
const [userSettings, customSkins] = await Promise.all([
user.getSettings(uid),
CSS.getCustomSkins(),
]);
const defaultSkin = _.capitalize(meta.config.bootswatchSkin) || '[[user:no-skin]]';
const defaultSkins = [
{ name: `[[user:default, ${defaultSkin}]]`, value: '', selected: userSettings.bootswatchSkin === '' },
{ name: '[[user:no-skin]]', value: 'noskin', selected: userSettings.bootswatchSkin === 'noskin' },
];
const lightSkins = [
'cerulean', 'cosmo', 'flatly', 'journal', 'litera',
'lumen', 'lux', 'materia', 'minty', 'morph', 'pulse', 'sandstone',
'simplex', 'sketchy', 'spacelab', 'united', 'yeti', 'zephyr',
];
const darkSkins = [
'cyborg', 'darkly', 'quartz', 'slate', 'solar', 'superhero', 'vapor',
];
function parseSkins(skins) {
skins = skins.map(skin => ({
name: _.capitalize(skin),
value: skin,
}));
skins.forEach((skin) => {
skin.selected = skin.value === userSettings.bootswatchSkin;
});
return skins;
}
return {
default: defaultSkins,
custom: customSkins.map(s => ({...s, selected: s.value === userSettings.bootswatchSkin })),
light: parseSkins(lightSkins),
dark: parseSkins(darkSkins),
};
};
CSS.getCustomSkins = async function (opts = {}) {
const meta = require('./index');
const slugify = require('../slugify');
const { loadVariables } = opts;
const customSkins = await meta.settings.get('custom-skins');
const returnSkins = [];
if (customSkins && Array.isArray(customSkins['custom-skin-list'])) {
customSkins['custom-skin-list'].forEach((customSkin) => {
if (customSkin) {
returnSkins.push({
name: customSkin['custom-skin-name'],
value: slugify(customSkin['custom-skin-name']),
_variables: loadVariables ? customSkin._variables : undefined,
});
}
});
}
return returnSkins;
};
CSS.isSkinValid = async function (skin) {
return CSS.supportedSkins.includes(skin) || await CSS.isCustomSkin(skin);
};
CSS.isCustomSkin = async function (skin) {
const skins = await CSS.getCustomSkins();
return !!skins.find(s => s.value === skin);
};
CSS.getCustomSkin = async function (skin) {
const skins = await CSS.getCustomSkins({ loadVariables: true });
return skins.find(s => s.value === skin);
};
CSS.buildBundle = async function (target, fork) {
if (target === 'client') {
let files = await fs.promises.readdir(path.join(__dirname, '../../build/public'));

@ -229,13 +229,19 @@ middleware.delayLoading = function delayLoading(req, res, next) {
middleware.buildSkinAsset = helpers.try(async (req, res, next) => {
// If this middleware is reached, a skin was requested, so it is built on-demand
const target = path.basename(req.originalUrl).match(/(client-[a-z]+)/);
if (!target) {
const targetSkin = path.basename(req.originalUrl).split('.css')[0];
if (!targetSkin) {
return next();
}
const skins = (await meta.css.getCustomSkins()).map(skin => skin.value);
const found = skins.concat(meta.css.supportedSkins).find(skin => `client-${skin}` === targetSkin);
if (!found) {
return next();
}
await plugins.prepareForBuild(['client side styles']);
const [ltr, rtl] = await meta.css.buildBundle(target[0], true);
const [ltr, rtl] = await meta.css.buildBundle(targetSkin, true);
require('../meta/minifier').killAll();
res.status(200).type('text/css').send(req.originalUrl.includes('-rtl') ? rtl : ltr);
});

@ -191,11 +191,8 @@ function addCoreRoutes(app, router, middleware, mounts) {
res.redirect(`${relativePath}/assets/plugins${req.path}${req._parsedUrl.search || ''}`);
});
// Skins
meta.css.supportedSkins.forEach((skin) => {
app.use(`${relativePath}/assets/client-${skin}.css`, middleware.buildSkinAsset);
app.use(`${relativePath}/assets/client-${skin}-rtl.css`, middleware.buildSkinAsset);
});
app.use(`${relativePath}/assets/client-*.css`, middleware.buildSkinAsset);
app.use(`${relativePath}/assets/client-*-rtl.css`, middleware.buildSkinAsset);
app.use(controllers['404'].handle404);
app.use(controllers.errors.handleURIErrors);

@ -1,7 +1,11 @@
<div id="skins" class="d-flex flex-column gap-2 px-lg-4">
<div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
<div class="">
<div class="d-flex flex-wrap gap-3 align-items-center">
<h4 class="fw-bold tracking-tight mb-0">[[admin/appearance/skins:skins]]</h4>
<ul class="nav nav-pills d-flex gap-1 text-sm">
<li class="nav-item"><a href="#" class="nav-link active px-2 py-1" data-bs-target="#skins-tab" data-bs-toggle="tab">[[admin/appearance/skins:bootswatch-skins]]</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 py-1" data-bs-target="#custom-skins-tab" data-bs-toggle="tab">[[admin/appearance/skins:custom-skins]]</a></li>
</ul>
</div>
<div class="d-flex align-items-center gap-1">
<div data-type="bootswatch" data-theme="" data-css="">
@ -9,9 +13,26 @@
</div>
</div>
</div>
<div class="skins px-2">
<div class="directory row text-center" id="bootstrap_themes">
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
<div class="tab-content">
<div class="tab-pane fade show active" role="tabpanel" id="skins-tab">
<div class="skins px-2">
<div class="directory row text-center" id="bootstrap_themes">
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
</div>
</div>
</div>
<div class="tab-pane fade" role="tabpanel" id="custom-skins-tab">
<form role="form" class="custom-skin-settings">
<div class="mb-3" data-type="sorted-list" data-sorted-list="custom-skin-list" data-item-template="admin/partials/appearance/skins/item-custom-skin" data-form-template="admin/partials/appearance/skins/form-custom-skin">
<input hidden="text" name="custom-skin-list">
<div class="d-flex gap-1 mb-3 justify-content-between">
<button type="button" data-type="add" class="btn btn-sm btn-light">[[admin/appearance/skins:add-skin]]</button>
<button id="save-custom-skins" class="btn btn-sm btn-primary">[[admin/appearance/skins:save-custom-skins]]</button>
</div>
<ul data-type="list" class="list-group mb-3"></ul>
</div>
</form>
</div>
</div>
</div>

@ -0,0 +1,11 @@
<form>
<div class="mb-3">
<label for="name">[[admin/appearance/skins:custom-skin-name]]</label>
<input type="text" name="custom-skin-name" class="form-control">
</div>
<div class="mb-3">
<label for="_variables">[[admin/appearance/skins:custom-skin-variables]]</label>
<textarea name="_variables" class="form-control" rows="20"></textarea>
</div>
</form>

@ -0,0 +1,11 @@
<li data-type="item" class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="">
<strong>{custom-skin-name}</strong>
</div>
<div class="">
<button type="button" data-type="edit" class="btn btn-sm btn-light"><i class="fa fa-edit text-primary"></i></button>
<button type="button" data-type="remove" class="btn btn-sm btn-light"><i class="fa fa-trash-o text-danger"></i></button>
</div>
</div>
</li>
Loading…
Cancel
Save