mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(mobile): rooms, live and upload capture, and the altitude-band identity
The Now feed mixes sensor, eye, track, number, witness and room cards across Orbit, Sky, Ground and Inside, ordered so neighbours differ, with composited thumbnails and live public-camera frames. Every situation has a room: typed, place- and time-anchored posts, evidence badges, bridging-rated notes and a cited Known / Disputed / Unknown local synthesis, with cross-tab presence over BroadcastChannel. Example posts are labelled as such. Capture adds Live (10 s segments sealed into a hash chain, shown on the globe as a view frustum) and Upload (EXIF placement, Unverified tier), and an attach sheet that files any capture into a nearby situation. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
38 files changed
+9837
-1081
No files matched your search
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Watch a live broadcast from another tab on this device: frames arrive over
|
||||
* BroadcastChannel about once a second with the broadcaster's telemetry and
|
||||
* sealed segment count. Watching reports back so the broadcaster's viewer
|
||||
* count is real.
|
||||
*/
|
||||
|
||||
import { esc, haptic } from './dom.js';
|
||||
import { createSheet } from './sheet.js';
|
||||
import { frame } from './cards.js';
|
||||
import { llShort, cardinal } from './bands.js';
|
||||
|
||||
export function createLiveViewer({ rt, globe }) {
|
||||
const sheet = createSheet({
|
||||
label: 'Live broadcast',
|
||||
detents: ['large'],
|
||||
modal: true,
|
||||
dismissible: true,
|
||||
closeButton: true,
|
||||
className: 'eye-sheet opaque',
|
||||
heights: { large: (H) => Math.round(Math.min(H * 0.86, 760)) },
|
||||
});
|
||||
let cur = null;
|
||||
let tick = 0;
|
||||
let pin = null;
|
||||
|
||||
function paint(l) {
|
||||
if (!cur || l.id !== cur.id) return;
|
||||
cur = { ...cur, ...l };
|
||||
const img = sheet.body.querySelector('.ev-frame img');
|
||||
if (img && l.frame) img.src = l.frame;
|
||||
const t = sheet.body.querySelector('[data-lv-tele]');
|
||||
if (t)
|
||||
t.textContent = `INSIDE · ${llShort(l.lat, l.lon, 3)} · HDG ${Math.round(l.headingDeg || 0)}° ${cardinal(l.headingDeg)} · ${l.segments || 0} SEALED SEGMENTS`;
|
||||
if (pin) globe.removePin(pin);
|
||||
if (Number.isFinite(l.lat))
|
||||
pin = globe.addPin({
|
||||
id: `watch:${l.id}`,
|
||||
kind: 'live',
|
||||
lon: l.lon,
|
||||
lat: l.lat,
|
||||
headingDeg: l.headingDeg || 0,
|
||||
fovDeg: 62,
|
||||
label: `${l.handle} · live`,
|
||||
});
|
||||
}
|
||||
|
||||
function open(w) {
|
||||
const l = rt.lives().find((x) => x.id === w.liveId) || {
|
||||
id: w.liveId,
|
||||
handle: w.handle,
|
||||
startedAt: w.startedAt,
|
||||
};
|
||||
cur = l;
|
||||
sheet.head.innerHTML = `<div class="c-band tele" data-band="inside"><i class="bd"></i>Inside<b>·</b>Live from a person</div>
|
||||
<h2 class="sheet-title">${esc(l.handle || 'Someone')} is live</h2>
|
||||
<div class="sheet-sub">${esc(l.situationTitle ? `Near ${l.situationTitle}` : 'Broadcasting from this device’s other tab')}</div>`;
|
||||
sheet.body.innerHTML = `<div class="ev-frame" data-band="inside"><img class="front" alt="Latest broadcast frame" ${l.frame ? `src="${esc(l.frame)}"` : ''}/>${frame()}
|
||||
<div class="c-top"><span class="c-live tele onair"><i></i>LIVE <span data-lv-since></span></span><span class="c-sid tele">SEALED EVERY 10 S</span></div>
|
||||
<div class="c-strip tele" data-lv-tele></div></div>
|
||||
<p class="footnote" style="padding:12px 2px 0">Frames reach this tab over BroadcastChannel about once a second (a prototype stand-in for a streaming server). The broadcaster’s file is sealed as a hash chain every 10 seconds; the bundle appears in their You tab when they end.</p>`;
|
||||
paint(l);
|
||||
sheet.open('large');
|
||||
rt.watch(l.id);
|
||||
haptic('light');
|
||||
clearInterval(tick);
|
||||
tick = setInterval(() => {
|
||||
const el = sheet.body.querySelector('[data-lv-since]');
|
||||
if (!el || !cur?.startedAt) return;
|
||||
const s = Math.max(0, Math.round((Date.now() - cur.startedAt) / 1000));
|
||||
el.textContent = `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
rt.on('live', paint);
|
||||
rt.on('live-end', ({ id }) => {
|
||||
if (cur?.id !== id) return;
|
||||
const t = sheet.body.querySelector('[data-lv-tele]');
|
||||
if (t)
|
||||
t.textContent =
|
||||
'BROADCAST ENDED · BUNDLE SEALED ON THE BROADCASTER’S DEVICE';
|
||||
sheet.body.querySelector('.c-live')?.classList.add('ended');
|
||||
});
|
||||
sheet.on('close', () => {
|
||||
clearInterval(tick);
|
||||
rt.watch(null);
|
||||
if (pin) globe.removePin(pin);
|
||||
pin = null;
|
||||
cur = null;
|
||||
});
|
||||
|
||||
return { open };
|
||||
}
|
||||
Reference in new issue
Block a user