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:
Winkler, Stefan
2026-07-22 12:59:54 +02:00
commit 868f31fa9a
28 changed files with 8971 additions and 0 deletions
+125
View File
@@ -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();