Orbit Companion: a phone can keep its own look (from release 0.108.75)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,8 +104,11 @@ let tab = (() => { try { return localStorage.getItem('orbit.tab') || 'remote'; }
|
||||
if (!TABS.some(t => t[0] === tab)) tab = 'remote';
|
||||
let state = {now:null, guide:null, catchup:null, rooms:null, filter:'all'};
|
||||
|
||||
function look(l) {
|
||||
if (!l) return; const r = document.documentElement.style;
|
||||
// The phone wears the TV's look unless it keeps its own (Paul, 6 November 2026), kept on this phone only.
|
||||
const keptLook = () => { try { return localStorage.getItem('orbit.look'); } catch { return null; } };
|
||||
function look(tv) {
|
||||
const own = keptLook() && state.looks?.find(l => l.name === keptLook());
|
||||
const l = own || tv; if (!l) return; const r = document.documentElement.style;
|
||||
const map = {bg:l.background, surface:l.surface, raised:l.raised, deep:l.deep, accent:l.accent, 'on-accent':l.onAccent, ink:l.ink, muted:l.muted, mint:l.mint, sky:l.sky};
|
||||
for (const [k, v] of Object.entries(map)) if (v) r.setProperty('--' + k, v);
|
||||
document.querySelector('meta[name=theme-color]').content = l.background;
|
||||
@@ -191,7 +194,10 @@ function rooms() {
|
||||
function more() {
|
||||
return head('More') + `<div class="label">Your channels</div>
|
||||
<a class="line" href="/create" style="text-decoration:none;color:inherit">Create a channel <span>Name, shows, style, hours</span></a>
|
||||
<div class="label">Look</div><div class="card small">This phone wears the TV's look${state.now?.look ? ': <b style="color:var(--ink)">' + esc(state.now.look.name) + '</b>' : ''}. Change it on the TV in Settings › Look.</div>
|
||||
<div class="label">Look</div>
|
||||
<div class="chips" style="flex-wrap:wrap"><button class="${keptLook() ? '' : 'on'}" data-look="">Follow this TV${state.now?.look ? ' (' + esc(state.now.look.name) + ')' : ''}</button>
|
||||
${(state.looks || []).map(l => `<button class="${keptLook() === l.name ? 'on' : ''}" data-look="${esc(l.name)}">${esc(l.name)}</button>`).join('')}</div>
|
||||
<div class="small">Following the TV, each room's remote wears that room's look. Pick one to keep it on this phone instead, whatever the TV is set to.</div>
|
||||
<div class="label">About</div><div class="card small">Orbit Companion runs on the TV itself, over your home Wi-Fi. Nothing to install, no account. It works while Orbit is open on the TV.
|
||||
To stop this phone, remove it in Settings › Rooms and phones.</div>`;
|
||||
}
|
||||
@@ -209,6 +215,7 @@ async function load(what) {
|
||||
if (what === 'guide') state.guide = await api('/api/guide');
|
||||
if (what === 'catchup') state.catchup = await api('/api/catchup');
|
||||
if (what === 'rooms') state.rooms = await api('/api/rooms');
|
||||
if (what === 'more') { state.looks = (await api('/api/looks')).looks; look(state.now?.look); }
|
||||
offline = false;
|
||||
} catch (e) { if (!offline) toast(e.message === 'Failed to fetch' ? "Orbit isn't open on the TV" : e.message); offline = true; }
|
||||
render();
|
||||
@@ -219,18 +226,19 @@ const send = (path, body, done) => { buzz(); api(path, body).then(j => { done?.(
|
||||
document.addEventListener('click', e => {
|
||||
const b = e.target.closest('button,[data-tab]'); if (!b) return;
|
||||
const d = b.dataset;
|
||||
if (d.tab) { tab = d.tab; try { localStorage.setItem('orbit.tab', tab); } catch {} closeSheet(); render(); if (tab !== 'remote' && tab !== 'more') load(tab); return; }
|
||||
if (d.tab) { tab = d.tab; try { localStorage.setItem('orbit.tab', tab); } catch {} closeSheet(); render(); if (tab !== 'remote') load(tab); return; }
|
||||
if (d.key) send('/api/key', {key: d.key});
|
||||
if (d.vol) send('/api/volume', {change: d.vol});
|
||||
if (d.ch) send('/api/channel', {step: +d.ch});
|
||||
if (d.sheet) openSheet(moreControls());
|
||||
if (d.go) send('/api/section', {section: d.go}, () => { closeSheet(); toast('On the TV now'); });
|
||||
if (d.look !== undefined) { try { d.look ? localStorage.setItem('orbit.look', d.look) : localStorage.removeItem('orbit.look'); } catch {} look(state.now?.look); render(); return; }
|
||||
if (d.filter) { state.filter = d.filter; render(); }
|
||||
if (d.pick) openSheet(pickSheet(+d.pick));
|
||||
if (d.tune) send('/api/tune', {number: +d.tune}, () => { closeSheet(); toast('Watching on the TV'); setTimeout(() => load('guide'), 700); });
|
||||
if (d.cu) { const [number, startMs] = d.cu.split(':').map(Number); send('/api/catchup/watch', {number, startMs}, () => toast('Playing from the start on the TV')); }
|
||||
});
|
||||
render(); load('now'); if (tab !== 'remote' && tab !== 'more') load(tab);
|
||||
render(); (keptLook() || tab === 'more' ? load('more') : Promise.resolve()).then(() => load('now')); if (tab !== 'remote' && tab !== 'more') load(tab);
|
||||
// Kept fresh while the phone is looking: what's on every 15 seconds, and the open list every minute.
|
||||
setInterval(() => { if (!document.hidden) load('now'); }, 15000);
|
||||
setInterval(() => { if (!document.hidden && ['guide','catchup','rooms'].includes(tab)) load(tab); }, 60000);
|
||||
|
||||
@@ -55,13 +55,16 @@ internal object OrbitCompanion {
|
||||
val volumes = mapOf("up" to AudioManager.ADJUST_RAISE, "down" to AudioManager.ADJUST_LOWER, "mute" to AudioManager.ADJUST_TOGGLE_MUTE)
|
||||
|
||||
private fun hex(c: Color) = "#%06X".format(c.toArgb() and 0xFFFFFF)
|
||||
/** The TV's look, so the phone wears the same colours. */
|
||||
fun look(): JSONObject = OrbitTheme.palette.let { p ->
|
||||
JSONObject().put("name", OrbitTheme.look.title).put("light", p.light).put("background", hex(p.background)).put("surface", hex(p.surface))
|
||||
/** The TV's look, so the phone wears the same colours (Paul: by default; a phone can keep a look of its own from [looks]). */
|
||||
fun look(look: OrbitLook = OrbitTheme.look): JSONObject = look.palette.let { p ->
|
||||
JSONObject().put("name", look.title).put("light", p.light).put("background", hex(p.background)).put("surface", hex(p.surface))
|
||||
.put("raised", hex(p.raised)).put("deep", hex(p.deep)).put("accent", hex(p.accent)).put("onAccent", hex(p.onAccent))
|
||||
.put("ink", hex(p.ink)).put("muted", hex(p.muted)).put("mint", hex(p.mint)).put("sky", hex(p.sky)).put("gold", hex(p.gold))
|
||||
}
|
||||
|
||||
/** Every look, for a phone that keeps its own rather than following the TV. */
|
||||
fun looks(): JSONArray = JSONArray(OrbitLook.entries.map { look(it) })
|
||||
|
||||
fun programme(b: SampleBroadcast): JSONObject = guideTitleParts(b.media.item.title).let { (title, sub) ->
|
||||
JSONObject().put("title", title).put("sub", sub ?: JSONObject.NULL).put("startMs", b.startMs).put("endMs", b.endMs)
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ internal object PhoneLink {
|
||||
}
|
||||
}
|
||||
|
||||
private val companionRoutes = setOf("GET /api/now", "GET /api/guide", "GET /api/catchup", "GET /api/rooms", "POST /api/key", "POST /api/volume",
|
||||
private val companionRoutes = setOf("GET /api/now", "GET /api/guide", "GET /api/catchup", "GET /api/rooms", "GET /api/looks", "POST /api/key", "POST /api/volume",
|
||||
"POST /api/channel", "POST /api/tune", "POST /api/section", "POST /api/catchup/watch")
|
||||
/** The Companion's remote, guide, Catch Up and rooms (see [OrbitCompanion]); false when [r] is for Create your own. */
|
||||
private fun companion(r: Request, out: OutputStream): Boolean {
|
||||
@@ -340,6 +340,7 @@ internal object PhoneLink {
|
||||
"GET /api/guide" -> json(out, 200, OrbitCompanion.guide(s, now))
|
||||
"GET /api/catchup" -> json(out, 200, JSONObject().put("shelves", OrbitCompanion.catchUp(s, now)))
|
||||
"GET /api/rooms" -> json(out, 200, OrbitCompanion.rooms(appContext))
|
||||
"GET /api/looks" -> json(out, 200, JSONObject().put("looks", OrbitCompanion.looks()))
|
||||
"POST /api/key" -> {
|
||||
val code = OrbitCompanion.keys[body().optString("key")] ?: return json(out, 400, JSONObject().put("error", "Unknown button"))
|
||||
OrbitCompanion.press(code); json(out, 202, JSONObject().put("ok", true))
|
||||
|
||||
@@ -153,6 +153,8 @@ class PhoneLinkTest {
|
||||
val n = JSONObject(call("/api/now", cookie = cookie).body)
|
||||
assertEquals(101, n.getJSONObject("channel").getInt("number")); assertEquals("Friends", n.getJSONObject("now").getString("title"))
|
||||
assertTrue(n.getJSONObject("look").getString("accent").startsWith("#")); assertTrue(n.getBoolean("watching"))
|
||||
// Every look, for a phone that keeps its own.
|
||||
assertEquals(OrbitLook.entries.size, JSONObject(call("/api/looks", cookie = cookie).body).getJSONArray("looks").length())
|
||||
val g = JSONObject(call("/api/guide", cookie = cookie).body).getJSONArray("channels")
|
||||
assertEquals(listOf(101, 308), (0 until g.length()).map { g.getJSONObject(it).getInt("number") })
|
||||
// Buttons are pressed as the TV's own remote would, and only the known ones.
|
||||
|
||||
Reference in New Issue
Block a user