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
96 lines
3.0 KiB
JavaScript
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)`);
|
|
}
|