This phone wears the TV's look${state.now?.look ? ': ' + esc(state.now.look.name) + '' : ''}. Change it on the TV in Settings › Look.
+
Look
+
+ ${(state.looks || []).map(l => ``).join('')}
+
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.
About
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.
`;
}
@@ -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);
diff --git a/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt b/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt
index 0bc916f..6d0f49d 100644
--- a/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt
+++ b/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt
@@ -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)
}
diff --git a/android/app/src/main/java/tv/orbit/classic/PhoneLink.kt b/android/app/src/main/java/tv/orbit/classic/PhoneLink.kt
index 19823fc..c6c906a 100644
--- a/android/app/src/main/java/tv/orbit/classic/PhoneLink.kt
+++ b/android/app/src/main/java/tv/orbit/classic/PhoneLink.kt
@@ -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))
diff --git a/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt b/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt
index 45e980a..685839e 100644
--- a/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt
+++ b/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt
@@ -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.