openvibers/openvibe.shared app-icons.js
app-icons.js
| 1 | ; |
| 2 | // ═══════════════════════════════════════════════════════════════ |
| 3 | // openvibe-shared/app-icons — every OpenVibe site's installable app icon set. |
| 4 | // |
| 5 | // The icons are pre-rendered PNGs in icons/<variant>[-maskable]-<size>.png (192 and 512), one set per app-icon |
| 6 | // tint variant (most sites share the network's), drawn by scripts/build-icons.js from app-icon.svg({ site: variant, |
| 7 | // maskable }); icons/sites.json names each service's variant (social.iconSite). A site serves its |
| 8 | // pinned copy at /shared/icons/<name> through openvibe-shared/serve, and the web app manifest served at |
| 9 | // /shared/manifest/<service>.webmanifest points at the same hashed URLs. (The `icons` subpath is already the |
| 10 | // ring-icon module `ov-icons.js`, so the per-site icon set lives here.) |
| 11 | // |
| 12 | // appIcons.has(service) is there an icon set for this service id |
| 13 | // appIcons.url(service, size, { maskable }) '/shared/icons/<variant>[-maskable]-<size>.png?v=<hash>', or null |
| 14 | // appIcons.site(service) { name, description, icon } from icons/sites.json, or null |
| 15 | // appIcons.isIcon(name) 'icons/<variant>[-maskable]-<size>.png' names serve.js may answer |
| 16 | // appIcons.path(name) the file on disk (throws for anything that is not an icon) |
| 17 | // |
| 18 | // shell.page() uses appIcons.url(service, 192) for the apple-touch-icon when the page names its service. |
| 19 | // ═══════════════════════════════════════════════════════════════ |
| 20 | const fs = require('fs'); |
| 21 | const path = require('path'); |
| 22 | const { hashAsset } = require('./assets'); |
| 23 | |
| 24 | const DIR = path.join(__dirname, 'icons'); |
| 25 | const NAME_RE = /^icons\/([a-z0-9][a-z0-9-]{0,40})-(maskable-)?(192|512)\.png$/; |
| 26 | const ID_RE = /^[a-z0-9][a-z0-9-]{0,40}$/; |
| 27 | const hashes = new Map(); |
| 28 | let sites = null; |
| 29 | /** icons/sites.json: { <service>: { name, description, icon: <variant> } }, read once. */ |
| 30 | function site(service) { |
| 31 | if (!sites) { try { sites = JSON.parse(fs.readFileSync(path.join(DIR, 'sites.json'), 'utf8')); } catch { sites = {}; } } |
| 32 | const id = String(service || ''); |
| 33 | return ID_RE.test(id) && Object.prototype.hasOwnProperty.call(sites, id) ? sites[id] : null; |
| 34 | } |
| 35 | |
| 36 | function isIcon(name) { |
| 37 | const m = NAME_RE.exec(String(name || '')); |
| 38 | return Boolean(m) && fs.existsSync(path.join(DIR, `${m[1]}-${m[2] || ''}${m[3]}.png`)); |
| 39 | } |
| 40 | |
| 41 | function iconPath(name) { |
| 42 | if (!isIcon(name)) throw new Error(`openvibe-shared/app-icons: ${name} is not an app icon`); |
| 43 | return path.join(__dirname, name); |
| 44 | } |
| 45 | |
| 46 | const has = (service) => { const s = site(service); return Boolean(s && s.icon && isIcon(`icons/${s.icon}-192.png`)); }; |
| 47 | |
| 48 | function url(service, size, { maskable = false } = {}) { |
| 49 | const meta = site(service); |
| 50 | if (!meta || !meta.icon) return null; |
| 51 | const s = Number(size); |
| 52 | if (s !== 192 && s !== 512) return null; |
| 53 | const name = `icons/${meta.icon}-${maskable ? 'maskable-' : ''}${s}.png`; |
| 54 | if (!isIcon(name)) return null; |
| 55 | if (!hashes.has(name)) hashes.set(name, hashAsset(fs.readFileSync(iconPath(name)))); |
| 56 | return `/shared/${name}?v=${hashes.get(name)}`; |
| 57 | } |
| 58 | |
| 59 | module.exports = { has, url, site, isIcon, path: iconPath, DIR }; |
| 60 |