Initial commit: Claude Live Dashboard
Always-on-Top Electron-Widget für die Claude-Plan-Auslastung unter Windows. Liest Transkripte lokal, fragt optional den /usage-Endpunkt ab, bietet einen Einstellungsdialog und lässt sich als NSIS-Installer oder portable Fassung bauen.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Erzeugt die Tray-Symbole als PNG — ohne Bildbibliothek, nur mit zlib aus der
|
||||
* Standardbibliothek. Einmalig aufzurufen:
|
||||
*
|
||||
* node tools/make-icon.js
|
||||
*
|
||||
* Motiv: ein Ring mit einem Sektor, der den Füllstand andeutet.
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const zlib = require('node:zlib');
|
||||
|
||||
const OUT_DIR = path.join(__dirname, '..', 'assets');
|
||||
|
||||
/** CRC32 nach PNG-Spezifikation. */
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Int32Array(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;
|
||||
table[n] = c;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(buf) {
|
||||
let c = -1;
|
||||
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
||||
return (c ^ -1) >>> 0;
|
||||
}
|
||||
|
||||
function chunk(type, data) {
|
||||
const out = Buffer.alloc(data.length + 12);
|
||||
out.writeUInt32BE(data.length, 0);
|
||||
out.write(type, 4, 'ascii');
|
||||
data.copy(out, 8);
|
||||
const crcInput = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
||||
out.writeUInt32BE(crc32(crcInput), data.length + 8);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} size Kantenlänge
|
||||
* @param {(x: number, y: number) => [number, number, number, number]} shade RGBA je Pixel
|
||||
*/
|
||||
function makePng(size, shade) {
|
||||
// Scanlines mit Filterbyte 0 je Zeile
|
||||
const raw = Buffer.alloc(size * (size * 4 + 1));
|
||||
let p = 0;
|
||||
for (let y = 0; y < size; y++) {
|
||||
raw[p++] = 0;
|
||||
for (let x = 0; x < size; x++) {
|
||||
const [r, g, b, a] = shade(x, y);
|
||||
raw[p++] = r;
|
||||
raw[p++] = g;
|
||||
raw[p++] = b;
|
||||
raw[p++] = a;
|
||||
}
|
||||
}
|
||||
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(size, 0);
|
||||
ihdr.writeUInt32BE(size, 4);
|
||||
ihdr[8] = 8; // Bittiefe
|
||||
ihdr[9] = 6; // Farbtyp RGBA
|
||||
ihdr[10] = 0;
|
||||
ihdr[11] = 0;
|
||||
ihdr[12] = 0;
|
||||
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||
chunk('IHDR', ihdr),
|
||||
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
||||
chunk('IEND', Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Ring mit Füllsektor; kantengeglättet über 4×4-Supersampling. */
|
||||
function ringShader(size, color, fillFraction = 0.68) {
|
||||
const c = (size - 1) / 2;
|
||||
const rOuter = size * 0.46;
|
||||
const rInner = size * 0.28;
|
||||
const SS = 4;
|
||||
|
||||
return (x, y) => {
|
||||
let hits = 0;
|
||||
for (let sy = 0; sy < SS; sy++) {
|
||||
for (let sx = 0; sx < SS; sx++) {
|
||||
const px = x + (sx + 0.5) / SS - 0.5;
|
||||
const py = y + (sy + 0.5) / SS - 0.5;
|
||||
const dx = px - c;
|
||||
const dy = py - c;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist > rOuter || dist < rInner) continue;
|
||||
// Winkel ab 12 Uhr im Uhrzeigersinn
|
||||
let ang = Math.atan2(dx, -dy);
|
||||
if (ang < 0) ang += Math.PI * 2;
|
||||
if (ang <= fillFraction * Math.PI * 2) hits++;
|
||||
}
|
||||
}
|
||||
const a = Math.round((hits / (SS * SS)) * 255);
|
||||
return [color[0], color[1], color[2], a];
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
const orange = [217, 89, 38]; // Slot 2 der Palette
|
||||
|
||||
for (const size of [16, 32]) {
|
||||
const file = path.join(OUT_DIR, `tray${size === 16 ? '' : `@${size}`}.png`);
|
||||
fs.writeFileSync(file, makePng(size, ringShader(size, orange)));
|
||||
console.log(`geschrieben: ${file}`);
|
||||
}
|
||||
|
||||
// Fenster- und Anwendungssymbol
|
||||
const file = path.join(OUT_DIR, 'icon.png');
|
||||
fs.writeFileSync(file, makePng(256, ringShader(256, orange)));
|
||||
console.log(`geschrieben: ${file}`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user