Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baef47ba8e | ||
|
|
92d02d0008 |
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
# EuerChat - euer.tv IRC & Livestream Client (Windows 64-bit)
|
# EuerChat - euer.tv IRC & Livestream Client (Windows 64-bit)
|
||||||
|
|
||||||
|
<p align="center"><img src="assets/logo-wide.png" alt="euer.tv chat" width="560"></p>
|
||||||
|
|
||||||
Ein moderner, performanter und schlanker Desktop-Client für Windows 64-bit, gebaut mit **Rust + Tauri v2** und einem ansprechenden **Dark-Mode Web-Frontend**.
|
Ein moderner, performanter und schlanker Desktop-Client für Windows 64-bit, gebaut mit **Rust + Tauri v2** und einem ansprechenden **Dark-Mode Web-Frontend**.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -38,14 +40,17 @@ Ein moderner, performanter und schlanker Desktop-Client für Windows 64-bit, geb
|
|||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
1. **Installieren:** `EuerChat_<Version>_x64-setup.exe` ausführen (Installation pro Benutzer, keine Administratorrechte nötig). Beim ersten Start einfach den Namen eingeben und verbinden.
|
1. **Installieren:** `EuerChat_<Version>_x64-setup.exe` ausführen und die Installationsart wählen. Beim ersten Start einfach den Namen eingeben und verbinden.
|
||||||
* Die Konfiguration liegt in `%APPDATA%\EuerChat\config.json` und enthält eventuell Passwörter. Sie ist nicht Teil des Installers und gehört nicht ins Repository (steht in `.gitignore`).
|
* **Standard** (empfohlen): für alle Benutzer, standardmäßig nach `C:\Program Files\EuerChat`, mit Startmenü- und Desktop-Verknüpfung. Erfordert Administratorrechte (UAC-Abfrage), Deinstallation über die Windows-Einstellungen. Die Konfiguration liegt in `%APPDATA%\EuerChat\config.json`.
|
||||||
* Alternativ ohne Installation: `EuerChat.exe` direkt starten. Eine alte `config.json` neben der EXE wird noch gelesen und beim nächsten Speichern nach `%APPDATA%` übernommen.
|
* **Portable:** nur Kopieren in einen frei wählbaren Ordner (Vorschlag: neben dem Installer), z. B. auf einen USB-Stick. Keine Administratorrechte, keine Registry-Einträge. Die Konfiguration liegt im Unterordner `data` (Marker-Datei `portable.flag`). Entfernen: Ordner löschen.
|
||||||
|
* Die Konfiguration kann Passwörter enthalten. Sie ist nicht Teil des Installers und gehört nicht ins Repository (steht in `.gitignore`).
|
||||||
|
* Der Installer lässt sich auch still nutzen: `/S /MODE=standard|portable /D=<Ordner>` (`/D=` immer zuletzt). Der Updater verwendet genau diese Parameter und aktualisiert Standard- und Portable-Installationen an Ort und Stelle (Standard mit UAC-Abfrage, Portable ohne). Einstellungen bleiben erhalten.
|
||||||
|
* Alternativ ohne Installer: `EuerChat.exe` direkt starten. Eine alte `config.json` neben der EXE wird noch gelesen und beim nächsten Speichern nach `%APPDATA%` übernommen.
|
||||||
2. **Installer bauen:**
|
2. **Installer bauen:**
|
||||||
```powershell
|
```powershell
|
||||||
.\build.bat
|
.\build.bat
|
||||||
```
|
```
|
||||||
Benötigt Rust und Node.js. Der Installer liegt danach in `dist\`, die portable `EuerChat.exe` im Hauptordner.
|
Benötigt Rust, Node.js und NSIS 3 (`winget install NSIS.NSIS`; der von Tauri geladene NSIS-Cache wird ebenfalls gefunden). Das Skript baut die App ohne Tauri-Bundler und dann den eigenen Installer `installer\EuerChat.nsi`. Ergebnis: `dist\EuerChat_<Version>_x64-setup.exe`, dazu die lose `EuerChat.exe` im Hauptordner.
|
||||||
3. **Tests:** `cd src-tauri; cargo test`
|
3. **Tests:** `cd src-tauri; cargo test`
|
||||||
4. **Release veröffentlichen:**
|
4. **Release veröffentlichen:**
|
||||||
1. Version in `src-tauri\tauri.conf.json` und `src-tauri\Cargo.toml` erhöhen, optional Release-Notizen in `RELEASE_NOTES.md` schreiben.
|
1. Version in `src-tauri\tauri.conf.json` und `src-tauri\Cargo.toml` erhöhen, optional Release-Notizen in `RELEASE_NOTES.md` schreiben.
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="iorb" cx="50%" cy="32%" r="82%">
|
||||||
|
<stop offset="0" stop-color="#ff5a3d"/><stop offset=".36" stop-color="#e50b0b"/>
|
||||||
|
<stop offset=".76" stop-color="#8c0000"/><stop offset="1" stop-color="#3d0000"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="iring" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset=".45" stop-color="#8a8a8a"/><stop offset="1" stop-color="#e9e9e9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ichrome" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#555"/><stop offset=".2" stop-color="#bdbdbd"/><stop offset=".4" stop-color="#fff"/>
|
||||||
|
<stop offset=".58" stop-color="#cfcfcf"/><stop offset=".82" stop-color="#777"/><stop offset="1" stop-color="#454545"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="igloss" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ibubble" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#c3ccd3"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="iorbClip"><circle cx="256" cy="256" r="214"/></clipPath>
|
||||||
|
<clipPath id="iheadClip"><rect x="198" y="118" width="116" height="172" rx="58"/></clipPath>
|
||||||
|
<filter id="ishadow" x="-40%" y="-30%" width="180%" height="170%">
|
||||||
|
<feDropShadow dx="3" dy="9" stdDeviation="7" flood-color="#000" flood-opacity=".6"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="iblur" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="6"/></filter>
|
||||||
|
</defs>
|
||||||
|
<circle cx="256" cy="256" r="252" fill="url(#iring)"/>
|
||||||
|
<circle cx="256" cy="256" r="237" fill="#0b0b0b"/>
|
||||||
|
<circle cx="256" cy="256" r="214" fill="url(#iorb)"/>
|
||||||
|
<g clip-path="url(#iorbClip)">
|
||||||
|
<ellipse cx="256" cy="118" rx="172" ry="86" fill="url(#igloss)" opacity=".75"/>
|
||||||
|
<ellipse cx="256" cy="478" rx="200" ry="96" fill="#ff3a1c" opacity=".5"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#ishadow)" transform="rotate(-10 256 260)">
|
||||||
|
<rect x="198" y="118" width="116" height="172" rx="58" fill="url(#ichrome)"/>
|
||||||
|
<g clip-path="url(#iheadClip)"><line x1="196" x2="316" y1="144" y2="144" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="150.5" y2="150.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="161" y2="161" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="167.5" y2="167.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="178" y2="178" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="184.5" y2="184.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="195" y2="195" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="201.5" y2="201.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="212" y2="212" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="218.5" y2="218.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="229" y2="229" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="235.5" y2="235.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="246" y2="246" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="252.5" y2="252.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="263" y2="263" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="269.5" y2="269.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/></g>
|
||||||
|
<rect x="198" y="118" width="116" height="172" rx="58" fill="none" stroke="#2c2c2c" stroke-width="5"/>
|
||||||
|
<rect x="214" y="288" width="84" height="26" rx="10" fill="url(#ichrome)" stroke="#2c2c2c" stroke-width="4"/>
|
||||||
|
<path d="M236 314 H276 L268 372 H244 Z" fill="url(#ichrome)" stroke="#2c2c2c" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<rect x="230" y="372" width="52" height="26" rx="6" fill="#2a2a2a" stroke="#777" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="296" cy="150" r="16" fill="#fff" opacity=".9" filter="url(#iblur)"/>
|
||||||
|
<path d="M296 122 L301 145 L324 150 L301 155 L296 178 L291 155 L268 150 L291 145 Z" fill="#fff"/>
|
||||||
|
<g filter="url(#ishadow)">
|
||||||
|
<path d="M322 392 L294 446 L362 396 Z" fill="url(#ibubble)" stroke="#0b0b0b" stroke-width="10" stroke-linejoin="round"/>
|
||||||
|
<rect x="288" y="296" width="146" height="102" rx="32" fill="url(#ibubble)" stroke="#0b0b0b" stroke-width="10"/>
|
||||||
|
<path d="M322 392 L318 400 L352 394 Z" fill="url(#ibubble)"/>
|
||||||
|
<circle cx="326" cy="347" r="12" fill="#d00000"/><circle cx="361" cy="347" r="12" fill="#d00000"/><circle cx="396" cy="347" r="12" fill="#d00000"/>
|
||||||
|
</g></svg>
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 264 KiB |
@@ -0,0 +1,82 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1911" height="560" viewBox="0 0 1911 560">
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
<linearGradient id="wsilver" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff"/><stop offset=".5" stop-color="#9c9c9c"/><stop offset="1" stop-color="#f1f1f1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wletters" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset=".55" stop-color="#e6e6e6"/><stop offset="1" stop-color="#bcbcbc"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="wtshadow" x="-10%" y="-20%" width="120%" height="160%">
|
||||||
|
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-color="#000" flood-opacity=".5"/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="wpillStroke" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ff5a45"/><stop offset=".4" stop-color="#e00000"/><stop offset="1" stop-color="#7d0000"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wpillFill" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#d9d9d9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="wpillShadow" x="-5%" y="-20%" width="110%" height="150%">
|
||||||
|
<feDropShadow dx="0" dy="10" stdDeviation="9" flood-color="#000" flood-opacity=".45"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect x="46" y="86" width="1819" height="388" rx="194" fill="url(#wpillFill)" stroke="url(#wpillStroke)" stroke-width="56" filter="url(#wpillShadow)"/>
|
||||||
|
<rect x="74" y="114" width="1763" height="332" rx="166" fill="none" stroke="#6d0000" stroke-opacity=".55" stroke-width="6"/>
|
||||||
|
|
||||||
|
<text x="468" y="335" font-family="'Segoe UI Black','Arial Black',Verdana,sans-serif" font-size="170" text-anchor="middle" textLength="596" lengthAdjust="spacingAndGlyphs" stroke-linejoin="round" fill="#1a1a1a" stroke="#1a1a1a" stroke-width="34" filter="url(#wtshadow)">euer.tv</text>
|
||||||
|
<text x="468" y="335" font-family="'Segoe UI Black','Arial Black',Verdana,sans-serif" font-size="170" text-anchor="middle" textLength="596" lengthAdjust="spacingAndGlyphs" stroke-linejoin="round" fill="none" stroke="url(#wsilver)" stroke-width="22.1">euer.tv</text>
|
||||||
|
<text x="468" y="335" font-family="'Segoe UI Black','Arial Black',Verdana,sans-serif" font-size="170" text-anchor="middle" textLength="596" lengthAdjust="spacingAndGlyphs" stroke-linejoin="round" fill="url(#wletters)" stroke="#5e5e5e" stroke-width="2.04">euer.tv</text>
|
||||||
|
|
||||||
|
<text x="1563.5" y="335" font-family="'Segoe UI Black','Arial Black',Verdana,sans-serif" font-size="170" text-anchor="middle" textLength="355" lengthAdjust="spacingAndGlyphs" stroke-linejoin="round" fill="#1a1a1a" stroke="#1a1a1a" stroke-width="34" filter="url(#wtshadow)">chat</text>
|
||||||
|
<text x="1563.5" y="335" font-family="'Segoe UI Black','Arial Black',Verdana,sans-serif" font-size="170" text-anchor="middle" textLength="355" lengthAdjust="spacingAndGlyphs" stroke-linejoin="round" fill="none" stroke="url(#wsilver)" stroke-width="22.1">chat</text>
|
||||||
|
<text x="1563.5" y="335" font-family="'Segoe UI Black','Arial Black',Verdana,sans-serif" font-size="170" text-anchor="middle" textLength="355" lengthAdjust="spacingAndGlyphs" stroke-linejoin="round" fill="url(#wletters)" stroke="#5e5e5e" stroke-width="2.04">chat</text>
|
||||||
|
<svg x="806" y="10" width="540" height="540" viewBox="0 0 512 512">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="worb" cx="50%" cy="32%" r="82%">
|
||||||
|
<stop offset="0" stop-color="#ff5a3d"/><stop offset=".36" stop-color="#e50b0b"/>
|
||||||
|
<stop offset=".76" stop-color="#8c0000"/><stop offset="1" stop-color="#3d0000"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="wring" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset=".45" stop-color="#8a8a8a"/><stop offset="1" stop-color="#e9e9e9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wchrome" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#555"/><stop offset=".2" stop-color="#bdbdbd"/><stop offset=".4" stop-color="#fff"/>
|
||||||
|
<stop offset=".58" stop-color="#cfcfcf"/><stop offset=".82" stop-color="#777"/><stop offset="1" stop-color="#454545"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wgloss" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wbubble" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#c3ccd3"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="worbClip"><circle cx="256" cy="256" r="214"/></clipPath>
|
||||||
|
<clipPath id="wheadClip"><rect x="198" y="118" width="116" height="172" rx="58"/></clipPath>
|
||||||
|
<filter id="wshadow" x="-40%" y="-30%" width="180%" height="170%">
|
||||||
|
<feDropShadow dx="3" dy="9" stdDeviation="7" flood-color="#000" flood-opacity=".6"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="wblur" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="6"/></filter>
|
||||||
|
</defs>
|
||||||
|
<circle cx="256" cy="256" r="252" fill="url(#wring)"/>
|
||||||
|
<circle cx="256" cy="256" r="237" fill="#0b0b0b"/>
|
||||||
|
<circle cx="256" cy="256" r="214" fill="url(#worb)"/>
|
||||||
|
<g clip-path="url(#worbClip)">
|
||||||
|
<ellipse cx="256" cy="118" rx="172" ry="86" fill="url(#wgloss)" opacity=".75"/>
|
||||||
|
<ellipse cx="256" cy="478" rx="200" ry="96" fill="#ff3a1c" opacity=".5"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#wshadow)" transform="rotate(-10 256 260)">
|
||||||
|
<rect x="198" y="118" width="116" height="172" rx="58" fill="url(#wchrome)"/>
|
||||||
|
<g clip-path="url(#wheadClip)"><line x1="196" x2="316" y1="144" y2="144" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="150.5" y2="150.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="161" y2="161" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="167.5" y2="167.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="178" y2="178" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="184.5" y2="184.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="195" y2="195" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="201.5" y2="201.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="212" y2="212" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="218.5" y2="218.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="229" y2="229" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="235.5" y2="235.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="246" y2="246" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="252.5" y2="252.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/><line x1="196" x2="316" y1="263" y2="263" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/><line x1="204" x2="308" y1="269.5" y2="269.5" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/></g>
|
||||||
|
<rect x="198" y="118" width="116" height="172" rx="58" fill="none" stroke="#2c2c2c" stroke-width="5"/>
|
||||||
|
<rect x="214" y="288" width="84" height="26" rx="10" fill="url(#wchrome)" stroke="#2c2c2c" stroke-width="4"/>
|
||||||
|
<path d="M236 314 H276 L268 372 H244 Z" fill="url(#wchrome)" stroke="#2c2c2c" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<rect x="230" y="372" width="52" height="26" rx="6" fill="#2a2a2a" stroke="#777" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="296" cy="150" r="16" fill="#fff" opacity=".9" filter="url(#wblur)"/>
|
||||||
|
<path d="M296 122 L301 145 L324 150 L301 155 L296 178 L291 155 L268 150 L291 145 Z" fill="#fff"/>
|
||||||
|
<g filter="url(#wshadow)">
|
||||||
|
<path d="M322 392 L294 446 L362 396 Z" fill="url(#wbubble)" stroke="#0b0b0b" stroke-width="10" stroke-linejoin="round"/>
|
||||||
|
<rect x="288" y="296" width="146" height="102" rx="32" fill="url(#wbubble)" stroke="#0b0b0b" stroke-width="10"/>
|
||||||
|
<path d="M322 392 L318 400 L352 394 Z" fill="url(#wbubble)"/>
|
||||||
|
<circle cx="326" cy="347" r="12" fill="#d00000"/><circle cx="361" cy="347" r="12" fill="#d00000"/><circle cx="396" cy="347" r="12" fill="#d00000"/>
|
||||||
|
</g></svg>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.0 KiB |
@@ -8,14 +8,26 @@ if not exist node_modules (
|
|||||||
if errorlevel 1 goto :fail
|
if errorlevel 1 goto :fail
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Baue Installer (NSIS)...
|
echo Baue EuerChat.exe ...
|
||||||
call npx tauri build
|
call npx tauri build --no-bundle
|
||||||
if errorlevel 1 goto :fail
|
if errorlevel 1 goto :fail
|
||||||
|
|
||||||
|
for /f "usebackq delims=" %%v in (`node -p "require('./src-tauri/tauri.conf.json').version"`) do set VERSION=%%v
|
||||||
|
|
||||||
|
set MAKENSIS=%LOCALAPPDATA%\tauri\NSIS\makensis.exe
|
||||||
|
if not exist "%MAKENSIS%" set MAKENSIS=%ProgramFiles(x86)%\NSIS\makensis.exe
|
||||||
|
if not exist "%MAKENSIS%" (
|
||||||
|
echo [FEHLER] makensis nicht gefunden. NSIS 3 installieren: winget install NSIS.NSIS
|
||||||
|
goto :fail
|
||||||
|
)
|
||||||
|
|
||||||
if not exist dist mkdir dist
|
if not exist dist mkdir dist
|
||||||
copy /y "src-tauri\target\release\bundle\nsis\EuerChat_*_x64-setup.exe" dist\ >nul
|
echo Baue Installer %VERSION% ...
|
||||||
|
"%MAKENSIS%" /V2 /DVERSION=%VERSION% installer\EuerChat.nsi
|
||||||
|
if errorlevel 1 goto :fail
|
||||||
|
|
||||||
copy /y src-tauri\target\release\euer-chat.exe EuerChat.exe >nul
|
copy /y src-tauri\target\release\euer-chat.exe EuerChat.exe >nul
|
||||||
echo [OK] Installer liegt in dist\ - EuerChat.exe (portabel) im Hauptordner.
|
echo [OK] Installer: dist\EuerChat_%VERSION%_x64-setup.exe
|
||||||
pause
|
pause
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,394 @@
|
|||||||
|
; EuerChat-Installer
|
||||||
|
; Standard (Program Files, alle Benutzer) oder Portable (frei waehlbarer Ordner).
|
||||||
|
; Parameter, auch fuer stille Updates: /S /UPDATE /MODE=standard|portable /NS /D=<Ordner> (/D= immer zuletzt)
|
||||||
|
|
||||||
|
Unicode true
|
||||||
|
ManifestDPIAware true
|
||||||
|
SetCompressor /SOLID lzma
|
||||||
|
RequestExecutionLevel user
|
||||||
|
|
||||||
|
!ifndef VERSION
|
||||||
|
!define VERSION "0.0.0"
|
||||||
|
!endif
|
||||||
|
!ifndef EXE_SRC
|
||||||
|
!define EXE_SRC "..\src-tauri\target\release\euer-chat.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef OUT_FILE
|
||||||
|
!define OUT_FILE "..\dist\EuerChat_${VERSION}_x64-setup.exe"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!define PRODUCT "EuerChat"
|
||||||
|
!define PUBLISHER "euer.tv"
|
||||||
|
!define MAINEXE "euer-chat.exe"
|
||||||
|
!define BUNDLEID "tv.euer.chat"
|
||||||
|
!define WEBVIEW2APPGUID "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
|
||||||
|
!define PLACEHOLDER_DIR "placeholder\${PRODUCT}"
|
||||||
|
|
||||||
|
; TEST_USER_CONTEXT: Testbuild ohne Administratorrechte (HKCU, eigener Registry-Schluessel).
|
||||||
|
!ifdef TEST_USER_CONTEXT
|
||||||
|
!define ROOT HKCU
|
||||||
|
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT}-Test"
|
||||||
|
!else
|
||||||
|
!define ROOT HKLM
|
||||||
|
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
Name "${PRODUCT}"
|
||||||
|
OutFile "${OUT_FILE}"
|
||||||
|
BrandingText "${PRODUCT} ${VERSION}"
|
||||||
|
InstallDir "${PLACEHOLDER_DIR}"
|
||||||
|
|
||||||
|
VIProductVersion "${VERSION}.0"
|
||||||
|
VIAddVersionKey "ProductName" "${PRODUCT}"
|
||||||
|
VIAddVersionKey "FileDescription" "${PRODUCT} Installer"
|
||||||
|
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||||
|
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||||
|
VIAddVersionKey "CompanyName" "${PUBLISHER}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${PUBLISHER}"
|
||||||
|
|
||||||
|
!include MUI2.nsh
|
||||||
|
!include FileFunc.nsh
|
||||||
|
!include LogicLib.nsh
|
||||||
|
!include nsDialogs.nsh
|
||||||
|
!include x64.nsh
|
||||||
|
|
||||||
|
!insertmacro GetParameters
|
||||||
|
!insertmacro GetOptions
|
||||||
|
!insertmacro un.GetParameters
|
||||||
|
!insertmacro un.GetOptions
|
||||||
|
|
||||||
|
Var Portable
|
||||||
|
Var UpdateMode
|
||||||
|
Var NoShortcut
|
||||||
|
Var Elevated
|
||||||
|
Var ModeGiven
|
||||||
|
Var RadioStandard
|
||||||
|
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
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
Page custom ModePageCreate ModePageLeave
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE
|
||||||
|
!define MUI_FINISHPAGE_RUN
|
||||||
|
!define MUI_FINISHPAGE_RUN_TEXT "${PRODUCT} jetzt starten"
|
||||||
|
!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "German"
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; Hilfsfunktionen
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Function SetDefaultDir
|
||||||
|
${If} $Portable = 1
|
||||||
|
StrCpy $INSTDIR "$EXEDIR\${PRODUCT}"
|
||||||
|
${Else}
|
||||||
|
ReadRegStr $0 ${ROOT} "${UNINSTKEY}" "InstallLocation"
|
||||||
|
${If} $0 != ""
|
||||||
|
StrCpy $INSTDIR $0
|
||||||
|
${Else}
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCT}"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Startet den Installer bei Bedarf mit Administratorrechten neu (nur fuer die Standard-Installation).
|
||||||
|
Function RequireAdmin
|
||||||
|
!ifndef TEST_USER_CONTEXT
|
||||||
|
System::Call 'shell32::IsUserAnAdmin()i.r0'
|
||||||
|
${If} $0 = 0
|
||||||
|
${If} $Elevated = 1
|
||||||
|
${IfNot} ${Silent}
|
||||||
|
MessageBox MB_ICONSTOP "Für die Standard-Installation sind Administratorrechte nötig."
|
||||||
|
${EndIf}
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $1 "/MODE=standard /ELEVATED"
|
||||||
|
${If} ${Silent}
|
||||||
|
StrCpy $1 "$1 /S"
|
||||||
|
${EndIf}
|
||||||
|
${If} $UpdateMode = 1
|
||||||
|
StrCpy $1 "$1 /UPDATE"
|
||||||
|
${EndIf}
|
||||||
|
${If} $NoShortcut = 1
|
||||||
|
StrCpy $1 "$1 /NS"
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $1 "$1 /D=$INSTDIR"
|
||||||
|
System::Call 'shell32::ShellExecute(i 0, t "runas", t "$EXEPATH", t "$1", t "", i 1) i .r2'
|
||||||
|
${If} $2 > 32
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
${IfNot} ${Silent}
|
||||||
|
MessageBox MB_ICONEXCLAMATION "Ohne Administratorrechte kann die Standard-Installation nicht fortgesetzt werden. Wähle stattdessen „Portable“."
|
||||||
|
${EndIf}
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function CloseRunningApp
|
||||||
|
nsExec::ExecToStack 'cmd /c tasklist /FI "IMAGENAME eq ${MAINEXE}" /NH | find /I "${MAINEXE}"'
|
||||||
|
Pop $0
|
||||||
|
Pop $1
|
||||||
|
${If} $0 = 0
|
||||||
|
${IfNot} ${Silent}
|
||||||
|
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "${PRODUCT} läuft noch und wird für die Installation beendet." IDOK +2
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
nsExec::ExecToStack 'taskkill /F /IM "${MAINEXE}"'
|
||||||
|
Pop $0
|
||||||
|
Pop $1
|
||||||
|
Sleep 800
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function RunMainBinary
|
||||||
|
; Ueber den Explorer starten, damit die App ohne Administratorrechte laeuft.
|
||||||
|
Exec '"$WINDIR\explorer.exe" "$INSTDIR\${MAINEXE}"'
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; Installer
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
${If} ${RunningX64}
|
||||||
|
SetRegView 64
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${GetParameters} $R9
|
||||||
|
${GetOptions} $R9 "/UPDATE" $0
|
||||||
|
${IfNot} ${Errors}
|
||||||
|
StrCpy $UpdateMode 1
|
||||||
|
${EndIf}
|
||||||
|
${GetOptions} $R9 "/NS" $0
|
||||||
|
${IfNot} ${Errors}
|
||||||
|
StrCpy $NoShortcut 1
|
||||||
|
${EndIf}
|
||||||
|
${GetOptions} $R9 "/ELEVATED" $0
|
||||||
|
${IfNot} ${Errors}
|
||||||
|
StrCpy $Elevated 1
|
||||||
|
${EndIf}
|
||||||
|
${GetOptions} $R9 "/MODE=" $0
|
||||||
|
${IfNot} ${Errors}
|
||||||
|
StrCpy $ModeGiven 1
|
||||||
|
${If} $0 == "portable"
|
||||||
|
StrCpy $Portable 1
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
; Stille Installation ohne Angabe: Standard
|
||||||
|
${If} ${Silent}
|
||||||
|
StrCpy $ModeGiven 1
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} $INSTDIR == "${PLACEHOLDER_DIR}"
|
||||||
|
Call SetDefaultDir
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} $ModeGiven = 1
|
||||||
|
${AndIf} $Portable <> 1
|
||||||
|
Call RequireAdmin
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function WelcomePre
|
||||||
|
${If} $Elevated = 1
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function ModePageCreate
|
||||||
|
${If} $ModeGiven = 1
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
!insertmacro MUI_HEADER_TEXT "Installationsart" "Wie soll ${PRODUCT} installiert werden?"
|
||||||
|
nsDialogs::Create 1018
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${NSD_CreateRadioButton} 0 6u 100% 12u "Standard-Installation (empfohlen)"
|
||||||
|
Pop $RadioStandard
|
||||||
|
${NSD_AddStyle} $RadioStandard ${WS_GROUP}
|
||||||
|
${NSD_CreateLabel} 12u 20u 94% 38u "Installiert ${PRODUCT} für alle Benutzer, standardmäßig nach C:\Program Files. Mit Startmenü- und Desktop-Verknüpfung; deinstallierbar über die Windows-Einstellungen. Erfordert Administratorrechte."
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${NSD_CreateRadioButton} 0 66u 100% 12u "Portable (Ordner frei wählbar)"
|
||||||
|
Pop $RadioPortable
|
||||||
|
${NSD_CreateLabel} 12u 80u 94% 46u "Kopiert ${PRODUCT} nur in einen Ordner deiner Wahl, z. B. auf einen USB-Stick. Keine Administratorrechte und keine Einträge in der Registry. Die Einstellungen liegen im Unterordner „data“. Zum Entfernen genügt es, den Ordner zu löschen."
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${If} $Portable = 1
|
||||||
|
${NSD_Check} $RadioPortable
|
||||||
|
${Else}
|
||||||
|
${NSD_Check} $RadioStandard
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
nsDialogs::Show
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function ModePageLeave
|
||||||
|
${NSD_GetState} $RadioPortable $0
|
||||||
|
${If} $0 = ${BST_CHECKED}
|
||||||
|
StrCpy $Portable 1
|
||||||
|
${Else}
|
||||||
|
StrCpy $Portable 0
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
Call SetDefaultDir
|
||||||
|
|
||||||
|
${If} $Portable = 0
|
||||||
|
Call RequireAdmin
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section "WebView2"
|
||||||
|
${If} $UpdateMode <> 1
|
||||||
|
ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv"
|
||||||
|
${If} $4 == ""
|
||||||
|
ReadRegStr $4 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv"
|
||||||
|
${EndIf}
|
||||||
|
${If} $4 == ""
|
||||||
|
DetailPrint "Microsoft Edge WebView2 wird heruntergeladen ..."
|
||||||
|
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
Pop $0
|
||||||
|
${If} $0 != "success"
|
||||||
|
Abort "WebView2 konnte nicht heruntergeladen werden. Bitte die Internetverbindung prüfen und erneut versuchen."
|
||||||
|
${EndIf}
|
||||||
|
DetailPrint "Microsoft Edge WebView2 wird installiert ..."
|
||||||
|
ExecWait '"$TEMP\MicrosoftEdgeWebview2Setup.exe" /silent /install' $1
|
||||||
|
${If} $1 <> 0
|
||||||
|
Abort "Die Installation von WebView2 ist fehlgeschlagen."
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Install"
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
Call CloseRunningApp
|
||||||
|
File "${EXE_SRC}"
|
||||||
|
|
||||||
|
${If} $Portable = 1
|
||||||
|
; Die Marker-Datei schaltet den portablen Datenordner der App frei.
|
||||||
|
FileOpen $0 "$INSTDIR\portable.flag" w
|
||||||
|
FileWrite $0 "${PRODUCT} portable: Einstellungen liegen im Ordner data.$\r$\n"
|
||||||
|
FileClose $0
|
||||||
|
CreateDirectory "$INSTDIR\data"
|
||||||
|
${Else}
|
||||||
|
Delete "$INSTDIR\portable.flag"
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
!ifdef TEST_USER_CONTEXT
|
||||||
|
SetShellVarContext current
|
||||||
|
!else
|
||||||
|
SetShellVarContext all
|
||||||
|
!endif
|
||||||
|
|
||||||
|
WriteRegStr ${ROOT} "${UNINSTKEY}" "DisplayName" "${PRODUCT}"
|
||||||
|
WriteRegStr ${ROOT} "${UNINSTKEY}" "DisplayVersion" "${VERSION}"
|
||||||
|
WriteRegStr ${ROOT} "${UNINSTKEY}" "Publisher" "${PUBLISHER}"
|
||||||
|
WriteRegStr ${ROOT} "${UNINSTKEY}" "InstallLocation" "$INSTDIR"
|
||||||
|
WriteRegStr ${ROOT} "${UNINSTKEY}" "DisplayIcon" "$INSTDIR\${MAINEXE}"
|
||||||
|
WriteRegStr ${ROOT} "${UNINSTKEY}" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||||
|
WriteRegDWORD ${ROOT} "${UNINSTKEY}" "NoModify" 1
|
||||||
|
WriteRegDWORD ${ROOT} "${UNINSTKEY}" "NoRepair" 1
|
||||||
|
WriteRegDWORD ${ROOT} "${UNINSTKEY}" "EstimatedSize" 7600
|
||||||
|
|
||||||
|
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}" "" "$INSTDIR\${MAINEXE}" 0
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; Deinstallation (nur Standard-Installation)
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Function un.onInit
|
||||||
|
${If} ${RunningX64}
|
||||||
|
SetRegView 64
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${GetParameters} $R9
|
||||||
|
${GetOptions} $R9 "/ELEVATED" $0
|
||||||
|
${IfNot} ${Errors}
|
||||||
|
StrCpy $Elevated 1
|
||||||
|
${EndIf}
|
||||||
|
${GetOptions} $R9 "/UPDATE" $0
|
||||||
|
${IfNot} ${Errors}
|
||||||
|
StrCpy $UpdateMode 1
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
!ifndef TEST_USER_CONTEXT
|
||||||
|
System::Call 'shell32::IsUserAnAdmin()i.r0'
|
||||||
|
${If} $0 = 0
|
||||||
|
${If} $Elevated = 1
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $1 "/ELEVATED"
|
||||||
|
${If} ${Silent}
|
||||||
|
StrCpy $1 "$1 /S"
|
||||||
|
${EndIf}
|
||||||
|
${If} $UpdateMode = 1
|
||||||
|
StrCpy $1 "$1 /UPDATE"
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $1 "$1 _?=$INSTDIR"
|
||||||
|
System::Call 'shell32::ShellExecute(i 0, t "runas", t "$EXEPATH", t "$1", t "", i 1) i .r2'
|
||||||
|
${If} $2 > 32
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section "Uninstall"
|
||||||
|
!ifdef TEST_USER_CONTEXT
|
||||||
|
SetShellVarContext current
|
||||||
|
!else
|
||||||
|
SetShellVarContext all
|
||||||
|
!endif
|
||||||
|
|
||||||
|
nsExec::ExecToStack 'taskkill /F /IM "${MAINEXE}"'
|
||||||
|
Pop $0
|
||||||
|
Pop $1
|
||||||
|
Sleep 500
|
||||||
|
|
||||||
|
Delete "$INSTDIR\${MAINEXE}"
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
RMDir "$INSTDIR"
|
||||||
|
Delete "$SMPROGRAMS\${PRODUCT}.lnk"
|
||||||
|
Delete "$DESKTOP\${PRODUCT}.lnk"
|
||||||
|
DeleteRegKey ${ROOT} "${UNINSTKEY}"
|
||||||
|
|
||||||
|
${If} $UpdateMode <> 1
|
||||||
|
${AndIfNot} ${Silent}
|
||||||
|
MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Sollen auch die Einstellungen von ${PRODUCT} (Konfiguration und gespeicherte Passwörter) gelöscht werden?" IDNO skip_data
|
||||||
|
SetShellVarContext current
|
||||||
|
RMDir /r "$APPDATA\${PRODUCT}"
|
||||||
|
RMDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||||
|
skip_data:
|
||||||
|
${EndIf}
|
||||||
|
SectionEnd
|
||||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 151 KiB |
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"build": "tauri build"
|
"build": "tauri build",
|
||||||
|
"brand": "node tools/make-branding.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.2.0"
|
"@tauri-apps/cli": "^2.2.0"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
fn main() {
|
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()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 119 KiB |
@@ -5,14 +5,22 @@ use std::path::PathBuf;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{PhysicalPosition, PhysicalSize};
|
use tauri::{PhysicalPosition, PhysicalSize};
|
||||||
|
|
||||||
fn exe_dir() -> PathBuf {
|
pub fn exe_dir() -> PathBuf {
|
||||||
std::env::current_exe()
|
std::env::current_exe()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|p| p.parent().map(PathBuf::from))
|
.and_then(|p| p.parent().map(PathBuf::from))
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Der Installer legt bei der portablen Installation die Marker-Datei `portable.flag` neben die EXE.
|
||||||
|
pub fn is_portable() -> bool {
|
||||||
|
exe_dir().join("portable.flag").is_file()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn data_dir() -> PathBuf {
|
pub fn data_dir() -> PathBuf {
|
||||||
|
if is_portable() {
|
||||||
|
return exe_dir().join("data");
|
||||||
|
}
|
||||||
std::env::var_os("APPDATA")
|
std::env::var_os("APPDATA")
|
||||||
.map(|d| PathBuf::from(d).join("EuerChat"))
|
.map(|d| PathBuf::from(d).join("EuerChat"))
|
||||||
.unwrap_or_else(exe_dir)
|
.unwrap_or_else(exe_dir)
|
||||||
|
|||||||
@@ -366,28 +366,65 @@ async fn download_to_file(
|
|||||||
// Installation
|
// Installation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
enum InstallKind {
|
||||||
|
/// Mit dem Standard-Installer installiert (uninstall.exe neben der EXE), meist in Program Files.
|
||||||
|
Standard,
|
||||||
|
/// Portable Installation (portable.flag neben der EXE).
|
||||||
|
Portable,
|
||||||
|
/// Lose EXE ohne Installation, z. B. aus dem Build-Ordner.
|
||||||
|
Loose,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_install_kind(dir: &Path) -> InstallKind {
|
||||||
|
if dir.join("portable.flag").is_file() {
|
||||||
|
InstallKind::Portable
|
||||||
|
} else if dir.join("uninstall.exe").is_file() {
|
||||||
|
InstallKind::Standard
|
||||||
|
} else {
|
||||||
|
InstallKind::Loose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PowerShell-Skript: wartet auf das Ende dieser App, installiert still und startet EuerChat neu.
|
||||||
|
fn build_update_script(pid: u32, installer: &Path, current_exe: &Path, kind: InstallKind) -> String {
|
||||||
|
let quote = |p: &Path| p.to_string_lossy().replace('\'', "''");
|
||||||
|
let dir = current_exe.parent().map(quote).unwrap_or_default();
|
||||||
|
|
||||||
|
// Standard-Installationen liegen meist in Program Files und brauchen Administratorrechte (UAC).
|
||||||
|
let (args, elevate) = match kind {
|
||||||
|
InstallKind::Standard => (format!("/S /UPDATE /MODE=standard /D={dir}"), true),
|
||||||
|
InstallKind::Portable => (format!("/S /UPDATE /MODE=portable /D={dir}"), false),
|
||||||
|
InstallKind::Loose => ("/S /UPDATE /MODE=standard".to_string(), true),
|
||||||
|
};
|
||||||
|
let verb = if elevate { " -Verb RunAs" } else { "" };
|
||||||
|
let relaunch = match kind {
|
||||||
|
InstallKind::Loose => format!(
|
||||||
|
"$t = Join-Path $env:ProgramFiles 'EuerChat\\euer-chat.exe'; \
|
||||||
|
if (Test-Path $t) {{ Start-Process -FilePath $t }} else {{ Start-Process -FilePath '{}' }}",
|
||||||
|
quote(current_exe)
|
||||||
|
),
|
||||||
|
_ => format!("Start-Process -FilePath '{}'", quote(current_exe)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bricht der Nutzer die UAC-Abfrage ab, wird trotzdem die bisherige Version neu gestartet.
|
||||||
|
format!(
|
||||||
|
"Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; \
|
||||||
|
try {{ Start-Process -FilePath '{installer}' -ArgumentList '{args}'{verb} -Wait }} catch {{}}; \
|
||||||
|
{relaunch}",
|
||||||
|
installer = quote(installer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Startet den Installer erst, wenn diese App beendet ist, und danach EuerChat neu.
|
/// Startet den Installer erst, wenn diese App beendet ist, und danach EuerChat neu.
|
||||||
fn spawn_installer_after_exit(installer: &Path) -> std::io::Result<()> {
|
fn spawn_installer_after_exit(installer: &Path) -> std::io::Result<()> {
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
let quote = |p: &Path| p.to_string_lossy().replace('\'', "''");
|
|
||||||
let current = std::env::current_exe()?;
|
let current = std::env::current_exe()?;
|
||||||
let installed_here = current
|
let kind = current
|
||||||
.parent()
|
.parent()
|
||||||
.is_some_and(|dir| dir.join("uninstall.exe").exists());
|
.map_or(InstallKind::Loose, detect_install_kind);
|
||||||
|
let script = build_update_script(std::process::id(), installer, ¤t, kind);
|
||||||
// Eine portable EXE wird per Installer in %LOCALAPPDATA%\EuerChat aktualisiert; dort neu starten.
|
|
||||||
let script = format!(
|
|
||||||
"Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; \
|
|
||||||
Start-Process -FilePath '{installer}' -ArgumentList '/S','/UPDATE' -Wait; \
|
|
||||||
$installed = Join-Path $env:LOCALAPPDATA 'EuerChat\\euer-chat.exe'; \
|
|
||||||
if (-not {installed_here} -and (Test-Path $installed)) {{ Start-Process -FilePath $installed }} \
|
|
||||||
else {{ Start-Process -FilePath '{current}' }}",
|
|
||||||
pid = std::process::id(),
|
|
||||||
installer = quote(installer),
|
|
||||||
installed_here = if installed_here { "$true" } else { "$false" },
|
|
||||||
current = quote(¤t),
|
|
||||||
);
|
|
||||||
|
|
||||||
std::process::Command::new("powershell.exe")
|
std::process::Command::new("powershell.exe")
|
||||||
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &script])
|
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &script])
|
||||||
@@ -529,6 +566,40 @@ mod tests {
|
|||||||
assert!(parse_expected_checksum(&other, "x.exe").unwrap_err().contains("x.exe"));
|
assert!(parse_expected_checksum(&other, "x.exe").unwrap_err().contains("x.exe"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_kind_detection() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("euerchat-kind-test-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(detect_install_kind(&dir), InstallKind::Loose);
|
||||||
|
std::fs::write(dir.join("uninstall.exe"), b"x").unwrap();
|
||||||
|
assert_eq!(detect_install_kind(&dir), InstallKind::Standard);
|
||||||
|
std::fs::write(dir.join("portable.flag"), b"x").unwrap();
|
||||||
|
assert_eq!(detect_install_kind(&dir), InstallKind::Portable);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_script_per_install_kind() {
|
||||||
|
let installer = Path::new(r"C:\Users\O'Brien\AppData\Roaming\EuerChat\updates\EuerChat_0.2.0_x64-setup.exe");
|
||||||
|
let exe = Path::new(r"C:\Program Files\EuerChat\euer-chat.exe");
|
||||||
|
|
||||||
|
let standard = build_update_script(42, installer, exe, InstallKind::Standard);
|
||||||
|
assert!(standard.contains("Wait-Process -Id 42"));
|
||||||
|
assert!(standard.contains("/MODE=standard /D=C:\\Program Files\\EuerChat'"));
|
||||||
|
assert!(standard.contains("-Verb RunAs"));
|
||||||
|
assert!(standard.contains("O''Brien"), "Apostrophe müssen verdoppelt werden");
|
||||||
|
assert!(standard.contains("Start-Process -FilePath 'C:\\Program Files\\EuerChat\\euer-chat.exe'"));
|
||||||
|
|
||||||
|
let portable = build_update_script(1, installer, Path::new(r"E:\Tools\EuerChat\euer-chat.exe"), InstallKind::Portable);
|
||||||
|
assert!(portable.contains("/MODE=portable /D=E:\\Tools\\EuerChat'"));
|
||||||
|
assert!(!portable.contains("RunAs"), "portable Updates brauchen keine Administratorrechte");
|
||||||
|
|
||||||
|
let loose = build_update_script(1, installer, Path::new(r"D:\build\euer-chat.exe"), InstallKind::Loose);
|
||||||
|
assert!(loose.contains("/MODE=standard'"));
|
||||||
|
assert!(!loose.contains("/D="));
|
||||||
|
assert!(loose.contains("$env:ProgramFiles"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn managed_installer_files() {
|
fn managed_installer_files() {
|
||||||
assert!(is_managed_installer_file("EuerChat_0.2.0_x64-setup.exe"));
|
assert!(is_managed_installer_file("EuerChat_0.2.0_x64-setup.exe"));
|
||||||
|
|||||||
@@ -39,16 +39,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": false,
|
||||||
"targets": ["nsis"],
|
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"]
|
||||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"],
|
|
||||||
"shortDescription": "EuerChat - IRC-Chat und Livestream-Client fuer euer.tv",
|
|
||||||
"windows": {
|
|
||||||
"nsis": {
|
|
||||||
"installMode": "currentUser",
|
|
||||||
"languages": ["German"],
|
|
||||||
"displayLanguageSelector": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(`<line x1="196" x2="316" y1="${y}" y2="${y}" stroke="#1c1c1c" stroke-width="9" stroke-linecap="round"/>`);
|
||||||
|
slots.push(`<line x1="204" x2="308" y1="${y + 6.5}" y2="${y + 6.5}" stroke="#fff" stroke-opacity=".45" stroke-width="2" stroke-linecap="round"/>`);
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="${p}orb" cx="50%" cy="32%" r="82%">
|
||||||
|
<stop offset="0" stop-color="#ff5a3d"/><stop offset=".36" stop-color="#e50b0b"/>
|
||||||
|
<stop offset=".76" stop-color="#8c0000"/><stop offset="1" stop-color="#3d0000"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="${p}ring" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset=".45" stop-color="#8a8a8a"/><stop offset="1" stop-color="#e9e9e9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="${p}chrome" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#555"/><stop offset=".2" stop-color="#bdbdbd"/><stop offset=".4" stop-color="#fff"/>
|
||||||
|
<stop offset=".58" stop-color="#cfcfcf"/><stop offset=".82" stop-color="#777"/><stop offset="1" stop-color="#454545"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="${p}gloss" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="${p}bubble" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#c3ccd3"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="${p}orbClip"><circle cx="256" cy="256" r="214"/></clipPath>
|
||||||
|
<clipPath id="${p}headClip"><rect x="198" y="118" width="116" height="172" rx="58"/></clipPath>
|
||||||
|
<filter id="${p}shadow" x="-40%" y="-30%" width="180%" height="170%">
|
||||||
|
<feDropShadow dx="3" dy="9" stdDeviation="7" flood-color="#000" flood-opacity=".6"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="${p}blur" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="6"/></filter>
|
||||||
|
</defs>
|
||||||
|
<circle cx="256" cy="256" r="252" fill="url(#${p}ring)"/>
|
||||||
|
<circle cx="256" cy="256" r="237" fill="#0b0b0b"/>
|
||||||
|
<circle cx="256" cy="256" r="214" fill="url(#${p}orb)"/>
|
||||||
|
<g clip-path="url(#${p}orbClip)">
|
||||||
|
<ellipse cx="256" cy="118" rx="172" ry="86" fill="url(#${p}gloss)" opacity=".75"/>
|
||||||
|
<ellipse cx="256" cy="478" rx="200" ry="96" fill="#ff3a1c" opacity=".5"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#${p}shadow)" transform="rotate(-10 256 260)">
|
||||||
|
<rect x="198" y="118" width="116" height="172" rx="58" fill="url(#${p}chrome)"/>
|
||||||
|
<g clip-path="url(#${p}headClip)">${slots.join('')}</g>
|
||||||
|
<rect x="198" y="118" width="116" height="172" rx="58" fill="none" stroke="#2c2c2c" stroke-width="5"/>
|
||||||
|
<rect x="214" y="288" width="84" height="26" rx="10" fill="url(#${p}chrome)" stroke="#2c2c2c" stroke-width="4"/>
|
||||||
|
<path d="M236 314 H276 L268 372 H244 Z" fill="url(#${p}chrome)" stroke="#2c2c2c" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<rect x="230" y="372" width="52" height="26" rx="6" fill="#2a2a2a" stroke="#777" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="296" cy="150" r="16" fill="#fff" opacity=".9" filter="url(#${p}blur)"/>
|
||||||
|
<path d="M296 122 L301 145 L324 150 L301 155 L296 178 L291 155 L268 150 L291 145 Z" fill="#fff"/>
|
||||||
|
<g filter="url(#${p}shadow)">
|
||||||
|
<path d="M322 392 L294 446 L362 396 Z" fill="url(#${p}bubble)" stroke="#0b0b0b" stroke-width="10" stroke-linejoin="round"/>
|
||||||
|
<rect x="288" y="296" width="146" height="102" rx="32" fill="url(#${p}bubble)" stroke="#0b0b0b" stroke-width="10"/>
|
||||||
|
<path d="M322 392 L318 400 L352 394 Z" fill="url(#${p}bubble)"/>
|
||||||
|
<circle cx="326" cy="347" r="12" fill="#d00000"/><circle cx="361" cy="347" r="12" fill="#d00000"/><circle cx="396" cy="347" r="12" fill="#d00000"/>
|
||||||
|
</g>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconSvg(size) {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 512 512">${iconInner('i')}</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 ${common} fill="#1a1a1a" stroke="#1a1a1a" stroke-width="${size * 0.2}" filter="url(#${p}tshadow)">${text}</text>
|
||||||
|
<text ${common} fill="none" stroke="url(#${p}silver)" stroke-width="${size * 0.13}">${text}</text>
|
||||||
|
<text ${common} fill="url(#${p}letters)" stroke="#5e5e5e" stroke-width="${size * 0.012}">${text}</text>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textDefs(p) {
|
||||||
|
return `
|
||||||
|
<linearGradient id="${p}silver" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff"/><stop offset=".5" stop-color="#9c9c9c"/><stop offset="1" stop-color="#f1f1f1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="${p}letters" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset=".55" stop-color="#e6e6e6"/><stop offset="1" stop-color="#bcbcbc"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="${p}tshadow" x="-10%" y="-20%" width="120%" height="160%">
|
||||||
|
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-color="#000" flood-opacity=".5"/>
|
||||||
|
</filter>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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: `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||||
|
<defs>
|
||||||
|
${textDefs('w')}
|
||||||
|
<linearGradient id="wpillStroke" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ff5a45"/><stop offset=".4" stop-color="#e00000"/><stop offset="1" stop-color="#7d0000"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wpillFill" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#d9d9d9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="wpillShadow" x="-5%" y="-20%" width="110%" height="150%">
|
||||||
|
<feDropShadow dx="0" dy="10" stdDeviation="9" flood-color="#000" flood-opacity=".45"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect x="46" y="86" width="${width - 92}" height="388" rx="194" fill="url(#wpillFill)" stroke="url(#wpillStroke)" stroke-width="56" filter="url(#wpillShadow)"/>
|
||||||
|
<rect x="74" y="114" width="${width - 148}" height="332" rx="166" fill="none" stroke="#6d0000" stroke-opacity=".55" stroke-width="6"/>
|
||||||
|
${chromeText('w', padEnd + wL / 2 + 20, baseline, size, 'euer.tv', wL)}
|
||||||
|
${chromeText('w', width - padEnd - wR / 2 - 20, baseline, size, 'chat', wR)}
|
||||||
|
<svg x="${cx - orb / 2}" y="${(height - orb) / 2}" width="${orb}" height="${orb}" viewBox="0 0 512 512">${iconInner('w')}</svg>
|
||||||
|
</svg>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hochformat fuer die Willkommensseite des Installers (164x314). */
|
||||||
|
function sidebarSvg() {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="164" height="314" viewBox="0 0 164 314">
|
||||||
|
<defs>
|
||||||
|
${textDefs('s')}
|
||||||
|
<linearGradient id="sbg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2b0606"/><stop offset="1" stop-color="#050000"/></linearGradient>
|
||||||
|
<radialGradient id="sglow" cx="50%" cy="34%" r="60%"><stop offset="0" stop-color="#ff2a10" stop-opacity=".55"/><stop offset="1" stop-color="#ff2a10" stop-opacity="0"/></radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="164" height="314" fill="url(#sbg)"/>
|
||||||
|
<rect width="164" height="314" fill="url(#sglow)"/>
|
||||||
|
<svg x="17" y="28" width="130" height="130" viewBox="0 0 512 512">${iconInner('s')}</svg>
|
||||||
|
${chromeText('s', 82, 214, 31, 'euer.tv', 126)}
|
||||||
|
${chromeText('s', 82, 262, 40, 'chat', 92)}
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headerSvg(wide) {
|
||||||
|
const w = 150, h = 57;
|
||||||
|
const scale = Math.min(w / wide.width, h / wide.height);
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
||||||
|
<rect width="${w}" height="${h}" fill="#fff"/>
|
||||||
|
<svg x="${(w - wide.width * scale) / 2}" y="${(h - wide.height * scale) / 2}" width="${wide.width * scale}" height="${wide.height * scale}" viewBox="0 0 ${wide.width} ${wide.height}">${wide.svg.replace(/^<svg[^>]*>/, '').replace(/<\/svg>\s*$/, '')}</svg>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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 = `<!doctype html><html><body style="margin:0;background:transparent;overflow:hidden">${markup}</body></html>`;
|
||||||
|
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(`<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><text id="a" font-family="${FONT}" font-size="170">euer.tv</text><text id="b" font-family="${FONT}" font-size="170">chat</text></svg>`, 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();
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="header-brand">
|
<div class="header-brand">
|
||||||
|
<img class="header-logo" src="logo.png" alt="">
|
||||||
<span class="header-brand-title">EuerChat</span>
|
<span class="header-brand-title">EuerChat</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Nur sichtbar, wenn ein Lua-Skript (.luac) geladen und aktiv ist -->
|
<!-- Nur sichtbar, wenn ein Lua-Skript (.luac) geladen und aktiv ist -->
|
||||||
|
|||||||
|
After Width: | Height: | Size: 17 KiB |
@@ -78,6 +78,14 @@ body.dark-theme {
|
|||||||
.header-brand {
|
.header-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
height: 38px;
|
||||||
|
width: 38px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-brand-title {
|
.header-brand-title {
|
||||||
|
|||||||