Add local ADB web remote for Android TV
Node/Express server shells out to adb (connect/pair/keyevent/text/app launch) and serves a mobile-first installable PWA remote: power, mute, input switch, volume/channel rockers, D-pad, media transport, text entry, and one-tap streaming app launch. Supports multiple saved TVs and the Android 11+ wireless-debugging pairing flow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSVyYcrjrbmrMKT85G4fyn
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// One-off generator for the PWA app icons: a rounded remote-control glyph
|
||||
// on a dark background. Pure Node (zlib + zlib.crc32), no image deps.
|
||||
import { deflateSync, crc32 } from "node:zlib";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
|
||||
const BG = [11, 13, 18]; // #0b0d12
|
||||
const FG = [88, 214, 141]; // accent green
|
||||
const FG_DIM = [46, 58, 74];
|
||||
|
||||
function chunk(type, data) {
|
||||
const typeBuf = Buffer.from(type, "ascii");
|
||||
const len = Buffer.alloc(4);
|
||||
len.writeUInt32BE(data.length, 0);
|
||||
const crcInput = Buffer.concat([typeBuf, data]);
|
||||
const crc = Buffer.alloc(4);
|
||||
crc.writeUInt32BE(crc32(crcInput) >>> 0, 0);
|
||||
return Buffer.concat([len, typeBuf, data, crc]);
|
||||
}
|
||||
|
||||
function inRoundedRect(x, y, w, h, left, top, right, bottom, radius) {
|
||||
if (x < left || x > right || y < top || y > bottom) return false;
|
||||
const cx = x < left + radius ? left + radius : x > right - radius ? right - radius : x;
|
||||
const cy = y < top + radius ? top + radius : y > bottom - radius ? bottom - radius : y;
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
return dx * dx + dy * dy <= radius * radius || (x >= left + radius && x <= right - radius) || (y >= top + radius && y <= bottom - radius);
|
||||
}
|
||||
|
||||
function makeIcon(size) {
|
||||
const margin = Math.round(size * 0.2);
|
||||
const left = margin;
|
||||
const right = size - margin;
|
||||
const top = Math.round(size * 0.08);
|
||||
const bottom = size - top;
|
||||
const radius = Math.round(size * 0.14);
|
||||
|
||||
const btnRadius = Math.max(2, Math.round(size * 0.035));
|
||||
const btnCenters = [
|
||||
[size * 0.5, size * 0.28],
|
||||
[size * 0.5, size * 0.46],
|
||||
[size * 0.32, size * 0.62],
|
||||
[size * 0.68, size * 0.62],
|
||||
[size * 0.5, size * 0.78],
|
||||
];
|
||||
|
||||
const raw = Buffer.alloc(size * (1 + size * 4));
|
||||
for (let y = 0; y < size; y++) {
|
||||
let rowStart = y * (1 + size * 4);
|
||||
raw[rowStart] = 0; // filter type: none
|
||||
for (let x = 0; x < size; x++) {
|
||||
let [r, g, b] = BG;
|
||||
if (inRoundedRect(x, y, size, size, left, top, right, bottom, radius)) {
|
||||
[r, g, b] = FG_DIM;
|
||||
for (const [bx, by] of btnCenters) {
|
||||
const dx = x - bx;
|
||||
const dy = y - by;
|
||||
if (dx * dx + dy * dy <= btnRadius * btnRadius) {
|
||||
[r, g, b] = FG;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const off = rowStart + 1 + x * 4;
|
||||
raw[off] = r;
|
||||
raw[off + 1] = g;
|
||||
raw[off + 2] = b;
|
||||
raw[off + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(size, 0);
|
||||
ihdr.writeUInt32BE(size, 4);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 6; // color type RGBA
|
||||
ihdr[10] = 0;
|
||||
ihdr[11] = 0;
|
||||
ihdr[12] = 0;
|
||||
|
||||
const idat = deflateSync(raw);
|
||||
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
return Buffer.concat([
|
||||
signature,
|
||||
chunk("IHDR", ihdr),
|
||||
chunk("IDAT", idat),
|
||||
chunk("IEND", Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
mkdirSync(new URL("../public/icons", import.meta.url), { recursive: true });
|
||||
for (const size of [192, 512]) {
|
||||
const png = makeIcon(size);
|
||||
writeFileSync(new URL(`../public/icons/icon-${size}.png`, import.meta.url), png);
|
||||
console.log(`wrote icon-${size}.png (${png.length} bytes)`);
|
||||
}
|
||||
Reference in New Issue
Block a user