mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-05-07 05:37:35 +02:00
feat: screenshot upload in ACP, send fallback brand icons in manifest, serve assets for richer PWA install UI
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
"description": "Site Description",
|
"description": "Site Description",
|
||||||
"keywords": "Site Keywords",
|
"keywords": "Site Keywords",
|
||||||
"keywords-placeholder": "Keywords describing your community, comma-separated",
|
"keywords-placeholder": "Keywords describing your community, comma-separated",
|
||||||
"logo-and-icons": "Site Logo & Icons",
|
"logo-and-icons": "Media & Branding",
|
||||||
"logo.image": "Image",
|
"logo.image": "Image",
|
||||||
"logo.image-placeholder": "Path to a logo to display on forum header",
|
"logo.image-placeholder": "Path to a logo to display on forum header",
|
||||||
"logo.upload": "Upload",
|
"logo.upload": "Upload",
|
||||||
@@ -35,6 +35,8 @@
|
|||||||
"touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.",
|
"touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.",
|
||||||
"maskable-icon": "Maskable (Homescreen) Icon",
|
"maskable-icon": "Maskable (Homescreen) Icon",
|
||||||
"maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.",
|
"maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot",
|
||||||
"outgoing-links": "Outgoing Links",
|
"outgoing-links": "Outgoing Links",
|
||||||
"outgoing-links.warning-page": "Use Outgoing Links Warning Page",
|
"outgoing-links.warning-page": "Use Outgoing Links Warning Page",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ uploadsController.uploadTouchIcon = async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
uploadsController.uploadMaskableIcon = async function (req, res, next) {
|
uploadsController.uploadMaskableIcon = async function (req, res, next) {
|
||||||
const uploadedFile = req.files[0];
|
const uploadedFile = req.files[0];
|
||||||
const allowedTypes = ['image/png'];
|
const allowedTypes = ['image/png'];
|
||||||
@@ -214,6 +213,21 @@ uploadsController.uploadMaskableIcon = async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uploadsController.uploadScreenshot = async function (req, res, next) {
|
||||||
|
const uploadedFile = req.files[0];
|
||||||
|
const allowedTypes = ['image/png', 'image/jpeg'];
|
||||||
|
|
||||||
|
await validateUpload(uploadedFile, allowedTypes);
|
||||||
|
try {
|
||||||
|
const imageObj = await file.saveFileToLocal('screenshot.png', 'system', uploadedFile.path);
|
||||||
|
res.json([{ name: uploadedFile.name, url: imageObj.url }]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
} finally {
|
||||||
|
file.delete(uploadedFile.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
uploadsController.uploadFile = async function (req, res, next) {
|
uploadsController.uploadFile = async function (req, res, next) {
|
||||||
const uploadedFile = req.files[0];
|
const uploadedFile = req.files[0];
|
||||||
let params;
|
let params;
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
|
const mime = require('mime');
|
||||||
|
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
|
const image = require('../image');
|
||||||
const privilegesHelpers = require('../privileges/helpers');
|
const privilegesHelpers = require('../privileges/helpers');
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
@@ -271,6 +274,7 @@ Controllers.manifest = async function (req, res) {
|
|||||||
const manifest = {
|
const manifest = {
|
||||||
name: meta.config.title || 'NodeBB',
|
name: meta.config.title || 'NodeBB',
|
||||||
short_name: meta.config['title:short'] || meta.config.title || 'NodeBB',
|
short_name: meta.config['title:short'] || meta.config.title || 'NodeBB',
|
||||||
|
...(meta.config.description && { description: meta.config.description }),
|
||||||
start_url: nconf.get('url'),
|
start_url: nconf.get('url'),
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
@@ -279,6 +283,33 @@ Controllers.manifest = async function (req, res) {
|
|||||||
icons: [],
|
icons: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (meta.config['brand:screenshot']) {
|
||||||
|
let sizes;
|
||||||
|
try {
|
||||||
|
const { width, height } = await image.size(path.join(nconf.get('base_dir'), meta.config['brand:screenshot'].replace('assets', 'public')));
|
||||||
|
sizes = `${width}x${height}`;
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
manifest.screenshots = [
|
||||||
|
{
|
||||||
|
src: `${nconf.get('relative_path')}${meta.config['brand:screenshot']}`,
|
||||||
|
...(sizes && { sizes }),
|
||||||
|
type: mime.getType(meta.config['brand:screenshot']),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
manifest.screenshots = [
|
||||||
|
{
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/screenshot-default.png`,
|
||||||
|
sizes: '446x778',
|
||||||
|
type: 'image/png',
|
||||||
|
form_factor: 'narrow',
|
||||||
|
label: 'Default home page of a vanilla NodeBB installation.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (meta.config['brand:touchIcon']) {
|
if (meta.config['brand:touchIcon']) {
|
||||||
manifest.icons.push({
|
manifest.icons.push({
|
||||||
src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-36.png`,
|
src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-36.png`,
|
||||||
@@ -316,6 +347,43 @@ Controllers.manifest = async function (req, res) {
|
|||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
density: 10.0,
|
density: 10.0,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
manifest.icons.push({
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/touch/36.png`,
|
||||||
|
sizes: '36x36',
|
||||||
|
type: 'image/png',
|
||||||
|
density: 0.75,
|
||||||
|
}, {
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/touch/48.png`,
|
||||||
|
sizes: '48x48',
|
||||||
|
type: 'image/png',
|
||||||
|
density: 1.0,
|
||||||
|
}, {
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/touch/72.png`,
|
||||||
|
sizes: '72x72',
|
||||||
|
type: 'image/png',
|
||||||
|
density: 1.5,
|
||||||
|
}, {
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/touch/96.png`,
|
||||||
|
sizes: '96x96',
|
||||||
|
type: 'image/png',
|
||||||
|
density: 2.0,
|
||||||
|
}, {
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/touch/144.png`,
|
||||||
|
sizes: '144x144',
|
||||||
|
type: 'image/png',
|
||||||
|
density: 3.0,
|
||||||
|
}, {
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/touch/192.png`,
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
density: 4.0,
|
||||||
|
}, {
|
||||||
|
src: `${nconf.get('relative_path')}/assets/images/touch/512.png`,
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
density: 10.0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ privsAdmin.routeMap = {
|
|||||||
uploadfavicon: 'admin:settings',
|
uploadfavicon: 'admin:settings',
|
||||||
uploadTouchIcon: 'admin:settings',
|
uploadTouchIcon: 'admin:settings',
|
||||||
uploadMaskableIcon: 'admin:settings',
|
uploadMaskableIcon: 'admin:settings',
|
||||||
|
uploadScreenshot: 'admin:settings',
|
||||||
uploadlogo: 'admin:settings',
|
uploadlogo: 'admin:settings',
|
||||||
uploadOgImage: 'admin:settings',
|
uploadOgImage: 'admin:settings',
|
||||||
uploadDefaultAvatar: 'admin:settings',
|
uploadDefaultAvatar: 'admin:settings',
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ function apiRoutes(router, name, middleware, controllers) {
|
|||||||
router.post(`/api/${name}/uploadfavicon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadFavicon));
|
router.post(`/api/${name}/uploadfavicon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadFavicon));
|
||||||
router.post(`/api/${name}/uploadTouchIcon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadTouchIcon));
|
router.post(`/api/${name}/uploadTouchIcon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadTouchIcon));
|
||||||
router.post(`/api/${name}/uploadMaskableIcon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadMaskableIcon));
|
router.post(`/api/${name}/uploadMaskableIcon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadMaskableIcon));
|
||||||
|
router.post(`/api/${name}/uploadScreenshot`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadScreenshot));
|
||||||
router.post(`/api/${name}/uploadlogo`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadLogo));
|
router.post(`/api/${name}/uploadlogo`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadLogo));
|
||||||
router.post(`/api/${name}/uploadOgImage`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadOgImage));
|
router.post(`/api/${name}/uploadOgImage`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadOgImage));
|
||||||
router.post(`/api/${name}/upload/file`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadFile));
|
router.post(`/api/${name}/upload/file`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadFile));
|
||||||
|
|||||||
@@ -153,6 +153,19 @@
|
|||||||
[[admin/settings/general:maskable-icon.help]]
|
[[admin/settings/general:maskable-icon.help]]
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="screenshotUrl">[[admin/settings/general:screenshot]]</label>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<input id="screenshotUrl" type="text" class="form-control" data-field="brand:screenshot" />
|
||||||
|
|
||||||
|
<input data-action="upload" data-target="screenshotUrl" data-route="{config.relative_path}/api/admin/uploadScreenshot" type="button" class="btn btn-light" value="[[admin/settings/general:touch-icon.upload]]" />
|
||||||
|
<button data-action="removeMaskableIcon" type="button" class="btn btn-light"><i class="fa fa-trash text-danger"></i></button>
|
||||||
|
</div>
|
||||||
|
<p class="form-text">
|
||||||
|
[[admin/settings/general:screenshot.help]]
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|||||||
Reference in New Issue
Block a user