Files
Claude 6db1db70a0 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
2026-09-26 12:40:01 +00:00

96 lines
3.0 KiB
JavaScript

// 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)`);
}