openvibers/openvibe.tools Commits
Shared navbar options and shared footer across the gateway and satellite apps
Changes
@@@ -328,23 +328,11 @@</div><!-- Footer -->-- <footer class="site-footer">-- <div class="footer-inner">-- <span><i class="fa-solid fa-circle-nodes"></i> Audio.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>-- <span class="footer-links">-- <a href="https://openvibe.tools">OpenVibe.Tools</a>-- <a href="https://openvibe.live">OpenVibe.Live</a>-- <a href="https://openvibe.games">Scraplandia</a>-- <a href="https://img.openvibe.tools">Img.OpenVibe</a>-- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>-- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>-- <a href="https://food.openvibe.tools">Food.OpenVibe</a>-- </span>-- </div>-- </footer>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/theme-loader.js"></script><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/notification-ui.js"></script><script src="https://openvibe.network/shared/account-switcher.js"></script><script src="/js/app.js"></script>@@@ -107,16 +107,42 @@} catch { /* invalid token */ }}++ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").if (typeof OpenVibeNavbar !== 'undefined') {OpenVibeNavbar.init({service: 'audio',-- brandName: ctx?.brandName || undefined,-- brandIcon: ctx?.faIcon || undefined,token,user: ctx?.user || user,apiBase: 'https://openvibe.network',++ history: { type: 'tool', title: ctx?.brandName || 'Audio.OpenVibe' },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',});}++ initFooter();++ }++++ /* ---------- Footer (shared OpenVibeFooter) ---------- */++ // Full footer on the hub host, the compact strip on the single-tool hosts.++ function initFooter() {++ if (typeof OpenVibeFooter === 'undefined') return;++ try {++ OpenVibeFooter.init({++ service: 'audio',++ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',++ mount: '#ov-footer',++ apiBase: 'https://openvibe.network',++ links: [{ heading: 'Audio tools', items: [++ { name: 'Convert', url: 'https://convert.audio.openvibe.tools' },++ { name: 'Trim', url: 'https://trim.openvibe.tools' },++ { name: 'Merge', url: 'https://merge.openvibe.tools' },++ { name: 'Pitch', url: 'https://pitch.openvibe.tools' },++ { name: 'Speed', url: 'https://speed.openvibe.tools' },++ { name: 'Normalize', url: 'https://normalize.openvibe.tools' },++ { name: 'MP3', url: 'https://mp3.openvibe.tools' },++ { name: 'WAV', url: 'https://wav.openvibe.tools' },++ ] }],++ });++ } catch { /* optional */ }}/* ---------- Notifications (OpenVibe bell + toasts) ---------- */@@@ -213,22 +213,11 @@</div><!-- Footer -->-- <footer class="site-footer">-- <div class="footer-inner">-- <span><i class="fa-solid fa-file-pdf"></i> Docs.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>-- <span class="footer-links">-- <a href="https://openvibe.tools">OpenVibe.Tools</a>-- <a href="https://openvibe.live">OpenVibe.Live</a>-- <a href="https://openvibe.games">Scraplandia</a>-- <a href="https://img.openvibe.tools">Img.OpenVibe</a>-- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>-- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>-- </span>-- </div>-- </footer>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/theme-loader.js"></script><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/notification-ui.js"></script><script src="https://openvibe.network/shared/account-switcher.js"></script><script src="/js/app.js"></script>@@@ -140,16 +140,42 @@} catch { /* invalid token */ }}++ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").if (typeof OpenVibeNavbar !== 'undefined') {OpenVibeNavbar.init({service: 'docs',-- brandName: ctx?.brandName || undefined,-- brandIcon: ctx?.faIcon || undefined,token,user: ctx?.user || user,apiBase: 'https://openvibe.network',++ history: { type: 'tool', title: ctx?.brandName || 'Docs.OpenVibe' },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',});}++ initFooter();++ }++++ /* ---------- Footer (shared OpenVibeFooter) ---------- */++ // Full footer on the hub host, the compact strip on the single-tool hosts.++ function initFooter() {++ if (typeof OpenVibeFooter === 'undefined') return;++ try {++ OpenVibeFooter.init({++ service: 'docs',++ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',++ mount: '#ov-footer',++ apiBase: 'https://openvibe.network',++ links: [{ heading: 'Docs tools', items: [++ { name: 'Merge PDF', url: 'https://mergepdf.openvibe.tools' },++ { name: 'Split PDF', url: 'https://splitpdf.openvibe.tools' },++ { name: 'Compress PDF', url: 'https://compresspdf.openvibe.tools' },++ { name: 'Rotate PDF', url: 'https://rotatepdf.openvibe.tools' },++ { name: 'Watermark PDF', url: 'https://watermarkpdf.openvibe.tools' },++ { name: 'Protect PDF', url: 'https://protectpdf.openvibe.tools' },++ { name: 'Images to PDF', url: 'https://image2pdf.openvibe.tools' },++ { name: 'PDF to images', url: 'https://pdf2jpg.openvibe.tools' },++ ] }],++ });++ } catch { /* optional */ }}function initNotifications() {@@@ -215,10 +215,10 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var@keyframes spin { to { transform:rotate(360deg); } }/* Footer */--#footer {-- display:flex; align-items:center; justify-content:space-between;-- padding:0 20px; height:32px; background:var(--bg-card);-- border-top:1px solid var(--border); font-size:11px; color:var(--text-muted); flex-shrink:0;++/* Data attribution line above the shared footer (OpenVibeFooter mounts on #ov-footer) */++.data-credit {++ padding:10px 20px 0; font-size:11px; color:var(--text-muted); flex-shrink:0;++ border-top:1px solid var(--border);}/* Mobile */@@@ -37,22 +37,6 @@<link rel="stylesheet" href="/css/style.css"></head><body>-- <!-- Header -->-- <header id="header">-- <div class="header-left">-- <div class="brand">-- <i class="fa-solid fa-utensils brand-icon"></i>-- <span class="brand-text">OPENVIBE<span class="green">FOOD</span></span>-- </div>-- <div class="header-divider"></div>-- <span class="header-tagline"><i class="fa-solid fa-apple-whole"></i> Budget Grocery & Meal Planning</span>-- </div>-- <div class="header-right">-- <a href="https://maps.openvibe.tools" class="header-btn" title="Maps.OpenVibe"><i class="fa-solid fa-map-location-dot"></i></a>-- <button id="btn-theme" class="header-btn" title="Toggle theme"><i class="fa-solid fa-moon"></i></button>-- </div>-- </header>--<!-- Location Bar --><div id="loc-bar"><div class="loc-search">@@@ -176,15 +160,12 @@</div></main>-- <!-- Footer -->-- <footer id="footer">-- <span><i class="fa-solid fa-utensils"></i> food.openvibe.tools</span>-- <span>Data from USDA, OSM, store pricing research</span>-- <a href="https://maps.openvibe.tools" style="color:var(--green);text-decoration:none"><i class="fa-solid fa-map"></i> maps.openvibe.tools</a>-- </footer>++ <p class="data-credit"><i class="fa-solid fa-database"></i> Data from USDA, OSM and store pricing research</p>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/theme-loader.js"></script><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/notification-ui.js"></script><script src="https://openvibe.network/shared/account-switcher.js"></script><script src="/js/app.js"></script>@@@ -198,7 +179,19 @@let user = null;if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }if (typeof OpenVibeNavbar !== 'undefined') {-- OpenVibeNavbar.init({ service: 'food', brandName: 'Food.OpenVibe', brandIcon: 'fa-utensils', token, user, apiBase: 'https://openvibe.network' });++ OpenVibeNavbar.init({ service: 'food', token, user, apiBase: 'https://openvibe.network', history: { type: 'tool', title: 'Food.OpenVibe' }, silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}' });++ }++ if (typeof OpenVibeFooter !== 'undefined') {++ try {++ OpenVibeFooter.init({++ service: 'food', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [{ heading: 'Food.OpenVibe', items: [++ { name: 'Find food', url: 'https://food.openvibe.tools' },++ { name: 'Maps.OpenVibe', url: 'https://maps.openvibe.tools' },++ { name: 'All tools', url: 'https://openvibe.tools' },++ ] }],++ });++ } catch (e) { /* optional */ }}if (typeof OpenVibeAccountSwitcher !== 'undefined') {OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });@@@ -280,7 +280,7 @@$('#db-shelf')?.addEventListener('change', filterFoods);// Theme-- $('#btn-theme').addEventListener('click', toggleTheme);++ $('#btn-theme')?.addEventListener('click', toggleTheme); // theme switching lives in the shared navbar now}// ═══ Init ═══@@@ -21,6 +21,7 @@</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">++ <script src="https://openvibe.network/shared/theme-loader.js"></script><style>:root {--bg-primary: #0b0c12;@@@ -93,7 +94,7 @@.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }-- footer { text-align: center; padding: 40px 20px; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }++ .note { text-align: center; padding: 40px 20px 0; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }footer a { color: var(--accent-light); text-decoration: none; }footer a:hover { text-decoration: underline; }@@@ -125,15 +126,15 @@<div class="stats-section" id="stats"></div></div>-- <footer>-- <p>All audio processing is performed locally in your browser. No files are uploaded to our servers. <a href="https://github.com/OpenVibe.Live">Learn more →</a></p>-- </footer>++ <p class="note">All audio processing is performed locally in your browser. No files are uploaded to our servers.</p></div>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script>const AUDIO_TOOLS = {-- 'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },++ 'convert': { name: 'Converter', domain: 'convert.audio.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge', category: 'Mix' },'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },@@@ -191,7 +192,37 @@}// Initialize++ function getCookie(n) {++ const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));++ return m ? decodeURIComponent(m[1]) : null;++ }++ function initChrome() {++ const token = getCookie('ov_token') || localStorage.getItem('ov_token');++ if (window.OpenVibeNavbar) {++ try {++ OpenVibeNavbar.init({++ service: 'audio', apiBase: 'https://openvibe.network', token,++ history: { type: 'tool', title: document.title.split(' — ')[0] },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',++ });++ } catch { /* non-critical */ }++ }++ if (window.OpenVibeFooter) {++ try {++ const items = Object.values(AUDIO_TOOLS).map(t => ({ name: t.name, url: 'https://' + t.domain }));++ OpenVibeFooter.init({++ service: 'audio', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [++ { heading: 'Tools', items: items.slice(0, Math.ceil(items.length / 2)) },++ { heading: 'Formats', items: items.slice(Math.ceil(items.length / 2)) },++ ],++ });++ } catch { /* non-critical */ }++ }++ }++window.addEventListener('DOMContentLoaded', () => {++ initChrome();const showcase = document.getElementById('showcase-section');showcase.style.display = 'block';buildShowcaseGrid();@@@ -137,8 +137,11 @@ a{color:inherit;text-decoration:none}<div id="hub"></div><div class="rel" id="rel"></div></div></div>++<div id="ov-footer"></div><div class="toast" id="toast"></div><script src="https://openvibe.network/shared/navbar.js"></script>++<script src="https://openvibe.network/shared/footer.js"></script>++<script src="https://openvibe.network/shared/theme-loader.js"></script><script>'use strict';const API='https://openvibe.tools/api/dev',$=s=>document.getElementById(s),@@@ -180,7 +183,13 @@ let ACT=T[sub]?sub:'dev',MODE='';document.addEventListener('DOMContentLoaded',()=>{const tok=localStorage.getItem('ov_token');-- if(window.OpenVibeNavbar)try{OpenVibeNavbar.init({service:'dev',apiBase:'https://openvibe.network',token:tok})}catch(e){}++ const cur=T[ACT]||T.dev;++ if(window.OpenVibeNavbar)try{OpenVibeNavbar.init({service:'dev',apiBase:'https://openvibe.network',token:tok,history:{type:'tool',title:ACT==='dev'?'Dev.OpenVibe':cur.n},silentLogin:'https://openvibe.tools/auth/login?silent=1&next={url}'})}catch(e){}++ if(window.OpenVibeFooter)try{++ // Footer columns come from the same catalog as the hub, one column per category.++ const links=Object.entries(CATS).map(([cid,cat])=>({heading:cat.l,items:Object.entries(T).filter(([,t])=>t.c===cid).map(([id,t])=>({name:t.n,url:'https://'+id+'.openvibe.tools'}))})).filter(g=>g.items.length);++ OpenVibeFooter.init({service:'dev',variant:ACT==='dev'?'full':'compact',mount:'#ov-footer',apiBase:'https://openvibe.network',links});++ }catch(e){}ACT==='dev'?showHub():showTool();});@@@ -21,6 +21,7 @@</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">++ <script src="https://openvibe.network/shared/theme-loader.js"></script><style>:root {--bg-primary: #0b0c12;@@@ -93,7 +94,7 @@.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }-- footer { text-align: center; padding: 40px 20px; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }++ .note { text-align: center; padding: 40px 20px 0; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }footer a { color: var(--accent-light); text-decoration: none; }footer a:hover { text-decoration: underline; }@@@ -125,12 +126,12 @@<div class="stats-section" id="stats"></div></div>-- <footer>-- <p>All image processing is performed locally in your browser. No files are uploaded to our servers. <a href="https://github.com/OpenVibe.Live">Learn more →</a></p>-- </footer>++ <p class="note">All image processing is performed locally in your browser. No files are uploaded to our servers.</p></div>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script>const IMAGE_TOOLS = {'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },@@@ -191,7 +192,37 @@}// Initialize++ function getCookie(n) {++ const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));++ return m ? decodeURIComponent(m[1]) : null;++ }++ function initChrome() {++ const token = getCookie('ov_token') || localStorage.getItem('ov_token');++ if (window.OpenVibeNavbar) {++ try {++ OpenVibeNavbar.init({++ service: 'img', apiBase: 'https://openvibe.network', token,++ history: { type: 'tool', title: document.title.split(' — ')[0] },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',++ });++ } catch { /* non-critical */ }++ }++ if (window.OpenVibeFooter) {++ try {++ const items = Object.values(IMAGE_TOOLS).map(t => ({ name: t.name, url: 'https://' + t.domain }));++ OpenVibeFooter.init({++ service: 'img', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [++ { heading: 'Tools', items: items.slice(0, Math.ceil(items.length / 2)) },++ { heading: 'Formats', items: items.slice(Math.ceil(items.length / 2)) },++ ],++ });++ } catch { /* non-critical */ }++ }++ }++window.addEventListener('DOMContentLoaded', () => {++ initChrome();const showcase = document.getElementById('showcase-section');showcase.style.display = 'block';buildShowcaseGrid();@@@ -88,19 +88,6 @@a { color: inherit; text-decoration: none; }.shell { max-width: 1180px; margin: 0 auto; padding: 0 24px; }-- /* ─── Top bar ─── */-- .topbar {-- display: flex; align-items: center; justify-content: space-between;-- padding: 18px 0; gap: 16px;-- }-- .brand { display: flex; align-items: center; gap: 11px; font-weight: 900; font-size: 1.12rem; letter-spacing: -0.01em; }-- .brand-badge {-- width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center;-- background: linear-gradient(135deg, var(--accent), var(--accent-dark));-- color: #fff; font-size: 1rem; box-shadow: 0 6px 18px rgba(139,92,246,0.35);-- }-- .brand small { display: block; font-size: 0.68rem; font-weight: 600; color: var(--text-muted); letter-spacing: 0.06em; text-transform: uppercase; }-- .topbar-actions { display: flex; align-items: center; gap: 10px; }.btn {display: inline-flex; align-items: center; gap: 8px; cursor: pointer;padding: 10px 18px; border-radius: 12px; font-weight: 700; font-size: 0.9rem;@@@ -113,18 +100,6 @@border-color: transparent; color: #fff; box-shadow: 0 8px 22px rgba(139,92,246,0.35);}.btn-primary:hover { filter: brightness(1.1); background: linear-gradient(135deg, var(--accent), var(--accent-dark)); }-- .user-chip {-- display: none; align-items: center; gap: 10px; padding: 7px 12px;-- border: 1px solid var(--border-light); border-radius: 999px; font-size: 0.88rem; font-weight: 600;-- }-- .user-chip.visible { display: inline-flex; }-- .user-chip .avatar {-- width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;-- background: var(--accent); color: #fff; font-size: 0.72rem; font-weight: 800; overflow: hidden;-- }-- .user-chip .avatar img { width: 100%; height: 100%; object-fit: cover; }-- .user-chip a { color: var(--text-muted); font-size: 0.8rem; }-- .user-chip a:hover { color: var(--text-primary); }/* ─── Hero ─── */.hero { padding: 64px 0 40px; text-align: center; }@@@ -259,37 +234,15 @@.dir-empty i { display: block; font-size: 1.6rem; margin-bottom: 10px; }/* ─── Footer ─── */-- footer { border-top: 1px solid var(--border); margin-top: 40px; padding: 34px 0 44px; }-- .foot { display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; color: var(--text-muted); font-size: 0.84rem; }-- .foot a { color: var(--text-secondary); }-- .foot a:hover { color: var(--text-primary); }-- .foot-links { display: flex; gap: 18px; flex-wrap: wrap; }@media (max-width: 640px) {.hero { padding: 40px 0 28px; }.hero-stats { gap: 18px; }-- .topbar-actions .btn span { display: none; }}</style></head><body><div class="shell">-- <!-- ─── Top bar ─── -->-- <header class="topbar">-- <a class="brand" href="/">-- <span class="brand-badge"><i class="fa-solid fa-circle-nodes"></i></span>-- <span>OpenVibe.Tools<small>Part of the OpenVibe network</small></span>-- </a>-- <div class="topbar-actions">-- <span class="user-chip" id="user-chip">-- <span class="avatar" id="user-avatar">?</span>-- <span id="user-name"></span>-- <a href="/auth/logout" title="Sign out"><i class="fa-solid fa-right-from-bracket"></i></a>-- </span>-- <a class="btn btn-primary" id="signin-btn" href="/auth/login"><i class="fa-solid fa-right-to-bracket"></i> <span>Sign in</span></a>-- </div>-- </header>--<!-- ─── Hero ─── --><section class="hero"><h1>Every tool you need,<br><span class="grad">one vibe apart.</span></h1>@@@ -426,20 +379,10 @@</div></div>-- <footer>-- <div class="shell foot">-- <div>© OpenVibe — open tools, community run. <a href="https://openvibe.network">One Account. All of OpenVibe.</a></div>-- <div class="foot-links">-- <a href="https://openvibe.live">Live</a>-- <a href="https://openvibe.games">Games</a>-- <a href="https://openvibe.network">Network</a>-- <a href="https://openvibe.live/tos" rel="noopener">Terms</a>-- <a href="https://openvibe.live/dmca" rel="noopener">DMCA</a>-- </div>-- </div>-- </footer>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/navbar.js" defer></script>++ <script src="https://openvibe.network/shared/footer.js" defer></script><script>'use strict';@@@ -749,41 +692,53 @@document.getElementById('stat-tools').textContent =DIR_DATA.reduce((sum, c) => sum + c.tools.length, 0) + '+';-- /* ─── Session (OAuth client of OpenVibe.Network) ─── */++ /* ─── Shared chrome (navbar + footer from openvibe.network) ─── */function getCookie(n) {const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));return m ? decodeURIComponent(m[1]) : null;}-- async function initSession() {-- let user = null;-- try {-- const res = await fetch('/auth/me', { credentials: 'include' });-- if (res.ok) user = (await res.json()).user;-- } catch { /* signed out */ }---- const chip = document.getElementById('user-chip');-- const signin = document.getElementById('signin-btn');-- if (user) {-- document.getElementById('user-name').textContent = user.display_name || user.username || 'Account';-- const avatar = document.getElementById('user-avatar');-- if (user.avatar_url) avatar.innerHTML = `<img src="${escHtml(user.avatar_url)}" alt="">`;-- else avatar.textContent = (user.display_name || user.username || '?').slice(0, 1).toUpperCase();-- chip.classList.add('visible');-- signin.style.display = 'none';-- } else {-- chip.classList.remove('visible');-- signin.style.display = '';-- }---- // Shared navbar (loaded from openvibe.network) — best effort++ // The navbar owns the account UI (sign-in, avatar, menu) and boots from the shared++ // ov_token cookie straight away — no /auth/me round trip before it can render.++ function initChrome() {if (window.OpenVibeNavbar) {try {-- OpenVibeNavbar.init({ service: 'tools', apiBase: 'https://openvibe.network', token: getCookie('ov_token'), user });++ OpenVibeNavbar.init({++ service: 'tools',++ apiBase: 'https://openvibe.network',++ token: getCookie('ov_token') || localStorage.getItem('ov_token'),++ history: { type: 'tool', title: 'OpenVibe.Tools' },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',++ });++ } catch { /* non-critical */ }++ }++ if (window.OpenVibeFooter) {++ try {++ OpenVibeFooter.init({++ service: 'tools', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [++ { heading: 'Tool suites', items: [++ { name: 'Net.OpenVibe', url: 'https://net.openvibe.tools' },++ { name: 'Dev.OpenVibe', url: 'https://dev.openvibe.tools' },++ { name: 'Paste.OpenVibe', url: 'https://pastes.openvibe.tools' },++ { name: 'Img.OpenVibe', url: 'https://img.openvibe.tools' },++ { name: 'Audio.OpenVibe', url: 'https://audio.openvibe.tools' },++ { name: 'Text.OpenVibe', url: 'https://text.openvibe.tools' },++ ] },++ { heading: 'More suites', items: [++ { name: 'Logo.OpenVibe', url: 'https://logo.openvibe.tools' },++ { name: 'Docs.OpenVibe', url: 'https://docs.openvibe.tools' },++ { name: 'YT.OpenVibe', url: 'https://yt.openvibe.tools' },++ { name: 'Maps.OpenVibe', url: 'https://maps.openvibe.tools' },++ { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },++ { name: 'All tools', url: '#', onclick: 'openDirectory(); return false;' },++ ] },++ ],++ });} catch { /* non-critical */ }}}-- window.addEventListener('DOMContentLoaded', initSession);++ window.addEventListener('DOMContentLoaded', initChrome);</script></body></html>@@@ -280,12 +280,6 @@.tab-content.active { display: block; }/* ─── Footer ─── */-- .footer {-- text-align: center; padding: 32px 0; margin-top: 40px;-- border-top: 1px solid var(--border); color: var(--text-muted); font-size: 12px;-- }-- .footer a { color: var(--text-secondary); }-- .footer .footer-links { display: flex; gap: 16px; justify-content: center; margin-bottom: 8px; flex-wrap: wrap; }/* ─── Responsive ─── */@media (max-width: 640px) {@@@ -587,22 +581,14 @@<!-- Hub: tool directory (shown on net.openvibe.tools) --><div class="shell" id="hub-directory" style="display:none"></div>-- <!-- Footer -->-- <div class="footer">-- <div class="footer-links">-- <a href="https://openvibe.network">OpenVibe</a>-- <a href="https://net.openvibe.tools">Net.OpenVibe</a>-- <a href="https://openvibe.live">OpenVibe.Live</a>-- <a href="https://openvibe.games">Scraplandia</a>-- </div>-- <p>Part of the <a href="https://openvibe.network">OpenVibe</a>. Open source. Built in the open.</p>-- </div>++ <div id="ov-footer"></div></div><div class="toast-container" id="toasts"></div><!-- Shared OpenVibe Navbar --><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/theme-loader.js"></script><script>'use strict';@@@ -709,10 +695,36 @@// ── Init ─────────────────────────────────────────────────document.addEventListener('DOMContentLoaded', () => {-- // Init navbar++ // Shared navbar + footer (openvibe.network)try {const token = localStorage.getItem('ov_token') || getCookie('ov_token');-- OpenVibeNavbar.init({ service: 'net', apiBase: 'https://openvibe.network', token });++ OpenVibeNavbar.init({++ service: 'net', apiBase: 'https://openvibe.network', token,++ history: { type: 'tool', title: isHub ? 'Net.OpenVibe' : tool.name },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',++ });++ } catch {}++ try {++ OpenVibeFooter.init({++ service: 'net', variant: isHub ? 'full' : 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [++ { heading: 'Network tools', items: [++ { name: 'Domain & IP lookup', url: 'https://lookup.openvibe.tools' },++ { name: 'DNS lookup', url: 'https://dns.openvibe.tools' },++ { name: 'Ping', url: 'https://ping.openvibe.tools' },++ { name: 'SSL checker', url: 'https://ssl.openvibe.tools' },++ { name: 'Whois', url: 'https://whois.openvibe.tools' },++ { name: 'HTTP headers', url: 'https://headers.openvibe.tools' },++ ] },++ { heading: 'Email & uptime', items: [++ { name: 'SPF checker', url: 'https://spf.openvibe.tools' },++ { name: 'DMARC checker', url: 'https://dmarc.openvibe.tools' },++ { name: 'Blacklist check', url: 'https://blacklist.openvibe.tools' },++ { name: 'Uptime check', url: 'https://uptime.openvibe.tools' },++ { name: 'All network tools', url: 'https://net.openvibe.tools' },++ ] },++ ],++ });} catch {}setupUI();@@@ -299,9 +299,11 @@</div></div>++ <div id="ov-footer"></div><div class="toast-container" id="toasts"></div><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/theme-loader.js"></script><script>'use strict';@@@ -425,8 +427,10 @@const data = await api(`/${slug}`);const p = data.paste;renderPasteView(p);++ initChrome({ type: 'paste', title: p.title || slug });loadComments(slug);} catch (err) {++ initChrome();detail.innerHTML = `<div class="empty">${esc(err.message)}</div>`;}}@@@ -568,12 +572,40 @@document.getElementById('view-paste').classList.toggle('hidden', view !== 'paste');}++ // ── Shared chrome ──────────────────────────────────────++ // The navbar records this visit in the account's cross-network history, so on a paste++ // page it waits for the paste's title (viewPaste calls it); everywhere else it boots at once.++ let chromeReady = false;++ function initChrome(history) {++ if (chromeReady) return;++ chromeReady = true;++ try {++ OpenVibeNavbar.init({++ service: 'paste', apiBase: 'https://openvibe.network', token,++ history: history || { type: 'tool', title: 'Paste.OpenVibe' },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',++ menu: { after: [{ id: 'my-pastes', label: 'My pastes', href: 'https://openvibe.community/my', icon: 'fa-clipboard-list' }] },++ });++ } catch {}++ try {++ OpenVibeFooter.init({++ service: 'paste', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [{ heading: 'Pastes', items: [++ { name: 'New paste', url: '/new' },++ { name: 'Browse pastes', url: '/' },++ { name: 'My pastes', url: 'https://openvibe.community/my' },++ ] }],++ });++ } catch {}++ }++function route() {const path = location.pathname;if (path.startsWith('/p/')) {const slug = path.split('/')[2];if (slug) { viewPaste(slug); return; }}++ initChrome();if (path === '/new') { showView('new'); return; }showView('browse');}@@@ -593,11 +625,6 @@// ── Init ───────────────────────────────────────────────document.addEventListener('DOMContentLoaded', () => {token = localStorage.getItem('ov_token') || getCookie('ov_token');---- try {-- OpenVibeNavbar.init({ service: 'paste', apiBase: 'https://openvibe.network', token });-- } catch {}--route();});</script>@@@ -162,22 +162,11 @@</div><!-- Footer -->-- <footer class="site-footer">-- <div class="footer-inner">-- <span><i class="fa-solid fa-circle-nodes"></i> Img.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>-- <span class="footer-links">-- <a href="https://openvibe.tools">OpenVibe.Tools</a>-- <a href="https://openvibe.live">OpenVibe.Live</a>-- <a href="https://openvibe.games">Scraplandia</a>-- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>-- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>-- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>-- </span>-- </div>-- </footer>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/theme-loader.js"></script><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/notification-ui.js"></script><script src="https://openvibe.network/shared/account-switcher.js"></script><script src="/js/app.js"></script>@@@ -119,16 +119,42 @@} catch { /* invalid token */ }}++ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").if (typeof OpenVibeNavbar !== 'undefined') {OpenVibeNavbar.init({service: 'img',-- brandName: ctx?.brandName || undefined,-- brandIcon: ctx?.faIcon || undefined,token,user: ctx?.user || user,apiBase: 'https://openvibe.network',++ history: { type: 'tool', title: ctx?.brandName || 'Img.OpenVibe' },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',});}++ initFooter();++ }++++ /* ---------- Footer (shared OpenVibeFooter) ---------- */++ // Full footer on the hub host, the compact strip on the single-tool hosts.++ function initFooter() {++ if (typeof OpenVibeFooter === 'undefined') return;++ try {++ OpenVibeFooter.init({++ service: 'img',++ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',++ mount: '#ov-footer',++ apiBase: 'https://openvibe.network',++ links: [{ heading: 'Img tools', items: [++ { name: 'Convert', url: 'https://convert.openvibe.tools' },++ { name: 'Compress', url: 'https://compress.openvibe.tools' },++ { name: 'Resize', url: 'https://resize.openvibe.tools' },++ { name: 'Crop', url: 'https://crop.openvibe.tools' },++ { name: 'Favicon', url: 'https://favicon.openvibe.tools' },++ { name: 'PNG', url: 'https://png.openvibe.tools' },++ { name: 'JPG', url: 'https://jpg.openvibe.tools' },++ { name: 'WebP', url: 'https://webp.openvibe.tools' },++ ] }],++ });++ } catch { /* optional */ }}// ── Notifications (OpenVibe bell + toasts) ───────────@@@ -236,6 +236,7 @@<span id="footer-coords"></span><span id="footer-weather"></span></footer>++ <div id="ov-footer"></div></div><!-- ═══ Detail Panel ═══ -->@@@ -390,6 +391,7 @@<script src="https://openvibe.network/shared/theme-loader.js"></script><script src="https://openvibe.network/shared/navbar.js"></script>++<script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/notification-ui.js"></script><script src="https://openvibe.network/shared/account-switcher.js"></script><script src="/js/app.js"></script>@@@ -403,7 +405,19 @@let user = null;if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }if (typeof OpenVibeNavbar !== 'undefined') {-- OpenVibeNavbar.init({ service: 'maps', brandName: 'Maps.OpenVibe', brandIcon: 'fa-person-shelter', token, user, apiBase: 'https://openvibe.network' });++ OpenVibeNavbar.init({ service: 'maps', token, user, apiBase: 'https://openvibe.network', history: { type: 'tool', title: 'Maps.OpenVibe' }, silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}' });++ }++ if (typeof OpenVibeFooter !== 'undefined') {++ try {++ OpenVibeFooter.init({++ service: 'maps', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [{ heading: 'Maps.OpenVibe', items: [++ { name: 'Survival map', url: 'https://maps.openvibe.tools' },++ { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },++ { name: 'All tools', url: 'https://openvibe.tools' },++ ] }],++ });++ } catch (e) { /* optional */ }}if (typeof OpenVibeAccountSwitcher !== 'undefined') {OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });@@@ -116,23 +116,11 @@</div><!-- Footer -->-- <footer class="site-footer">-- <div class="footer-inner">-- <span><i class="fa-solid fa-circle-nodes"></i> YT.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>-- <span class="footer-links">-- <a href="https://openvibe.tools">OpenVibe.Tools</a>-- <a href="https://openvibe.live">OpenVibe.Live</a>-- <a href="https://openvibe.games">Scraplandia</a>-- <a href="https://img.openvibe.tools">Img.OpenVibe</a>-- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>-- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>-- <a href="https://food.openvibe.tools">Food.OpenVibe</a>-- </span>-- </div>-- </footer>++ <div id="ov-footer"></div><script src="https://openvibe.network/shared/theme-loader.js"></script><script src="https://openvibe.network/shared/navbar.js"></script>++ <script src="https://openvibe.network/shared/footer.js"></script><script src="https://openvibe.network/shared/notification-ui.js"></script><script src="https://openvibe.network/shared/account-switcher.js"></script><script src="/js/app.js"></script>@@@ -369,12 +369,24 @@if (typeof OpenVibeNavbar !== 'undefined') {OpenVibeNavbar.init({service: 'yt',-- brandName: 'YT.OpenVibe',-- brandIcon: 'fa-circle-play',token, user,apiBase: 'https://openvibe.network',++ history: { type: 'tool', title: 'YT.OpenVibe' },++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',});}++ if (typeof OpenVibeFooter !== 'undefined') {++ try {++ OpenVibeFooter.init({++ service: 'yt', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',++ links: [{ heading: 'YT.OpenVibe', items: [++ { name: 'Video downloader', url: 'https://yt.openvibe.tools' },++ { name: 'Audio converter', url: 'https://audio.openvibe.tools' },++ { name: 'All tools', url: 'https://openvibe.tools' },++ ] }],++ });++ } catch { /* optional */ }++ }if (typeof OpenVibeAccountSwitcher !== 'undefined') {OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });}
@@ -328,23 +328,11 @@
@@ -328,23 +328,11 @@
</div>
</div>
<!-- Footer -->
<!-- Footer -->
- <footer class="site-footer">
- <div class="footer-inner">
- <span><i class="fa-solid fa-circle-nodes"></i> Audio.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
- <span class="footer-links">
- <a href="https://openvibe.tools">OpenVibe.Tools</a>
- <a href="https://openvibe.live">OpenVibe.Live</a>
- <a href="https://openvibe.games">Scraplandia</a>
- <a href="https://img.openvibe.tools">Img.OpenVibe</a>
- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>
- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>
- <a href="https://food.openvibe.tools">Food.OpenVibe</a>
- </span>
- </div>
- </footer>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.js"></script>
@@ -107,16 +107,42 @@
@@ -107,16 +107,42 @@
} catch { /* invalid token */ }
} catch { /* invalid token */ }
}
}
+ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").
if (typeof OpenVibeNavbar !== 'undefined') {
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
OpenVibeNavbar.init({
service: 'audio',
service: 'audio',
- brandName: ctx?.brandName || undefined,
- brandIcon: ctx?.faIcon || undefined,
token,
token,
user: ctx?.user || user,
user: ctx?.user || user,
apiBase: 'https://openvibe.network',
apiBase: 'https://openvibe.network',
+ history: { type: 'tool', title: ctx?.brandName || 'Audio.OpenVibe' },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
});
}
}
+ initFooter();
+ }
+
+ /* ---------- Footer (shared OpenVibeFooter) ---------- */
+ // Full footer on the hub host, the compact strip on the single-tool hosts.
+ function initFooter() {
+ if (typeof OpenVibeFooter === 'undefined') return;
+ try {
+ OpenVibeFooter.init({
+ service: 'audio',
+ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',
+ mount: '#ov-footer',
+ apiBase: 'https://openvibe.network',
+ links: [{ heading: 'Audio tools', items: [
+ { name: 'Convert', url: 'https://convert.audio.openvibe.tools' },
+ { name: 'Trim', url: 'https://trim.openvibe.tools' },
+ { name: 'Merge', url: 'https://merge.openvibe.tools' },
+ { name: 'Pitch', url: 'https://pitch.openvibe.tools' },
+ { name: 'Speed', url: 'https://speed.openvibe.tools' },
+ { name: 'Normalize', url: 'https://normalize.openvibe.tools' },
+ { name: 'MP3', url: 'https://mp3.openvibe.tools' },
+ { name: 'WAV', url: 'https://wav.openvibe.tools' },
+ ] }],
+ });
+ } catch { /* optional */ }
}
}
/* ---------- Notifications (OpenVibe bell + toasts) ---------- */
/* ---------- Notifications (OpenVibe bell + toasts) ---------- */
@@ -213,22 +213,11 @@
@@ -213,22 +213,11 @@
</div>
</div>
<!-- Footer -->
<!-- Footer -->
- <footer class="site-footer">
- <div class="footer-inner">
- <span><i class="fa-solid fa-file-pdf"></i> Docs.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
- <span class="footer-links">
- <a href="https://openvibe.tools">OpenVibe.Tools</a>
- <a href="https://openvibe.live">OpenVibe.Live</a>
- <a href="https://openvibe.games">Scraplandia</a>
- <a href="https://img.openvibe.tools">Img.OpenVibe</a>
- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>
- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>
- </span>
- </div>
- </footer>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.js"></script>
@@ -140,16 +140,42 @@
@@ -140,16 +140,42 @@
} catch { /* invalid token */ }
} catch { /* invalid token */ }
}
}
+ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").
if (typeof OpenVibeNavbar !== 'undefined') {
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
OpenVibeNavbar.init({
service: 'docs',
service: 'docs',
- brandName: ctx?.brandName || undefined,
- brandIcon: ctx?.faIcon || undefined,
token,
token,
user: ctx?.user || user,
user: ctx?.user || user,
apiBase: 'https://openvibe.network',
apiBase: 'https://openvibe.network',
+ history: { type: 'tool', title: ctx?.brandName || 'Docs.OpenVibe' },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
});
}
}
+ initFooter();
+ }
+
+ /* ---------- Footer (shared OpenVibeFooter) ---------- */
+ // Full footer on the hub host, the compact strip on the single-tool hosts.
+ function initFooter() {
+ if (typeof OpenVibeFooter === 'undefined') return;
+ try {
+ OpenVibeFooter.init({
+ service: 'docs',
+ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',
+ mount: '#ov-footer',
+ apiBase: 'https://openvibe.network',
+ links: [{ heading: 'Docs tools', items: [
+ { name: 'Merge PDF', url: 'https://mergepdf.openvibe.tools' },
+ { name: 'Split PDF', url: 'https://splitpdf.openvibe.tools' },
+ { name: 'Compress PDF', url: 'https://compresspdf.openvibe.tools' },
+ { name: 'Rotate PDF', url: 'https://rotatepdf.openvibe.tools' },
+ { name: 'Watermark PDF', url: 'https://watermarkpdf.openvibe.tools' },
+ { name: 'Protect PDF', url: 'https://protectpdf.openvibe.tools' },
+ { name: 'Images to PDF', url: 'https://image2pdf.openvibe.tools' },
+ { name: 'PDF to images', url: 'https://pdf2jpg.openvibe.tools' },
+ ] }],
+ });
+ } catch { /* optional */ }
}
}
function initNotifications() {
function initNotifications() {
@@ -215,10 +215,10 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var
@@ -215,10 +215,10 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var
@keyframes spin { to { transform:rotate(360deg); } }
@keyframes spin { to { transform:rotate(360deg); } }
/* Footer */
/* Footer */
-#footer {
- display:flex; align-items:center; justify-content:space-between;
- padding:0 20px; height:32px; background:var(--bg-card);
- border-top:1px solid var(--border); font-size:11px; color:var(--text-muted); flex-shrink:0;
+/* Data attribution line above the shared footer (OpenVibeFooter mounts on #ov-footer) */
+.data-credit {
+ padding:10px 20px 0; font-size:11px; color:var(--text-muted); flex-shrink:0;
+ border-top:1px solid var(--border);
}
}
/* Mobile */
/* Mobile */
@@ -37,22 +37,6 @@
@@ -37,22 +37,6 @@
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/style.css">
</head>
</head>
<body>
<body>
- <!-- Header -->
- <header id="header">
- <div class="header-left">
- <div class="brand">
- <i class="fa-solid fa-utensils brand-icon"></i>
- <span class="brand-text">OPENVIBE<span class="green">FOOD</span></span>
- </div>
- <div class="header-divider"></div>
- <span class="header-tagline"><i class="fa-solid fa-apple-whole"></i> Budget Grocery & Meal Planning</span>
- </div>
- <div class="header-right">
- <a href="https://maps.openvibe.tools" class="header-btn" title="Maps.OpenVibe"><i class="fa-solid fa-map-location-dot"></i></a>
- <button id="btn-theme" class="header-btn" title="Toggle theme"><i class="fa-solid fa-moon"></i></button>
- </div>
- </header>
-
<!-- Location Bar -->
<!-- Location Bar -->
<div id="loc-bar">
<div id="loc-bar">
<div class="loc-search">
<div class="loc-search">
@@ -176,15 +160,12 @@
@@ -176,15 +160,12 @@
</div>
</div>
</main>
</main>
- <!-- Footer -->
- <footer id="footer">
- <span><i class="fa-solid fa-utensils"></i> food.openvibe.tools</span>
- <span>Data from USDA, OSM, store pricing research</span>
- <a href="https://maps.openvibe.tools" style="color:var(--green);text-decoration:none"><i class="fa-solid fa-map"></i> maps.openvibe.tools</a>
- </footer>
+ <p class="data-credit"><i class="fa-solid fa-database"></i> Data from USDA, OSM and store pricing research</p>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.js"></script>
@@ -198,7 +179,19 @@
@@ -198,7 +179,19 @@
let user = null;
let user = null;
if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }
if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }
if (typeof OpenVibeNavbar !== 'undefined') {
if (typeof OpenVibeNavbar !== 'undefined') {
- OpenVibeNavbar.init({ service: 'food', brandName: 'Food.OpenVibe', brandIcon: 'fa-utensils', token, user, apiBase: 'https://openvibe.network' });
+ OpenVibeNavbar.init({ service: 'food', token, user, apiBase: 'https://openvibe.network', history: { type: 'tool', title: 'Food.OpenVibe' }, silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}' });
+ }
+ if (typeof OpenVibeFooter !== 'undefined') {
+ try {
+ OpenVibeFooter.init({
+ service: 'food', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [{ heading: 'Food.OpenVibe', items: [
+ { name: 'Find food', url: 'https://food.openvibe.tools' },
+ { name: 'Maps.OpenVibe', url: 'https://maps.openvibe.tools' },
+ { name: 'All tools', url: 'https://openvibe.tools' },
+ ] }],
+ });
+ } catch (e) { /* optional */ }
}
}
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
@@ -280,7 +280,7 @@
@@ -280,7 +280,7 @@
$('#db-shelf')?.addEventListener('change', filterFoods);
$('#db-shelf')?.addEventListener('change', filterFoods);
// Theme
// Theme
- $('#btn-theme').addEventListener('click', toggleTheme);
+ $('#btn-theme')?.addEventListener('click', toggleTheme); // theme switching lives in the shared navbar now
}
}
// ═══ Init ═══
// ═══ Init ═══
@@ -21,6 +21,7 @@
@@ -21,6 +21,7 @@
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<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">
+ <script src="https://openvibe.network/shared/theme-loader.js"></script>
<style>
<style>
:root {
:root {
--bg-primary: #0b0c12;
--bg-primary: #0b0c12;
@@ -93,7 +94,7 @@
@@ -93,7 +94,7 @@
.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }
.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }
.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }
.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }
- footer { text-align: center; padding: 40px 20px; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
+ .note { text-align: center; padding: 40px 20px 0; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
footer a { color: var(--accent-light); text-decoration: none; }
footer a { color: var(--accent-light); text-decoration: none; }
footer a:hover { text-decoration: underline; }
footer a:hover { text-decoration: underline; }
@@ -125,15 +126,15 @@
@@ -125,15 +126,15 @@
<div class="stats-section" id="stats"></div>
<div class="stats-section" id="stats"></div>
</div>
</div>
- <footer>
- <p>All audio processing is performed locally in your browser. No files are uploaded to our servers. <a href="https://github.com/OpenVibe.Live">Learn more →</a></p>
- </footer>
+ <p class="note">All audio processing is performed locally in your browser. No files are uploaded to our servers.</p>
</div>
</div>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script>
<script>
const AUDIO_TOOLS = {
const AUDIO_TOOLS = {
- 'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
+ 'convert': { name: 'Converter', domain: 'convert.audio.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge', category: 'Mix' },
'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge', category: 'Mix' },
'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },
'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },
@@ -191,7 +192,37 @@
@@ -191,7 +192,37 @@
}
}
// Initialize
// Initialize
+ function getCookie(n) {
+ const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
+ return m ? decodeURIComponent(m[1]) : null;
+ }
+ function initChrome() {
+ const token = getCookie('ov_token') || localStorage.getItem('ov_token');
+ if (window.OpenVibeNavbar) {
+ try {
+ OpenVibeNavbar.init({
+ service: 'audio', apiBase: 'https://openvibe.network', token,
+ history: { type: 'tool', title: document.title.split(' — ')[0] },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
+ });
+ } catch { /* non-critical */ }
+ }
+ if (window.OpenVibeFooter) {
+ try {
+ const items = Object.values(AUDIO_TOOLS).map(t => ({ name: t.name, url: 'https://' + t.domain }));
+ OpenVibeFooter.init({
+ service: 'audio', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [
+ { heading: 'Tools', items: items.slice(0, Math.ceil(items.length / 2)) },
+ { heading: 'Formats', items: items.slice(Math.ceil(items.length / 2)) },
+ ],
+ });
+ } catch { /* non-critical */ }
+ }
+ }
+
window.addEventListener('DOMContentLoaded', () => {
window.addEventListener('DOMContentLoaded', () => {
+ initChrome();
const showcase = document.getElementById('showcase-section');
const showcase = document.getElementById('showcase-section');
showcase.style.display = 'block';
showcase.style.display = 'block';
buildShowcaseGrid();
buildShowcaseGrid();
@@ -137,8 +137,11 @@ a{color:inherit;text-decoration:none}
@@ -137,8 +137,11 @@ a{color:inherit;text-decoration:none}
<div id="hub"></div>
<div id="hub"></div>
<div class="rel" id="rel"></div>
<div class="rel" id="rel"></div>
</div></div>
</div></div>
+<div id="ov-footer"></div>
<div class="toast" id="toast"></div>
<div class="toast" id="toast"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+<script src="https://openvibe.network/shared/footer.js"></script>
+<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script>
<script>
'use strict';
'use strict';
const API='https://openvibe.tools/api/dev',$=s=>document.getElementById(s),
const API='https://openvibe.tools/api/dev',$=s=>document.getElementById(s),
@@ -180,7 +183,13 @@ let ACT=T[sub]?sub:'dev',MODE='';
@@ -180,7 +183,13 @@ let ACT=T[sub]?sub:'dev',MODE='';
document.addEventListener('DOMContentLoaded',()=>{
document.addEventListener('DOMContentLoaded',()=>{
const tok=localStorage.getItem('ov_token');
const tok=localStorage.getItem('ov_token');
- if(window.OpenVibeNavbar)try{OpenVibeNavbar.init({service:'dev',apiBase:'https://openvibe.network',token:tok})}catch(e){}
+ const cur=T[ACT]||T.dev;
+ if(window.OpenVibeNavbar)try{OpenVibeNavbar.init({service:'dev',apiBase:'https://openvibe.network',token:tok,history:{type:'tool',title:ACT==='dev'?'Dev.OpenVibe':cur.n},silentLogin:'https://openvibe.tools/auth/login?silent=1&next={url}'})}catch(e){}
+ if(window.OpenVibeFooter)try{
+ // Footer columns come from the same catalog as the hub, one column per category.
+ const links=Object.entries(CATS).map(([cid,cat])=>({heading:cat.l,items:Object.entries(T).filter(([,t])=>t.c===cid).map(([id,t])=>({name:t.n,url:'https://'+id+'.openvibe.tools'}))})).filter(g=>g.items.length);
+ OpenVibeFooter.init({service:'dev',variant:ACT==='dev'?'full':'compact',mount:'#ov-footer',apiBase:'https://openvibe.network',links});
+ }catch(e){}
ACT==='dev'?showHub():showTool();
ACT==='dev'?showHub():showTool();
});
});
@@ -21,6 +21,7 @@
@@ -21,6 +21,7 @@
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<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">
+ <script src="https://openvibe.network/shared/theme-loader.js"></script>
<style>
<style>
:root {
:root {
--bg-primary: #0b0c12;
--bg-primary: #0b0c12;
@@ -93,7 +94,7 @@
@@ -93,7 +94,7 @@
.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }
.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }
.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }
.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }
- footer { text-align: center; padding: 40px 20px; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
+ .note { text-align: center; padding: 40px 20px 0; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
footer a { color: var(--accent-light); text-decoration: none; }
footer a { color: var(--accent-light); text-decoration: none; }
footer a:hover { text-decoration: underline; }
footer a:hover { text-decoration: underline; }
@@ -125,12 +126,12 @@
@@ -125,12 +126,12 @@
<div class="stats-section" id="stats"></div>
<div class="stats-section" id="stats"></div>
</div>
</div>
- <footer>
- <p>All image processing is performed locally in your browser. No files are uploaded to our servers. <a href="https://github.com/OpenVibe.Live">Learn more →</a></p>
- </footer>
+ <p class="note">All image processing is performed locally in your browser. No files are uploaded to our servers.</p>
</div>
</div>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script>
<script>
const IMAGE_TOOLS = {
const IMAGE_TOOLS = {
'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
@@ -191,7 +192,37 @@
@@ -191,7 +192,37 @@
}
}
// Initialize
// Initialize
+ function getCookie(n) {
+ const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
+ return m ? decodeURIComponent(m[1]) : null;
+ }
+ function initChrome() {
+ const token = getCookie('ov_token') || localStorage.getItem('ov_token');
+ if (window.OpenVibeNavbar) {
+ try {
+ OpenVibeNavbar.init({
+ service: 'img', apiBase: 'https://openvibe.network', token,
+ history: { type: 'tool', title: document.title.split(' — ')[0] },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
+ });
+ } catch { /* non-critical */ }
+ }
+ if (window.OpenVibeFooter) {
+ try {
+ const items = Object.values(IMAGE_TOOLS).map(t => ({ name: t.name, url: 'https://' + t.domain }));
+ OpenVibeFooter.init({
+ service: 'img', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [
+ { heading: 'Tools', items: items.slice(0, Math.ceil(items.length / 2)) },
+ { heading: 'Formats', items: items.slice(Math.ceil(items.length / 2)) },
+ ],
+ });
+ } catch { /* non-critical */ }
+ }
+ }
+
window.addEventListener('DOMContentLoaded', () => {
window.addEventListener('DOMContentLoaded', () => {
+ initChrome();
const showcase = document.getElementById('showcase-section');
const showcase = document.getElementById('showcase-section');
showcase.style.display = 'block';
showcase.style.display = 'block';
buildShowcaseGrid();
buildShowcaseGrid();
@@ -88,19 +88,6 @@
@@ -88,19 +88,6 @@
a { color: inherit; text-decoration: none; }
a { color: inherit; text-decoration: none; }
.shell { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
.shell { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
- /* ─── Top bar ─── */
- .topbar {
- display: flex; align-items: center; justify-content: space-between;
- padding: 18px 0; gap: 16px;
- }
- .brand { display: flex; align-items: center; gap: 11px; font-weight: 900; font-size: 1.12rem; letter-spacing: -0.01em; }
- .brand-badge {
- width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center;
- background: linear-gradient(135deg, var(--accent), var(--accent-dark));
- color: #fff; font-size: 1rem; box-shadow: 0 6px 18px rgba(139,92,246,0.35);
- }
- .brand small { display: block; font-size: 0.68rem; font-weight: 600; color: var(--text-muted); letter-spacing: 0.06em; text-transform: uppercase; }
- .topbar-actions { display: flex; align-items: center; gap: 10px; }
.btn {
.btn {
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
padding: 10px 18px; border-radius: 12px; font-weight: 700; font-size: 0.9rem;
padding: 10px 18px; border-radius: 12px; font-weight: 700; font-size: 0.9rem;
@@ -113,18 +100,6 @@
@@ -113,18 +100,6 @@
border-color: transparent; color: #fff; box-shadow: 0 8px 22px rgba(139,92,246,0.35);
border-color: transparent; color: #fff; box-shadow: 0 8px 22px rgba(139,92,246,0.35);
}
}
.btn-primary:hover { filter: brightness(1.1); background: linear-gradient(135deg, var(--accent), var(--accent-dark)); }
.btn-primary:hover { filter: brightness(1.1); background: linear-gradient(135deg, var(--accent), var(--accent-dark)); }
- .user-chip {
- display: none; align-items: center; gap: 10px; padding: 7px 12px;
- border: 1px solid var(--border-light); border-radius: 999px; font-size: 0.88rem; font-weight: 600;
- }
- .user-chip.visible { display: inline-flex; }
- .user-chip .avatar {
- width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
- background: var(--accent); color: #fff; font-size: 0.72rem; font-weight: 800; overflow: hidden;
- }
- .user-chip .avatar img { width: 100%; height: 100%; object-fit: cover; }
- .user-chip a { color: var(--text-muted); font-size: 0.8rem; }
- .user-chip a:hover { color: var(--text-primary); }
/* ─── Hero ─── */
/* ─── Hero ─── */
.hero { padding: 64px 0 40px; text-align: center; }
.hero { padding: 64px 0 40px; text-align: center; }
@@ -259,37 +234,15 @@
@@ -259,37 +234,15 @@
.dir-empty i { display: block; font-size: 1.6rem; margin-bottom: 10px; }
.dir-empty i { display: block; font-size: 1.6rem; margin-bottom: 10px; }
/* ─── Footer ─── */
/* ─── Footer ─── */
- footer { border-top: 1px solid var(--border); margin-top: 40px; padding: 34px 0 44px; }
- .foot { display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; color: var(--text-muted); font-size: 0.84rem; }
- .foot a { color: var(--text-secondary); }
- .foot a:hover { color: var(--text-primary); }
- .foot-links { display: flex; gap: 18px; flex-wrap: wrap; }
@media (max-width: 640px) {
@media (max-width: 640px) {
.hero { padding: 40px 0 28px; }
.hero { padding: 40px 0 28px; }
.hero-stats { gap: 18px; }
.hero-stats { gap: 18px; }
- .topbar-actions .btn span { display: none; }
}
}
</style>
</style>
</head>
</head>
<body>
<body>
<div class="shell">
<div class="shell">
- <!-- ─── Top bar ─── -->
- <header class="topbar">
- <a class="brand" href="/">
- <span class="brand-badge"><i class="fa-solid fa-circle-nodes"></i></span>
- <span>OpenVibe.Tools<small>Part of the OpenVibe network</small></span>
- </a>
- <div class="topbar-actions">
- <span class="user-chip" id="user-chip">
- <span class="avatar" id="user-avatar">?</span>
- <span id="user-name"></span>
- <a href="/auth/logout" title="Sign out"><i class="fa-solid fa-right-from-bracket"></i></a>
- </span>
- <a class="btn btn-primary" id="signin-btn" href="/auth/login"><i class="fa-solid fa-right-to-bracket"></i> <span>Sign in</span></a>
- </div>
- </header>
-
<!-- ─── Hero ─── -->
<!-- ─── Hero ─── -->
<section class="hero">
<section class="hero">
<h1>Every tool you need,<br><span class="grad">one vibe apart.</span></h1>
<h1>Every tool you need,<br><span class="grad">one vibe apart.</span></h1>
@@ -426,20 +379,10 @@
@@ -426,20 +379,10 @@
</div>
</div>
</div>
</div>
- <footer>
- <div class="shell foot">
- <div>© OpenVibe — open tools, community run. <a href="https://openvibe.network">One Account. All of OpenVibe.</a></div>
- <div class="foot-links">
- <a href="https://openvibe.live">Live</a>
- <a href="https://openvibe.games">Games</a>
- <a href="https://openvibe.network">Network</a>
- <a href="https://openvibe.live/tos" rel="noopener">Terms</a>
- <a href="https://openvibe.live/dmca" rel="noopener">DMCA</a>
- </div>
- </div>
- </footer>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js" defer></script>
<script src="https://openvibe.network/shared/navbar.js" defer></script>
+ <script src="https://openvibe.network/shared/footer.js" defer></script>
<script>
<script>
'use strict';
'use strict';
@@ -749,41 +692,53 @@
@@ -749,41 +692,53 @@
document.getElementById('stat-tools').textContent =
document.getElementById('stat-tools').textContent =
DIR_DATA.reduce((sum, c) => sum + c.tools.length, 0) + '+';
DIR_DATA.reduce((sum, c) => sum + c.tools.length, 0) + '+';
- /* ─── Session (OAuth client of OpenVibe.Network) ─── */
+ /* ─── Shared chrome (navbar + footer from openvibe.network) ─── */
function getCookie(n) {
function getCookie(n) {
const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
return m ? decodeURIComponent(m[1]) : null;
return m ? decodeURIComponent(m[1]) : null;
}
}
- async function initSession() {
- let user = null;
- try {
- const res = await fetch('/auth/me', { credentials: 'include' });
- if (res.ok) user = (await res.json()).user;
- } catch { /* signed out */ }
-
- const chip = document.getElementById('user-chip');
- const signin = document.getElementById('signin-btn');
- if (user) {
- document.getElementById('user-name').textContent = user.display_name || user.username || 'Account';
- const avatar = document.getElementById('user-avatar');
- if (user.avatar_url) avatar.innerHTML = `<img src="${escHtml(user.avatar_url)}" alt="">`;
- else avatar.textContent = (user.display_name || user.username || '?').slice(0, 1).toUpperCase();
- chip.classList.add('visible');
- signin.style.display = 'none';
- } else {
- chip.classList.remove('visible');
- signin.style.display = '';
- }
-
- // Shared navbar (loaded from openvibe.network) — best effort
+ // The navbar owns the account UI (sign-in, avatar, menu) and boots from the shared
+ // ov_token cookie straight away — no /auth/me round trip before it can render.
+ function initChrome() {
if (window.OpenVibeNavbar) {
if (window.OpenVibeNavbar) {
try {
try {
- OpenVibeNavbar.init({ service: 'tools', apiBase: 'https://openvibe.network', token: getCookie('ov_token'), user });
+ OpenVibeNavbar.init({
+ service: 'tools',
+ apiBase: 'https://openvibe.network',
+ token: getCookie('ov_token') || localStorage.getItem('ov_token'),
+ history: { type: 'tool', title: 'OpenVibe.Tools' },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
+ });
+ } catch { /* non-critical */ }
+ }
+ if (window.OpenVibeFooter) {
+ try {
+ OpenVibeFooter.init({
+ service: 'tools', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [
+ { heading: 'Tool suites', items: [
+ { name: 'Net.OpenVibe', url: 'https://net.openvibe.tools' },
+ { name: 'Dev.OpenVibe', url: 'https://dev.openvibe.tools' },
+ { name: 'Paste.OpenVibe', url: 'https://pastes.openvibe.tools' },
+ { name: 'Img.OpenVibe', url: 'https://img.openvibe.tools' },
+ { name: 'Audio.OpenVibe', url: 'https://audio.openvibe.tools' },
+ { name: 'Text.OpenVibe', url: 'https://text.openvibe.tools' },
+ ] },
+ { heading: 'More suites', items: [
+ { name: 'Logo.OpenVibe', url: 'https://logo.openvibe.tools' },
+ { name: 'Docs.OpenVibe', url: 'https://docs.openvibe.tools' },
+ { name: 'YT.OpenVibe', url: 'https://yt.openvibe.tools' },
+ { name: 'Maps.OpenVibe', url: 'https://maps.openvibe.tools' },
+ { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },
+ { name: 'All tools', url: '#', onclick: 'openDirectory(); return false;' },
+ ] },
+ ],
+ });
} catch { /* non-critical */ }
} catch { /* non-critical */ }
}
}
}
}
- window.addEventListener('DOMContentLoaded', initSession);
+ window.addEventListener('DOMContentLoaded', initChrome);
</script>
</script>
</body>
</body>
</html>
</html>
@@ -280,12 +280,6 @@
@@ -280,12 +280,6 @@
.tab-content.active { display: block; }
.tab-content.active { display: block; }
/* ─── Footer ─── */
/* ─── Footer ─── */
- .footer {
- text-align: center; padding: 32px 0; margin-top: 40px;
- border-top: 1px solid var(--border); color: var(--text-muted); font-size: 12px;
- }
- .footer a { color: var(--text-secondary); }
- .footer .footer-links { display: flex; gap: 16px; justify-content: center; margin-bottom: 8px; flex-wrap: wrap; }
/* ─── Responsive ─── */
/* ─── Responsive ─── */
@media (max-width: 640px) {
@media (max-width: 640px) {
@@ -587,22 +581,14 @@
@@ -587,22 +581,14 @@
<!-- Hub: tool directory (shown on net.openvibe.tools) -->
<!-- Hub: tool directory (shown on net.openvibe.tools) -->
<div class="shell" id="hub-directory" style="display:none"></div>
<div class="shell" id="hub-directory" style="display:none"></div>
- <!-- Footer -->
- <div class="footer">
- <div class="footer-links">
- <a href="https://openvibe.network">OpenVibe</a>
- <a href="https://net.openvibe.tools">Net.OpenVibe</a>
- <a href="https://openvibe.live">OpenVibe.Live</a>
- <a href="https://openvibe.games">Scraplandia</a>
- </div>
- <p>Part of the <a href="https://openvibe.network">OpenVibe</a>. Open source. Built in the open.</p>
- </div>
+ <div id="ov-footer"></div>
</div>
</div>
<div class="toast-container" id="toasts"></div>
<div class="toast-container" id="toasts"></div>
<!-- Shared OpenVibe Navbar -->
<!-- Shared OpenVibe Navbar -->
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script>
<script>
'use strict';
'use strict';
@@ -709,10 +695,36 @@
@@ -709,10 +695,36 @@
// ── Init ─────────────────────────────────────────────────
// ── Init ─────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', () => {
- // Init navbar
+ // Shared navbar + footer (openvibe.network)
try {
try {
const token = localStorage.getItem('ov_token') || getCookie('ov_token');
const token = localStorage.getItem('ov_token') || getCookie('ov_token');
- OpenVibeNavbar.init({ service: 'net', apiBase: 'https://openvibe.network', token });
+ OpenVibeNavbar.init({
+ service: 'net', apiBase: 'https://openvibe.network', token,
+ history: { type: 'tool', title: isHub ? 'Net.OpenVibe' : tool.name },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
+ });
+ } catch {}
+ try {
+ OpenVibeFooter.init({
+ service: 'net', variant: isHub ? 'full' : 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [
+ { heading: 'Network tools', items: [
+ { name: 'Domain & IP lookup', url: 'https://lookup.openvibe.tools' },
+ { name: 'DNS lookup', url: 'https://dns.openvibe.tools' },
+ { name: 'Ping', url: 'https://ping.openvibe.tools' },
+ { name: 'SSL checker', url: 'https://ssl.openvibe.tools' },
+ { name: 'Whois', url: 'https://whois.openvibe.tools' },
+ { name: 'HTTP headers', url: 'https://headers.openvibe.tools' },
+ ] },
+ { heading: 'Email & uptime', items: [
+ { name: 'SPF checker', url: 'https://spf.openvibe.tools' },
+ { name: 'DMARC checker', url: 'https://dmarc.openvibe.tools' },
+ { name: 'Blacklist check', url: 'https://blacklist.openvibe.tools' },
+ { name: 'Uptime check', url: 'https://uptime.openvibe.tools' },
+ { name: 'All network tools', url: 'https://net.openvibe.tools' },
+ ] },
+ ],
+ });
} catch {}
} catch {}
setupUI();
setupUI();
@@ -299,9 +299,11 @@
@@ -299,9 +299,11 @@
</div>
</div>
</div>
</div>
+ <div id="ov-footer"></div>
<div class="toast-container" id="toasts"></div>
<div class="toast-container" id="toasts"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script>
<script>
'use strict';
'use strict';
@@ -425,8 +427,10 @@
@@ -425,8 +427,10 @@
const data = await api(`/${slug}`);
const data = await api(`/${slug}`);
const p = data.paste;
const p = data.paste;
renderPasteView(p);
renderPasteView(p);
+ initChrome({ type: 'paste', title: p.title || slug });
loadComments(slug);
loadComments(slug);
} catch (err) {
} catch (err) {
+ initChrome();
detail.innerHTML = `<div class="empty">${esc(err.message)}</div>`;
detail.innerHTML = `<div class="empty">${esc(err.message)}</div>`;
}
}
}
}
@@ -568,12 +572,40 @@
@@ -568,12 +572,40 @@
document.getElementById('view-paste').classList.toggle('hidden', view !== 'paste');
document.getElementById('view-paste').classList.toggle('hidden', view !== 'paste');
}
}
+ // ── Shared chrome ──────────────────────────────────────
+ // The navbar records this visit in the account's cross-network history, so on a paste
+ // page it waits for the paste's title (viewPaste calls it); everywhere else it boots at once.
+ let chromeReady = false;
+ function initChrome(history) {
+ if (chromeReady) return;
+ chromeReady = true;
+ try {
+ OpenVibeNavbar.init({
+ service: 'paste', apiBase: 'https://openvibe.network', token,
+ history: history || { type: 'tool', title: 'Paste.OpenVibe' },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
+ menu: { after: [{ id: 'my-pastes', label: 'My pastes', href: 'https://openvibe.community/my', icon: 'fa-clipboard-list' }] },
+ });
+ } catch {}
+ try {
+ OpenVibeFooter.init({
+ service: 'paste', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [{ heading: 'Pastes', items: [
+ { name: 'New paste', url: '/new' },
+ { name: 'Browse pastes', url: '/' },
+ { name: 'My pastes', url: 'https://openvibe.community/my' },
+ ] }],
+ });
+ } catch {}
+ }
+
function route() {
function route() {
const path = location.pathname;
const path = location.pathname;
if (path.startsWith('/p/')) {
if (path.startsWith('/p/')) {
const slug = path.split('/')[2];
const slug = path.split('/')[2];
if (slug) { viewPaste(slug); return; }
if (slug) { viewPaste(slug); return; }
}
}
+ initChrome();
if (path === '/new') { showView('new'); return; }
if (path === '/new') { showView('new'); return; }
showView('browse');
showView('browse');
}
}
@@ -593,11 +625,6 @@
@@ -593,11 +625,6 @@
// ── Init ───────────────────────────────────────────────
// ── Init ───────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', () => {
token = localStorage.getItem('ov_token') || getCookie('ov_token');
token = localStorage.getItem('ov_token') || getCookie('ov_token');
-
- try {
- OpenVibeNavbar.init({ service: 'paste', apiBase: 'https://openvibe.network', token });
- } catch {}
-
route();
route();
});
});
</script>
</script>
@@ -162,22 +162,11 @@
@@ -162,22 +162,11 @@
</div>
</div>
<!-- Footer -->
<!-- Footer -->
- <footer class="site-footer">
- <div class="footer-inner">
- <span><i class="fa-solid fa-circle-nodes"></i> Img.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
- <span class="footer-links">
- <a href="https://openvibe.tools">OpenVibe.Tools</a>
- <a href="https://openvibe.live">OpenVibe.Live</a>
- <a href="https://openvibe.games">Scraplandia</a>
- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>
- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>
- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>
- </span>
- </div>
- </footer>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.js"></script>
@@ -119,16 +119,42 @@
@@ -119,16 +119,42 @@
} catch { /* invalid token */ }
} catch { /* invalid token */ }
}
}
+ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").
if (typeof OpenVibeNavbar !== 'undefined') {
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
OpenVibeNavbar.init({
service: 'img',
service: 'img',
- brandName: ctx?.brandName || undefined,
- brandIcon: ctx?.faIcon || undefined,
token,
token,
user: ctx?.user || user,
user: ctx?.user || user,
apiBase: 'https://openvibe.network',
apiBase: 'https://openvibe.network',
+ history: { type: 'tool', title: ctx?.brandName || 'Img.OpenVibe' },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
});
}
}
+ initFooter();
+ }
+
+ /* ---------- Footer (shared OpenVibeFooter) ---------- */
+ // Full footer on the hub host, the compact strip on the single-tool hosts.
+ function initFooter() {
+ if (typeof OpenVibeFooter === 'undefined') return;
+ try {
+ OpenVibeFooter.init({
+ service: 'img',
+ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',
+ mount: '#ov-footer',
+ apiBase: 'https://openvibe.network',
+ links: [{ heading: 'Img tools', items: [
+ { name: 'Convert', url: 'https://convert.openvibe.tools' },
+ { name: 'Compress', url: 'https://compress.openvibe.tools' },
+ { name: 'Resize', url: 'https://resize.openvibe.tools' },
+ { name: 'Crop', url: 'https://crop.openvibe.tools' },
+ { name: 'Favicon', url: 'https://favicon.openvibe.tools' },
+ { name: 'PNG', url: 'https://png.openvibe.tools' },
+ { name: 'JPG', url: 'https://jpg.openvibe.tools' },
+ { name: 'WebP', url: 'https://webp.openvibe.tools' },
+ ] }],
+ });
+ } catch { /* optional */ }
}
}
// ── Notifications (OpenVibe bell + toasts) ───────────
// ── Notifications (OpenVibe bell + toasts) ───────────
@@ -236,6 +236,7 @@
@@ -236,6 +236,7 @@
<span id="footer-coords"></span>
<span id="footer-coords"></span>
<span id="footer-weather"></span>
<span id="footer-weather"></span>
</footer>
</footer>
+ <div id="ov-footer"></div>
</div>
</div>
<!-- ═══ Detail Panel ═══ -->
<!-- ═══ Detail Panel ═══ -->
@@ -390,6 +391,7 @@
@@ -390,6 +391,7 @@
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+<script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.js"></script>
@@ -403,7 +405,19 @@
@@ -403,7 +405,19 @@
let user = null;
let user = null;
if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }
if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }
if (typeof OpenVibeNavbar !== 'undefined') {
if (typeof OpenVibeNavbar !== 'undefined') {
- OpenVibeNavbar.init({ service: 'maps', brandName: 'Maps.OpenVibe', brandIcon: 'fa-person-shelter', token, user, apiBase: 'https://openvibe.network' });
+ OpenVibeNavbar.init({ service: 'maps', token, user, apiBase: 'https://openvibe.network', history: { type: 'tool', title: 'Maps.OpenVibe' }, silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}' });
+ }
+ if (typeof OpenVibeFooter !== 'undefined') {
+ try {
+ OpenVibeFooter.init({
+ service: 'maps', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [{ heading: 'Maps.OpenVibe', items: [
+ { name: 'Survival map', url: 'https://maps.openvibe.tools' },
+ { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },
+ { name: 'All tools', url: 'https://openvibe.tools' },
+ ] }],
+ });
+ } catch (e) { /* optional */ }
}
}
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
@@ -116,23 +116,11 @@
@@ -116,23 +116,11 @@
</div>
</div>
<!-- Footer -->
<!-- Footer -->
- <footer class="site-footer">
- <div class="footer-inner">
- <span><i class="fa-solid fa-circle-nodes"></i> YT.OpenVibe — Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
- <span class="footer-links">
- <a href="https://openvibe.tools">OpenVibe.Tools</a>
- <a href="https://openvibe.live">OpenVibe.Live</a>
- <a href="https://openvibe.games">Scraplandia</a>
- <a href="https://img.openvibe.tools">Img.OpenVibe</a>
- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>
- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>
- <a href="https://food.openvibe.tools">Food.OpenVibe</a>
- </span>
- </div>
- </footer>
+ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
+ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.js"></script>
@@ -369,12 +369,24 @@
@@ -369,12 +369,24 @@
if (typeof OpenVibeNavbar !== 'undefined') {
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
OpenVibeNavbar.init({
service: 'yt',
service: 'yt',
- brandName: 'YT.OpenVibe',
- brandIcon: 'fa-circle-play',
token, user,
token, user,
apiBase: 'https://openvibe.network',
apiBase: 'https://openvibe.network',
+ history: { type: 'tool', title: 'YT.OpenVibe' },
+ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
});
}
}
+ if (typeof OpenVibeFooter !== 'undefined') {
+ try {
+ OpenVibeFooter.init({
+ service: 'yt', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
+ links: [{ heading: 'YT.OpenVibe', items: [
+ { name: 'Video downloader', url: 'https://yt.openvibe.tools' },
+ { name: 'Audio converter', url: 'https://audio.openvibe.tools' },
+ { name: 'All tools', url: 'https://openvibe.tools' },
+ ] }],
+ });
+ } catch { /* optional */ }
+ }
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
}
}