Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configurable app icon #1377

Merged
merged 9 commits into from
Oct 10, 2024
1 change: 1 addition & 0 deletions docs/nodes/config/ui-base.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
description: Configure the base UI settings of Node-RED Dashboard 2.0 to tailor the dashboard environment to your needs.
props:
Path: The endpoint proceeding the host of Node-RED where your UI will be accessible
App Icon: Allows you to set a custom icon for your application. Provide the URL to the App Icon, which will be displayed as the app icon and in the browser tab.
Include Page Path in Label: The side navigation lists all available Pages for the Dashboard. By default, this will just show the page name, but this option allows you to also show the page's path.
Side Navigation Style: The style the side navigation menu should use (default, fixed, icon, temporary, none)
---
Expand Down
7 changes: 7 additions & 0 deletions nodes/config/locales/en-US/ui_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@
</ul>
</dd>
</dl>
<dt>
App Icon
<span class="property-type">URL</span>
</dt>
<dd>
You can provide a custom URL for your application's icon, which will be displayed as the app icon and in the browser tab.
</dd>
</script>
1 change: 1 addition & 0 deletions nodes/config/locales/en-US/ui_base.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"label": {
"uiName": "UI Name",
"path": "Path",
"appIcon": "App Icon",
"category": "dashboard 2",
"dashboard2": "Dashboard 2.0",
"editSettings": "Edit Settings",
Expand Down
13 changes: 13 additions & 0 deletions nodes/config/ui_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@
value: '/dashboard',
required: true
},
appIcon: {
value: ''
},
includeClientData: {
value: true
},
Expand Down Expand Up @@ -364,6 +367,10 @@
// update the jquery checkbox
$('#node-config-input-showPageTitle').prop('checked', true)
}

if (this.appIcon) {
$('#node-config-input-appIcon').val(this.appIcon)
}
},
onpaletteadd: function () {
// add the Dashboard 2.0 sidebar
Expand Down Expand Up @@ -391,6 +398,7 @@
icon: null,
color: null,
isSubflowInstance: false,
appIcon: node.appIcon,
node
}
if (hasProperty(node, 'group')) { item.group = node.group }
Expand Down Expand Up @@ -1988,6 +1996,11 @@
<input type="text" id="node-config-input-path" disabled>
<span style="display: block; margin-left: 105px; margin-top: 0px; font-style: italic; color: #bbb; font-size: 8pt;">This option is currently disabled and still in-development.</span>
</div>
<div class="form-row">
<label for="node-config-input-appIcon"><i class="fa fa-cube"></i> <span data-i18n="ui-base.label.appIcon"></label>
<input type="text" id="node-config-input-appIcon">
<span style="display: block; margin-left: 105px; margin-top: 0px; font-style: italic; color: #bbb; font-size: 8pt;">Enter the url of your app icon here</span>
</div>
<div class="form-row" style="margin-bottom: 0;">
<label style="font-weight: 600; width: auto;" data-i18n="ui-base.label.header"></label>
</div>
Expand Down
22 changes: 22 additions & 0 deletions nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,28 @@ module.exports = function (RED) {
res.sendFile(path.join(__dirname, '../../dist/index.html'))
})

uiShared.app.get('/dashboard/manifest.webmanifest', (req, res) => {
const hasAppIcon = (config.appIcon && config.appIcon.trim() !== '')
const manifest = {
name: 'Node-RED Dashboard 2.0',
short_name: 'Dashboard',
start_url: './',
display: 'standalone',
background_color: '#ffffff',
lang: 'en',
scope: './',
description: 'Node-RED Dashboard 2.0',
theme_color: '#ffffff',
icons: [
{ src: hasAppIcon ? config.appIcon : 'pwa-64x64.png', sizes: '64x64', type: 'image/png' },
{ src: hasAppIcon ? config.appIcon : 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: hasAppIcon ? config.appIcon : 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{ src: hasAppIcon ? config.appIcon : 'pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
]
}
return res.json(manifest)
})

uiShared.app.get(config.path + '/*', uiShared.httpMiddleware, (req, res) => {
res.sendFile(path.join(__dirname, '../../dist/index.html'))
})
Expand Down
2 changes: 1 addition & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Node-RED Dashboard 2.0</title>
<meta name="description" content="Node-RED Dashboard 2.0">
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link href="data:image/x-icon;base64,AAABAAEAEBAQAAAAAAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAsC8qAP+EAACzh1cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAACAAAAAAAAACAAAAAAAAEiAAAAADAAAiAAAAAAMzAiAAAAAAAAMzAAAAAAAAAiMzMAAAAAAAADAzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" rel="icon" type="image/x-icon">
<link rel="alternate icon" href="/favicon.ico" type="image/png" sizes="16x16">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" crossorigin="use-credentials" href="./manifest.webmanifest">
Expand Down
47 changes: 45 additions & 2 deletions ui/src/layouts/Baseline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ function getContrast (bg) {

// http://www.w3.org/TR/AERT#color-contrast
const brightness = Math.round(((parseInt(bgRgb[0]) * 299) +
(parseInt(bgRgb[1]) * 587) +
(parseInt(bgRgb[2]) * 114)) / 1000)
(parseInt(bgRgb[1]) * 587) +
(parseInt(bgRgb[2]) * 114)) / 1000)

const textColor = (brightness > 125) ? '#000000' : '#ffffff'
return textColor
Expand Down Expand Up @@ -154,6 +154,9 @@ export default {
appBarStyle: function () {
return this.dashboard.titleBarStyle || 'default'
},
appIcon () {
return this.dashboard.appIcon
},
navigationStyle: function () {
const style = this.dashboard.navigationStyle
if (![null, 'default', 'fixed', 'icon', 'temporary', 'none'].includes(style)) {
Expand Down Expand Up @@ -184,6 +187,46 @@ export default {
this.rail = true
}
}
},
appIcon: {
immediate: true,
handler (url) {
// extract the file extension from the URL
const getFileTypeFromURL = (url) => {
const segments = url.split('.')
const extension = segments[segments.length - 1]
return extension.toLowerCase()
}
// The existing rel types in the index.html
const relTypes = ['icon', 'alternate icon', 'apple-touch-icon']
if (url) {
const fileType = getFileTypeFromURL(url)
relTypes.forEach((relType) => {
// iterate through the rel types and update the link tag
const link = document.querySelector(`link[rel="${relType}"]`)
if (link) {
// set the type and href attributes
link.setAttribute('type', `image/${fileType}`)
link.setAttribute('href', url)
}
})
} else {
relTypes.forEach((relType) => {
// iterate through the rel types and update the link tag
const link = document.querySelector(`link[rel="${relType}"]`)
if (relType === 'icon') {
link.setAttribute('type', 'image/x-icon')
link.setAttribute('href', '/dashboard/favicon.ico')
} else if (relType === 'alternate icon') {
link.setAttribute('type', 'image/svg+xml')
link.setAttribute('href', '/dashboard/favicon.svg')
} else if (relType === 'apple-touch-icon') {
link.setAttribute('type', 'image/png')
link.setAttribute('href', '/dashboard/apple-touch-icon.png')
}
})
}
}
}
},
mounted () {
Expand Down
5 changes: 1 addition & 4 deletions vite.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'

import manifest from './manifest.json'

/**
* Vite is used to build the UI for the dashboard,
* is is not used for the nodes themselves.
Expand All @@ -24,7 +21,7 @@ export default defineConfig({
registerType: 'autoUpdate',
injectRegister: false,

manifest,
manifest: false,

injectManifest: {
maximumFileSizeToCacheInBytes: 3000000,
Expand Down
Loading