- 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
114 lines
3.9 KiB
JavaScript
114 lines
3.9 KiB
JavaScript
#!/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/');
|