openvibers/openvibe.media Commits
Brand assets and manifest; vendor sync
Changes
nnew file mode 100644BBinary files /dev/null and b/server/brand/logo-192.png differnnew file mode 100644BBinary files /dev/null and b/server/brand/logo-512.png differnnew file mode 100644BBinary files /dev/null and b/server/brand/logo-72.png differnnew file mode 100644BBinary files /dev/null and b/server/brand/logo-maskable-192.png differnnew file mode 100644BBinary files /dev/null and b/server/brand/logo-maskable-512.png differnnew file mode 100644@@@ -0,0 +1,8 @@++<svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 -32 64 64"><defs><linearGradient id="ring" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbbf24"/><stop offset="1" stop-color="#2563eb"/></linearGradient>++<radialGradient id="glow"><stop offset="0" stop-color="#fbbf24" stop-opacity=".34"/><stop offset="1" stop-color="#fbbf24" stop-opacity="0"/></radialGradient>++<radialGradient id="bg" cx=".5" cy=".38" r=".8"><stop offset="0" stop-color="#16223a"/><stop offset=".6" stop-color="#0a0f1c"/><stop offset="1" stop-color="#060912"/></radialGradient></defs><rect x="-32" y="-32" width="64" height="64" rx="15" fill="url(#bg)"/><g transform="scale(1.22)">++<circle r="21" fill="url(#glow)"/>++<circle r="18" fill="none" stroke="url(#ring)" stroke-width="4" opacity=".38"/>++<path d="M0,-18 A18,18 0 0,1 17.4,4.6" fill="none" stroke="url(#ring)" stroke-width="4.4" stroke-linecap="round"/>++<path d="M-9.5,-7 L0,10 L9.5,-7" fill="none" stroke="#fff" stroke-width="4.8" stroke-linecap="round" stroke-linejoin="round"/>++<circle cx="17.4" cy="4.6" r="3.2" fill="#ffffff"/></g></svg>\\ No newline at end of filennew file mode 100644@@@ -0,0 +1,48 @@++{++ "name": "OpenVibe.Media",++ "short_name": "Media",++ "description": "Every public video, clip and file on OpenVibe",++ "start_url": "/",++ "scope": "/",++ "display": "standalone",++ "background_color": "#0a0f1c",++ "theme_color": "#3b82f6",++ "icons": [++ {++ "src": "/assets/logo-72.png",++ "sizes": "72x72",++ "type": "image/png",++ "purpose": "any"++ },++ {++ "src": "/assets/logo-192.png",++ "sizes": "192x192",++ "type": "image/png",++ "purpose": "any"++ },++ {++ "src": "/assets/logo-512.png",++ "sizes": "512x512",++ "type": "image/png",++ "purpose": "any"++ },++ {++ "src": "/assets/logo-maskable-192.png",++ "sizes": "192x192",++ "type": "image/png",++ "purpose": "maskable"++ },++ {++ "src": "/assets/logo-maskable-512.png",++ "sizes": "512x512",++ "type": "image/png",++ "purpose": "maskable"++ },++ {++ "src": "/assets/logo.svg",++ "sizes": "any",++ "type": "image/svg+xml",++ "purpose": "any"++ }++ ]++}@@@ -39,6 +39,9 @@ app.use((req, _res, next) => {next();});{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'media', service: 'media', host: 'openvibe.media', name: 'OpenVibe.Media', profile: 'hosting' })); app.get('/tos', (_req, res) => res.redirect(301, '/terms')); }++// Brand assets: icon set + web app manifest generated by openvibe-shared/scripts/build-app-icons.js++app.use('/assets', express.static(require('path').join(__dirname, 'brand'), { maxAge: '7d' }));++app.get('/manifest.webmanifest', (_req, res) => res.type('application/manifest+json').sendFile(require('path').join(__dirname, 'brand', 'manifest.webmanifest')));const userAuth = require('./user-auth');const userAuthConfig = {baseUrl: config.publicUrl, networkUrl: config.network.url, networkInternalUrl: config.network.internalUrl,@@@ -266,7 +266,7 @@ nav.tabs a .n{opacity:.75;font-size:.78rem;margin-left:.3rem}.empty{padding:3rem;text-align:center;color:var(--muted)}</style><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">--${require('openvibe-shared/app-icon').headTags({ site: 'media' })}++${require('openvibe-shared/app-icon').headTags({ site: 'media', iconBase: '/assets' })}</head><body><div id="navbar-mount"></div><header class="intro"><h1>Every public file on the network</h1><span class="sub">videos, clips, images, pastes & thumbnails from all OpenVibe sites</span></header>@@@ -99,6 +99,14 @@.ovnav-launcher .ovl-soon { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 4px 4px; }.ovnav-launcher .ovl-soon a { font-size: 12px; font-weight: 600; padding: 4px 9px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.1)); color: var(--text-secondary, #a8b3c4); }.ovnav-launcher .ovl-soon a:hover { border-color: var(--accent, #3b82f6); color: var(--text-primary, #e6edf7); background: none; }++ .ovnav-launcher .ovl-display { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; padding: 10px 4px 2px; border-top: 1px solid var(--border, rgba(255,255,255,.08)); font-size: 12px; color: var(--text-muted, #7d8aa0); }++ .ovnav-launcher .ovl-display[hidden] { display: none; }++ .ovnav-launcher .ovl-dl { font-weight: 700; letter-spacing: .6px; text-transform: uppercase; font-size: 11px; margin-right: 2px; }++ .ovnav-launcher .ovl-seg { display: inline-flex; border: 1px solid var(--border, rgba(255,255,255,.12)); border-radius: 9px; overflow: hidden; }++ .ovnav-launcher .ovl-seg button { all: unset; cursor: pointer; padding: 5px 9px; font-size: 12px; font-weight: 650; color: var(--text-secondary, #a8b3c4); }++ .ovnav-launcher .ovl-seg button[aria-pressed="true"] { background: var(--accent, #3b82f6); color: var(--on-accent, #fff); }++ .ovnav-launcher .ovl-seg button:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }++ .ovnav-launcher .ovl-display a { margin-left: auto; color: var(--accent-light, var(--accent, #60a5fa)); font-weight: 600; }.ovnav-launcher .ovl-empty { padding: 18px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13px; }@media (max-width: 480px) { .ovnav-launcher { left: 8px; } .ovnav-launcher .ovl-fams { grid-template-columns: 1fr; } }@media (prefers-reduced-motion: reduce) { .ovnav-launcher { transition: none; } }@@@ -643,7 +651,8 @@if (panel && panel.classList.contains('open')) return close();if (!panel) {panel = document.createElement('div'); panel.className = 'ovnav-launcher'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'OpenVibe sites and tools');-- panel.innerHTML = '<input type="search" class="ovl-q" placeholder="Find a tool or site" aria-label="Find a tool or site"><div class="ovl-body"></div>';++ panel.innerHTML = '<input type="search" class="ovl-q" placeholder="Find a tool or site" aria-label="Find a tool or site"><div class="ovl-body"></div><div class="ovl-display" hidden></div>';++ bindDisplayControls(panel.querySelector('.ovl-display'));nav.appendChild(panel);if (!root.OpenVibeIcons && !document.getElementById('ov-icons-loader')) { const sc = document.createElement('script'); sc.id = 'ov-icons-loader'; sc.src = 'https://openvibe.network/shared/ov-icons.js'; sc.async = true; document.head.appendChild(sc); }let cat = null; paint(null, '');@@@ -691,6 +700,21 @@return `<span class="ovnav-sep" aria-hidden="true"></span>` + pick.map(n => `<a class="ovnav-net" href="${escapeAttr(n.url)}" title="${escapeAttr(n.tagline)}">${escapeAttr(n.name)}</a>`).join('');}++ /** Display settings in the launcher, for guests and members alike (theme-loader.js owns the state). */++ function bindDisplayControls(box) {++ const L = root.OpenVibeThemeLoader; if (!box || !L || !L.display) return;++ const paint = () => {++ const d = L.display.get();++ const seg = (key, val, label, title) => `<button type="button" data-k="${key}" data-v="${val}" aria-pressed="${String(d[key]) === val}" title="${title}">${label}</button>`;++ box.innerHTML = `<span class="ovl-dl">Display</span><span class="ovl-seg" role="group" aria-label="Text size">${seg('text', '100', 'A', 'Default text size')}${seg('text', '112', 'A+', 'Larger text')}${seg('text', '125', 'A++', 'Largest text')}</span>++ <span class="ovl-seg" role="group" aria-label="Motion">${seg('motion', 'auto', 'Motion', 'Animations on')}${seg('motion', 'reduced', 'Calm', 'Reduce animations')}</span><a href="https://openvibe.network/themes">Themes</a>`;++ box.hidden = false;++ };++ box.addEventListener('click', (e) => { const b = e.target.closest('button[data-k]'); if (!b) return; L.display.set({ [b.dataset.k]: b.dataset.v }); paint(); });++ root.addEventListener('ov:display', paint);++ paint();++ }++// ── Notification bell (mounted by the navbar itself) ─────function mountBell(nav, u) {if (_config.notifications === false || !u || u.is_anon || !_config.token) return;@@@ -241,11 +241,47 @@}// ── Auto-apply on load (synchronous, prevents FOUC) ─────++ // ── Display preferences: motion + text size, one setting for every OpenVibe site ─────────────++ // OpenVibeThemeLoader.display.get() → { motion: 'auto'|'reduced', text: '100'|'112'|'125' }++ // OpenVibeThemeLoader.display.set({ motion, text }) applies here, in other tabs, and saves to the account++ // Applied as <html data-ov-motion> and --ov-text-scale before first paint; the shared stylesheet++ // below makes every shared component honour them, and sites can key their own CSS off the same hooks.++ const DISPLAY_KEY = 'ov_display', DISPLAY_OPTS = { motion: ['auto', 'reduced'], text: ['100', '112', '125'] };++ function readDisplay() {++ const out = { motion: 'auto', text: '100' };++ try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null') || {}; for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(d[k])) >= 0) out[k] = String(d[k]); } catch (e) { /* */ }++ return out;++ }++ function applyDisplay(d) {++ if (typeof document === 'undefined') return;++ const el = document.documentElement;++ if (d.motion === 'reduced') el.setAttribute('data-ov-motion', 'reduced'); else el.removeAttribute('data-ov-motion');++ if (d.text === '100') { el.style.removeProperty('--ov-text-scale'); el.removeAttribute('data-ov-text'); } else { el.style.setProperty('--ov-text-scale', String(parseInt(d.text, 10) / 100)); el.setAttribute('data-ov-text', d.text); }++ if (!document.getElementById('ov-display-css') && document.head) {++ const st = document.createElement('style'); st.id = 'ov-display-css';++ st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}html[data-ov-motion=reduced] *,html[data-ov-motion=reduced] *::before,html[data-ov-motion=reduced] *::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}html[data-ov-motion=reduced] .ov-mark .o,html[data-ov-motion=reduced] .ov-icon .ovi-comet{display:none}';++ document.head.appendChild(st);++ }++ try { window.dispatchEvent(new CustomEvent('ov:display', { detail: d })); } catch (e) { /* */ }++ }++ function setDisplay(patch, opts) {++ const d = readDisplay();++ for (const k in DISPLAY_OPTS) if (patch && DISPLAY_OPTS[k].indexOf(String(patch[k])) >= 0) d[k] = String(patch[k]);++ try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(d)); } catch (e) { /* */ }++ applyDisplay(d);++ if (!(opts && opts.local) && typeof fetch !== 'undefined') {++ const token = authToken(); const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = 'Bearer ' + token;++ if (token || /(?:^|; )ov_sso_hint=account/.test(document.cookie)) fetch(_config.apiBase + '/api/themes/me/display', { method: 'PUT', headers: headers, credentials: 'include', body: JSON.stringify(d) }).catch(function () {});++ }++ return d;++ }++ applyDisplay(readDisplay());++const activeId = resolveAndApply();// The icon link may be parsed after this script: repaint once the head is complete.if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _chromeKey = ''; syncChrome(); });// A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.-- if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { resolveAndApply(); } catch (err) { /* */ } } });++ if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) applyDisplay(readDisplay()); });// Look up a theme we don't ship in the built-in map (community/custom themes) from the// central catalog. Without this, any non-built-in id fell through to the default 'vibe'@@@ -271,6 +307,7 @@fetch(_config.apiBase + '/api/themes/me/active', { headers: headers, credentials: 'include' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (data) {++ if (data && data.display) { const cur = readDisplay(); if (JSON.stringify({ motion: data.display.motion || cur.motion, text: data.display.text || cur.text }) !== JSON.stringify(cur)) setDisplay(data.display, { local: true }); }if (!data || !data.theme_id) return;const serverId = data.theme_id;const custom = (data.custom_variables && typeof data.custom_variables === 'object'@@@ -292,6 +329,7 @@// ── Public API ───────────────────────────────────────────const OpenVibeThemeLoader = {THEMES: THEMES,++ display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS },apply: applyById,applyVars: applyVars,save: save,
new file mode 100644
new file mode 100644
Binary files /dev/null and b/server/brand/logo-192.png differ
Binary files /dev/null and b/server/brand/logo-192.png differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/server/brand/logo-512.png differ
Binary files /dev/null and b/server/brand/logo-512.png differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/server/brand/logo-72.png differ
Binary files /dev/null and b/server/brand/logo-72.png differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/server/brand/logo-maskable-192.png differ
Binary files /dev/null and b/server/brand/logo-maskable-192.png differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/server/brand/logo-maskable-512.png differ
Binary files /dev/null and b/server/brand/logo-maskable-512.png differ
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 -32 64 64"><defs><linearGradient id="ring" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbbf24"/><stop offset="1" stop-color="#2563eb"/></linearGradient>
+<radialGradient id="glow"><stop offset="0" stop-color="#fbbf24" stop-opacity=".34"/><stop offset="1" stop-color="#fbbf24" stop-opacity="0"/></radialGradient>
+<radialGradient id="bg" cx=".5" cy=".38" r=".8"><stop offset="0" stop-color="#16223a"/><stop offset=".6" stop-color="#0a0f1c"/><stop offset="1" stop-color="#060912"/></radialGradient></defs><rect x="-32" y="-32" width="64" height="64" rx="15" fill="url(#bg)"/><g transform="scale(1.22)">
+<circle r="21" fill="url(#glow)"/>
+<circle r="18" fill="none" stroke="url(#ring)" stroke-width="4" opacity=".38"/>
+<path d="M0,-18 A18,18 0 0,1 17.4,4.6" fill="none" stroke="url(#ring)" stroke-width="4.4" stroke-linecap="round"/>
+<path d="M-9.5,-7 L0,10 L9.5,-7" fill="none" stroke="#fff" stroke-width="4.8" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="17.4" cy="4.6" r="3.2" fill="#ffffff"/></g></svg>
\ No newline at end of file
\ No newline at end of file
new file mode 100644
new file mode 100644
@@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
+{
+ "name": "OpenVibe.Media",
+ "short_name": "Media",
+ "description": "Every public video, clip and file on OpenVibe",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "background_color": "#0a0f1c",
+ "theme_color": "#3b82f6",
+ "icons": [
+ {
+ "src": "/assets/logo-72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/logo-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/logo-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/logo-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/logo-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/logo.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ }
+ ]
+}
@@ -39,6 +39,9 @@ app.use((req, _res, next) => {
@@ -39,6 +39,9 @@ app.use((req, _res, next) => {
next();
next();
});
});
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'media', service: 'media', host: 'openvibe.media', name: 'OpenVibe.Media', profile: 'hosting' })); app.get('/tos', (_req, res) => res.redirect(301, '/terms')); }
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'media', service: 'media', host: 'openvibe.media', name: 'OpenVibe.Media', profile: 'hosting' })); app.get('/tos', (_req, res) => res.redirect(301, '/terms')); }
+// Brand assets: icon set + web app manifest generated by openvibe-shared/scripts/build-app-icons.js
+app.use('/assets', express.static(require('path').join(__dirname, 'brand'), { maxAge: '7d' }));
+app.get('/manifest.webmanifest', (_req, res) => res.type('application/manifest+json').sendFile(require('path').join(__dirname, 'brand', 'manifest.webmanifest')));
const userAuth = require('./user-auth');
const userAuth = require('./user-auth');
const userAuthConfig = {
const userAuthConfig = {
baseUrl: config.publicUrl, networkUrl: config.network.url, networkInternalUrl: config.network.internalUrl,
baseUrl: config.publicUrl, networkUrl: config.network.url, networkInternalUrl: config.network.internalUrl,
@@ -266,7 +266,7 @@ nav.tabs a .n{opacity:.75;font-size:.78rem;margin-left:.3rem}
@@ -266,7 +266,7 @@ nav.tabs a .n{opacity:.75;font-size:.78rem;margin-left:.3rem}
.empty{padding:3rem;text-align:center;color:var(--muted)}
.empty{padding:3rem;text-align:center;color:var(--muted)}
</style>
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
-${require('openvibe-shared/app-icon').headTags({ site: 'media' })}
+${require('openvibe-shared/app-icon').headTags({ site: 'media', iconBase: '/assets' })}
</head><body>
</head><body>
<div id="navbar-mount"></div>
<div id="navbar-mount"></div>
<header class="intro"><h1>Every public file on the network</h1><span class="sub">videos, clips, images, pastes & thumbnails from all OpenVibe sites</span></header>
<header class="intro"><h1>Every public file on the network</h1><span class="sub">videos, clips, images, pastes & thumbnails from all OpenVibe sites</span></header>
@@ -99,6 +99,14 @@
@@ -99,6 +99,14 @@
.ovnav-launcher .ovl-soon { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 4px 4px; }
.ovnav-launcher .ovl-soon { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 4px 4px; }
.ovnav-launcher .ovl-soon a { font-size: 12px; font-weight: 600; padding: 4px 9px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.1)); color: var(--text-secondary, #a8b3c4); }
.ovnav-launcher .ovl-soon a { font-size: 12px; font-weight: 600; padding: 4px 9px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.1)); color: var(--text-secondary, #a8b3c4); }
.ovnav-launcher .ovl-soon a:hover { border-color: var(--accent, #3b82f6); color: var(--text-primary, #e6edf7); background: none; }
.ovnav-launcher .ovl-soon a:hover { border-color: var(--accent, #3b82f6); color: var(--text-primary, #e6edf7); background: none; }
+ .ovnav-launcher .ovl-display { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; padding: 10px 4px 2px; border-top: 1px solid var(--border, rgba(255,255,255,.08)); font-size: 12px; color: var(--text-muted, #7d8aa0); }
+ .ovnav-launcher .ovl-display[hidden] { display: none; }
+ .ovnav-launcher .ovl-dl { font-weight: 700; letter-spacing: .6px; text-transform: uppercase; font-size: 11px; margin-right: 2px; }
+ .ovnav-launcher .ovl-seg { display: inline-flex; border: 1px solid var(--border, rgba(255,255,255,.12)); border-radius: 9px; overflow: hidden; }
+ .ovnav-launcher .ovl-seg button { all: unset; cursor: pointer; padding: 5px 9px; font-size: 12px; font-weight: 650; color: var(--text-secondary, #a8b3c4); }
+ .ovnav-launcher .ovl-seg button[aria-pressed="true"] { background: var(--accent, #3b82f6); color: var(--on-accent, #fff); }
+ .ovnav-launcher .ovl-seg button:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }
+ .ovnav-launcher .ovl-display a { margin-left: auto; color: var(--accent-light, var(--accent, #60a5fa)); font-weight: 600; }
.ovnav-launcher .ovl-empty { padding: 18px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13px; }
.ovnav-launcher .ovl-empty { padding: 18px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13px; }
@media (max-width: 480px) { .ovnav-launcher { left: 8px; } .ovnav-launcher .ovl-fams { grid-template-columns: 1fr; } }
@media (max-width: 480px) { .ovnav-launcher { left: 8px; } .ovnav-launcher .ovl-fams { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .ovnav-launcher { transition: none; } }
@media (prefers-reduced-motion: reduce) { .ovnav-launcher { transition: none; } }
@@ -643,7 +651,8 @@
@@ -643,7 +651,8 @@
if (panel && panel.classList.contains('open')) return close();
if (panel && panel.classList.contains('open')) return close();
if (!panel) {
if (!panel) {
panel = document.createElement('div'); panel.className = 'ovnav-launcher'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'OpenVibe sites and tools');
panel = document.createElement('div'); panel.className = 'ovnav-launcher'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'OpenVibe sites and tools');
- panel.innerHTML = '<input type="search" class="ovl-q" placeholder="Find a tool or site" aria-label="Find a tool or site"><div class="ovl-body"></div>';
+ panel.innerHTML = '<input type="search" class="ovl-q" placeholder="Find a tool or site" aria-label="Find a tool or site"><div class="ovl-body"></div><div class="ovl-display" hidden></div>';
+ bindDisplayControls(panel.querySelector('.ovl-display'));
nav.appendChild(panel);
nav.appendChild(panel);
if (!root.OpenVibeIcons && !document.getElementById('ov-icons-loader')) { const sc = document.createElement('script'); sc.id = 'ov-icons-loader'; sc.src = 'https://openvibe.network/shared/ov-icons.js'; sc.async = true; document.head.appendChild(sc); }
if (!root.OpenVibeIcons && !document.getElementById('ov-icons-loader')) { const sc = document.createElement('script'); sc.id = 'ov-icons-loader'; sc.src = 'https://openvibe.network/shared/ov-icons.js'; sc.async = true; document.head.appendChild(sc); }
let cat = null; paint(null, '');
let cat = null; paint(null, '');
@@ -691,6 +700,21 @@
@@ -691,6 +700,21 @@
return `<span class="ovnav-sep" aria-hidden="true"></span>` + pick.map(n => `<a class="ovnav-net" href="${escapeAttr(n.url)}" title="${escapeAttr(n.tagline)}">${escapeAttr(n.name)}</a>`).join('');
return `<span class="ovnav-sep" aria-hidden="true"></span>` + pick.map(n => `<a class="ovnav-net" href="${escapeAttr(n.url)}" title="${escapeAttr(n.tagline)}">${escapeAttr(n.name)}</a>`).join('');
}
}
+ /** Display settings in the launcher, for guests and members alike (theme-loader.js owns the state). */
+ function bindDisplayControls(box) {
+ const L = root.OpenVibeThemeLoader; if (!box || !L || !L.display) return;
+ const paint = () => {
+ const d = L.display.get();
+ const seg = (key, val, label, title) => `<button type="button" data-k="${key}" data-v="${val}" aria-pressed="${String(d[key]) === val}" title="${title}">${label}</button>`;
+ box.innerHTML = `<span class="ovl-dl">Display</span><span class="ovl-seg" role="group" aria-label="Text size">${seg('text', '100', 'A', 'Default text size')}${seg('text', '112', 'A+', 'Larger text')}${seg('text', '125', 'A++', 'Largest text')}</span>
+ <span class="ovl-seg" role="group" aria-label="Motion">${seg('motion', 'auto', 'Motion', 'Animations on')}${seg('motion', 'reduced', 'Calm', 'Reduce animations')}</span><a href="https://openvibe.network/themes">Themes</a>`;
+ box.hidden = false;
+ };
+ box.addEventListener('click', (e) => { const b = e.target.closest('button[data-k]'); if (!b) return; L.display.set({ [b.dataset.k]: b.dataset.v }); paint(); });
+ root.addEventListener('ov:display', paint);
+ paint();
+ }
+
// ── Notification bell (mounted by the navbar itself) ─────
// ── Notification bell (mounted by the navbar itself) ─────
function mountBell(nav, u) {
function mountBell(nav, u) {
if (_config.notifications === false || !u || u.is_anon || !_config.token) return;
if (_config.notifications === false || !u || u.is_anon || !_config.token) return;
@@ -241,11 +241,47 @@
@@ -241,11 +241,47 @@
}
}
// ── Auto-apply on load (synchronous, prevents FOUC) ─────
// ── Auto-apply on load (synchronous, prevents FOUC) ─────
+ // ── Display preferences: motion + text size, one setting for every OpenVibe site ─────────────
+ // OpenVibeThemeLoader.display.get() → { motion: 'auto'|'reduced', text: '100'|'112'|'125' }
+ // OpenVibeThemeLoader.display.set({ motion, text }) applies here, in other tabs, and saves to the account
+ // Applied as <html data-ov-motion> and --ov-text-scale before first paint; the shared stylesheet
+ // below makes every shared component honour them, and sites can key their own CSS off the same hooks.
+ const DISPLAY_KEY = 'ov_display', DISPLAY_OPTS = { motion: ['auto', 'reduced'], text: ['100', '112', '125'] };
+ function readDisplay() {
+ const out = { motion: 'auto', text: '100' };
+ try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null') || {}; for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(d[k])) >= 0) out[k] = String(d[k]); } catch (e) { /* */ }
+ return out;
+ }
+ function applyDisplay(d) {
+ if (typeof document === 'undefined') return;
+ const el = document.documentElement;
+ if (d.motion === 'reduced') el.setAttribute('data-ov-motion', 'reduced'); else el.removeAttribute('data-ov-motion');
+ if (d.text === '100') { el.style.removeProperty('--ov-text-scale'); el.removeAttribute('data-ov-text'); } else { el.style.setProperty('--ov-text-scale', String(parseInt(d.text, 10) / 100)); el.setAttribute('data-ov-text', d.text); }
+ if (!document.getElementById('ov-display-css') && document.head) {
+ const st = document.createElement('style'); st.id = 'ov-display-css';
+ st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}html[data-ov-motion=reduced] *,html[data-ov-motion=reduced] *::before,html[data-ov-motion=reduced] *::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}html[data-ov-motion=reduced] .ov-mark .o,html[data-ov-motion=reduced] .ov-icon .ovi-comet{display:none}';
+ document.head.appendChild(st);
+ }
+ try { window.dispatchEvent(new CustomEvent('ov:display', { detail: d })); } catch (e) { /* */ }
+ }
+ function setDisplay(patch, opts) {
+ const d = readDisplay();
+ for (const k in DISPLAY_OPTS) if (patch && DISPLAY_OPTS[k].indexOf(String(patch[k])) >= 0) d[k] = String(patch[k]);
+ try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(d)); } catch (e) { /* */ }
+ applyDisplay(d);
+ if (!(opts && opts.local) && typeof fetch !== 'undefined') {
+ const token = authToken(); const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = 'Bearer ' + token;
+ if (token || /(?:^|; )ov_sso_hint=account/.test(document.cookie)) fetch(_config.apiBase + '/api/themes/me/display', { method: 'PUT', headers: headers, credentials: 'include', body: JSON.stringify(d) }).catch(function () {});
+ }
+ return d;
+ }
+ applyDisplay(readDisplay());
+
const activeId = resolveAndApply();
const activeId = resolveAndApply();
// The icon link may be parsed after this script: repaint once the head is complete.
// The icon link may be parsed after this script: repaint once the head is complete.
if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _chromeKey = ''; syncChrome(); });
if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _chromeKey = ''; syncChrome(); });
// A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.
// A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.
- if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { resolveAndApply(); } catch (err) { /* */ } } });
+ if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) applyDisplay(readDisplay()); });
// Look up a theme we don't ship in the built-in map (community/custom themes) from the
// Look up a theme we don't ship in the built-in map (community/custom themes) from the
// central catalog. Without this, any non-built-in id fell through to the default 'vibe'
// central catalog. Without this, any non-built-in id fell through to the default 'vibe'
@@ -271,6 +307,7 @@
@@ -271,6 +307,7 @@
fetch(_config.apiBase + '/api/themes/me/active', { headers: headers, credentials: 'include' })
fetch(_config.apiBase + '/api/themes/me/active', { headers: headers, credentials: 'include' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
.then(function (data) {
+ if (data && data.display) { const cur = readDisplay(); if (JSON.stringify({ motion: data.display.motion || cur.motion, text: data.display.text || cur.text }) !== JSON.stringify(cur)) setDisplay(data.display, { local: true }); }
if (!data || !data.theme_id) return;
if (!data || !data.theme_id) return;
const serverId = data.theme_id;
const serverId = data.theme_id;
const custom = (data.custom_variables && typeof data.custom_variables === 'object'
const custom = (data.custom_variables && typeof data.custom_variables === 'object'
@@ -292,6 +329,7 @@
@@ -292,6 +329,7 @@
// ── Public API ───────────────────────────────────────────
// ── Public API ───────────────────────────────────────────
const OpenVibeThemeLoader = {
const OpenVibeThemeLoader = {
THEMES: THEMES,
THEMES: THEMES,
+ display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS },
apply: applyById,
apply: applyById,
applyVars: applyVars,
applyVars: applyVars,
save: save,
save: save,