openvibers/openvibe.connect Commits
A share's page sees its computer connect; the picker scene shows the choice being made (#5)
Changes
@@@ -43,8 +43,12 @@.cn-pick { width: min(100%, 340px); margin: 0 auto; display: grid; gap: 8px; }.cn-pick-row { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--ovk-line); background: var(--ovk-panel); font-size: 14px; color: var(--ovk-muted); }.cn-pick-row i { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--ovk-line); flex: none; }--.cn-pick-row.is-on { color: var(--ovk-text); border-color: color-mix(in srgb, var(--room-accent) 55%, var(--ovk-line)); }--.cn-pick-row.is-on i { border-color: var(--room-accent); background: radial-gradient(circle, var(--room-accent) 0 4px, transparent 5px); }++.cn-pick-row.is-on, .cn-pick-row[data-ov-ghost-on] { color: var(--ovk-text); border-color: color-mix(in srgb, var(--room-accent) 55%, var(--ovk-line)); }++.cn-pick-row.is-on i, .cn-pick-row[data-ov-ghost-on] i { border-color: var(--room-accent); background: radial-gradient(circle, var(--room-accent) 0 4px, transparent 5px); }++/* While the ghost cursor is choosing, only its choice looks chosen. */++.cn-pick:has([data-ov-ghost-on]) .cn-pick-row.is-on:not([data-ov-ghost-on]) { color: var(--ovk-muted); border-color: var(--ovk-line); }++.cn-pick:has([data-ov-ghost-on]) .cn-pick-row.is-on:not([data-ov-ghost-on]) i { border-color: var(--ovk-line); background: none; }++.cn-pick-row { transition: border-color .2s ease, color .2s ease; }.cn-pick-names { display: flex; gap: 6px; padding-left: 40px; }.cn-pick-names b { padding: 3px 10px; border-radius: 999px; font-size: 12.5px; font-weight: 600; background: color-mix(in srgb, var(--room-accent) 16%, transparent); color: var(--room-strong); }nnew file mode 100644@@@ -0,0 +1,32 @@++/* OpenVibe Connect: a share's page notices when its computer connects (or drops), so someone running the command for++ the first time sees "Connected" without reloading. It asks /api/v1/shares/:id every 4 seconds while the page is++ visible, for at most ten minutes, and changes only the state line. Without this script the page is complete. */++(function () {++ 'use strict';++ var head = document.querySelector('[data-cn-share]');++ if (!head || !window.fetch) return;++ var id = head.getAttribute('data-cn-share');++ var state = head.querySelector('.cn-state');++ var line = head.querySelector('[data-cn-line]');++ if (!state || !line || state.classList.contains('is-closed')) return;++ var started = Date.now();++ var last = state.classList.contains('is-on');++ function show(on) {++ if (on === last) return;++ last = on;++ state.classList.toggle('is-on', on);++ state.classList.toggle('is-off', !on);++ state.lastChild.textContent = on ? 'Connected' : 'Not connected';++ line.textContent = on ? 'Connected just now.' : 'The computer disconnected. Run ov-connect again to bring it back.';++ }++ function ask() {++ if (Date.now() - started > 10 * 60 * 1000) return;++ if (document.hidden) { setTimeout(ask, 4000); return; }++ fetch('/api/v1/shares/' + encodeURIComponent(id), { credentials: 'same-origin', headers: { accept: 'application/json' } })++ .then(function (r) { return r.ok ? r.json() : null; })++ .then(function (j) { if (j && j.share && j.share.status === 'active') show(!!j.share.connected); })++ .catch(function () { /* the next ask tries again */ })++ .then(function () { setTimeout(ask, 4000); });++ }++ setTimeout(ask, 4000);++})();@@@ -85,6 +85,7 @@ function layout(o) {<link rel="stylesheet" href="${esc(ovServe.url('room.css'))}"><link rel="stylesheet" href="${esc(ovServe.url('showcase.css'))}"><link rel="stylesheet" href="${asset('css/connect.css')}">++<script src="${asset('js/connect.js')}" defer></script><script src="${esc(ovServe.url('kit-web.js'))}" defer></script><script src="${esc(ovServe.url('motion.js'))}" defer></script><meta name="ov-boost" content="connect@${esc(RELEASE)}">`,@@@ -52,7 +52,8 @@ function phone() {}function picker(mode = 'named') {-- return `<div class="cn-pick" role="img" aria-label="Choosing who can open a share">${Object.entries(MODE_TEXT).map(([k, v]) => `<span class="cn-pick-row${k === mode ? ' is-on' : ''}"><i></i>${esc(v)}</span>`).join('')}++ // The ghost cursor (openvibe-shared 4.106.0 motion.js) walks the three choices; the server draws "named" chosen.++ return `<div class="cn-pick" role="img" aria-label="Choosing who can open a share" data-ov-ghost>${Object.entries(MODE_TEXT).map(([k, v], i) => `<span class="cn-pick-row${k === mode ? ' is-on' : ''}" data-ov-ghost-step="${i + 1}"><i></i>${esc(v)}</span>`).join('')}<span class="cn-pick-names"><b>@sam</b><b>@riley</b></span></div>`;}@@@ -216,9 +217,9 @@ ${modeChoices(share.mode, staff)}<p class="cn-back"><a href="/">${kit.icon('back')} Your shares</a></p>${notice ? `<p class="cn-notice" role="status">${esc(notice)}</p>` : ''}${error ? `<p class="cn-error" role="alert">${esc(error)}</p>` : ''}--<header class="cn-head"><span class="cn-state ${closed ? 'is-closed' : connected ? 'is-on' : 'is-off'}"><i aria-hidden="true"></i>${esc(state)}</span>++<header class="cn-head" data-cn-share="${esc(share.id)}"><span class="cn-state ${closed ? 'is-closed' : connected ? 'is-on' : 'is-off'}" role="status" aria-live="polite"><i aria-hidden="true"></i><span>${esc(state)}</span></span><h1>${closed ? esc(share.host) : `<a href="${esc(share.url)}">${esc(share.host)}</a>`}</h1>--<p>${esc(stateText)}</p></header>++<p data-cn-line>${esc(stateText)}</p></header>${key ? keyBlock(share, key, baseUrl) : ''}<dl class="cn-facts"><div><dt>Who can open it</dt><dd>${esc(MODE_TEXT[share.mode] || share.mode)}${share.mode === 'named' && share.named.length ? `: ${share.named.map((n) => `@${esc(n)}`).join(', ')}` : ''}</dd></div>
@@ -43,8 +43,12 @@
@@ -43,8 +43,12 @@
.cn-pick { width: min(100%, 340px); margin: 0 auto; display: grid; gap: 8px; }
.cn-pick { width: min(100%, 340px); margin: 0 auto; display: grid; gap: 8px; }
.cn-pick-row { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--ovk-line); background: var(--ovk-panel); font-size: 14px; color: var(--ovk-muted); }
.cn-pick-row { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--ovk-line); background: var(--ovk-panel); font-size: 14px; color: var(--ovk-muted); }
.cn-pick-row i { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--ovk-line); flex: none; }
.cn-pick-row i { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--ovk-line); flex: none; }
-.cn-pick-row.is-on { color: var(--ovk-text); border-color: color-mix(in srgb, var(--room-accent) 55%, var(--ovk-line)); }
-.cn-pick-row.is-on i { border-color: var(--room-accent); background: radial-gradient(circle, var(--room-accent) 0 4px, transparent 5px); }
+.cn-pick-row.is-on, .cn-pick-row[data-ov-ghost-on] { color: var(--ovk-text); border-color: color-mix(in srgb, var(--room-accent) 55%, var(--ovk-line)); }
+.cn-pick-row.is-on i, .cn-pick-row[data-ov-ghost-on] i { border-color: var(--room-accent); background: radial-gradient(circle, var(--room-accent) 0 4px, transparent 5px); }
+/* While the ghost cursor is choosing, only its choice looks chosen. */
+.cn-pick:has([data-ov-ghost-on]) .cn-pick-row.is-on:not([data-ov-ghost-on]) { color: var(--ovk-muted); border-color: var(--ovk-line); }
+.cn-pick:has([data-ov-ghost-on]) .cn-pick-row.is-on:not([data-ov-ghost-on]) i { border-color: var(--ovk-line); background: none; }
+.cn-pick-row { transition: border-color .2s ease, color .2s ease; }
.cn-pick-names { display: flex; gap: 6px; padding-left: 40px; }
.cn-pick-names { display: flex; gap: 6px; padding-left: 40px; }
.cn-pick-names b { padding: 3px 10px; border-radius: 999px; font-size: 12.5px; font-weight: 600; background: color-mix(in srgb, var(--room-accent) 16%, transparent); color: var(--room-strong); }
.cn-pick-names b { padding: 3px 10px; border-radius: 999px; font-size: 12.5px; font-weight: 600; background: color-mix(in srgb, var(--room-accent) 16%, transparent); color: var(--room-strong); }
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
+/* OpenVibe Connect: a share's page notices when its computer connects (or drops), so someone running the command for
+ the first time sees "Connected" without reloading. It asks /api/v1/shares/:id every 4 seconds while the page is
+ visible, for at most ten minutes, and changes only the state line. Without this script the page is complete. */
+(function () {
+ 'use strict';
+ var head = document.querySelector('[data-cn-share]');
+ if (!head || !window.fetch) return;
+ var id = head.getAttribute('data-cn-share');
+ var state = head.querySelector('.cn-state');
+ var line = head.querySelector('[data-cn-line]');
+ if (!state || !line || state.classList.contains('is-closed')) return;
+ var started = Date.now();
+ var last = state.classList.contains('is-on');
+ function show(on) {
+ if (on === last) return;
+ last = on;
+ state.classList.toggle('is-on', on);
+ state.classList.toggle('is-off', !on);
+ state.lastChild.textContent = on ? 'Connected' : 'Not connected';
+ line.textContent = on ? 'Connected just now.' : 'The computer disconnected. Run ov-connect again to bring it back.';
+ }
+ function ask() {
+ if (Date.now() - started > 10 * 60 * 1000) return;
+ if (document.hidden) { setTimeout(ask, 4000); return; }
+ fetch('/api/v1/shares/' + encodeURIComponent(id), { credentials: 'same-origin', headers: { accept: 'application/json' } })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (j) { if (j && j.share && j.share.status === 'active') show(!!j.share.connected); })
+ .catch(function () { /* the next ask tries again */ })
+ .then(function () { setTimeout(ask, 4000); });
+ }
+ setTimeout(ask, 4000);
+})();
@@ -85,6 +85,7 @@ function layout(o) {
@@ -85,6 +85,7 @@ function layout(o) {
<link rel="stylesheet" href="${esc(ovServe.url('room.css'))}">
<link rel="stylesheet" href="${esc(ovServe.url('room.css'))}">
<link rel="stylesheet" href="${esc(ovServe.url('showcase.css'))}">
<link rel="stylesheet" href="${esc(ovServe.url('showcase.css'))}">
<link rel="stylesheet" href="${asset('css/connect.css')}">
<link rel="stylesheet" href="${asset('css/connect.css')}">
+<script src="${asset('js/connect.js')}" defer></script>
<script src="${esc(ovServe.url('kit-web.js'))}" defer></script>
<script src="${esc(ovServe.url('kit-web.js'))}" defer></script>
<script src="${esc(ovServe.url('motion.js'))}" defer></script>
<script src="${esc(ovServe.url('motion.js'))}" defer></script>
<meta name="ov-boost" content="connect@${esc(RELEASE)}">`,
<meta name="ov-boost" content="connect@${esc(RELEASE)}">`,
@@ -52,7 +52,8 @@ function phone() {
@@ -52,7 +52,8 @@ function phone() {
}
}
function picker(mode = 'named') {
function picker(mode = 'named') {
- return `<div class="cn-pick" role="img" aria-label="Choosing who can open a share">${Object.entries(MODE_TEXT).map(([k, v]) => `<span class="cn-pick-row${k === mode ? ' is-on' : ''}"><i></i>${esc(v)}</span>`).join('')}
+ // The ghost cursor (openvibe-shared 4.106.0 motion.js) walks the three choices; the server draws "named" chosen.
+ return `<div class="cn-pick" role="img" aria-label="Choosing who can open a share" data-ov-ghost>${Object.entries(MODE_TEXT).map(([k, v], i) => `<span class="cn-pick-row${k === mode ? ' is-on' : ''}" data-ov-ghost-step="${i + 1}"><i></i>${esc(v)}</span>`).join('')}
<span class="cn-pick-names"><b>@sam</b><b>@riley</b></span></div>`;
<span class="cn-pick-names"><b>@sam</b><b>@riley</b></span></div>`;
}
}
@@ -216,9 +217,9 @@ ${modeChoices(share.mode, staff)}
@@ -216,9 +217,9 @@ ${modeChoices(share.mode, staff)}
<p class="cn-back"><a href="/">${kit.icon('back')} Your shares</a></p>
<p class="cn-back"><a href="/">${kit.icon('back')} Your shares</a></p>
${notice ? `<p class="cn-notice" role="status">${esc(notice)}</p>` : ''}
${notice ? `<p class="cn-notice" role="status">${esc(notice)}</p>` : ''}
${error ? `<p class="cn-error" role="alert">${esc(error)}</p>` : ''}
${error ? `<p class="cn-error" role="alert">${esc(error)}</p>` : ''}
-<header class="cn-head"><span class="cn-state ${closed ? 'is-closed' : connected ? 'is-on' : 'is-off'}"><i aria-hidden="true"></i>${esc(state)}</span>
+<header class="cn-head" data-cn-share="${esc(share.id)}"><span class="cn-state ${closed ? 'is-closed' : connected ? 'is-on' : 'is-off'}" role="status" aria-live="polite"><i aria-hidden="true"></i><span>${esc(state)}</span></span>
<h1>${closed ? esc(share.host) : `<a href="${esc(share.url)}">${esc(share.host)}</a>`}</h1>
<h1>${closed ? esc(share.host) : `<a href="${esc(share.url)}">${esc(share.host)}</a>`}</h1>
-<p>${esc(stateText)}</p></header>
+<p data-cn-line>${esc(stateText)}</p></header>
${key ? keyBlock(share, key, baseUrl) : ''}
${key ? keyBlock(share, key, baseUrl) : ''}
<dl class="cn-facts">
<dl class="cn-facts">
<div><dt>Who can open it</dt><dd>${esc(MODE_TEXT[share.mode] || share.mode)}${share.mode === 'named' && share.named.length ? `: ${share.named.map((n) => `@${esc(n)}`).join(', ')}` : ''}</dd></div>
<div><dt>Who can open it</dt><dd>${esc(MODE_TEXT[share.mode] || share.mode)}${share.mode === 'named' && share.named.length ? `: ${share.named.map((n) => `@${esc(n)}`).join(', ')}` : ''}</dd></div>