openvibers/openvibe.mediahub Commits
The bar: main places with icons and the phone app bar, the rest under More (What shipped and Search leave it) (#168)
Changes
@@@ -23,12 +23,12 @@ const NETWORK_URL = 'https://openvibe.network';const SITE_NAME = 'OpenVibe.Download';const TAGLINE = 'Your files, shared on your terms.';const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');--// The product's own navigation: one entry per public page it serves.++// The product's own places: the main ones carry an icon (phones get them as the bottom tab bar), the rest fold into++// the navbar's More (Shared 4.22). Search is the navbar's magnifier and What shipped is the footer's.const NAV = [-- { label: 'Your files', href: '/files' },-- { label: 'Backups', href: '/sync' },-- { label: 'Safety', href: '/safety' },-- { label: 'What shipped', href: '/updates' },++ { label: 'Your files', href: '/files', icon: 'fa-file-lines' },++ { label: 'Backups', href: '/sync', icon: 'fa-clock-rotate-left' },++ { label: 'Safety', href: '/safety', icon: 'fa-shield-halved', more: true },];const hashes = new Map();@@@ -59,7 +59,8 @@ function renderPage(o) {const nav = {service: 'media-hub',apiBase: NETWORK_URL,-- links: NAV.map((l) => ({ label: l.label, href: l.href, active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),++ links: NAV.map((l) => ({ label: l.label, href: l.href, ...(l.icon ? { icon: l.icon } : {}), ...(l.more ? { more: true } : {}), active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),++ appBar: true, // phones: the places with icons as the bottom tab bar (Shared 4.20)history: { type: 'page', title: o.title || SITE_NAME },silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',@@@ -79,7 +79,9 @@ ${render(o.body)}navbar: {service: 'media-hub',apiBase: NETWORK_URL,-- links: NAV_LINKS.map((l) => ({ ...l, active: l.href === '/' ? o.path === '/' : String(o.path || '').startsWith(l.href) })),++ links: NAV_LINKS.filter((l) => !l.noscriptOnly).map(({ noscriptOnly, ...l }) => ({ ...l, active: l.href === '/' ? o.path === '/' : String(o.path || '').startsWith(l.href) })),++ appBar: true, // phones: the places with icons as the bottom tab bar (Shared 4.20)++ search: { page: '/search', label: 'OpenVibe.Video' },history: { type: 'page', title },sessionUrl: '/auth/me',loginUrl: '/auth/login?next={path}',@@@ -95,7 +97,17 @@ ${render(o.body)}}// The site's own places in the navbar: the videos, and your playlists (signed in; the page asks you to sign in).--const NAV_LINKS = [{ label: 'Videos', href: '/' }, { label: 'Search', href: '/search' }, { label: 'Upload', href: '/upload' }, { label: 'Record', href: '/record' }, { label: 'Your videos', href: '/me/videos' }, { label: 'Playlists', href: '/playlists' }];++// The places: the main ones carry an icon (phones get them as the bottom tab bar), Playlists folds into the navbar's More++// (Shared 4.22). Search is the navbar's magnifier, which offers this site's own search first; the page without++// JavaScript keeps its Search link.++const NAV_LINKS = [++ { label: 'Videos', href: '/', icon: 'fa-play' },++ { label: 'Search', href: '/search', icon: 'fa-magnifying-glass', noscriptOnly: true },++ { label: 'Upload', href: '/upload', icon: 'fa-upload' },++ { label: 'Record', href: '/record', icon: 'fa-video' },++ { label: 'Your videos', href: '/me/videos', icon: 'fa-photo-film' },++ { label: 'Playlists', href: '/playlists', icon: 'fa-list-check', more: true },++];// A signed-out copy is never reused without asking: Cloudflare drops `Vary` (it keeps only Accept-Encoding), so a// browser keyed the guest copy by its URL alone and, for five minutes after a quiet sign-in, showed the signed-out page@@@ -501,10 +501,13 @@ function countOccurrences(haystack, needle) {assert.ok(none.text.includes('Nothing matched') && !none.text.includes('<script>alert(1)'), 'nothing found, and the words are escaped');});-- await check('the navbar names the site\'s places: Videos, Search, Upload, Record, Your videos and Playlists, the current one active', async () => {++ // The bar (2026-10-28): the places with icons (the phone app bar), Playlists under More, Search is the magnifier++ // (this site's search first); the page without JavaScript keeps its Search link.++ await check('the navbar names the site\'s places: Videos, Upload, Record, Your videos, and Playlists under More; Search is the magnifier', async () => {const home = (await getVideo('/')).text;-- assert.match(home, /"links":\[\{"label":"Videos","href":"\/","active":true\},\{"label":"Search","href":"\/search","active":false\},\{"label":"Upload","href":"\/upload","active":false\},\{"label":"Record","href":"\/record","active":false\},\{"label":"Your videos","href":"\/me\/videos","active":false\},\{"label":"Playlists","href":"\/playlists","active":false\}\]/);-- assert.ok(home.includes('href="/playlists"'), 'the no-JS nav links Playlists too');++ assert.match(home, /"links":\[\{"label":"Videos","href":"\/","icon":"fa-play","active":true\},\{"label":"Upload","href":"\/upload","icon":"fa-upload","active":false\},\{"label":"Record","href":"\/record","icon":"fa-video","active":false\},\{"label":"Your videos","href":"\/me\/videos","icon":"fa-photo-film","active":false\},\{"label":"Playlists","href":"\/playlists","icon":"fa-list-check","more":true,"active":false\}\]/);++ assert.ok(/"appBar":true/.test(home) && /"search":\{"page":"\/search","label":"OpenVibe.Video"\}/.test(home), 'the app bar, and the magnifier offers Video\'s search first');++ assert.ok(home.includes('href="/playlists"') && home.includes('href="/search"'), 'the no-JS nav links Playlists and Search too');});await check('/updates: the shared update log on the video host, one heading, in the sitemap', async () => {
@@ -23,12 +23,12 @@ const NETWORK_URL = 'https://openvibe.network';
@@ -23,12 +23,12 @@ const NETWORK_URL = 'https://openvibe.network';
const SITE_NAME = 'OpenVibe.Download';
const SITE_NAME = 'OpenVibe.Download';
const TAGLINE = 'Your files, shared on your terms.';
const TAGLINE = 'Your files, shared on your terms.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
-// The product's own navigation: one entry per public page it serves.
+// The product's own places: the main ones carry an icon (phones get them as the bottom tab bar), the rest fold into
+// the navbar's More (Shared 4.22). Search is the navbar's magnifier and What shipped is the footer's.
const NAV = [
const NAV = [
- { label: 'Your files', href: '/files' },
- { label: 'Backups', href: '/sync' },
- { label: 'Safety', href: '/safety' },
- { label: 'What shipped', href: '/updates' },
+ { label: 'Your files', href: '/files', icon: 'fa-file-lines' },
+ { label: 'Backups', href: '/sync', icon: 'fa-clock-rotate-left' },
+ { label: 'Safety', href: '/safety', icon: 'fa-shield-halved', more: true },
];
];
const hashes = new Map();
const hashes = new Map();
@@ -59,7 +59,8 @@ function renderPage(o) {
@@ -59,7 +59,8 @@ function renderPage(o) {
const nav = {
const nav = {
service: 'media-hub',
service: 'media-hub',
apiBase: NETWORK_URL,
apiBase: NETWORK_URL,
- links: NAV.map((l) => ({ label: l.label, href: l.href, active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),
+ links: NAV.map((l) => ({ label: l.label, href: l.href, ...(l.icon ? { icon: l.icon } : {}), ...(l.more ? { more: true } : {}), active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),
+ appBar: true, // phones: the places with icons as the bottom tab bar (Shared 4.20)
history: { type: 'page', title: o.title || SITE_NAME },
history: { type: 'page', title: o.title || SITE_NAME },
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
@@ -79,7 +79,9 @@ ${render(o.body)}
@@ -79,7 +79,9 @@ ${render(o.body)}
navbar: {
navbar: {
service: 'media-hub',
service: 'media-hub',
apiBase: NETWORK_URL,
apiBase: NETWORK_URL,
- links: NAV_LINKS.map((l) => ({ ...l, active: l.href === '/' ? o.path === '/' : String(o.path || '').startsWith(l.href) })),
+ links: NAV_LINKS.filter((l) => !l.noscriptOnly).map(({ noscriptOnly, ...l }) => ({ ...l, active: l.href === '/' ? o.path === '/' : String(o.path || '').startsWith(l.href) })),
+ appBar: true, // phones: the places with icons as the bottom tab bar (Shared 4.20)
+ search: { page: '/search', label: 'OpenVibe.Video' },
history: { type: 'page', title },
history: { type: 'page', title },
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
loginUrl: '/auth/login?next={path}',
loginUrl: '/auth/login?next={path}',
@@ -95,7 +97,17 @@ ${render(o.body)}
@@ -95,7 +97,17 @@ ${render(o.body)}
}
}
// The site's own places in the navbar: the videos, and your playlists (signed in; the page asks you to sign in).
// The site's own places in the navbar: the videos, and your playlists (signed in; the page asks you to sign in).
-const NAV_LINKS = [{ label: 'Videos', href: '/' }, { label: 'Search', href: '/search' }, { label: 'Upload', href: '/upload' }, { label: 'Record', href: '/record' }, { label: 'Your videos', href: '/me/videos' }, { label: 'Playlists', href: '/playlists' }];
+// The places: the main ones carry an icon (phones get them as the bottom tab bar), Playlists folds into the navbar's More
+// (Shared 4.22). Search is the navbar's magnifier, which offers this site's own search first; the page without
+// JavaScript keeps its Search link.
+const NAV_LINKS = [
+ { label: 'Videos', href: '/', icon: 'fa-play' },
+ { label: 'Search', href: '/search', icon: 'fa-magnifying-glass', noscriptOnly: true },
+ { label: 'Upload', href: '/upload', icon: 'fa-upload' },
+ { label: 'Record', href: '/record', icon: 'fa-video' },
+ { label: 'Your videos', href: '/me/videos', icon: 'fa-photo-film' },
+ { label: 'Playlists', href: '/playlists', icon: 'fa-list-check', more: true },
+];
// A signed-out copy is never reused without asking: Cloudflare drops `Vary` (it keeps only Accept-Encoding), so a
// A signed-out copy is never reused without asking: Cloudflare drops `Vary` (it keeps only Accept-Encoding), so a
// browser keyed the guest copy by its URL alone and, for five minutes after a quiet sign-in, showed the signed-out page
// browser keyed the guest copy by its URL alone and, for five minutes after a quiet sign-in, showed the signed-out page
@@ -501,10 +501,13 @@ function countOccurrences(haystack, needle) {
@@ -501,10 +501,13 @@ function countOccurrences(haystack, needle) {
assert.ok(none.text.includes('Nothing matched') && !none.text.includes('<script>alert(1)'), 'nothing found, and the words are escaped');
assert.ok(none.text.includes('Nothing matched') && !none.text.includes('<script>alert(1)'), 'nothing found, and the words are escaped');
});
});
- await check('the navbar names the site\'s places: Videos, Search, Upload, Record, Your videos and Playlists, the current one active', async () => {
+ // The bar (2026-10-28): the places with icons (the phone app bar), Playlists under More, Search is the magnifier
+ // (this site's search first); the page without JavaScript keeps its Search link.
+ await check('the navbar names the site\'s places: Videos, Upload, Record, Your videos, and Playlists under More; Search is the magnifier', async () => {
const home = (await getVideo('/')).text;
const home = (await getVideo('/')).text;
- assert.match(home, /"links":\[\{"label":"Videos","href":"\/","active":true\},\{"label":"Search","href":"\/search","active":false\},\{"label":"Upload","href":"\/upload","active":false\},\{"label":"Record","href":"\/record","active":false\},\{"label":"Your videos","href":"\/me\/videos","active":false\},\{"label":"Playlists","href":"\/playlists","active":false\}\]/);
- assert.ok(home.includes('href="/playlists"'), 'the no-JS nav links Playlists too');
+ assert.match(home, /"links":\[\{"label":"Videos","href":"\/","icon":"fa-play","active":true\},\{"label":"Upload","href":"\/upload","icon":"fa-upload","active":false\},\{"label":"Record","href":"\/record","icon":"fa-video","active":false\},\{"label":"Your videos","href":"\/me\/videos","icon":"fa-photo-film","active":false\},\{"label":"Playlists","href":"\/playlists","icon":"fa-list-check","more":true,"active":false\}\]/);
+ assert.ok(/"appBar":true/.test(home) && /"search":\{"page":"\/search","label":"OpenVibe.Video"\}/.test(home), 'the app bar, and the magnifier offers Video\'s search first');
+ assert.ok(home.includes('href="/playlists"') && home.includes('href="/search"'), 'the no-JS nav links Playlists and Search too');
});
});
await check('/updates: the shared update log on the video host, one heading, in the sitemap', async () => {
await check('/updates: the shared update log on the video host, one heading, in the sitemap', async () => {