Files
ytplayer/scripts/make-pwa-icons.js
Jonathan Sykes 4efa1d1182 feat: convert to PWA — OPFS storage, service worker, Bun/Hono server
- Move native shells (Zig/src, Tauri/src-tauri, app.zon, releases) to legacy/
- Add Bun + Hono server with yt-dlp proxy endpoints (search, channel, streams,
  download), libsql (concurrent SQLite fork) for fingerprint-keyed playlist/
  history sync, and static file serving for the frontend
- Add Dockerfile + docker-compose.yml (single container, volume-mounted DB)
- Add frontend/sw.js: app-shell cache-first, /api/* network-only,
  thumbnails stale-while-revalidate, SW_UPDATE_AVAILABLE broadcast,
  SKIP_WAITING message handler for seamless auto-update
- Add frontend/manifest.webmanifest: standalone PWA, vermilion theme,
  search/history shortcuts
- Add frontend/icons/icon-{192,512}.png: generated PWA icons
- Add frontend/fingerprint.js: canvas+UA djb2 fingerprint, localStorage-cached,
  exposes window.getFingerprint() for server-side playlist keying
- Add frontend/opfs.js: full OPFS video store (writeFromResponse streams
  directly without full-file buffering), exposes window.OPFS
- Add scripts/make-pwa-icons.js: regenerate icons without external deps
- Patch frontend/app.js: WEB mode detection, webFetch + opfs* bridge wrappers,
  API object routes to WEB helpers when no native bridge present,
  Player.loadVideo handles OPFS blob URLs + revokes them on next load,
  SW registration + update banner in boot()
- Patch frontend/index.html: manifest link, theme-color, Apple PWA meta,
  CSP blob:/worker-src, fingerprint.js + opfs.js script tags
- Patch frontend/styles.css: .toast-update + .toast-reload-btn for update banner
- Native Tauri/Zig builds unchanged — all new code is additive via WEB flag
2026-06-30 06:43:23 +08:00

114 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Generates frontend/icons/icon-192.png and icon-512.png for the PWA manifest.
* Same vermilion play-button design as appicon.png — no image libraries needed.
*
* Usage: node scripts/make-pwa-icons.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
// ---- PNG encoder (same approach as make-icon.js) ----
const CRC_TABLE = (() => {
const t = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
t[n] = c >>> 0;
}
return t;
})();
function crc32(buf) {
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
const tb = Buffer.from(type, 'ascii');
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([tb, data])), 0);
return Buffer.concat([len, tb, data, crc]);
}
function makePng(SIZE) {
const px = Buffer.alloc(SIZE * SIZE * 4);
function set(x, y, r, g, b, a = 255) {
const i = (y * SIZE + x) * 4;
px[i] = r; px[i + 1] = g; px[i + 2] = b; px[i + 3] = a;
}
// Rounded-rectangle background (vermilion gradient)
const radius = Math.round(SIZE * 0.22);
for (let y = 0; y < SIZE; y++) {
const t = y / SIZE;
const r = Math.round(0xff + (0xd4 - 0xff) * t);
const g = Math.round(0x6a + (0x32 - 0x6a) * t);
const b = Math.round(0x52 + (0x1d - 0x52) * t);
for (let x = 0; x < SIZE; x++) {
// Simple corner rounding via distance check
const dx = Math.max(0, Math.abs(x - SIZE / 2) - (SIZE / 2 - radius));
const dy = Math.max(0, Math.abs(y - SIZE / 2) - (SIZE / 2 - radius));
if (dx * dx + dy * dy > radius * radius) {
set(x, y, 0, 0, 0, 0); // transparent outside rounded rect
} else {
set(x, y, r, g, b);
}
}
}
// White play triangle, centered
const scale = SIZE / 512;
const ax = Math.round(196 * scale), ay = Math.round(150 * scale);
const bx = Math.round(196 * scale), by = Math.round(362 * scale);
const cx = Math.round(384 * scale), cy = Math.round(256 * scale);
function edge(px1, py1, px2, py2, x, y) {
return (x - px1) * (py2 - py1) - (y - py1) * (px2 - px1);
}
const yMin = Math.round(120 * scale), yMax = Math.round(392 * scale);
const xMin = Math.round(170 * scale), xMax = Math.round(400 * scale);
for (let y = yMin; y < yMax; y++) {
for (let x = xMin; x < xMax; x++) {
const w0 = edge(bx, by, cx, cy, x, y);
const w1 = edge(cx, cy, ax, ay, x, y);
const w2 = edge(ax, ay, bx, by, x, y);
if ((w0 <= 0 && w1 <= 0 && w2 <= 0) || (w0 >= 0 && w1 >= 0 && w2 >= 0)) {
// Only paint if inside the rounded-rect background
const cur = (y * SIZE + x) * 4;
if (px[cur + 3] > 0) {
px[cur] = 255; px[cur + 1] = 255; px[cur + 2] = 255; px[cur + 3] = 255;
}
}
}
}
// Encode to PNG
const raw = Buffer.alloc((SIZE * 4 + 1) * SIZE);
for (let y = 0; y < SIZE; y++) {
raw[y * (SIZE * 4 + 1)] = 0;
px.copy(raw, y * (SIZE * 4 + 1) + 1, y * SIZE * 4, (y + 1) * SIZE * 4);
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(SIZE, 0);
ihdr.writeUInt32BE(SIZE, 4);
ihdr[8] = 8; ihdr[9] = 6; // 8-bit RGBA
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', ihdr),
chunk('IDAT', zlib.deflateSync(raw, { level: 6 })),
chunk('IEND', Buffer.alloc(0)),
]);
}
const outDir = path.join(__dirname, '..', 'frontend', 'icons');
fs.mkdirSync(outDir, { recursive: true });
for (const size of [192, 512]) {
const out = path.join(outDir, `icon-${size}.png`);
const data = makePng(size);
fs.writeFileSync(out, data);
console.log(`Wrote ${out} (${size}×${size}, ${data.length} bytes)`);
}
console.log('Done — PWA icons ready in frontend/icons/');