diff --git a/README.md b/README.md
index 49e1a5d..6ebfce3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# EuerChat - euer.tv IRC & Livestream Client (Windows 64-bit)
+

+
Ein moderner, performanter und schlanker Desktop-Client für Windows 64-bit, gebaut mit **Rust + Tauri v2** und einem ansprechenden **Dark-Mode Web-Frontend**.
---
diff --git a/assets/logo-icon.svg b/assets/logo-icon.svg
new file mode 100644
index 0000000..c1f312f
--- /dev/null
+++ b/assets/logo-icon.svg
@@ -0,0 +1,49 @@
+
\ No newline at end of file
diff --git a/assets/logo-wide.png b/assets/logo-wide.png
new file mode 100644
index 0000000..3660e4c
Binary files /dev/null and b/assets/logo-wide.png differ
diff --git a/assets/logo-wide.svg b/assets/logo-wide.svg
new file mode 100644
index 0000000..b976e07
--- /dev/null
+++ b/assets/logo-wide.svg
@@ -0,0 +1,82 @@
+
\ No newline at end of file
diff --git a/installer/EuerChat.nsi b/installer/EuerChat.nsi
index 908511c..dbec3e4 100644
--- a/installer/EuerChat.nsi
+++ b/installer/EuerChat.nsi
@@ -68,6 +68,12 @@ Var RadioPortable
!define MUI_ICON "..\src-tauri\icons\icon.ico"
!define MUI_UNICON "..\src-tauri\icons\icon.ico"
!define MUI_ABORTWARNING
+!define MUI_WELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
+!define MUI_UNWELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
+!define MUI_HEADERIMAGE
+!define MUI_HEADERIMAGE_BITMAP "header.bmp"
+!define MUI_HEADERIMAGE_UNBITMAP "header.bmp"
+!define MUI_HEADERIMAGE_RIGHT
; Seiten
!define MUI_PAGE_CUSTOMFUNCTION_PRE WelcomePre
@@ -307,10 +313,11 @@ Section "Install"
WriteRegDWORD ${ROOT} "${UNINSTKEY}" "NoRepair" 1
WriteRegDWORD ${ROOT} "${UNINSTKEY}" "EstimatedSize" 7600
- CreateShortcut "$SMPROGRAMS\${PRODUCT}.lnk" "$INSTDIR\${MAINEXE}"
+ CreateShortcut "$SMPROGRAMS\${PRODUCT}.lnk" "$INSTDIR\${MAINEXE}" "" "$INSTDIR\${MAINEXE}" 0
+ ; Desktop-Verknuepfung bei jeder Neuinstallation (nicht bei stillen Updates oder /NS)
${If} $UpdateMode <> 1
${AndIf} $NoShortcut <> 1
- CreateShortcut "$DESKTOP\${PRODUCT}.lnk" "$INSTDIR\${MAINEXE}"
+ CreateShortcut "$DESKTOP\${PRODUCT}.lnk" "$INSTDIR\${MAINEXE}" "" "$INSTDIR\${MAINEXE}" 0
${EndIf}
${EndIf}
SectionEnd
diff --git a/installer/header.bmp b/installer/header.bmp
new file mode 100644
index 0000000..349e671
Binary files /dev/null and b/installer/header.bmp differ
diff --git a/installer/sidebar.bmp b/installer/sidebar.bmp
new file mode 100644
index 0000000..ab794e4
Binary files /dev/null and b/installer/sidebar.bmp differ
diff --git a/package.json b/package.json
index c94b6d7..4cccd35 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"scripts": {
"tauri": "tauri",
"dev": "tauri dev",
- "build": "tauri build"
+ "build": "tauri build",
+ "brand": "node tools/make-branding.mjs"
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.0"
diff --git a/src-tauri/build.rs b/src-tauri/build.rs
index d860e1e..60f6ec3 100644
--- a/src-tauri/build.rs
+++ b/src-tauri/build.rs
@@ -1,3 +1,5 @@
fn main() {
+ // Tauri bettet das Symbol nur beim Ausfuehren dieses Skripts ein; ohne diese Zeile bliebe ein geaendertes Icon unbemerkt.
+ println!("cargo:rerun-if-changed=icons/icon.ico");
tauri_build::build()
}
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png
index 613c57a..ce0929a 100644
Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ
diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png
index 5e71b91..7bab16d 100644
Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ
diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico
index 34da097..44304ec 100644
Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ
diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png
index 613c57a..a614aac 100644
Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ
diff --git a/tools/make-branding.mjs b/tools/make-branding.mjs
new file mode 100644
index 0000000..3953461
--- /dev/null
+++ b/tools/make-branding.mjs
@@ -0,0 +1,350 @@
+// Erzeugt das EuerChat-Logo (Icon, Wortmarke, Installer-Bilder) aus SVG und rendert es mit Edge (Headless).
+// Aufruf: node tools/make-branding.mjs
+import { spawn } from 'node:child_process';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import zlib from 'node:zlib';
+import { fileURLToPath } from 'node:url';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const EDGE = [
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
+].find(fs.existsSync);
+if (!EDGE) throw new Error('Microsoft Edge nicht gefunden');
+
+const FONT = "'Segoe UI Black','Arial Black',Verdana,sans-serif";
+
+// ---------------------------------------------------------------------------
+// SVG-Bausteine
+// ---------------------------------------------------------------------------
+
+/** Rotes Orb-Symbol mit Chrom-Mikrofon und Chat-Blase (512x512). `p` macht die IDs eindeutig. */
+function iconInner(p) {
+ const slots = [];
+ for (let y = 144; y <= 268; y += 17) {
+ slots.push(``);
+ slots.push(``);
+ }
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${slots.join('')}
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+}
+
+function iconSvg(size) {
+ return `${iconInner('i')}`;
+}
+
+/** Chrom-Schriftzug: dunkle Kontur, silberner Rand, helle Fuellung. */
+function chromeText(p, x, y, size, text, width) {
+ const common = `x="${x}" y="${y}" font-family="${FONT}" font-size="${size}" text-anchor="middle" textLength="${width}" lengthAdjust="spacingAndGlyphs" stroke-linejoin="round"`;
+ return `
+${text}
+${text}
+${text}`;
+}
+
+function textDefs(p) {
+ return `
+
+
+
+
+
+
+
+
+`;
+}
+
+/** Breites Logo im Stil des euer.radio-Logos: [euer.tv] (Orb) [chat] in einer roten Pille. */
+function wideSvg(measure) {
+ const size = 170;
+ const wL = Math.round(measure.left);
+ const wR = Math.round(measure.right);
+ const orb = 540;
+ const gap = 60;
+ const padEnd = 150;
+ const width = padEnd + wL + gap + orb + gap + wR + padEnd;
+ const height = 560;
+ const cx = padEnd + wL + gap + orb / 2;
+ const baseline = 335;
+ return {
+ width,
+ height,
+ svg: `
+
+ ${textDefs('w')}
+
+
+
+
+
+
+
+
+
+
+
+
+${chromeText('w', padEnd + wL / 2 + 20, baseline, size, 'euer.tv', wL)}
+${chromeText('w', width - padEnd - wR / 2 - 20, baseline, size, 'chat', wR)}
+${iconInner('w')}
+`,
+ };
+}
+
+/** Hochformat fuer die Willkommensseite des Installers (164x314). */
+function sidebarSvg() {
+ return `
+
+ ${textDefs('s')}
+
+
+
+
+
+${iconInner('s')}
+${chromeText('s', 82, 214, 31, 'euer.tv', 126)}
+${chromeText('s', 82, 262, 40, 'chat', 92)}
+`;
+}
+
+function headerSvg(wide) {
+ const w = 150, h = 57;
+ const scale = Math.min(w / wide.width, h / wide.height);
+ return `
+
+${wide.svg.replace(/^]*>/, '').replace(/<\/svg>\s*$/, '')}
+`;
+}
+
+// ---------------------------------------------------------------------------
+// Rendering mit Edge (Chrome DevTools Protocol)
+// ---------------------------------------------------------------------------
+
+async function startEdge() {
+ const port = 9341;
+ const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'euerchat-brand-'));
+ const proc = spawn(EDGE, [
+ '--headless=new', `--remote-debugging-port=${port}`, `--user-data-dir=${profile}`,
+ '--hide-scrollbars', '--force-device-scale-factor=1', '--disable-gpu', 'about:blank',
+ ], { stdio: 'ignore' });
+ let page;
+ for (let i = 0; i < 50 && !page; i++) {
+ await new Promise((r) => setTimeout(r, 200));
+ try {
+ const targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
+ page = targets.find((t) => t.type === 'page');
+ } catch { /* Edge startet noch */ }
+ }
+ if (!page) throw new Error('Edge-Debugport nicht erreichbar');
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise((r) => (ws.onopen = r));
+ let id = 0;
+ const pending = new Map();
+ ws.onmessage = (e) => {
+ const m = JSON.parse(e.data);
+ if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result ?? m.error); pending.delete(m.id); }
+ };
+ const send = (method, params = {}) => new Promise((res) => { const i = ++id; pending.set(i, res); ws.send(JSON.stringify({ id: i, method, params })); });
+ await send('Page.enable');
+ await send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
+ return {
+ send,
+ async html(markup, width, height) {
+ await send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false });
+ const doc = `${markup}`;
+ await send('Page.navigate', { url: 'data:text/html;base64,' + Buffer.from(doc).toString('base64') });
+ await new Promise((r) => setTimeout(r, 500));
+ await send('Runtime.evaluate', { expression: 'document.fonts.ready.then(()=>1)', awaitPromise: true });
+ },
+ async png() {
+ const r = await send('Page.captureScreenshot', { format: 'png', fromSurface: true });
+ return Buffer.from(r.data, 'base64');
+ },
+ async eval(expression) {
+ const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
+ return r.result?.value;
+ },
+ close() { ws.close(); proc.kill(); },
+ };
+}
+
+// ---------------------------------------------------------------------------
+// ICO / BMP
+// ---------------------------------------------------------------------------
+
+/** ICO mit PNG-Frames, groesste zuerst (Tauri nimmt das erste Bild als Fenstersymbol). */
+function makeIco(frames) {
+ const sorted = [...frames].sort((a, b) => b.size - a.size);
+ const header = Buffer.alloc(6);
+ header.writeUInt16LE(1, 2);
+ header.writeUInt16LE(sorted.length, 4);
+ let offset = 6 + sorted.length * 16;
+ const entries = sorted.map((f) => {
+ const e = Buffer.alloc(16);
+ e[0] = f.size >= 256 ? 0 : f.size;
+ e[1] = f.size >= 256 ? 0 : f.size;
+ e.writeUInt16LE(1, 4);
+ e.writeUInt16LE(32, 6);
+ e.writeUInt32LE(f.png.length, 8);
+ e.writeUInt32LE(offset, 12);
+ offset += f.png.length;
+ return e;
+ });
+ return Buffer.concat([header, ...entries, ...sorted.map((f) => f.png)]);
+}
+
+function decodePng(buf) {
+ let pos = 8;
+ let width = 0, height = 0, colorType = 0;
+ const idat = [];
+ while (pos < buf.length) {
+ const len = buf.readUInt32BE(pos);
+ const type = buf.toString('ascii', pos + 4, pos + 8);
+ const data = buf.subarray(pos + 8, pos + 8 + len);
+ if (type === 'IHDR') { width = data.readUInt32BE(0); height = data.readUInt32BE(4); colorType = data[9]; }
+ if (type === 'IDAT') idat.push(data);
+ pos += 12 + len;
+ }
+ const bpp = colorType === 6 ? 4 : 3;
+ const raw = zlib.inflateSync(Buffer.concat(idat));
+ const stride = width * bpp;
+ const px = Buffer.alloc(height * stride);
+ for (let y = 0; y < height; y++) {
+ const filter = raw[y * (stride + 1)];
+ for (let x = 0; x < stride; x++) {
+ const v = raw[y * (stride + 1) + 1 + x];
+ const a = x >= bpp ? px[y * stride + x - bpp] : 0;
+ const b = y > 0 ? px[(y - 1) * stride + x] : 0;
+ const c = x >= bpp && y > 0 ? px[(y - 1) * stride + x - bpp] : 0;
+ let out;
+ if (filter === 0) out = v;
+ else if (filter === 1) out = v + a;
+ else if (filter === 2) out = v + b;
+ else if (filter === 3) out = v + ((a + b) >> 1);
+ else {
+ const p = a + b - c;
+ const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
+ out = v + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
+ }
+ px[y * stride + x] = out & 255;
+ }
+ }
+ return { width, height, bpp, px };
+}
+
+/** 24-Bit-BMP (NSIS kann nur BMP), Transparenz wird auf `bg` gemischt. */
+function pngToBmp(png, bg = [255, 255, 255]) {
+ const { width, height, bpp, px } = decodePng(png);
+ const rowSize = Math.ceil((width * 3) / 4) * 4;
+ const out = Buffer.alloc(54 + rowSize * height);
+ out.write('BM', 0);
+ out.writeUInt32LE(out.length, 2);
+ out.writeUInt32LE(54, 10);
+ out.writeUInt32LE(40, 14);
+ out.writeInt32LE(width, 18);
+ out.writeInt32LE(height, 22);
+ out.writeUInt16LE(1, 26);
+ out.writeUInt16LE(24, 28);
+ out.writeUInt32LE(rowSize * height, 34);
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const i = (y * width + x) * bpp;
+ const alpha = bpp === 4 ? px[i + 3] / 255 : 1;
+ const o = 54 + (height - 1 - y) * rowSize + x * 3;
+ for (let c = 0; c < 3; c++) out[o + c] = Math.round(px[i + 2 - c] * alpha + bg[2 - c] * (1 - alpha));
+ }
+ }
+ return out;
+}
+
+// ---------------------------------------------------------------------------
+
+const edge = await startEdge();
+try {
+ // Textbreiten der Wortmarke messen, damit nichts verzerrt wird
+ await edge.html(`euer.tvchat`, 400, 200);
+ const measure = {
+ left: await edge.eval("document.getElementById('a').getComputedTextLength()"),
+ right: await edge.eval("document.getElementById('b').getComputedTextLength()"),
+ };
+ console.log('Textbreiten:', measure);
+
+ const render = async (svg, w, h) => { await edge.html(svg, w, h); return edge.png(); };
+ const out = {
+ icons: path.join(root, 'src-tauri', 'icons'),
+ assets: path.join(root, 'assets'),
+ installer: path.join(root, 'installer'),
+ ui: path.join(root, 'ui'),
+ };
+ for (const d of Object.values(out)) fs.mkdirSync(d, { recursive: true });
+
+ const frames = [];
+ for (const size of [256, 128, 64, 48, 32, 24, 16]) frames.push({ size, png: await render(iconSvg(size), size, size) });
+ fs.writeFileSync(path.join(out.icons, 'icon.ico'), makeIco(frames));
+ const bySize = (s) => frames.find((f) => f.size === s).png;
+ fs.writeFileSync(path.join(out.icons, '32x32.png'), bySize(32));
+ fs.writeFileSync(path.join(out.icons, '128x128.png'), bySize(128));
+ fs.writeFileSync(path.join(out.icons, 'icon.png'), await render(iconSvg(512), 512, 512));
+ fs.writeFileSync(path.join(out.ui, 'logo.png'), bySize(128));
+
+ const wide = wideSvg(measure);
+ fs.writeFileSync(path.join(out.assets, 'logo-wide.png'), await render(wide.svg, wide.width, wide.height));
+ fs.writeFileSync(path.join(out.assets, 'logo-icon.svg'), iconSvg(512));
+ fs.writeFileSync(path.join(out.assets, 'logo-wide.svg'), wide.svg);
+
+ fs.writeFileSync(path.join(out.installer, 'sidebar.bmp'), pngToBmp(await render(sidebarSvg(), 164, 314), [5, 0, 0]));
+ fs.writeFileSync(path.join(out.installer, 'header.bmp'), pngToBmp(await render(headerSvg(wide), 150, 57)));
+ console.log('Fertig:', Object.values(out).join('\n '));
+} finally {
+ edge.close();
+}
diff --git a/ui/index.html b/ui/index.html
index 35f5648..deb738d 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -12,6 +12,7 @@