Improve Android app UI and controller flows

This commit is contained in:
2026-10-09 16:43:58 +08:00
parent c0e26b97fe
commit 608229a497
90 changed files with 8103 additions and 1133 deletions
@@ -0,0 +1,54 @@
// Fixed 192x192 RGBA PNG codec, independent of DOM and shared by Node tests.
import { zlibSync, unzlibSync } from "./vendor/fflate/fflate.js";
const SIDE = 192, STRIDE = SIDE * 4;
const SIGNATURE = [137,80,78,71,13,10,26,10];
const crcTable = Uint32Array.from({ length: 256 }, (_, n) => {
for (let k = 0; k < 8; k++) n = n & 1 ? 0xedb88320 ^ (n >>> 1) : n >>> 1;
return n >>> 0;
});
function crc(bytes) { let n = 0xffffffff; for (const b of bytes) n = crcTable[(n ^ b) & 255] ^ (n >>> 8); return (n ^ 0xffffffff) >>> 0; }
function chunk(name, data) {
const out = new Uint8Array(data.length + 12), view = new DataView(out.buffer);
view.setUint32(0, data.length); out.set(Array.from(name, c => c.charCodeAt(0)), 4); out.set(data, 8);
view.setUint32(out.length - 4, crc(out.subarray(4, out.length - 4))); return out;
}
export function encodePixels(rgba) {
if (rgba.length !== SIDE * STRIDE) throw new Error("画笔像素长度无效");
const ihdr = new Uint8Array(13), v = new DataView(ihdr.buffer);
v.setUint32(0, SIDE); v.setUint32(4, SIDE); ihdr[8] = 8; ihdr[9] = 6;
const raw = new Uint8Array((STRIDE + 1) * SIDE);
for (let y = 0; y < SIDE; y++) raw.set(rgba.subarray(y * STRIDE, (y + 1) * STRIDE), y * (STRIDE + 1) + 1);
const parts = [new Uint8Array(SIGNATURE), chunk("IHDR", ihdr), chunk("IDAT", zlibSync(raw, { level: 6 })), chunk("IEND", new Uint8Array())];
const png = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let at = 0;
for (const p of parts) { png.set(p, at); at += p.length; }
let binary = ""; for (const b of png) binary += String.fromCharCode(b);
return btoa(binary);
}
function paeth(a,b,c) { const p=a+b-c, x=Math.abs(p-a), y=Math.abs(p-b), z=Math.abs(p-c); return x<=y && x<=z ? a : y<=z ? b : c; }
export function decodePixels(value) {
if (typeof value !== "string" || value.length > 300000 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) throw new Error("画笔PNG无效");
const bytes = Uint8Array.from(atob(value), c => c.charCodeAt(0)), view = new DataView(bytes.buffer);
if (!SIGNATURE.every((b,i) => bytes[i] === b)) throw new Error("画笔PNG签名无效");
const parts=[]; let header=false, ended=false;
for (let at=8; at+12<=bytes.length;) {
const n=view.getUint32(at); if (n>bytes.length-at-12) throw new Error("PNG截断");
const name=String.fromCharCode(...bytes.subarray(at+4,at+8)), data=bytes.subarray(at+8,at+8+n);
if (crc(bytes.subarray(at+4,at+8+n))!==view.getUint32(at+8+n)) throw new Error("PNG校验失败");
if (name==="IHDR") { if (header || n!==13 || view.getUint32(at+8)!==SIDE || view.getUint32(at+12)!==SIDE || data[8]!==8 || data[9]!==6 || data[10] || data[11] || data[12]) throw new Error("PNG必须为192×192 RGBA8"); header=true; }
else if (name==="IDAT") { if (!header || ended) throw new Error("PNG顺序无效"); parts.push(data); }
else if (name==="IEND") { if (n || at+12!==bytes.length) throw new Error("PNG结尾无效"); ended=true; }
else if (name[0]===name[0].toUpperCase()) throw new Error("PNG不支持此块");
at+=n+12;
}
if (!header || !ended || !parts.length) throw new Error("PNG不完整");
const compressed=new Uint8Array(parts.reduce((n,p)=>n+p.length,0)); let at=0; for (const p of parts) { compressed.set(p,at); at+=p.length; }
const expected=(STRIDE+1)*SIDE, raw=unzlibSync(compressed,{out:new Uint8Array(expected+1)});
if (raw.length!==expected) throw new Error("PNG展开长度无效");
const out=new Uint8ClampedArray(SIDE*STRIDE);
for (let y=0;y<SIDE;y++) { const filter=raw[y*(STRIDE+1)]; if (filter>4) throw new Error("PNG过滤器无效");
for (let x=0;x<STRIDE;x++) { const i=y*STRIDE+x, a=x>=4?out[i-4]:0, b=y?out[i-STRIDE]:0, c=y&&x>=4?out[i-STRIDE-4]:0;
out[i]=(raw[y*(STRIDE+1)+1+x]+([0,a,b,Math.floor((a+b)/2),paeth(a,b,c)][filter]))&255;
}
}
return out;
}