Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ class Launcher {
this.settings.licenseType = process.env.FORGE_LICENSE_TYPE
this.settings.broker = this.options.broker
this.settings.launcherVersion = this.options?.versions?.launcher || ''
this.settings.nodeRedVersion = this.options?.versions?.['node-red'] || ''

this.settings.storageDir = path.normalize(path.join(this.settings.rootDir, this.settings.userDir, 'storage'))
await this.logBuffer.setLogPath(path.join(this.settings.storageDir, 'ff-logs'))
Expand Down
17 changes: 15 additions & 2 deletions lib/runtimeSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,26 @@ function getSettingsFile (settings) {
}
}
if (projectSettings.theme) {
const nrMajor = parseInt((settings.nodeRedVersion || '0.0.0').split('.')[0], 10) || 0
let activeTheme = projectSettings.theme

if (nrMajor >= 5) {
// NR5+: route any FF theme choice to the new `forge` plugin so the schemes API kicks in.
if (activeTheme === 'forge-light' || activeTheme === 'forge-dark' || activeTheme === 'forge') {
activeTheme = 'forge'
}
} else if (activeTheme === 'forge') {
// NR<5 has no schemes API; fall back to the coloured legacy plugin.
activeTheme = 'forge-light'
}

const themeSettings = {
launcherVersion: settings.launcherVersion,
forgeURL: settings.forgeURL,
projectURL: `${settings.forgeURL}/instance/${settings.projectID}`
}
projectSettings.themeSettings = `"${projectSettings.theme}": ${JSON.stringify(themeSettings)},`
projectSettings.theme = `theme: '${projectSettings.theme}',`
projectSettings.themeSettings = `"${activeTheme}": ${JSON.stringify(themeSettings)},`
projectSettings.theme = `theme: '${activeTheme}',`
}

let nodesDir = ''
Expand Down
125 changes: 125 additions & 0 deletions lib/theme/forge/forge-common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
(function () {
/* global RED, $ */
// monitorInsertion: derived from https://github.com/naugtur/insertionQuery/blob/master/insQ.min.js (MIT License Copyright (c) 2014-present Zbyszek Tenerowicz <naugtur@gmail.com>)
// eslint-disable-next-line
document.head = (document.head || document.getElementsByTagName('head')[0]);
// eslint-disable-next-line
const monitorInsertion = (function () { 'use strict'; let m = 100; let t = !1; let u = 'animationName'; let d = ''; const n = 'Webkit Moz O ms Khtml'.split(' '); let e = ''; const i = document.createElement('div'); const s = { strictlyNew: !0, timeout: 20, addImportant: !1 }; if (i.style.animationName && (t = !0), !1 === t) for (let o = 0; o < n.length; o++) if (void 0 !== i.style[n[o] + 'AnimationName']) { e = n[o], u = e + 'AnimationName', d = '-' + e.toLowerCase() + '-', t = !0; break } function c (t) { return s.strictlyNew && !0 === t.QinsQ } function r (t, n) { function e (t) { t.animationName !== o && t[u] !== o || c(t.target) || n(t.target) } let i; var o = 'insQ_' + m++; const r = s.addImportant ? ' !important' : ''; (i = document.createElement('style')).innerHTML = '@' + d + 'keyframes ' + o + ' { from { outline: 1px solid transparent } to { outline: 0px solid transparent } }\n' + t + ' { animation-duration: 0.001s' + r + '; animation-name: ' + o + r + '; ' + d + 'animation-duration: 0.001s' + r + '; ' + d + 'animation-name: ' + o + r + '; } ', document.head.appendChild(i); const a = setTimeout(function () { document.addEventListener('animationstart', e, !1), document.addEventListener('MSAnimationStart', e, !1), document.addEventListener('webkitAnimationStart', e, !1) }, s.timeout); return { destroy: function () { clearTimeout(a), i && (document.head.removeChild(i), i = null), document.removeEventListener('animationstart', e), document.removeEventListener('MSAnimationStart', e), document.removeEventListener('webkitAnimationStart', e) } } } function a (t) { t.QinsQ = !0 } function f (t) { if (t) for (a(t), t = t.firstChild; t; t = t.nextSibling) void 0 !== t && t.nodeType === 1 && f(t) } function l (t, n) { let e; let i = []; const o = function () { clearTimeout(e), e = setTimeout(function () { i.forEach(f), n(i), i = [] }, 10) }; return r(t, function (t) { if (!c(t)) { a(t); const n = (function t (n) { return c(n.parentNode) || n.nodeName === 'BODY' ? n : t(n.parentNode) }(t)); i.indexOf(n) < 0 && i.push(n), o() } }) } function v (n) { return !(!t || !n.match(/[^{}]/)) && (s.strictlyNew && f(document.body), { every: function (t) { return r(n, t) }, summary: function (t) { return l(n, t) } }) } return v.config = function (t) { for (const n in t)t.hasOwnProperty(n) && (s[n] = t[n]) }, v }()); typeof module !== 'undefined' && void 0 !== module.exports && (module.exports = monitorInsertion)
const context = {
isEmbedded: window.parent !== window.self,
shouldEmitInsteadOfRedirect: false
}
const navigateTo = (url) => {
if (context.isEmbedded) {
window.parent.postMessage({
type: 'navigate',
payload: url
}, '*')
} else {
window.location = url
}
}
const interceptLogoClick = (url) => {
const logoAnchor = document.querySelector('.red-ui-header-logo a')

if (logoAnchor) {
logoAnchor.addEventListener(
'click',
(e) => {
e.preventDefault()
navigateTo(url)
})
}
}
const interceptLogOutClick = (url) => {
document.querySelector('#usermenu-item-logout')
.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
if (context.shouldEmitInsteadOfRedirect) {
window.parent.postMessage({
type: 'logout'
}, '*')
}
})
}

function changeFavicon (src) {
const link = document.createElement('link')
const oldLink = $('link[href="favicon.ico"]')[0] || $('#dynamic-favicon"]')[0]
link.id = 'dynamic-favicon'
link.rel = 'shortcut icon'
link.href = src
if (oldLink) {
document.head.removeChild(oldLink)
}
document.head.appendChild(link)
}

function handleMessage (event) {
if (event.data.type === 'prevent-redirect') {
context.shouldEmitInsteadOfRedirect = event.data.payload
}
}

if (context.isEmbedded) {
window.parent.postMessage({ type: 'load', payload: true }, '*')
window.addEventListener('message', handleMessage)
}

window.addEventListener('load', (_event) => {
// set favicon
const favicon32 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADzUlEQVR4AcXB206UVxzG4d+71jcbBmYGpjJIg2iHiNYEJaSKrQe4QeMNcOaR11BP2wuwV2JvQSMkxFAiGk1Q8UCjAoJsAkgRZuZb/wJtk6YtuEN4Ht3o7f0B+AXoBhy7w4Bh4FoEXAe+Z3cJ6AauO+Ake+c7B0TsHe/YY4495thjjj0WsdvMMNaZsSHiC7MQwAwkXBSZT6eJ0ml8MmlyziK+ADODEPDJpNU0N1t9qUT94cPkWlvJNDaSzGbxqZTkHBE7yOIYeU/d/v2h2NVFc3c3De3tShcKOO/Ffynic5lhIRDV1FjhyBFr6emh+dQpMk1NTs7xPhGfyEJgQyqft6auLjt48SL7OjqUyGTER4j4CBYCmOGiyOpaWqz59GlrPXdO9W1tcomEeB8z4nLZqqurhEoFC4HI4pgtSWyQc/h02moKBWtob2f/yZNW7OxUTWOjl8R24nLZlicnbe7RI+bHxmx5fFxri4vE5bIsBKJvr1wxzLAQYlsnkLx38t58MkmUTiuZy1Hb1ERtc7PSDQ3Ie8d2zHg3N2dTd++GicFBzY+NaW1hQRbHbJLYJBF1XL3q+FPEZ7I4tsUXL+xVf79NDA7q7fi4s2pVcg4k5D3/FrEDQqVibx48CC9v3tTUyIhW5+cd6+Qc8p7tOHZAqFRs8flz5p8+ZW1hQZghiQ8RsQOiTMYd6evj4IUL9np4OLy8dYu5x49VXVkRziGJrUTjAwO2KYQYEOvknJP35hMJ+XSaVD5PuqFByWwWeS+2kC4U9M3lyzrQ02Ozo6M2PjAQpu/d08rMjKxaFRKS2CSxQb9euhTMTJgZGyQEQjIknPf4VIp0Q4PlDh2i8cQJKx4/rrqWFrkoEtuwEOz36WmbffjQ3ty/r4Vnz1hbWFB1dZVQrWIhoBu9vQEQ2zHDzMAMOUe6ULB9HR124OxZip2dSmaz4j0sBKssL7O2tGSVt2+prKwQl8tEfAgJSfzt3fy8Xt2+rck7d6y+rc1az5+3r8+cobZYdEj8HzmnZC5HMpcT/+D7SqWfAPERJCHnsBC0MjOj6ZERvR4asnczMyFRV0cqn0feiw/g+0qlnwHxiSSxYW1xUbOjo25icJD5J08slMuWqKsjUVODnBNbiNghco4N5aUlTQwOanJoyGqLRQpHj9pXx46FfKmkTLFIMpuVT6Vw3oNzROw0CXkPZlqemmJ5clIv+/uJUilLZrOk8nlL5nJEmQw+kSACYsDxBUgC79kQl8tamZ1lZWZGmPGX2AHD7BJJyDnkPfIeeX/XAdeAO0Bg9wTgN+DHPwAIHJAeMJ00fgAAAABJRU5ErkJggg=='
changeFavicon(favicon32)

// monitor #red-ui-header-button-sidemenu & add main menu entries
monitorInsertion('#red-ui-header-button-sidemenu').summary(function (_arrayOfInsertedNodes) {
if (!RED) { return }

RED.menu.addItem('red-ui-header-button-sidemenu', null) // menu seperator
// add main menu item "About FlowFuse"
RED.menu.addItem('red-ui-header-button-sidemenu', {
id: 'usermenu-item-ffsite',
label: 'About FlowFuse',
onselect: function () {
navigateTo('https://flowfuse.com/')
}
})
// gather info from settings and page - prep for next 2 menu items
const ffThemeSettings = RED.settings['forge']
let projectURL = ''
if (ffThemeSettings && ffThemeSettings.projectURL) {
projectURL = ffThemeSettings.projectURL
} else {
const img = $('#red-ui-header > span > a > img')
const ownerHref = img.parent().prop('href')
// Test the URL is FlowFuse Project alike
if (ownerHref && /http[s]*:\/\/.*\/project\/\w+-\w+-\w+-\w+-\w+.*/.test(ownerHref)) {
projectURL = ownerHref
}
}
// if projectURL is present, show link to project in main menu
if (projectURL) {
RED.menu.addItem('red-ui-header-button-sidemenu', {
id: 'usermenu-item-ffmain',
label: 'FlowFuse Application',
onselect: function () {
navigateTo(projectURL)
}
})
}
// if theme settings are present, add launcher version entry in main menu
if (ffThemeSettings && ffThemeSettings.launcherVersion) {
RED.menu.addItem('red-ui-header-button-sidemenu', {
id: 'usermenu-item-fflv',
label: 'FlowFuse Launcher v' + ffThemeSettings.launcherVersion,
onselect: function () {
// do nothing
}
})
}
interceptLogOutClick()
interceptLogoClick(projectURL)
})
})
})()
28 changes: 28 additions & 0 deletions lib/theme/forge/forge.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
:root {
--red-ui-header-height: 45px;
}

/* Replace the header image with the FlowFuse logo. CSS trick:
* https://css-tricks.com/replace-the-image-in-an-img-with-css/ */
#red-ui-header .red-ui-header-logo img {
display: inline-block;
box-sizing: border-box;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIyIiBoZWlnaHQ9IjIyMiIgdmlld0JveD0iMCAwIDIyMiAyMjIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xMjIuNCAxMzQuMUMxNDUuMSAxNDMuNyAxNjYuOSAxNTUuMSAxOTAuNCAxNjAuNEMyMDAuOSAxNjIuMSAyMTEuNSAxNjMgMjIxLjkgMTYzVjEwNS4ySDIxMy4yQzE4MSAxMDUuMiAxNTEuMiAxMjEuOSAxMjIuNCAxMzQuMVoiIGZpbGw9IiNEQTNEMEIiLz4KPHBhdGggZD0iTTU1LjMgMTUyLjVDNDQuOCAxNTEuNiAzNC4zIDE1MS42IDIzLjkgMTUxLjZIMFYyMDQuN0MwIDIxNC4zIDcuOCAyMjIgMTcuMyAyMjJIMjA0LjZDMjE0LjIgMjIyIDIyMS45IDIxNC4yIDIyMS45IDIwNC43VjE5OEMxOTkuMiAxOTggMTc1LjYgMTk1LjEgMTU0LjYgMTg2LjNDMTIxLjYgMTc1IDkxLjEgMTUzLjQgNTUuMyAxNTIuNVoiIGZpbGw9IiNEQTNEMEIiLz4KPHBhdGggZD0iTTIwNC43IDBIMTcuM0M3LjggMCAwIDcuOCAwIDE3LjRWMTE1LjhDMjEgMTE1LjggNDMuMSAxMTYuNyA2NCAxMTQuOUM5Mi44IDExMi4zIDExOC4xIDk2LjUgMTQ1LjEgODZDMTY5LjUgNzUuNSAxOTUuOCA2OS40IDIyMiA3MC4yVjE3LjRDMjIyIDcuOCAyMTQuMiAwIDIwNC43IDBaIiBmaWxsPSIjREEzRDBCIi8+CjxwYXRoIGQ9Ik0yMjIgMTA1LjJWNzAuMkMxOTUuOCA2OS4zIDE2OS42IDc1LjUgMTQ1LjEgODZDMTE4IDk2LjUgOTIuOCAxMTIuMyA2NCAxMTQuOUM0My4xIDExNi43IDIxIDExNS44IDAgMTE1LjhWMTUxLjdIMjMuOUMzNC4zIDE1MS43IDQ0LjggMTUxLjcgNTUuMyAxNTIuNkM5MS4xIDE1My41IDEyMS42IDE3NS4xIDE1NC43IDE4Ni40QzE3NS43IDE5NS4yIDE5OS4zIDE5OC4xIDIyMiAxOTguMVYxNjMuMUMyMTEuNiAxNjMuMSAyMDAuOSAxNjIuMiAxOTAuNSAxNjAuNUMxNjYuOSAxNTUuMiAxNDUuMiAxNDMuOSAxMjIuNSAxMzQuMkMxNTEuMyAxMjEuOSAxODEuMSAxMDUuMyAyMTMuMyAxMDUuM0wyMjIgMTA1LjJaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K') no-repeat;
width: 26px;
height: 26px;
padding-left: 26px;
background-size: contain;
}

#red-ui-header .red-ui-header-logo span {
font-weight: 600;
}

#usermenu-item-fflv > a {
cursor: default;
opacity: 0.7;
}

#usermenu-item-fflv > a:hover {
background: transparent;
}
48 changes: 48 additions & 0 deletions lib/theme/forge/forge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { existsSync } = require('fs')

module.exports = function (RED) {
let headerImage = 'resources/@flowfuse/nr-theme/ff-nr.png'
let favicon = 'resources/@flowfuse/nr-theme/favicon-16x16.png'
if (!existsSync(headerImage)) {
headerImage = 'resources/@flowfuse/nr-launcher/ff-nr.png'
}
if (!existsSync(favicon)) {
favicon = 'resources/@flowfuse/nr-launcher/favicon-16x16.png'
}

RED.plugins.registerPlugin('forge', {
type: 'node-red-theme',
schemes: ['light', 'dark'],
scripts: [
'lib/theme/forge/forge-common.js'
],
css: [
'lib/theme/forge/forge.css'
],
settings: {
theme: {
value: 'forge',
exportable: true
},
headerImage: {
value: headerImage,
exportable: true
},
favicon: {
value: favicon,
exportable: true
},
launcherVersion: {
exportable: true
},
forgeURL: {
exportable: true
},
projectURL: {
exportable: true
}
}
})

RED.log.info('FlowFuse Theme Plugin loaded')
}
5 changes: 3 additions & 2 deletions lib/theme/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flowfuse/nr-theme",
"version": "1.9.0",
"version": "1.10.0",
"description": "FlowFuse themes for Node-RED",
"scripts": {
"prepack": "node scripts/prepack.mjs",
Expand Down Expand Up @@ -35,7 +35,8 @@
"version": ">=2.2.0",
"plugins": {
"forge-light": "lib/theme/forge-light/forge-light.js",
"forge-dark": "lib/theme/forge-dark/forge-dark.js"
"forge-dark": "lib/theme/forge-dark/forge-dark.js",
"forge": "lib/theme/forge/forge.js"
}
},
"engines": {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"flowfuse-blueprint-library": "lib/storage/blueprintPlugin.js",
"forge-light": "lib/theme/forge-light/forge-light.js",
"forge-dark": "lib/theme/forge-dark/forge-dark.js",
"forge": "lib/theme/forge/forge.js",
"forge-resources": "lib/resources/resourcePlugin.js"
}
},
Expand Down
Loading