696 lines
16 KiB
JavaScript
696 lines
16 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
var STORAGE_KEY = "webos-reference-lab-state";
|
|
var focusables = [];
|
|
var focusedIndex = 0;
|
|
var logItems = [];
|
|
var networkSubscribed = false;
|
|
var lastNetworkStatus = null;
|
|
var canvas = document.getElementById("motionCanvas");
|
|
var ctx = canvas.getContext("2d");
|
|
var sceneNames = ["Aurora Signal", "Ember Orbit", "Orchid Pulse"];
|
|
var themes = ["theme-aurora", "theme-ember", "theme-orchid"];
|
|
var state = readStorage();
|
|
var lastFrame = 0;
|
|
var requestFrame = window.requestAnimationFrame || function (callback) {
|
|
return window.setTimeout(function () {
|
|
callback(new Date().getTime());
|
|
}, 16);
|
|
};
|
|
|
|
function $(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function readStorage() {
|
|
var fallback = {
|
|
scene: 0,
|
|
theme: 0,
|
|
opens: 0,
|
|
lastAction: "Fresh install"
|
|
};
|
|
|
|
try {
|
|
var raw = window.localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) {
|
|
return fallback;
|
|
}
|
|
|
|
var parsed = JSON.parse(raw);
|
|
fallback.scene = Number(parsed.scene) || 0;
|
|
fallback.theme = Number(parsed.theme) || 0;
|
|
fallback.opens = Number(parsed.opens) || 0;
|
|
fallback.lastAction = parsed.lastAction || fallback.lastAction;
|
|
return fallback;
|
|
} catch (error) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function writeStorage() {
|
|
try {
|
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
} catch (error) {
|
|
addLog("Storage", "localStorage unavailable");
|
|
}
|
|
}
|
|
|
|
function setText(id, value) {
|
|
var node = $(id);
|
|
if (node) {
|
|
node.textContent = value;
|
|
}
|
|
}
|
|
|
|
function compactJson(value) {
|
|
try {
|
|
return JSON.stringify(value, null, 2);
|
|
} catch (error) {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function setDetail(title, payload) {
|
|
setText("detailTitle", title);
|
|
setText("detailJson", compactJson(payload));
|
|
}
|
|
|
|
function addLog(label, message) {
|
|
var stamp = new Date();
|
|
var time = two(stamp.getHours()) + ":" + two(stamp.getMinutes()) + ":" + two(stamp.getSeconds());
|
|
logItems.unshift({
|
|
label: label,
|
|
message: message,
|
|
time: time
|
|
});
|
|
logItems = logItems.slice(0, 4);
|
|
renderLog();
|
|
}
|
|
|
|
function renderLog() {
|
|
var log = $("eventLog");
|
|
var html = "";
|
|
var i;
|
|
|
|
for (i = 0; i < logItems.length; i += 1) {
|
|
html += '<div class="log-line"><strong>' + escapeHtml(logItems[i].time + " " + logItems[i].label) + '</strong> ' + escapeHtml(logItems[i].message) + "</div>";
|
|
}
|
|
|
|
log.innerHTML = html;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function two(value) {
|
|
value = Number(value) || 0;
|
|
return value < 10 ? "0" + value : String(value);
|
|
}
|
|
|
|
function applyTheme() {
|
|
var i;
|
|
|
|
for (i = 0; i < themes.length; i += 1) {
|
|
document.body.classList.remove(themes[i]);
|
|
}
|
|
|
|
state.scene = state.scene % sceneNames.length;
|
|
state.theme = state.theme % themes.length;
|
|
document.body.classList.add(themes[state.theme]);
|
|
setText("sceneLabel", sceneNames[state.scene]);
|
|
}
|
|
|
|
function initFocus() {
|
|
focusables = Array.prototype.slice.call(document.querySelectorAll("[data-focusable]"));
|
|
|
|
focusables.forEach(function (node, index) {
|
|
node.addEventListener("focus", function () {
|
|
focusCard(index);
|
|
});
|
|
|
|
node.addEventListener("click", function () {
|
|
focusCard(index);
|
|
runAction(node.getAttribute("data-action"));
|
|
});
|
|
});
|
|
|
|
focusCard(0);
|
|
}
|
|
|
|
function focusCard(index) {
|
|
var i;
|
|
|
|
if (!focusables.length) {
|
|
return;
|
|
}
|
|
|
|
if (index < 0) {
|
|
index = focusables.length - 1;
|
|
}
|
|
|
|
if (index >= focusables.length) {
|
|
index = 0;
|
|
}
|
|
|
|
focusedIndex = index;
|
|
for (i = 0; i < focusables.length; i += 1) {
|
|
focusables[i].classList.toggle("is-focused", i === focusedIndex);
|
|
}
|
|
|
|
if (document.activeElement !== focusables[focusedIndex]) {
|
|
focusables[focusedIndex].focus();
|
|
}
|
|
}
|
|
|
|
function moveFocus(direction) {
|
|
var current = focusables[focusedIndex];
|
|
var currentRect;
|
|
var currentCenter;
|
|
var bestIndex = focusedIndex;
|
|
var bestScore = Infinity;
|
|
var i;
|
|
|
|
if (!current) {
|
|
return;
|
|
}
|
|
|
|
currentRect = current.getBoundingClientRect();
|
|
currentCenter = centerOf(currentRect);
|
|
|
|
for (i = 0; i < focusables.length; i += 1) {
|
|
var candidate = focusables[i];
|
|
var rect = candidate.getBoundingClientRect();
|
|
var center = centerOf(rect);
|
|
var dx = center.x - currentCenter.x;
|
|
var dy = center.y - currentCenter.y;
|
|
var primary;
|
|
var secondary;
|
|
var score;
|
|
|
|
if (i === focusedIndex) {
|
|
continue;
|
|
}
|
|
|
|
if (direction === "left" && dx >= -4) {
|
|
continue;
|
|
}
|
|
|
|
if (direction === "right" && dx <= 4) {
|
|
continue;
|
|
}
|
|
|
|
if (direction === "up" && dy >= -4) {
|
|
continue;
|
|
}
|
|
|
|
if (direction === "down" && dy <= 4) {
|
|
continue;
|
|
}
|
|
|
|
primary = direction === "left" || direction === "right" ? Math.abs(dx) : Math.abs(dy);
|
|
secondary = direction === "left" || direction === "right" ? Math.abs(dy) : Math.abs(dx);
|
|
score = primary * 8 + secondary;
|
|
|
|
if (score < bestScore) {
|
|
bestScore = score;
|
|
bestIndex = i;
|
|
}
|
|
}
|
|
|
|
if (bestIndex !== focusedIndex) {
|
|
focusCard(bestIndex);
|
|
}
|
|
}
|
|
|
|
function centerOf(rect) {
|
|
return {
|
|
x: rect.left + rect.width / 2,
|
|
y: rect.top + rect.height / 2
|
|
};
|
|
}
|
|
|
|
function handleKeydown(event) {
|
|
var code = event.keyCode || event.which;
|
|
var keyName = event.key || String(code);
|
|
|
|
setText("keyValue", keyName);
|
|
|
|
if (code === 37) {
|
|
event.preventDefault();
|
|
moveFocus("left");
|
|
} else if (code === 38) {
|
|
event.preventDefault();
|
|
moveFocus("up");
|
|
} else if (code === 39) {
|
|
event.preventDefault();
|
|
moveFocus("right");
|
|
} else if (code === 40) {
|
|
event.preventDefault();
|
|
moveFocus("down");
|
|
} else if (code === 13) {
|
|
event.preventDefault();
|
|
runAction(focusables[focusedIndex].getAttribute("data-action"));
|
|
} else if (code === 461 || code === 10009 || code === 27) {
|
|
event.preventDefault();
|
|
handleBack();
|
|
} else if (code === 403) {
|
|
cycleScene();
|
|
} else if (code === 404) {
|
|
refreshAll();
|
|
} else if (code === 405) {
|
|
runAction("storage");
|
|
} else if (code === 406) {
|
|
logItems = [];
|
|
renderLog();
|
|
}
|
|
}
|
|
|
|
function handleBack() {
|
|
addLog("Back", "platformBack requested");
|
|
|
|
if (window.webOS && typeof window.webOS.platformBack === "function") {
|
|
window.webOS.platformBack();
|
|
}
|
|
}
|
|
|
|
function runAction(action) {
|
|
state.lastAction = action;
|
|
|
|
if (action === "clock") {
|
|
getClock();
|
|
} else if (action === "device") {
|
|
getDeviceInfo();
|
|
} else if (action === "network") {
|
|
getNetworkStatus();
|
|
} else if (action === "launch") {
|
|
launchBrowser();
|
|
} else if (action === "storage") {
|
|
saveLocalState();
|
|
} else if (action === "motion") {
|
|
cycleScene();
|
|
}
|
|
|
|
writeStorage();
|
|
}
|
|
|
|
function refreshAll() {
|
|
loadAppInfo();
|
|
loadLaunchParams();
|
|
getClock();
|
|
getDeviceInfo();
|
|
getNetworkStatus();
|
|
getLGUDID();
|
|
addLog("Refresh", "runtime probes requested");
|
|
}
|
|
|
|
function loadAppInfo() {
|
|
var fallback = {
|
|
id: window.webOS && window.webOS.fetchAppId ? window.webOS.fetchAppId() : "browser.preview",
|
|
title: "webOS TV Reference Lab",
|
|
root: window.webOS && window.webOS.fetchAppRootPath ? window.webOS.fetchAppRootPath() : window.location.href
|
|
};
|
|
|
|
if (window.webOS && typeof window.webOS.fetchAppInfo === "function") {
|
|
window.webOS.fetchAppInfo(function (info) {
|
|
var payload = info || fallback;
|
|
payload.root = fallback.root;
|
|
setText("appTitle", payload.title || payload.id || "App");
|
|
setDetail("App Info", payload);
|
|
addLog("App", "appinfo.json loaded");
|
|
});
|
|
} else {
|
|
setText("appTitle", fallback.title);
|
|
setDetail("App Info", fallback);
|
|
}
|
|
|
|
setText("platformValue", platformLabel());
|
|
}
|
|
|
|
function platformLabel() {
|
|
var platform = window.webOS && window.webOS.platform ? window.webOS.platform : {};
|
|
var labels = [];
|
|
var key;
|
|
|
|
for (key in platform) {
|
|
if (platform.hasOwnProperty(key) && platform[key]) {
|
|
labels.push(key);
|
|
}
|
|
}
|
|
|
|
if (window.PalmSystem && window.PalmSystem.deviceInfo) {
|
|
labels.push("PalmSystem");
|
|
}
|
|
|
|
return labels.length ? labels.join(", ") : "Browser";
|
|
}
|
|
|
|
function loadLaunchParams() {
|
|
var params = {};
|
|
|
|
if (window.webOSDev && typeof window.webOSDev.launchParams === "function") {
|
|
params = window.webOSDev.launchParams();
|
|
}
|
|
|
|
if (!params || Object.keys(params).length === 0) {
|
|
params = {
|
|
mode: "preview",
|
|
scene: sceneNames[state.scene],
|
|
opens: state.opens
|
|
};
|
|
}
|
|
|
|
setText("launchParams", compactJson(params));
|
|
}
|
|
|
|
function getClock() {
|
|
if (window.webOS && window.webOS.service) {
|
|
window.webOS.service.request("luna://com.palm.systemservice", {
|
|
method: "clock/getTime",
|
|
parameters: {},
|
|
onSuccess: function (args) {
|
|
var payload = {
|
|
utc: args.utc,
|
|
localtime: args.localtime,
|
|
timezone: args.timezone,
|
|
source: "LS2"
|
|
};
|
|
setText("clockValue", formatNow());
|
|
setDetail("System Clock", payload);
|
|
addLog("Clock", "LS2 response received");
|
|
},
|
|
onFailure: function (args) {
|
|
useBrowserClock(args);
|
|
}
|
|
});
|
|
} else {
|
|
useBrowserClock({
|
|
errorText: "PalmServiceBridge is not available"
|
|
});
|
|
}
|
|
}
|
|
|
|
function useBrowserClock(error) {
|
|
var payload = {
|
|
local: new Date().toString(),
|
|
timezone: window.webOS && window.webOS.systemInfo ? window.webOS.systemInfo().timezone : "browser",
|
|
source: "Browser fallback",
|
|
error: error && (error.errorText || error.errorCode)
|
|
};
|
|
|
|
setText("clockValue", formatNow());
|
|
setDetail("System Clock", payload);
|
|
addLog("Clock", "browser fallback used");
|
|
}
|
|
|
|
function formatNow() {
|
|
var now = new Date();
|
|
return two(now.getHours()) + ":" + two(now.getMinutes());
|
|
}
|
|
|
|
function getDeviceInfo() {
|
|
if (window.webOS && typeof window.webOS.deviceInfo === "function") {
|
|
window.webOS.deviceInfo(function (info) {
|
|
var payload = info || {};
|
|
payload.webOSTVjs = window.webOS.libVersion || "unknown";
|
|
setDetail("Device Info", payload);
|
|
addLog("Device", payload.modelName || "device info updated");
|
|
});
|
|
} else {
|
|
setDetail("Device Info", {
|
|
userAgent: window.navigator.userAgent,
|
|
screen: window.screen.width + "x" + window.screen.height,
|
|
webOSTVjs: "not loaded"
|
|
});
|
|
addLog("Device", "browser info shown");
|
|
}
|
|
}
|
|
|
|
function getNetworkStatus() {
|
|
if (networkSubscribed) {
|
|
if (lastNetworkStatus) {
|
|
setDetail("Connection", lastNetworkStatus);
|
|
}
|
|
addLog("Network", "subscription already active");
|
|
return;
|
|
}
|
|
|
|
networkSubscribed = true;
|
|
|
|
if (window.webOSDev && window.webOSDev.connection) {
|
|
window.webOSDev.connection.getStatus({
|
|
subscribe: true,
|
|
onSuccess: function (status) {
|
|
var online = status.isInternetConnectionAvailable || status.wired || status.wifi || window.navigator.onLine;
|
|
lastNetworkStatus = status;
|
|
setText("networkValue", online ? "Online" : "Offline");
|
|
setDetail("Connection", status);
|
|
addLog("Network", "connection status updated");
|
|
},
|
|
onFailure: function (error) {
|
|
networkSubscribed = false;
|
|
useBrowserNetwork(error);
|
|
}
|
|
});
|
|
} else {
|
|
networkSubscribed = false;
|
|
useBrowserNetwork({
|
|
errorText: "webOSDev.connection is not available"
|
|
});
|
|
}
|
|
}
|
|
|
|
function useBrowserNetwork(error) {
|
|
var payload = {
|
|
onLine: window.navigator.onLine,
|
|
source: "Browser fallback",
|
|
error: error && (error.errorText || error.errorCode)
|
|
};
|
|
|
|
lastNetworkStatus = payload;
|
|
setText("networkValue", payload.onLine ? "Online" : "Offline");
|
|
setDetail("Connection", payload);
|
|
addLog("Network", "browser status shown");
|
|
}
|
|
|
|
function getLGUDID() {
|
|
if (window.webOSDev && typeof window.webOSDev.LGUDID === "function") {
|
|
window.webOSDev.LGUDID({
|
|
onSuccess: function (payload) {
|
|
addLog("LGUDID", maskId(payload.id));
|
|
},
|
|
onFailure: function () {
|
|
addLog("LGUDID", "not available in this runtime");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function maskId(id) {
|
|
if (!id || id.length < 8) {
|
|
return "hidden";
|
|
}
|
|
|
|
return id.slice(0, 4) + "..." + id.slice(id.length - 4);
|
|
}
|
|
|
|
function launchBrowser() {
|
|
var target = "https://webostv.developer.lge.com/";
|
|
var payload = {
|
|
id: "com.webos.app.browser",
|
|
target: target
|
|
};
|
|
|
|
setDetail("Launch Browser", payload);
|
|
|
|
if (window.webOSDev && typeof window.webOSDev.launch === "function" && window.PalmServiceBridge) {
|
|
window.webOSDev.launch({
|
|
id: window.webOSDev.APP.BROWSER,
|
|
params: {
|
|
target: target
|
|
},
|
|
onSuccess: function () {
|
|
addLog("Launch", "browser launch requested");
|
|
},
|
|
onFailure: function (error) {
|
|
addLog("Launch", error.errorText || "launch failed");
|
|
}
|
|
});
|
|
} else {
|
|
window.open(target, "_blank");
|
|
addLog("Launch", "window.open fallback");
|
|
}
|
|
}
|
|
|
|
function saveLocalState() {
|
|
state.opens += 1;
|
|
state.theme = (state.theme + 1) % themes.length;
|
|
state.lastAction = "storage";
|
|
applyTheme();
|
|
writeStorage();
|
|
loadLaunchParams();
|
|
setDetail("Local State", {
|
|
key: STORAGE_KEY,
|
|
value: state
|
|
});
|
|
addLog("Storage", "state saved");
|
|
}
|
|
|
|
function cycleScene() {
|
|
state.scene = (state.scene + 1) % sceneNames.length;
|
|
state.theme = state.scene;
|
|
applyTheme();
|
|
writeStorage();
|
|
loadLaunchParams();
|
|
setDetail("Canvas Scene", {
|
|
scene: sceneNames[state.scene],
|
|
canvas: canvas.width + "x" + canvas.height,
|
|
animation: "requestAnimationFrame"
|
|
});
|
|
addLog("Canvas", sceneNames[state.scene]);
|
|
}
|
|
|
|
function resizeCanvas() {
|
|
var rect = canvas.getBoundingClientRect();
|
|
var ratio = window.devicePixelRatio || 1;
|
|
var width = Math.max(320, Math.floor(rect.width * ratio));
|
|
var height = Math.max(180, Math.floor(rect.height * ratio));
|
|
|
|
if (canvas.width !== width || canvas.height !== height) {
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
}
|
|
}
|
|
|
|
function drawFrame(timestamp) {
|
|
paintCanvas(timestamp);
|
|
requestFrame(drawFrame);
|
|
}
|
|
|
|
function paintCanvas(timestamp) {
|
|
var width;
|
|
var height;
|
|
var i;
|
|
var t;
|
|
|
|
if (!lastFrame) {
|
|
lastFrame = timestamp;
|
|
}
|
|
|
|
resizeCanvas();
|
|
width = canvas.width;
|
|
height = canvas.height;
|
|
t = timestamp * 0.001;
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
drawBackdrop(width, height, t);
|
|
|
|
for (i = 0; i < 9; i += 1) {
|
|
drawOrb(width, height, t, i);
|
|
}
|
|
|
|
drawBars(width, height, t);
|
|
lastFrame = timestamp;
|
|
}
|
|
|
|
function drawBackdrop(width, height, t) {
|
|
var gradient = ctx.createLinearGradient(0, 0, width, height);
|
|
|
|
if (state.scene === 1) {
|
|
gradient.addColorStop(0, "#2a1710");
|
|
gradient.addColorStop(0.55, "#11161b");
|
|
gradient.addColorStop(1, "#293227");
|
|
} else if (state.scene === 2) {
|
|
gradient.addColorStop(0, "#1b112a");
|
|
gradient.addColorStop(0.5, "#101620");
|
|
gradient.addColorStop(1, "#203226");
|
|
} else {
|
|
gradient.addColorStop(0, "#0b1720");
|
|
gradient.addColorStop(0.55, "#11141a");
|
|
gradient.addColorStop(1, "#1d2b21");
|
|
}
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.globalAlpha = 0.18;
|
|
ctx.fillStyle = "#ffffff";
|
|
|
|
for (var i = 0; i < 7; i += 1) {
|
|
var y = (height / 7) * i + Math.sin(t + i) * 18;
|
|
ctx.fillRect(0, y, width, 1);
|
|
}
|
|
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
function drawOrb(width, height, t, index) {
|
|
var colors = [
|
|
["#6ce3cf", "#f3cf56"],
|
|
["#ff7c67", "#83d96c"],
|
|
["#c49cff", "#6aa9ff"]
|
|
];
|
|
var palette = colors[state.scene];
|
|
var radius = width * (0.035 + (index % 3) * 0.012);
|
|
var x = width * (0.12 + index * 0.095) + Math.sin(t * (0.7 + index * 0.05)) * width * 0.045;
|
|
var y = height * (0.48 + Math.sin(t * 0.8 + index) * 0.27);
|
|
var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
|
|
gradient.addColorStop(0, palette[index % 2]);
|
|
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
ctx.globalAlpha = 0.78;
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
function drawBars(width, height, t) {
|
|
var x = width * 0.64;
|
|
var y = height * 0.22;
|
|
var barWidth = width * 0.035;
|
|
var gap = width * 0.018;
|
|
var i;
|
|
|
|
for (i = 0; i < 7; i += 1) {
|
|
var level = 0.22 + Math.abs(Math.sin(t * 1.5 + i * 0.7)) * 0.56;
|
|
ctx.fillStyle = i % 2 ? "rgba(255,255,255,0.26)" : "rgba(155,231,211,0.62)";
|
|
ctx.fillRect(x + i * (barWidth + gap), y + height * (0.52 - level), barWidth, height * level);
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
state.opens += 1;
|
|
applyTheme();
|
|
writeStorage();
|
|
initFocus();
|
|
loadAppInfo();
|
|
loadLaunchParams();
|
|
getClock();
|
|
getNetworkStatus();
|
|
getDeviceInfo();
|
|
window.addEventListener("keydown", handleKeydown);
|
|
window.addEventListener("online", function () {
|
|
useBrowserNetwork();
|
|
});
|
|
window.addEventListener("offline", function () {
|
|
useBrowserNetwork();
|
|
});
|
|
paintCanvas(0);
|
|
requestFrame(drawFrame);
|
|
addLog("Ready", "reference lab started");
|
|
}
|
|
|
|
window.onerror = function (message) {
|
|
addLog("Error", message);
|
|
};
|
|
|
|
init();
|
|
}());
|