Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9795839176 | ||
|
|
c63ec9c0c3 | ||
|
|
79fa9d905e | ||
|
|
781367aca4 | ||
|
|
9cde1a8e0c | ||
|
|
3bd806b42d | ||
|
|
a95d0f38ca | ||
|
|
9b2765c649 | ||
|
|
f2e712b490 | ||
|
|
70347fe237 | ||
|
|
090c540fb5 | ||
|
|
dec9081f81 | ||
|
|
4638ba45c4 | ||
|
|
e2c92b63e7 | ||
|
|
4dfde3be9f | ||
|
|
52bb349b6d | ||
|
|
180b0a5188 | ||
|
|
704a374d31 | ||
|
|
dbd1254da1 | ||
|
|
852c2648c2 | ||
|
|
3c71596596 | ||
|
|
1039c06067 | ||
|
|
fea53ff701 | ||
|
|
9bb88d5032 | ||
|
|
59518eee6e | ||
|
|
88c8c6e0e6 | ||
|
|
a18794acd7 | ||
|
|
45269bed3d | ||
|
|
7b27a9da1d | ||
|
|
27f48c88a7 | ||
|
|
724c4afb03 | ||
|
|
55ad30bc8f | ||
|
|
72526d570a | ||
|
|
2789764c79 | ||
|
|
e7ef6570e3 | ||
|
|
6d41e92962 | ||
|
|
6230edf487 | ||
|
|
8f7b90cc10 | ||
|
|
e9b3b4f029 | ||
|
|
a277cf1a96 | ||
|
|
ec0aedccee | ||
|
|
1a0acd4a36 | ||
|
|
dad0d6fd00 |
No files matched your search
@@ -1,12 +0,0 @@
|
||||
# Persoenliche Konfiguration (enthaelt Passwoerter) niemals einchecken
|
||||
config.json
|
||||
window_state.json
|
||||
|
||||
# Build-Artefakte
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
node_modules/
|
||||
*.exe
|
||||
|
||||
# Der Installer wird als Release-Anhang veroeffentlicht, nicht im Repository
|
||||
dist/
|
||||
@@ -1,60 +1,69 @@
|
||||
# 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>
|
||||
<p align="center"><img src="assets/logo-original.png" alt="euer radio" 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, schlanker Desktop-Client für Windows 64-bit: Chat, Livestream und Radio-Player von **euer.tv** in einem Fenster. EuerChat ist ein Werkzeug für die **Moderatoren von euer radio** – alle Moderationsfunktionen sind fest eingebaut, es wird kein Addon benötigt.
|
||||
|
||||
**Download:** Den aktuellen Installer und die PDF-Anleitung findest du unter [Releases](../../releases). Normale Releases sind die stabilen Versionen; Beta-Versionen sind als Pre-release gekennzeichnet.
|
||||
|
||||
---
|
||||
|
||||
## Highlights & Features
|
||||
## Highlights
|
||||
|
||||
* **Eine einzige EXE (`EuerChat.exe`):**
|
||||
* Startet standardmäßig sauber und übersichtlich für alle Chat-Gäste und Hörer.
|
||||
* Die Moderationswerkzeuge sind ausgeblendet, bis das Lua/Modi-Addon in den Optionen aktiviert wird.
|
||||
* **Integrierter Livestream & Radio-Player (Stumm beim Start):**
|
||||
* Direkter HLS-Videostream (`https://hls21.euer.tv/hls/1080p.m3u8` & `hls23.euer.tv`).
|
||||
* **Startet garantiert stumm (`muted`)** mit komfortabler Stummschalt-/Ton-Taste und Lautstärkeregler.
|
||||
* Live-Titelanzeige & Hörerzähler (`https://api.euer.tv/v2/getjsondata.php?status`).
|
||||
* Automatisches Abrufen des aktuellen Album-Covers (`https://api.euer.tv/v2/getjsondata.php`).
|
||||
* Ein-/ausblendbares Videofenster für Live-Übertragungen neben dem Chat.
|
||||
* **Moderations-Werkzeuge (optional):**
|
||||
* Erst bei manueller Aktivierung des Lua/Modi-Addons in den Optionen erscheint oben links der Button zum Moderator-Wechsel.
|
||||
* Automatisches Einloggen bei NickServ (`PRIVMSG NickServ :IDENTIFY <passwort>`).
|
||||
* Ein-Klick-Wechsel: Wechselt zum Moderatoren-Nick und betritt automatisch `#intern` (mit dem in den Optionen hinterlegten Passwort). Beim Zurückwechseln wird in allen offenen Räumen `!deop` gesendet und `#intern` verlassen.
|
||||
* **IRC-Verbindung zu euer.tv:**
|
||||
* **Direkt verbinden:** nur den eigenen Namen eingeben, ein Passwort ist optional (nur mit registriertem Account). Verbunden wird fest mit **`irc.euer.tv`**, Port **`6697`** (SSL/TLS, Zertifikate werden geprüft), Raum **`#erde`**.
|
||||
* **ZNC-Bouncer** für eigene Server (`user/network` + Passwort); selbstsignierte Zertifikate: `"acceptInvalidCerts": true` in der `config.json` setzen (unsicher).
|
||||
* Räume unten lassen sich per Maus verschieben, die Oberfläche lässt sich skalieren (75-200 %).
|
||||
* **Design & Anpassbarkeit:**
|
||||
* Schriftarten, Schriftgrößen (11px bis 22px), Zeilenabstände, Zeitstempel frei wählbar.
|
||||
* Animierte GIFs (`CATJAM`, `pepeDS` uvm.) & Smileys aus dem Webchat.
|
||||
* **Kein störendes Konsolenfenster:**
|
||||
* Nativ als reine Windows-GUI-Anwendung kompiliert (`#![windows_subsystem = "windows"]`).
|
||||
* **Saubere Server-Trennung:**
|
||||
* Sendet beim Schließen regulär `QUIT :Client beendet`.
|
||||
* **Integrierter Updater:**
|
||||
* Sucht beim Start (abschaltbar unter Optionen → Updates) im neuesten Gitea-Release nach einer neueren Version, zeigt die Release-Notizen und installiert auf Wunsch still und startet EuerChat neu.
|
||||
* Es werden nur HTTPS-Adressen des Gitea-Hosts akzeptiert, und der Installer läuft nur, wenn seine SHA-256-Prüfsumme zur Datei `<Installer>.sha256` im Release passt.
|
||||
### Livestream & Radio-Player
|
||||
* HLS-Videostream in mehreren Qualitätsstufen, **startet immer stumm**, mit Ton-Taste und Lautstärkeregler.
|
||||
* Live-Titelanzeige, Album-Cover, Hörerzähler und Grußbox direkt neben dem Chat.
|
||||
* **Vollbild** per Symbol oder Doppelklick; die Steuerung blendet sich bei Ruhe aus, **Esc** beendet.
|
||||
* Schnellschalter für den Videobereich: ein-/ausblenden sowie oben/unten neben der Nickliste.
|
||||
* Der Stream ist mit einem Rahmen in der Akzentfarbe (optional mit Neon-Glanz) eingefasst, Playerleiste und Titelzeile passen sich dem Farbschema an.
|
||||
|
||||
### Chat
|
||||
* Verbindung zu **`irc.euer.tv`**, Port **`6697`** (SSL/TLS, Zertifikate werden geprüft), Raum **`#erde`**. Name eingeben, verbinden, fertig.
|
||||
* Optional **ZNC-Bouncer** für eigene Server (`user/network` + Passwort).
|
||||
* Mehrere Räume und private Chats gleichzeitig, Räume lassen sich per Maus umsortieren.
|
||||
* Schwebende Leiste an jeder Nachricht (Antworten, Kopieren, Ignorieren), Zähler für Erwähnungen und Alias-Treffer an den Reitern.
|
||||
* Animierte GIFs und Smileys aus dem Webchat, optionale Bild-Vorschau für https-Links (standardmäßig aus). Neue oder entfernte Emotes erkennt EuerChat selbst (Abgleich mit dem Webchat jede Minute, Hinweis im Raum **#erde**); Bilder werden lokal zwischengespeichert und funktionieren auch, wenn der Server nicht erreichbar ist.
|
||||
* **Client-Version abfragen:** `/version <Nick>`, `/ctcp <Nick> version` oder Rechtsklick auf einen Nick; die Antwort erscheint im aktuellen Fenster.
|
||||
* Hinweise bei Erwähnung, privater Nachricht und Alias-Warnung: Ton, Taskleisten-Blinken und Windows-Meldung, wenn das Fenster im Hintergrund ist.
|
||||
|
||||
### Moderations-Werkzeuge
|
||||
* **Modi-Umschalter:** Ein Klick wechselt zwischen normalem Nick und Moderator-Nick inklusive `#intern`; automatischer NickServ-Login.
|
||||
* **Alias-Erkennung:** EuerChat merkt sich zu jedem Host die benutzten Nicks. Taucht jemand unter neuem Namen auf, erscheint eine Warnung („Hans war vorher als Davito unterwegs“) mit Chip in Nutzerliste und Chat. Nutzer, die gleichzeitig im Raum sind, werden nur als Haushalt-Info angezeigt.
|
||||
* **Profilkarte:** Host, frühere Nicks, Notiz, eigene Nickfarbe sowie PN, Ansprechen und Ignorieren; Hosts lassen sich als unbedenklich markieren.
|
||||
|
||||
### Design & Anpassbarkeit
|
||||
* **15 Farbschemata:** 10 dunkle (Dunkel, Mitternacht, AMOLED, Graphit, Synthwave, Wald, Bernstein, Karmesin, Tiefsee, Schiefer) und 5 helle (Hell, Creme, Minze, Rosé, Himmel).
|
||||
* **Über 25 Akzentfarben** (darunter Neon-Pink, Neon-Grün, Elektrisch-Blau, Neon-Gelb) und **Verlaufsakzente** wie Sonnenglut, Aurora, Feuer, Candy und Gletscher, plus frei wählbare eigene Farbe mit eigener Verlaufsfarbe.
|
||||
* **Hintergrund-Tönung** in der Akzentfarbe (Aus bis Maximal), regelbare **Akzent-Intensität** (Sanft bis Maximal) und optionaler **Neon-Glanz**.
|
||||
* **Schnellauswahl** mit 12 fertigen Design-Kombinationen, Vorschau in den Einstellungen in Echtzeit.
|
||||
* **Einstellungen sichern:** Aussehen, Verhalten, Nickfarben, Notizen und Ignorierliste lassen sich als Text exportieren und auf einem anderen Rechner importieren – nie mit Passwörtern oder Zugangsdaten.
|
||||
* **Lesbarkeit garantiert:** Textfarben werden für jede Kombination aus Schema, Akzent, Tönung und Intensität automatisch auf ausreichenden Kontrast geprüft.
|
||||
* Layout „Klassisch“ oder „Karten“, Animationen abschaltbar.
|
||||
* Schriftarten, Schriftgrößen (für Chat und Benutzerliste getrennt), Zeilenabstände und Zeitstempel frei wählbar; Oberfläche skalierbar (75-200 %).
|
||||
* Nickfarben mit vier Paletten (Lebhaft, Pastell, Kontrast, Farbenblind), eigene Farbe je Nutzer, Rollen-Symbole für Operator und Voice.
|
||||
* Übersichtliche Einstellungen in neun Reitern: Darstellung, Farben & Design, Allgemein, Benachrichtigungen, Server & ZNC, Radio & Modi, Alias-Erkennung, Updates und Über.
|
||||
|
||||
### Integrierter Updater
|
||||
* Sucht beim Start (abschaltbar unter Optionen → Updates) nach einer neueren Version, zeigt die Neuerungen und installiert auf Wunsch still; EuerChat startet danach neu. Per Knopf **„Nach Updates suchen“** lässt sich jederzeit von Hand prüfen.
|
||||
* Der **Changelog** der installierten Version steht direkt unter Optionen → Updates, übersichtlich als Liste.
|
||||
* **Update-Kanal:** *Stabil* (Standard) liefert nur geprüfte Versionen, *Beta* bringt neue Funktionen früher. Beta-Versionen sind auf Gitea als Pre-release markiert.
|
||||
* Der Installer wird nur ausgeführt, wenn seine SHA-256-Prüfsumme zum Release passt.
|
||||
* Unter **Optionen → Über** stehen Version, Build-Datum und Mitwirkende.
|
||||
|
||||
### Geschützte Oberfläche
|
||||
* Kein Browser-Rechtsklickmenü, keine Entwicklerwerkzeuge und keine Browser-Tastenkürzel; EuerChats eigene Menüs und Kopierfunktionen bleiben erhalten.
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart
|
||||
## Installation
|
||||
|
||||
1. **Installieren:** `EuerChat_<Version>_x64-setup.exe` ausführen und die Installationsart wählen. Beim ersten Start einfach den Namen eingeben und verbinden.
|
||||
* **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`.
|
||||
* **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:**
|
||||
```powershell
|
||||
.\build.bat
|
||||
```
|
||||
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`
|
||||
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.
|
||||
2. `.\build.bat` ausführen.
|
||||
3. `.\publish-release.ps1` ausführen. Es legt den Release `v<Version>` auf Gitea an und lädt `EuerChat_<Version>_x64-setup.exe` samt `.sha256` hoch.
|
||||
Der Token kommt aus der Umgebungsvariable `GITEA_TOKEN` oder aus `%LOCALAPPDATA%\EuerChat\developer\gitea-token.dpapi`.
|
||||
* Gitea-Adresse und Repository stehen oben in `src-tauri\src\updater.rs` (`GITEA_BASE_URL`, `GITEA_REPO`) und in `publish-release.ps1`. Das Repository muss öffentlich sein, damit der Updater ohne Token auskommt.
|
||||
`EuerChat_<Version>_x64-setup.exe` ausführen und die Installationsart wählen. Beim ersten Start den Namen eingeben und verbinden.
|
||||
|
||||
* **Standard** (empfohlen): für alle Benutzer, standardmäßig nach `C:\Program Files\EuerChat`, mit Startmenü- und Desktop-Verknüpfung. Erfordert Administratorrechte, Deinstallation über die Windows-Einstellungen. Die Einstellungen liegen in `%APPDATA%\EuerChat\config.json`.
|
||||
* **Portable:** kopiert EuerChat in einen frei wählbaren Ordner, z. B. auf einen USB-Stick. Keine Administratorrechte, keine Registry-Einträge. Die Einstellungen liegen im Unterordner `data`. Entfernen: Ordner löschen.
|
||||
|
||||
Die Einstellungsdatei kann Passwörter enthalten und sollte nicht weitergegeben werden.
|
||||
|
||||
## Anleitung
|
||||
|
||||
Zu jeder Version gibt es im Release eine **PDF-Anleitung** (`EuerChat_Anleitung_<Version>.pdf`) mit Screenshots, in der jede Option für Einsteiger erklärt ist.
|
||||
@@ -0,0 +1,4 @@
|
||||
# EuerChat Releases
|
||||
|
||||
Letzter veroeffentlichter Release: v1.4.0 (2026-11-05 17:18:16 UTC)
|
||||
Letzter Beta-Release: v1.4.0-beta.4 (2026-11-05 12:12:07 UTC)
|
||||
@@ -1,49 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
Before Width: | Height: | Size: 264 KiB |
@@ -1,82 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 8.0 KiB |
@@ -1,37 +0,0 @@
|
||||
@echo off
|
||||
title EuerChat Build
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist node_modules (
|
||||
echo Installiere Tauri-CLI...
|
||||
call npm install
|
||||
if errorlevel 1 goto :fail
|
||||
)
|
||||
|
||||
echo Baue EuerChat.exe ...
|
||||
call npx tauri build --no-bundle
|
||||
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
|
||||
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
|
||||
echo [OK] Installer: dist\EuerChat_%VERSION%_x64-setup.exe
|
||||
pause
|
||||
exit /b 0
|
||||
|
||||
:fail
|
||||
echo [FEHLER] Build fehlgeschlagen!
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -1,136 +0,0 @@
|
||||
{
|
||||
"base_url": "https://chat.euer.tv/static/emocache/",
|
||||
"emotes": {
|
||||
"-___-": "1f611.png",
|
||||
":'-)": "1f602.png",
|
||||
"':-)": "1f605.png",
|
||||
"':-D": "1f605.png",
|
||||
">:-)": "1f606.png",
|
||||
"':-(": "1f613.png",
|
||||
">:-(": "1f620.png",
|
||||
":'-(": "1f622.png",
|
||||
"O:-)": "1f607.png",
|
||||
"0:-3": "1f607.png",
|
||||
"0:-)": "1f607.png",
|
||||
"0;^)": "1f607.png",
|
||||
"O;-)": "1f607.png",
|
||||
"0;-)": "1f607.png",
|
||||
"O:-3": "1f607.png",
|
||||
"-__-": "1f611.png",
|
||||
":-Þ": "1f61b.png",
|
||||
"<3": "2764.png",
|
||||
"</3": "1f494.png",
|
||||
":')": "1f602.png",
|
||||
":-D": "1f603.png",
|
||||
"':)": "1f605.png",
|
||||
"'=)": "1f605.png",
|
||||
"':D": "1f605.png",
|
||||
"'=D": "1f605.png",
|
||||
">:)": "1f606.png",
|
||||
">;)": "1f606.png",
|
||||
">=)": "1f606.png",
|
||||
"XD": "1f606.png",
|
||||
"xD": "1f606.png",
|
||||
";-)": "1f609.png",
|
||||
"*-)": "1f609.png",
|
||||
";-]": "1f609.png",
|
||||
";^)": "1f609.png",
|
||||
"':(": "1f613.png",
|
||||
"'=(": "1f613.png",
|
||||
":-*": "1f618.png",
|
||||
":^*": "1f618.png",
|
||||
">:P": "1f61c.png",
|
||||
"X-P": "1f61c.png",
|
||||
">:[": "1f61e.png",
|
||||
":-(": "1f61e.png",
|
||||
":-[": "1f61e.png",
|
||||
">:(": "1f620.png",
|
||||
":'(": "1f622.png",
|
||||
";-(": "1f622.png",
|
||||
">.<": "1f623.png",
|
||||
"#-)": "1f635.png",
|
||||
"%-)": "1f635.png",
|
||||
"X-)": "1f635.png",
|
||||
"\\0/": "1f646.png",
|
||||
"\\O/": "1f646.png",
|
||||
"0:3": "1f607.png",
|
||||
"0:)": "1f607.png",
|
||||
"O:)": "1f607.png",
|
||||
"O=)": "1f607.png",
|
||||
"O:3": "1f607.png",
|
||||
"B-)": "1f60e.png",
|
||||
"8-)": "1f60e.png",
|
||||
"B-D": "1f60e.png",
|
||||
"8-D": "1f60e.png",
|
||||
"-_-": "1f611.png",
|
||||
">:\\": "1f615.png",
|
||||
">:/": "1f615.png",
|
||||
":-/": "1f615.png",
|
||||
":-.": "1f615.png",
|
||||
":-P": "1f61b.png",
|
||||
":Þ": "1f61b.png",
|
||||
":-b": "1f61b.png",
|
||||
":-O": "1f62e.png",
|
||||
"O_O": "1f62e.png",
|
||||
">:O": "1f62e.png",
|
||||
":-X": "1f636.png",
|
||||
":-#": "1f636.png",
|
||||
":-)": "1f642.png",
|
||||
"(y)": "1f44d.png",
|
||||
":D": "1f603.png",
|
||||
"=D": "1f603.png",
|
||||
";)": "1f609.png",
|
||||
"*)": "1f609.png",
|
||||
";]": "1f609.png",
|
||||
";D": "1f609.png",
|
||||
":*": "1f618.png",
|
||||
"=*": "1f618.png",
|
||||
":(": "1f61e.png",
|
||||
":[": "1f61e.png",
|
||||
"=(": "1f61e.png",
|
||||
":@": "1f620.png",
|
||||
";(": "1f622.png",
|
||||
"D:": "1f628.png",
|
||||
":$": "1f633.png",
|
||||
"=$": "1f633.png",
|
||||
"#)": "1f635.png",
|
||||
"%)": "1f635.png",
|
||||
"X)": "1f635.png",
|
||||
"B)": "1f60e.png",
|
||||
"8)": "1f60e.png",
|
||||
":/": "1f615.png",
|
||||
":\\": "1f615.png",
|
||||
"=/": "1f615.png",
|
||||
"=\\": "1f615.png",
|
||||
":L": "1f615.png",
|
||||
"=L": "1f615.png",
|
||||
":P": "1f61b.png",
|
||||
":p": "1f61b.png",
|
||||
"=P": "1f61b.png",
|
||||
":b": "1f61b.png",
|
||||
":O": "1f62e.png",
|
||||
":X": "1f636.png",
|
||||
":#": "1f636.png",
|
||||
"=X": "1f636.png",
|
||||
"=#": "1f636.png",
|
||||
":)": "1f642.png",
|
||||
"=]": "1f642.png",
|
||||
"=)": "1f642.png",
|
||||
":]": "1f642.png",
|
||||
"CATJAM": "catjam.gif",
|
||||
"Clap": "clap.gif",
|
||||
"AlienPls": "AlienPls.gif",
|
||||
"blobDance": "blobDance.gif",
|
||||
"ClapHD": "ClapHD.gif",
|
||||
"Dance": "Dance.gif",
|
||||
"KirbDance": "KirbDance.gif",
|
||||
"PartyParrot": "PartyParrot.gif",
|
||||
"pepeDS": "pepeDS.gif",
|
||||
"PepoCheer": "PepoCheer.gif",
|
||||
"PepoDance": "PepoDance.gif",
|
||||
"ppJedi": "ppJedi.gif",
|
||||
"pugPls": "pugPls.gif",
|
||||
"RareParrot": "RareParrot.gif",
|
||||
"xar2EDM": "xar2EDM.gif"
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
; 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
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 151 KiB |
@@ -1,247 +0,0 @@
|
||||
{
|
||||
"name": "euer-chat",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "euer-chat",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.2.tgz",
|
||||
"integrity": "sha512-PMSppzAnlT+BvZtRKuidxjRSm2r5t8kDpJ/czmmQUa0AtDNTwrjLE8vApK3phCWGg7isfnYL46Dl1jB4ArI0hA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.12.2",
|
||||
"@tauri-apps/cli-darwin-x64": "2.12.2",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.2",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.12.2",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.12.2",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.2",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.12.2",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.12.2",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.12.2",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.12.2",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.12.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.2.tgz",
|
||||
"integrity": "sha512-Lk3zaBTfzNvA7670Al5PNgo2M/95uaDPe1yp+ILJeDOmol87XiYuK5E5+FUbK5e6U6OORx9qpZdOo2U+5tETtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.2.tgz",
|
||||
"integrity": "sha512-2CyYjJ833JB3sKRz4B0W/Tv4oOVC16ws1bxdSCWj1XvrmanWwvZi99e5tNIoCHwHhcOm0jh+sh0PCGQyls6Z+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.2.tgz",
|
||||
"integrity": "sha512-uvt2xbone4JftKarelssi86Kqnl7JiG1Smh5+Ae+nZmtM+JUhTGmqkcqfUPXRW8Ygwog92POZvd4oGDN9j8eXQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.2.tgz",
|
||||
"integrity": "sha512-DZlDJ24u3tlocKOYqzR+b2M+Qr+5WFcoMBZ7EfdhHN5j3ZXwBBpms4G4gyNvPCrak5vMzmdZ65ecBXPJcC+5sA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.2.tgz",
|
||||
"integrity": "sha512-pb+RTZUYH/p0gR4j4ecCCq+FNfz3W3eKuUQvVEBn7MQZx6DzfLO7FaYzt2NyDKWpdNN3lP10mdzOvw3R5P5aAQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.2.tgz",
|
||||
"integrity": "sha512-nidEvDUJyHYNbIv9MUk6KiEix8IXfRh4Wj2G2SA5g8nFmgbBCwazUNWYiG/LhNeNbfZpBMLerRBUtoQ5K0GidA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.2.tgz",
|
||||
"integrity": "sha512-gkXyYn3w+Xfc8nz/DleEbkm1M7bBuU2cejqUXNmI98cA2P5kp+oPmS4ALTorb1FaNWjpsKoYZBWsbS9BXxYFkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.2.tgz",
|
||||
"integrity": "sha512-MrjgoZ09s8Hsn99XvxSNvK8NQTZxi7XiyK3FxxytzfHgz2jb3qUT69CZtsokRkN/zdi9rCgrMtq+w9XNICjcfQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.2.tgz",
|
||||
"integrity": "sha512-BR+CYM7iF3lkfyTtVkwMQU/s9hwegLpj3UqDjaxKCPmixQCa1csBBPy3C75V+FhLtOjIXNna4Gt6Vm9oxbV+qA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.2.tgz",
|
||||
"integrity": "sha512-zODJo9l3TbNBM+GrO/yPqGeELNMK5cjIc+QHh1WQc3l8tD+jDZGTQyYuAxViCsM6Uh5uWJ//RnCZG9LXrvsXww==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.2.tgz",
|
||||
"integrity": "sha512-cQXI2ZaZmwBDt0XATCqASVanYizxkEE3ceQVlVHXaQ51HzsQUWA04c9Nz7LrrY2lgAu3jFMyat+8Yz3Z2R2qBA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "euer-chat",
|
||||
"version": "0.1.0",
|
||||
"description": "EuerChat Client fuer euer.tv",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"tauri": "tauri",
|
||||
"dev": "tauri dev",
|
||||
"build": "tauri build",
|
||||
"brand": "node tools/make-branding.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
param(
|
||||
[string]$Version = "",
|
||||
[string]$Notes = "",
|
||||
[switch]$ValidateOnly
|
||||
)
|
||||
|
||||
# Veroeffentlicht den Installer aus dist\ als Gitea-Release (Tag v<Version>) samt SHA-256-Datei.
|
||||
# Token: Umgebungsvariable GITEA_TOKEN oder DPAPI-Datei %LOCALAPPDATA%\EuerChat\developer\gitea-token.dpapi
|
||||
# (ersatzweise die MusicBrain-Token-Datei).
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
|
||||
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$BaseUrl = "https://gitea.davito.synology.me"
|
||||
$Owner = "Davito"
|
||||
$Repository = "EuerChat"
|
||||
$LocalAppData = [Environment]::GetFolderPath("LocalApplicationData")
|
||||
$TokenPaths = @(
|
||||
(Join-Path $LocalAppData "EuerChat\developer\gitea-token.dpapi"),
|
||||
(Join-Path $LocalAppData "MusicBrain\developer\gitea-token.dpapi")
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||
$Version = (Get-Content -LiteralPath (Join-Path $Root "src-tauri\tauri.conf.json") -Raw -Encoding UTF8 | ConvertFrom-Json).version
|
||||
}
|
||||
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
|
||||
throw "Ungueltige Release-Version: $Version"
|
||||
}
|
||||
|
||||
$Tag = "v$Version"
|
||||
$InstallerName = "EuerChat_${Version}_x64-setup.exe"
|
||||
$ChecksumName = "$InstallerName.sha256"
|
||||
$InstallerPath = Join-Path $Root "dist\$InstallerName"
|
||||
$ChecksumPath = Join-Path $Root "dist\$ChecksumName"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $InstallerPath -PathType Leaf)) {
|
||||
throw "Installer fehlt: $InstallerPath (zuerst build.bat ausfuehren)"
|
||||
}
|
||||
|
||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||
$stream = [System.IO.File]::OpenRead($InstallerPath)
|
||||
try {
|
||||
$hash = -join ($sha256.ComputeHash($stream) | ForEach-Object { $_.ToString("x2") })
|
||||
}
|
||||
finally {
|
||||
$stream.Dispose()
|
||||
$sha256.Dispose()
|
||||
}
|
||||
[System.IO.File]::WriteAllText($ChecksumPath, "$hash $InstallerName`n", [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Notes)) {
|
||||
$notesFile = Join-Path $Root "RELEASE_NOTES.md"
|
||||
if (Test-Path -LiteralPath $notesFile) {
|
||||
$Notes = (Get-Content -LiteralPath $notesFile -Raw -Encoding UTF8).Trim()
|
||||
}
|
||||
}
|
||||
|
||||
$plainToken = $env:GITEA_TOKEN
|
||||
if ([string]::IsNullOrWhiteSpace($plainToken)) {
|
||||
$tokenPath = $TokenPaths | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
|
||||
if (-not $tokenPath) {
|
||||
throw "Gitea-Token fehlt. Erwartet: GITEA_TOKEN oder $($TokenPaths[0])"
|
||||
}
|
||||
$secure = (Get-Content -LiteralPath $tokenPath -Raw -Encoding UTF8).Trim() | ConvertTo-SecureString
|
||||
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||
try { $plainToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) }
|
||||
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) }
|
||||
}
|
||||
|
||||
$client = [System.Net.Http.HttpClient]::new()
|
||||
$client.Timeout = [TimeSpan]::FromMinutes(20)
|
||||
$client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new("token", $plainToken.Trim())
|
||||
$client.DefaultRequestHeaders.UserAgent.ParseAdd("EuerChat-Release-Publisher/$Version")
|
||||
$client.DefaultRequestHeaders.Accept.ParseAdd("application/json")
|
||||
|
||||
function Send-Gitea {
|
||||
param([string]$Method, [string]$Path, [object]$Body, [string]$Action, [switch]$AllowNotFound)
|
||||
|
||||
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), "$BaseUrl/api/v1$Path")
|
||||
try {
|
||||
if ($null -ne $Body) {
|
||||
$request.Content = [System.Net.Http.StringContent]::new(
|
||||
($Body | ConvertTo-Json -Depth 8 -Compress), [System.Text.Encoding]::UTF8, "application/json")
|
||||
}
|
||||
$response = $client.SendAsync($request).GetAwaiter().GetResult()
|
||||
try {
|
||||
$text = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||||
if ($AllowNotFound -and [int]$response.StatusCode -eq 404) { return $null }
|
||||
if (-not $response.IsSuccessStatusCode) {
|
||||
throw "$Action fehlgeschlagen (HTTP $([int]$response.StatusCode)): $text"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { return $null }
|
||||
return $text | ConvertFrom-Json
|
||||
}
|
||||
finally { $response.Dispose() }
|
||||
}
|
||||
finally { $request.Dispose() }
|
||||
}
|
||||
|
||||
try {
|
||||
$repo = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository" -Action "Repository-Zugriff pruefen"
|
||||
if (-not $repo.permissions.push) {
|
||||
throw "Der Token besitzt keine Schreibrechte fuer $Owner/$Repository."
|
||||
}
|
||||
Write-Host "Repository: $($repo.full_name) Version: $Version SHA256: $hash"
|
||||
if ($ValidateOnly) { return }
|
||||
|
||||
$encodedTag = [Uri]::EscapeDataString($Tag)
|
||||
$release = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository/releases/tags/$encodedTag" -Action "Release lesen" -AllowNotFound
|
||||
|
||||
if ($null -eq $release) {
|
||||
# Gitea datiert Releases nach dem Commit von main; ohne frischen Commit bleibt /releases/latest alt.
|
||||
$marker = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository/contents/RELEASES.md?ref=main" -Action "Release-Marker lesen" -AllowNotFound
|
||||
$markerText = "# EuerChat Releases`n`nLetzter veroeffentlichter Release: v$Version ($([DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss')) UTC)`n"
|
||||
$markerBody = @{
|
||||
message = "Release v$Version"
|
||||
content = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($markerText))
|
||||
branch = "main"
|
||||
}
|
||||
if ($marker) { $markerBody.sha = $marker.sha }
|
||||
$markerMethod = if ($marker) { "PUT" } else { "POST" }
|
||||
Send-Gitea -Method $markerMethod -Path "/repos/$Owner/$Repository/contents/RELEASES.md" -Body $markerBody -Action "Release-Marker-Commit anlegen" | Out-Null
|
||||
|
||||
Write-Host "Lege Gitea-Release $Tag an..."
|
||||
$release = Send-Gitea -Method POST -Path "/repos/$Owner/$Repository/releases" -Action "Release anlegen" -Body @{
|
||||
tag_name = $Tag
|
||||
target_commitish = "main"
|
||||
name = "EuerChat $Version"
|
||||
body = $Notes
|
||||
draft = $false
|
||||
prerelease = $false
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "Setze vorhandenen Release $Tag fort."
|
||||
}
|
||||
|
||||
$existing = @($release.assets | ForEach-Object { $_.name })
|
||||
foreach ($assetPath in @($InstallerPath, $ChecksumPath)) {
|
||||
$assetName = [System.IO.Path]::GetFileName($assetPath)
|
||||
if ($assetName -in $existing) {
|
||||
Write-Host "Bereits vorhanden: $assetName"
|
||||
continue
|
||||
}
|
||||
Write-Host "Lade hoch: $assetName"
|
||||
$uploadUrl = "$BaseUrl/api/v1/repos/$Owner/$Repository/releases/$($release.id)/assets?name=$([Uri]::EscapeDataString($assetName))"
|
||||
$multipart = [System.Net.Http.MultipartFormDataContent]::new()
|
||||
$fileStream = [System.IO.File]::OpenRead($assetPath)
|
||||
$fileContent = [System.Net.Http.StreamContent]::new($fileStream)
|
||||
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new("application/octet-stream")
|
||||
$multipart.Add($fileContent, "attachment", $assetName)
|
||||
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Post, $uploadUrl)
|
||||
$request.Content = $multipart
|
||||
try {
|
||||
$response = $client.SendAsync($request).GetAwaiter().GetResult()
|
||||
try {
|
||||
if (-not $response.IsSuccessStatusCode) {
|
||||
throw "Upload von $assetName fehlgeschlagen (HTTP $([int]$response.StatusCode)): $($response.Content.ReadAsStringAsync().GetAwaiter().GetResult())"
|
||||
}
|
||||
}
|
||||
finally { $response.Dispose() }
|
||||
}
|
||||
finally {
|
||||
$request.Dispose(); $multipart.Dispose(); $fileContent.Dispose(); $fileStream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$published = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository/releases/tags/$encodedTag" -Action "Veroeffentlichten Release pruefen"
|
||||
$names = @($published.assets | ForEach-Object { $_.name })
|
||||
foreach ($required in @($InstallerName, $ChecksumName)) {
|
||||
if ($required -notin $names) { throw "Release wurde angelegt, aber das Asset fehlt: $required" }
|
||||
}
|
||||
Write-Host "Fertig: $BaseUrl/$Owner/$Repository/releases/tag/$Tag"
|
||||
}
|
||||
finally {
|
||||
$client.Dispose()
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "euer-chat"
|
||||
version = "0.1.0"
|
||||
description = "EuerChat Client fuer euer.tv"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["rt", "net", "io-util", "time", "sync", "fs", "macros"] }
|
||||
tokio-native-tls = "0.3"
|
||||
native-tls = "0.2"
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["native-tls"] }
|
||||
bytes = "1"
|
||||
sha2 = "0.10"
|
||||
|
||||
# Kein panic = "abort": ein Panic in einem Netzwerk-Task soll nicht die ganze App beenden.
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
@@ -1,5 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Standard Berechtigungen fuer das Hauptfenster",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:event:default"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 119 KiB |
@@ -1,50 +0,0 @@
|
||||
// Kein Konsolenfenster unter Windows
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
mod irc;
|
||||
mod storage;
|
||||
mod system;
|
||||
mod updater;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.manage(irc::AppState::default())
|
||||
.manage(updater::UpdaterState::default())
|
||||
.setup(|app| {
|
||||
tauri::async_runtime::spawn(updater::cleanup_installers());
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
storage::restore_window_state(&window);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||
storage::save_window_state(window);
|
||||
// Dem Server kurz Zeit geben, das QUIT zu empfangen.
|
||||
if window.state::<irc::AppState>().request_quit() {
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
irc::connect_irc,
|
||||
irc::send_irc_message,
|
||||
irc::send_irc_raw,
|
||||
irc::disconnect_irc,
|
||||
irc::scan_clones,
|
||||
storage::save_app_config,
|
||||
storage::load_app_config,
|
||||
system::send_grussbox,
|
||||
system::get_installed_fonts,
|
||||
system::set_ui_zoom,
|
||||
updater::get_app_version,
|
||||
updater::check_for_update,
|
||||
updater::install_update,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
//! Persistenz: Benutzerkonfiguration und Fensterposition.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{PhysicalPosition, PhysicalSize};
|
||||
|
||||
pub fn exe_dir() -> PathBuf {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(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 {
|
||||
if is_portable() {
|
||||
return exe_dir().join("data");
|
||||
}
|
||||
std::env::var_os("APPDATA")
|
||||
.map(|d| PathBuf::from(d).join("EuerChat"))
|
||||
.unwrap_or_else(exe_dir)
|
||||
}
|
||||
|
||||
fn config_path() -> PathBuf {
|
||||
data_dir().join("config.json")
|
||||
}
|
||||
|
||||
/// Bis Version 0.1 lag die config.json neben der EXE; wird nur noch gelesen.
|
||||
fn legacy_config_path() -> PathBuf {
|
||||
exe_dir().join("config.json")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_app_config() -> Result<String, String> {
|
||||
for path in [config_path(), legacy_config_path()] {
|
||||
match tokio::fs::read_to_string(&path).await {
|
||||
Ok(content) => return Ok(content),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(e) => return Err(format!("Fehler beim Lesen der Konfigurationsdatei: {e}")),
|
||||
}
|
||||
}
|
||||
Err("Konfigurationsdatei existiert noch nicht.".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_app_config(config_content: String) -> Result<String, String> {
|
||||
serde_json::from_str::<serde_json::Value>(&config_content)
|
||||
.map_err(|e| format!("Ungültige Konfiguration: {e}"))?;
|
||||
|
||||
let path = config_path();
|
||||
let err = |e: std::io::Error| format!("Fehler beim Speichern der Konfiguration: {e}");
|
||||
|
||||
tokio::fs::create_dir_all(data_dir()).await.map_err(err)?;
|
||||
// Erst in eine Temp-Datei schreiben, damit ein Absturz die Konfiguration nicht zerstoert.
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
tokio::fs::write(&tmp, config_content).await.map_err(err)?;
|
||||
tokio::fs::rename(&tmp, &path).await.map_err(err)?;
|
||||
Ok(format!("Konfiguration gespeichert in: {}", path.display()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
struct WindowState {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
maximized: bool,
|
||||
}
|
||||
|
||||
fn window_state_path() -> PathBuf {
|
||||
data_dir().join("window_state.json")
|
||||
}
|
||||
|
||||
fn read_window_state() -> Option<WindowState> {
|
||||
serde_json::from_str(&std::fs::read_to_string(window_state_path()).ok()?).ok()
|
||||
}
|
||||
|
||||
/// Wird nur beim Schliessen aufgerufen. Maximiert/minimiert bleibt die letzte normale Geometrie erhalten.
|
||||
pub fn save_window_state(window: &tauri::Window) {
|
||||
let maximized = window.is_maximized().unwrap_or(false);
|
||||
let minimized = window.is_minimized().unwrap_or(false);
|
||||
|
||||
let state = if maximized || minimized {
|
||||
match read_window_state() {
|
||||
Some(previous) => WindowState { maximized, ..previous },
|
||||
None => return,
|
||||
}
|
||||
} else {
|
||||
let (Ok(pos), Ok(size)) = (window.outer_position(), window.outer_size()) else {
|
||||
return;
|
||||
};
|
||||
if size.width < 400 || size.height < 300 {
|
||||
return;
|
||||
}
|
||||
WindowState {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
maximized: false,
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(json) = serde_json::to_string(&state) {
|
||||
let _ = std::fs::create_dir_all(data_dir());
|
||||
let _ = std::fs::write(window_state_path(), json);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_on_a_monitor(window: &tauri::WebviewWindow, state: &WindowState) -> bool {
|
||||
let (px, py) = (state.x + 50, state.y + 10);
|
||||
window
|
||||
.available_monitors()
|
||||
.map(|monitors| {
|
||||
monitors.iter().any(|m| {
|
||||
let (pos, size) = (m.position(), m.size());
|
||||
px >= pos.x
|
||||
&& px < pos.x + size.width as i32
|
||||
&& py >= pos.y
|
||||
&& py < pos.y + size.height as i32
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn restore_window_state(window: &tauri::WebviewWindow) {
|
||||
let Some(state) = read_window_state() else {
|
||||
return;
|
||||
};
|
||||
if state.width < 600 || state.height < 400 {
|
||||
return;
|
||||
}
|
||||
let _ = window.set_size(PhysicalSize::new(state.width, state.height));
|
||||
if is_on_a_monitor(window, &state) {
|
||||
let _ = window.set_position(PhysicalPosition::new(state.x, state.y));
|
||||
} else {
|
||||
let _ = window.center();
|
||||
}
|
||||
if state.maximized {
|
||||
let _ = window.maximize();
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
//! Windows-spezifische Helfer und die Grussbox.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
const GRUSSBOX_HOST: &str = "www.euer-radio.de";
|
||||
const FONT_SUFFIXES: [&str; 15] = [
|
||||
" Bold Italic", " Bold", " Italic", " Light Italic", " Light",
|
||||
" Semibold Italic", " Semibold", " SemiLight Italic", " SemiLight",
|
||||
" Black Italic", " Black", " Medium Italic", " Medium",
|
||||
" ExtraBold", " Regular",
|
||||
];
|
||||
|
||||
fn read_installed_fonts() -> Vec<String> {
|
||||
let hives = [
|
||||
"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts",
|
||||
"HKCU\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts",
|
||||
];
|
||||
let mut fonts = HashSet::new();
|
||||
|
||||
for hive in hives {
|
||||
let Ok(output) = std::process::Command::new("reg")
|
||||
.args(["query", hive])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
let Some(pos) = line.find("REG_SZ") else {
|
||||
continue;
|
||||
};
|
||||
let mut name = line[..pos].trim();
|
||||
if let Some(paren) = name.find('(') {
|
||||
name = name[..paren].trim();
|
||||
}
|
||||
let cleaned = FONT_SUFFIXES
|
||||
.iter()
|
||||
.find_map(|s| name.strip_suffix(s))
|
||||
.unwrap_or(name)
|
||||
.trim();
|
||||
|
||||
// "Cambria & Cambria Math" enthaelt mehrere Familien.
|
||||
for family in cleaned.split('&').map(str::trim) {
|
||||
if family.len() > 1 {
|
||||
fonts.insert(family.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut list: Vec<String> = fonts.into_iter().collect();
|
||||
list.sort_by_key(|f| f.to_lowercase());
|
||||
list
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_installed_fonts() -> Vec<String> {
|
||||
tauri::async_runtime::spawn_blocking(read_installed_fonts)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Skaliert die gesamte Oberflaeche (echter Webview-Zoom, Layout passt sich an).
|
||||
#[tauri::command]
|
||||
pub fn set_ui_zoom(window: tauri::WebviewWindow, factor: f64) -> Result<(), String> {
|
||||
if !factor.is_finite() {
|
||||
return Err("Ungültiger Zoom-Faktor".to_string());
|
||||
}
|
||||
window.set_zoom(factor.clamp(0.5, 3.0)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn form_urlencode(input: &str) -> String {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
for b in input.bytes() {
|
||||
match b {
|
||||
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
|
||||
b' ' => out.push('+'),
|
||||
_ => out.push_str(&format!("%{b:02X}")),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
async fn post_grussbox(name: &str, text: &str) -> Result<String, String> {
|
||||
let body = format!("gbname={}&gbtext={}", form_urlencode(name), form_urlencode(text));
|
||||
|
||||
let connector = tokio_native_tls::TlsConnector::from(
|
||||
native_tls::TlsConnector::new().map_err(|e| e.to_string())?,
|
||||
);
|
||||
let tcp = TcpStream::connect((GRUSSBOX_HOST, 443))
|
||||
.await
|
||||
.map_err(|e| format!("Verbindung zum Grußbox-Server fehlgeschlagen: {e}"))?;
|
||||
let mut tls = connector
|
||||
.connect(GRUSSBOX_HOST, tcp)
|
||||
.await
|
||||
.map_err(|e| format!("SSL-Verbindung fehlgeschlagen: {e}"))?;
|
||||
|
||||
let request = format!(
|
||||
"POST /gbentry.php HTTP/1.1\r\nHost: {GRUSSBOX_HOST}\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nUser-Agent: EuerChat/{}\r\nConnection: close\r\n\r\n{body}",
|
||||
body.len(),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
tls.write_all(request.as_bytes()).await.map_err(|e| e.to_string())?;
|
||||
tls.flush().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut response = Vec::new();
|
||||
tls.take(64 * 1024)
|
||||
.read_to_end(&mut response)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let response = String::from_utf8_lossy(&response);
|
||||
|
||||
let status_ok = response.lines().next().is_some_and(|l| l.contains(" 200"));
|
||||
if status_ok || response.contains("bermittelt") {
|
||||
Ok("Eintrag erfolgreich übermittelt!".to_string())
|
||||
} else {
|
||||
Err("Der Grußbox-Server hat die Nachricht nicht angenommen.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_grussbox(name: String, text: String) -> Result<String, String> {
|
||||
let (name, text) = (name.trim(), text.trim());
|
||||
if name.is_empty() || text.is_empty() {
|
||||
return Err("Name und Text dürfen nicht leer sein.".to_string());
|
||||
}
|
||||
if name.chars().count() > 100 || text.chars().count() > 2000 {
|
||||
return Err("Name oder Text ist zu lang.".to_string());
|
||||
}
|
||||
timeout(Duration::from_secs(20), post_grussbox(name, text))
|
||||
.await
|
||||
.map_err(|_| "Zeitüberschreitung beim Senden der Grußbox.".to_string())?
|
||||
}
|
||||
@@ -1,756 +0,0 @@
|
||||
//! Selbst-Updater: laedt neue Versionen aus den Releases eines oeffentlichen Gitea-Repositories.
|
||||
//!
|
||||
//! Das Repository braucht kein Token. Es werden nur HTTPS-URLs des vertrauenswuerdigen Hosts
|
||||
//! akzeptiert (auch bei Weiterleitungen), und der Installer wird nur gestartet, wenn seine
|
||||
//! SHA-256-Pruefsumme mit der Datei `<installer>.sha256` aus demselben Release uebereinstimmt.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::{redirect, Client, Response, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const GITEA_BASE_URL: &str = "https://gitea.davito.synology.me";
|
||||
const GITEA_REPO: &str = "Davito/EuerChat";
|
||||
const MAX_RELEASE_BYTES: usize = 2 * 1024 * 1024;
|
||||
const MAX_CHECKSUM_BYTES: usize = 64 * 1024;
|
||||
const MAX_INSTALLER_BYTES: u64 = 300 * 1024 * 1024;
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const READ_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
const DETACHED_PROCESS: u32 = 0x0000_0008;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Source {
|
||||
base: Url,
|
||||
repo: String,
|
||||
// Nur fuer Tests mit lokalem Mock-Server.
|
||||
allow_http: bool,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
fn production() -> Self {
|
||||
Self {
|
||||
base: Url::parse(GITEA_BASE_URL).expect("gueltige Gitea-URL"),
|
||||
repo: GITEA_REPO.to_string(),
|
||||
allow_http: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn latest_release_url(&self) -> String {
|
||||
format!(
|
||||
"{}/api/v1/repos/{}/releases/latest",
|
||||
self.base.as_str().trim_end_matches('/'),
|
||||
self.repo
|
||||
)
|
||||
}
|
||||
|
||||
fn is_trusted(&self, url: &Url) -> bool {
|
||||
let scheme_ok = url.scheme() == "https" || (self.allow_http && url.scheme() == "http");
|
||||
scheme_ok
|
||||
&& url.host_str() == self.base.host_str()
|
||||
&& url.port_or_known_default() == self.base.port_or_known_default()
|
||||
&& url.username().is_empty()
|
||||
&& url.password().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Asset {
|
||||
name: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Release {
|
||||
version: String,
|
||||
tag: String,
|
||||
name: String,
|
||||
body: String,
|
||||
assets: Vec<Asset>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawRelease {
|
||||
tag_name: Option<String>,
|
||||
name: Option<String>,
|
||||
body: Option<String>,
|
||||
assets: Option<Vec<RawAsset>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawAsset {
|
||||
name: Option<String>,
|
||||
browser_download_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UpdaterState {
|
||||
pending: Mutex<Option<Release>>,
|
||||
busy: AtomicBool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UpdateCheck {
|
||||
current: String,
|
||||
latest: String,
|
||||
available: bool,
|
||||
name: String,
|
||||
notes: String,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reine Hilfen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn parse_version(value: &str) -> Option<(u32, u32, u32)> {
|
||||
let mut parts = value.trim().trim_start_matches('v').split('.');
|
||||
let major = parts.next()?.parse().ok()?;
|
||||
let minor = parts.next()?.parse().ok()?;
|
||||
let patch = match parts.next() {
|
||||
Some(p) => p.parse().ok()?,
|
||||
None => 0,
|
||||
};
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
Some((major, minor, patch))
|
||||
}
|
||||
|
||||
fn is_newer(candidate: &str, installed: &str) -> Result<bool, String> {
|
||||
let parse = |v: &str| parse_version(v).ok_or_else(|| format!("Ungültige Versionsnummer: {v:?}"));
|
||||
Ok(parse(candidate)? > parse(installed)?)
|
||||
}
|
||||
|
||||
fn installer_name(version: &str) -> String {
|
||||
format!("EuerChat_{version}_x64-setup.exe")
|
||||
}
|
||||
|
||||
fn is_managed_installer_file(name: &str) -> bool {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
let base = lower.strip_suffix(".part").unwrap_or(&lower);
|
||||
base.starts_with("euerchat_") && base.ends_with("_x64-setup.exe")
|
||||
}
|
||||
|
||||
fn select_assets(release: &Release) -> Result<(&Asset, &Asset), String> {
|
||||
let expected = installer_name(&release.version);
|
||||
let find = |wanted: &str| {
|
||||
release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.eq_ignore_ascii_case(wanted))
|
||||
};
|
||||
let installer = find(&expected).ok_or_else(|| {
|
||||
format!("Release {} enthält nicht den erwarteten Installer {expected}.", release.tag)
|
||||
})?;
|
||||
let checksum_name = format!("{expected}.sha256");
|
||||
let checksum = find(&checksum_name).ok_or_else(|| {
|
||||
format!("Release {} enthält keine SHA-256-Datei {checksum_name}.", release.tag)
|
||||
})?;
|
||||
Ok((installer, checksum))
|
||||
}
|
||||
|
||||
/// Akzeptiert einen reinen Hash oder das `sha256sum`-Format (`<hash> <datei>`).
|
||||
fn parse_expected_checksum(manifest: &str, filename: &str) -> Result<String, String> {
|
||||
let mut fallback = None;
|
||||
for line in manifest.lines().map(str::trim).filter(|l| !l.is_empty()) {
|
||||
let mut parts = line.split_whitespace();
|
||||
let Some(hash) = parts.next() else { continue };
|
||||
if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
continue;
|
||||
}
|
||||
let hash = hash.to_ascii_lowercase();
|
||||
match parts.next() {
|
||||
None => fallback = Some(hash),
|
||||
Some(listed) if listed.trim_start_matches('*').eq_ignore_ascii_case(filename) => {
|
||||
return Ok(hash)
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
fallback.ok_or_else(|| format!("Keine SHA-256-Prüfsumme für {filename} gefunden."))
|
||||
}
|
||||
|
||||
fn updates_dir() -> PathBuf {
|
||||
crate::storage::data_dir().join("updates")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Netzwerk
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_client(source: &Source) -> Result<Client, String> {
|
||||
let trusted = source.clone();
|
||||
Client::builder()
|
||||
.user_agent(format!("EuerChat-Updater/{}", env!("CARGO_PKG_VERSION")))
|
||||
.connect_timeout(CONNECT_TIMEOUT)
|
||||
.redirect(redirect::Policy::custom(move |attempt| {
|
||||
if attempt.previous().len() >= 5 {
|
||||
attempt.error("zu viele Weiterleitungen")
|
||||
} else if trusted.is_trusted(attempt.url()) {
|
||||
attempt.follow()
|
||||
} else {
|
||||
attempt.error("nicht vertrauenswürdige Weiterleitung")
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
.map_err(|e| format!("HTTP-Client konnte nicht erstellt werden: {e}"))
|
||||
}
|
||||
|
||||
async fn get(client: &Client, source: &Source, url: &str) -> Result<Response, String> {
|
||||
let parsed = Url::parse(url).map_err(|_| "Ungültige Update-URL.".to_string())?;
|
||||
if !source.is_trusted(&parsed) {
|
||||
return Err("Gitea lieferte eine nicht vertrauenswürdige URL.".to_string());
|
||||
}
|
||||
let response = client
|
||||
.get(parsed)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Der Update-Server ist nicht erreichbar: {e}"))?;
|
||||
if !source.is_trusted(response.url()) {
|
||||
return Err("Gitea leitete auf eine nicht vertrauenswürdige URL weiter.".to_string());
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn next_chunk(response: &mut Response) -> Result<Option<bytes::Bytes>, String> {
|
||||
timeout(READ_TIMEOUT, response.chunk())
|
||||
.await
|
||||
.map_err(|_| "Zeitüberschreitung beim Laden vom Update-Server.".to_string())?
|
||||
.map_err(|e| format!("Fehler beim Laden vom Update-Server: {e}"))
|
||||
}
|
||||
|
||||
async fn read_limited(mut response: Response, limit: usize) -> Result<Vec<u8>, String> {
|
||||
let too_big = || "Die Antwort des Update-Servers ist unerwartet groß.".to_string();
|
||||
if response.content_length().is_some_and(|l| l > limit as u64) {
|
||||
return Err(too_big());
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
while let Some(chunk) = next_chunk(&mut response).await? {
|
||||
if buf.len() + chunk.len() > limit {
|
||||
return Err(too_big());
|
||||
}
|
||||
buf.extend_from_slice(&chunk);
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
async fn fetch_latest_release(client: &Client, source: &Source) -> Result<Release, String> {
|
||||
let response = get(client, source, &source.latest_release_url()).await?;
|
||||
match response.status().as_u16() {
|
||||
200 => {}
|
||||
404 => return Err("Auf Gitea ist noch kein veröffentlichter Release vorhanden.".to_string()),
|
||||
code => return Err(format!("Gitea antwortete mit HTTP {code}.")),
|
||||
}
|
||||
let payload = read_limited(response, MAX_RELEASE_BYTES).await?;
|
||||
let raw: RawRelease = serde_json::from_slice(&payload)
|
||||
.map_err(|_| "Gitea lieferte keine gültigen Release-Daten.".to_string())?;
|
||||
|
||||
let tag = raw.tag_name.unwrap_or_default().trim().to_string();
|
||||
let (major, minor, patch) = parse_version(&tag)
|
||||
.ok_or_else(|| format!("Ungültige Versionsnummer im Release: {tag:?}"))?;
|
||||
|
||||
let mut assets = Vec::new();
|
||||
for asset in raw.assets.unwrap_or_default() {
|
||||
let (Some(name), Some(url)) = (asset.name, asset.browser_download_url) else {
|
||||
continue;
|
||||
};
|
||||
let parsed = Url::parse(&url).map_err(|_| "Gitea lieferte eine ungültige Asset-URL.".to_string())?;
|
||||
if !source.is_trusted(&parsed) {
|
||||
return Err("Gitea lieferte eine nicht vertrauenswürdige Asset-URL.".to_string());
|
||||
}
|
||||
assets.push(Asset { name, url });
|
||||
}
|
||||
|
||||
Ok(Release {
|
||||
version: format!("{major}.{minor}.{patch}"),
|
||||
name: raw.name.filter(|n| !n.trim().is_empty()).unwrap_or_else(|| tag.clone()),
|
||||
body: raw.body.unwrap_or_default(),
|
||||
tag,
|
||||
assets,
|
||||
})
|
||||
}
|
||||
|
||||
/// Laedt den Installer, prueft die SHA-256-Summe und gibt den endgueltigen Pfad zurueck.
|
||||
async fn download_installer(
|
||||
client: &Client,
|
||||
source: &Source,
|
||||
release: &Release,
|
||||
dir: &Path,
|
||||
mut progress: impl FnMut(u8),
|
||||
) -> Result<PathBuf, String> {
|
||||
let (installer, checksum) = select_assets(release)?;
|
||||
if installer.name.contains(['/', '\\']) {
|
||||
return Err("Der Installername im Release ist ungültig.".to_string());
|
||||
}
|
||||
|
||||
let manifest = read_limited(get(client, source, &checksum.url).await?, MAX_CHECKSUM_BYTES).await?;
|
||||
let expected = parse_expected_checksum(&String::from_utf8_lossy(&manifest), &installer.name)?;
|
||||
|
||||
tokio::fs::create_dir_all(dir)
|
||||
.await
|
||||
.map_err(|e| format!("Update-Ordner konnte nicht angelegt werden: {e}"))?;
|
||||
let final_path = dir.join(&installer.name);
|
||||
let partial_path = dir.join(format!("{}.part", installer.name));
|
||||
|
||||
let result = download_to_file(client, source, &installer.url, &partial_path, &expected, &mut progress).await;
|
||||
if let Err(e) = result {
|
||||
let _ = tokio::fs::remove_file(&partial_path).await;
|
||||
return Err(e);
|
||||
}
|
||||
tokio::fs::rename(&partial_path, &final_path)
|
||||
.await
|
||||
.map_err(|e| format!("Der Installer konnte nicht gespeichert werden: {e}"))?;
|
||||
progress(100);
|
||||
Ok(final_path)
|
||||
}
|
||||
|
||||
async fn download_to_file(
|
||||
client: &Client,
|
||||
source: &Source,
|
||||
url: &str,
|
||||
path: &Path,
|
||||
expected_sha256: &str,
|
||||
progress: &mut impl FnMut(u8),
|
||||
) -> Result<(), String> {
|
||||
let mut response = get(client, source, url).await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Der Installer-Download schlug fehl (HTTP {}).", response.status().as_u16()));
|
||||
}
|
||||
let total = response.content_length().unwrap_or(0);
|
||||
if total > MAX_INSTALLER_BYTES {
|
||||
return Err("Der angebotene Installer ist unerwartet groß.".to_string());
|
||||
}
|
||||
|
||||
let io_err = |e: std::io::Error| format!("Der Installer konnte nicht geschrieben werden: {e}");
|
||||
let mut file = tokio::fs::File::create(path).await.map_err(io_err)?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut last_percent = 0u8;
|
||||
|
||||
while let Some(chunk) = next_chunk(&mut response).await? {
|
||||
downloaded += chunk.len() as u64;
|
||||
if downloaded > MAX_INSTALLER_BYTES {
|
||||
return Err("Der geladene Installer ist unerwartet groß.".to_string());
|
||||
}
|
||||
hasher.update(&chunk);
|
||||
file.write_all(&chunk).await.map_err(io_err)?;
|
||||
if let Some(percent) = (downloaded * 100).checked_div(total) {
|
||||
let percent = percent.min(99) as u8;
|
||||
if percent != last_percent {
|
||||
last_percent = percent;
|
||||
progress(percent);
|
||||
}
|
||||
}
|
||||
}
|
||||
file.flush().await.map_err(io_err)?;
|
||||
drop(file);
|
||||
|
||||
if downloaded == 0 {
|
||||
return Err("Der heruntergeladene Installer ist leer.".to_string());
|
||||
}
|
||||
let actual: String = hasher.finalize().iter().map(|b| format!("{b:02x}")).collect();
|
||||
if actual != expected_sha256 {
|
||||
return Err("Die SHA-256-Prüfung des Installers ist fehlgeschlagen; die Datei wird nicht ausgeführt.".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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.
|
||||
fn spawn_installer_after_exit(installer: &Path) -> std::io::Result<()> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
let current = std::env::current_exe()?;
|
||||
let kind = current
|
||||
.parent()
|
||||
.map_or(InstallKind::Loose, detect_install_kind);
|
||||
let script = build_update_script(std::process::id(), installer, ¤t, kind);
|
||||
|
||||
std::process::Command::new("powershell.exe")
|
||||
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &script])
|
||||
.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Raeumt alte Installer-Downloads auf; gesperrte Dateien werden beim naechsten Start erneut versucht.
|
||||
pub async fn cleanup_installers() {
|
||||
let Ok(mut entries) = tokio::fs::read_dir(updates_dir()).await else {
|
||||
return;
|
||||
};
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let is_file = entry.file_type().await.map(|t| t.is_file()).unwrap_or(false);
|
||||
if is_file && is_managed_installer_file(&entry.file_name().to_string_lossy()) {
|
||||
let _ = tokio::fs::remove_file(entry.path()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tauri-Commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_version(app: AppHandle) -> String {
|
||||
app.package_info().version.to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_update(
|
||||
app: AppHandle,
|
||||
state: State<'_, UpdaterState>,
|
||||
) -> Result<UpdateCheck, String> {
|
||||
let current = app.package_info().version.to_string();
|
||||
let source = Source::production();
|
||||
let client = build_client(&source)?;
|
||||
let release = fetch_latest_release(&client, &source).await?;
|
||||
|
||||
let available = is_newer(&release.version, ¤t)?;
|
||||
if available {
|
||||
// Fehlende Dateien sofort melden, nicht erst beim Installieren.
|
||||
select_assets(&release)?;
|
||||
}
|
||||
|
||||
let check = UpdateCheck {
|
||||
current,
|
||||
latest: release.version.clone(),
|
||||
available,
|
||||
name: release.name.clone(),
|
||||
notes: release.body.clone(),
|
||||
};
|
||||
*state.pending.lock().await = available.then_some(release);
|
||||
Ok(check)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_update(
|
||||
app: AppHandle,
|
||||
state: State<'_, UpdaterState>,
|
||||
) -> Result<(), String> {
|
||||
if state.busy.swap(true, Ordering::SeqCst) {
|
||||
return Err("Ein Update läuft bereits.".to_string());
|
||||
}
|
||||
let result = run_install(&app, &state).await;
|
||||
if result.is_err() {
|
||||
state.busy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn run_install(app: &AppHandle, state: &UpdaterState) -> Result<(), String> {
|
||||
let release = state
|
||||
.pending
|
||||
.lock()
|
||||
.await
|
||||
.clone()
|
||||
.ok_or_else(|| "Es ist kein Update vorgemerkt. Bitte erneut nach Updates suchen.".to_string())?;
|
||||
|
||||
let source = Source::production();
|
||||
let client = build_client(&source)?;
|
||||
let installer = download_installer(&client, &source, &release, &updates_dir(), |percent| {
|
||||
let _ = app.emit("update-progress", percent);
|
||||
})
|
||||
.await?;
|
||||
|
||||
spawn_installer_after_exit(&installer)
|
||||
.map_err(|e| format!("Die Installation konnte nicht gestartet werden: {e}"))?;
|
||||
|
||||
// Dem IRC-Server noch ein sauberes QUIT schicken, dann beenden.
|
||||
if app.state::<crate::irc::AppState>().request_quit() {
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
}
|
||||
app.exit(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[test]
|
||||
fn version_parsing_and_comparison() {
|
||||
assert_eq!(parse_version("v1.2.3"), Some((1, 2, 3)));
|
||||
assert_eq!(parse_version("0.2"), Some((0, 2, 0)));
|
||||
assert_eq!(parse_version("1.2.3-beta"), None);
|
||||
assert_eq!(parse_version("1.2.3.4"), None);
|
||||
assert!(is_newer("0.10.0", "0.9.9").unwrap());
|
||||
assert!(!is_newer("v0.1.0", "0.1.0").unwrap());
|
||||
assert!(is_newer("x", "0.1.0").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_trusted_https_host_is_accepted() {
|
||||
let source = Source::production();
|
||||
let ok = |u: &str| source.is_trusted(&Url::parse(u).unwrap());
|
||||
assert!(ok("https://gitea.davito.synology.me/Davito/EuerChat/releases/download/v1/a.exe"));
|
||||
assert!(ok("https://gitea.davito.synology.me:443/x"));
|
||||
assert!(!ok("http://gitea.davito.synology.me/x"));
|
||||
assert!(!ok("https://gitea.davito.synology.me:8443/x"));
|
||||
assert!(!ok("https://evil.example.com/x"));
|
||||
assert!(!ok("https://gitea.davito.synology.me.evil.com/x"));
|
||||
assert!(!ok("https://user:pw@gitea.davito.synology.me/x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checksum_formats() {
|
||||
let hash = "a".repeat(64);
|
||||
assert_eq!(parse_expected_checksum(&hash, "x.exe").unwrap(), hash);
|
||||
assert_eq!(
|
||||
parse_expected_checksum(&format!("{} *X.exe\n", hash.to_uppercase()), "x.exe").unwrap(),
|
||||
hash
|
||||
);
|
||||
assert!(parse_expected_checksum("nonsense", "x.exe").is_err());
|
||||
let other = format!("{} other.exe", "b".repeat(64));
|
||||
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]
|
||||
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.part"));
|
||||
assert!(!is_managed_installer_file("config.json"));
|
||||
assert!(!is_managed_installer_file("other-setup.exe"));
|
||||
}
|
||||
|
||||
fn release_with(version: &str, names: &[&str]) -> Release {
|
||||
Release {
|
||||
version: version.to_string(),
|
||||
tag: format!("v{version}"),
|
||||
name: String::new(),
|
||||
body: String::new(),
|
||||
assets: names
|
||||
.iter()
|
||||
.map(|n| Asset { name: n.to_string(), url: String::new() })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_selection_requires_installer_and_checksum() {
|
||||
let both = release_with("0.2.0", &["EuerChat_0.2.0_x64-setup.exe", "EuerChat_0.2.0_x64-setup.exe.sha256"]);
|
||||
assert!(select_assets(&both).is_ok());
|
||||
let no_sum = release_with("0.2.0", &["EuerChat_0.2.0_x64-setup.exe"]);
|
||||
assert!(select_assets(&no_sum).is_err());
|
||||
let wrong = release_with("0.2.0", &["EuerChat_0.1.0_x64-setup.exe", "EuerChat_0.1.0_x64-setup.exe.sha256"]);
|
||||
assert!(select_assets(&wrong).is_err());
|
||||
}
|
||||
|
||||
type Routes = Vec<(String, u16, Option<String>, Vec<u8>)>;
|
||||
|
||||
/// Minimaler HTTP-Server: Pfad -> (Status, Header-Zusatz, Body).
|
||||
async fn serve(routes: Routes) -> u16 {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
spawn_server(listener, routes);
|
||||
port
|
||||
}
|
||||
|
||||
fn spawn_server(listener: TcpListener, routes: Routes) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let Ok((mut sock, _)) = listener.accept().await else { return };
|
||||
let routes = routes.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let n = sock.read(&mut buf).await.unwrap_or(0);
|
||||
let request = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||
let path = request.split_whitespace().nth(1).unwrap_or("/").to_string();
|
||||
let (status, extra, body) = routes
|
||||
.iter()
|
||||
.find(|(p, ..)| *p == path)
|
||||
.map(|(_, s, e, b)| (*s, e.clone(), b.clone()))
|
||||
.unwrap_or((404, None, Vec::new()));
|
||||
let head = format!(
|
||||
"HTTP/1.1 {status} X\r\nContent-Length: {}\r\n{}Connection: close\r\n\r\n",
|
||||
body.len(),
|
||||
extra.map(|e| format!("{e}\r\n")).unwrap_or_default()
|
||||
);
|
||||
let _ = sock.write_all(head.as_bytes()).await;
|
||||
let _ = sock.write_all(&body).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn test_source(port: u16) -> Source {
|
||||
Source {
|
||||
base: Url::parse(&format!("http://127.0.0.1:{port}")).unwrap(),
|
||||
repo: "Davito/EuerChat".to_string(),
|
||||
allow_http: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn sha256_hex(data: &[u8]) -> String {
|
||||
Sha256::digest(data).iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
async fn setup(installer: &[u8], checksum_hash: &str) -> (Source, Client) {
|
||||
// Port vorab reservieren, damit die Asset-URLs im JSON stimmen.
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
|
||||
let name = "EuerChat_0.2.0_x64-setup.exe";
|
||||
let json = serde_json::json!({
|
||||
"tag_name": "v0.2.0",
|
||||
"name": "EuerChat 0.2.0",
|
||||
"body": "Neu: Updater",
|
||||
"assets": [
|
||||
{"name": name, "browser_download_url": format!("http://127.0.0.1:{port}/dl/{name}")},
|
||||
{"name": format!("{name}.sha256"), "browser_download_url": format!("http://127.0.0.1:{port}/dl/{name}.sha256")}
|
||||
]
|
||||
});
|
||||
let routes = vec![
|
||||
("/api/v1/repos/Davito/EuerChat/releases/latest".to_string(), 200, None, json.to_string().into_bytes()),
|
||||
(format!("/dl/{name}"), 200, None, installer.to_vec()),
|
||||
(format!("/dl/{name}.sha256"), 200, None, format!("{checksum_hash} {name}\n").into_bytes()),
|
||||
("/redirect".to_string(), 302, Some("Location: https://evil.example.com/x".to_string()), Vec::new()),
|
||||
];
|
||||
spawn_server(listener, routes);
|
||||
let source = test_source(port);
|
||||
let client = build_client(&source).unwrap();
|
||||
(source, client)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetches_release_and_downloads_verified_installer() {
|
||||
let data = b"fake installer bytes".repeat(1000);
|
||||
let (source, client) = setup(&data, &sha256_hex(&data)).await;
|
||||
|
||||
let release = fetch_latest_release(&client, &source).await.unwrap();
|
||||
assert_eq!(release.version, "0.2.0");
|
||||
assert_eq!(release.body, "Neu: Updater");
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("euerchat-updater-test-{}", std::process::id()));
|
||||
let mut last = 0u8;
|
||||
let path = download_installer(&client, &source, &release, &dir, |p| last = p).await.unwrap();
|
||||
assert_eq!(std::fs::read(&path).unwrap(), data);
|
||||
assert_eq!(last, 100);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_installer_with_wrong_checksum_and_removes_partial_file() {
|
||||
let data = b"tampered".repeat(100);
|
||||
let (source, client) = setup(&data, &"0".repeat(64)).await;
|
||||
let release = fetch_latest_release(&client, &source).await.unwrap();
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("euerchat-updater-bad-{}", std::process::id()));
|
||||
let err = download_installer(&client, &source, &release, &dir, |_| {}).await.unwrap_err();
|
||||
assert!(err.contains("SHA-256"));
|
||||
let leftovers: Vec<_> = std::fs::read_dir(&dir).unwrap().flatten().collect();
|
||||
assert!(leftovers.is_empty(), "keine .part- oder fertige Datei erwartet");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_redirect_to_untrusted_host() {
|
||||
let (source, client) = setup(b"x", &"0".repeat(64)).await;
|
||||
let url = format!("{}/redirect", source.base.as_str().trim_end_matches('/'));
|
||||
assert!(get(&client, &source, &url).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_release_is_reported() {
|
||||
let port = serve(vec![]).await;
|
||||
let source = test_source(port);
|
||||
let client = build_client(&source).unwrap();
|
||||
let err = fetch_latest_release(&client, &source).await.unwrap_err();
|
||||
assert!(err.contains("noch kein"));
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "EuerChat",
|
||||
"version": "0.1.0",
|
||||
"identifier": "tv.euer.chat",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "EuerChat",
|
||||
"width": 1100,
|
||||
"height": 720,
|
||||
"minWidth": 800,
|
||||
"minHeight": 550,
|
||||
"center": true,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": "'self'",
|
||||
"script-src": "'self'",
|
||||
"style-src": "'self' 'unsafe-inline'",
|
||||
"style-src-attr": "'unsafe-inline'",
|
||||
"img-src": "'self' data: blob: https://chat.euer.tv",
|
||||
"media-src": "'self' blob: https://*.euer.tv",
|
||||
"worker-src": "'self' blob:",
|
||||
"connect-src": "'self' ipc: http://ipc.localhost https://*.euer.tv",
|
||||
"font-src": "'self'",
|
||||
"object-src": "'none'",
|
||||
"frame-src": "'none'",
|
||||
"base-uri": "'self'",
|
||||
"form-action": "'self'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false,
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"]
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
@echo off
|
||||
title EuerChat Client
|
||||
cd /d "%~dp0"
|
||||
if exist "EuerChat.exe" (
|
||||
start "" "EuerChat.exe"
|
||||
) else (
|
||||
echo Baue und starte EuerChat...
|
||||
cd src-tauri
|
||||
cargo run
|
||||
)
|
||||
@@ -1,350 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
{
|
||||
"base_url": "https://chat.euer.tv/static/emocache/",
|
||||
"emotes": {
|
||||
"-___-": "1f611.png",
|
||||
":'-)": "1f602.png",
|
||||
"':-)": "1f605.png",
|
||||
"':-D": "1f605.png",
|
||||
">:-)": "1f606.png",
|
||||
"':-(": "1f613.png",
|
||||
">:-(": "1f620.png",
|
||||
":'-(": "1f622.png",
|
||||
"O:-)": "1f607.png",
|
||||
"0:-3": "1f607.png",
|
||||
"0:-)": "1f607.png",
|
||||
"0;^)": "1f607.png",
|
||||
"O;-)": "1f607.png",
|
||||
"0;-)": "1f607.png",
|
||||
"O:-3": "1f607.png",
|
||||
"-__-": "1f611.png",
|
||||
":-Þ": "1f61b.png",
|
||||
"<3": "2764.png",
|
||||
"</3": "1f494.png",
|
||||
":')": "1f602.png",
|
||||
":-D": "1f603.png",
|
||||
"':)": "1f605.png",
|
||||
"'=)": "1f605.png",
|
||||
"':D": "1f605.png",
|
||||
"'=D": "1f605.png",
|
||||
">:)": "1f606.png",
|
||||
">;)": "1f606.png",
|
||||
">=)": "1f606.png",
|
||||
"XD": "1f606.png",
|
||||
"xD": "1f606.png",
|
||||
";-)": "1f609.png",
|
||||
"*-)": "1f609.png",
|
||||
";-]": "1f609.png",
|
||||
";^)": "1f609.png",
|
||||
"':(": "1f613.png",
|
||||
"'=(": "1f613.png",
|
||||
":-*": "1f618.png",
|
||||
":^*": "1f618.png",
|
||||
">:P": "1f61c.png",
|
||||
"X-P": "1f61c.png",
|
||||
">:[": "1f61e.png",
|
||||
":-(": "1f61e.png",
|
||||
":-[": "1f61e.png",
|
||||
">:(": "1f620.png",
|
||||
":'(": "1f622.png",
|
||||
";-(": "1f622.png",
|
||||
">.<": "1f623.png",
|
||||
"#-)": "1f635.png",
|
||||
"%-)": "1f635.png",
|
||||
"X-)": "1f635.png",
|
||||
"\\0/": "1f646.png",
|
||||
"\\O/": "1f646.png",
|
||||
"0:3": "1f607.png",
|
||||
"0:)": "1f607.png",
|
||||
"O:)": "1f607.png",
|
||||
"O=)": "1f607.png",
|
||||
"O:3": "1f607.png",
|
||||
"B-)": "1f60e.png",
|
||||
"8-)": "1f60e.png",
|
||||
"B-D": "1f60e.png",
|
||||
"8-D": "1f60e.png",
|
||||
"-_-": "1f611.png",
|
||||
">:\\": "1f615.png",
|
||||
">:/": "1f615.png",
|
||||
":-/": "1f615.png",
|
||||
":-.": "1f615.png",
|
||||
":-P": "1f61b.png",
|
||||
":Þ": "1f61b.png",
|
||||
":-b": "1f61b.png",
|
||||
":-O": "1f62e.png",
|
||||
"O_O": "1f62e.png",
|
||||
">:O": "1f62e.png",
|
||||
":-X": "1f636.png",
|
||||
":-#": "1f636.png",
|
||||
":-)": "1f642.png",
|
||||
"(y)": "1f44d.png",
|
||||
":D": "1f603.png",
|
||||
"=D": "1f603.png",
|
||||
";)": "1f609.png",
|
||||
"*)": "1f609.png",
|
||||
";]": "1f609.png",
|
||||
";D": "1f609.png",
|
||||
":*": "1f618.png",
|
||||
"=*": "1f618.png",
|
||||
":(": "1f61e.png",
|
||||
":[": "1f61e.png",
|
||||
"=(": "1f61e.png",
|
||||
":@": "1f620.png",
|
||||
";(": "1f622.png",
|
||||
"D:": "1f628.png",
|
||||
":$": "1f633.png",
|
||||
"=$": "1f633.png",
|
||||
"#)": "1f635.png",
|
||||
"%)": "1f635.png",
|
||||
"X)": "1f635.png",
|
||||
"B)": "1f60e.png",
|
||||
"8)": "1f60e.png",
|
||||
":/": "1f615.png",
|
||||
":\\": "1f615.png",
|
||||
"=/": "1f615.png",
|
||||
"=\\": "1f615.png",
|
||||
":L": "1f615.png",
|
||||
"=L": "1f615.png",
|
||||
":P": "1f61b.png",
|
||||
":p": "1f61b.png",
|
||||
"=P": "1f61b.png",
|
||||
":b": "1f61b.png",
|
||||
":O": "1f62e.png",
|
||||
":X": "1f636.png",
|
||||
":#": "1f636.png",
|
||||
"=X": "1f636.png",
|
||||
"=#": "1f636.png",
|
||||
":)": "1f642.png",
|
||||
"=]": "1f642.png",
|
||||
"=)": "1f642.png",
|
||||
":]": "1f642.png",
|
||||
"CATJAM": "catjam.gif",
|
||||
"Clap": "clap.gif",
|
||||
"AlienPls": "AlienPls.gif",
|
||||
"blobDance": "blobDance.gif",
|
||||
"ClapHD": "ClapHD.gif",
|
||||
"Dance": "Dance.gif",
|
||||
"KirbDance": "KirbDance.gif",
|
||||
"PartyParrot": "PartyParrot.gif",
|
||||
"pepeDS": "pepeDS.gif",
|
||||
"PepoCheer": "PepoCheer.gif",
|
||||
"PepoDance": "PepoDance.gif",
|
||||
"ppJedi": "ppJedi.gif",
|
||||
"pugPls": "pugPls.gif",
|
||||
"RareParrot": "RareParrot.gif",
|
||||
"xar2EDM": "xar2EDM.gif"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
!function e(t){var r,i;r=this,i=function(){"use strict";function r(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,i=Array(t);r<t;r++)i[r]=e[r];return i}function i(e,t,r,i,n,a,s){try{var o=e[a](s),l=o.value}catch(e){return void r(e)}o.done?t(l):Promise.resolve(l).then(i,n)}function n(e,t){for(var r=0;r<t.length;r++){var i=t[r];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,y(i.key),i)}}function a(e,t,r){return t&&n(e.prototype,t),r&&n(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}function s(e,t){var i="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(i)return(i=i.call(e)).next.bind(i);if(Array.isArray(e)||(i=function(e,t){if(e){if("string"==typeof e)return r(e,t);var i={}.toString.call(e).slice(8,-1);return"Object"===i&&e.constructor&&(i=e.constructor.name),"Map"===i||"Set"===i?Array.from(e):"Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?r(e,t):void 0}}(e))||t){i&&(e=i);var n=0;return function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function o(e,t,r){return(t=y(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function l(){return l=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var i in r)({}).hasOwnProperty.call(r,i)&&(e[i]=r[i])}return e},l.apply(null,arguments)}function u(e){return u=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},u(e)}function d(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,p(e,t)}function h(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(e){}return(h=function(){return!!e})()}function f(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,i)}return r}function c(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?f(Object(r),!0).forEach((function(t){o(e,t,r[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):f(Object(r)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))}))}return e}function g(e,t){if(null==e)return{};var r={};for(var i in e)if({}.hasOwnProperty.call(e,i)){if(-1!==t.indexOf(i))continue;r[i]=e[i]}return r}function v(){
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
|
||||
var e,t,r="function"==typeof Symbol?Symbol:{},i=r.iterator||"@@iterator",n=r.toStringTag||"@@toStringTag";function a(r,i,n,a){var l=i&&i.prototype instanceof o?i:o,u=Object.create(l.prototype);return m(u,"_invoke",function(r,i,n){var a,o,l,u=0,d=n||[],h=!1,f={p:0,n:0,v:e,a:c,f:c.bind(e,4),d:function(t,r){return a=t,o=0,l=e,f.n=r,s}};function c(r,i){for(o=r,l=i,t=0;!h&&u&&!n&&t<d.length;t++){var n,a=d[t],c=f.p,g=a[2];r>3?(n=g===i)&&(l=a[(o=a[4])?5:(o=3,3)],a[4]=a[5]=e):a[0]<=c&&((n=r<2&&c<a[1])?(o=0,f.v=i,f.n=a[1]):c<g&&(n=r<3||a[0]>i||i>g)&&(a[4]=r,a[5]=i,f.n=g,o=0))}if(n||r>1)return s;throw h=!0,i}return function(n,d,g){if(u>1)throw TypeError("Generator is already running");for(h&&1===d&&c(d,g),o=d,l=g;(t=o<2?e:l)||!h;){a||(o?o<3?(o>1&&(f.n=-1),c(o,l)):f.n=l:f.v=l);try{if(u=2,a){if(o||(n="next"),t=a[n]){if(!(t=t.call(a,l)))throw TypeError("iterator result is not an object");if(!t.done)return t;l=t.value,o<2&&(o=0)}else 1===o&&(t=a.return)&&t.call(a),o<2&&(l=TypeError("The iterator does not provide a '"+n+"' method"),o=1);a=e}else if((t=(h=f.n<0)?l:r.call(i,f))!==s)break}catch(t){a=e,o=1,l=t}finally{u=1}}return{value:t,done:h}}}(r,n,a),!0),u}var s={};function o(){}function l(){}function u(){}t=Object.getPrototypeOf;var d=[][i]?t(t([][i]())):(m(t={},i,(function(){return this})),t),h=u.prototype=o.prototype=Object.create(d);function f(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,u):(e.__proto__=u,m(e,n,"GeneratorFunction")),e.prototype=Object.create(h),e}return l.prototype=u,m(h,"constructor",u),m(u,"constructor",l),l.displayName="GeneratorFunction",m(u,n,"GeneratorFunction"),m(h),m(h,n,"Generator"),m(h,i,(function(){return this})),m(h,"toString",(function(){return"[object Generator]"})),(v=function(){return{w:a,m:f}})()}function m(e,t,r,i){var n=Object.defineProperty;try{n({},"",{})}catch(e){n=0}m=function(e,t,r,i){function a(t,r){m(e,t,(function(e){return this._invoke(t,r,e)}))}t?n?n(e,t,{value:r,enumerable:!i,configurable:!i,writable:!i}):e[t]=r:(a("next",0),a("throw",1),a("return",2))},m(e,t,r,i)}function p(e,t){return p=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},p(e,t)}function y(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var i=r.call(e,t);if("object"!=typeof i)return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}function E(e){var t="function"==typeof Map?new Map:void 0;return E=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf("[native code]")}catch(t){return"function"==typeof e}}(e))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,r)}function r(){return function(e,t,r){if(h())return Reflect.construct.apply(null,arguments);var i=[null];i.push.apply(i,t);var n=new(e.bind.apply(e,i));return r&&p(n,r.prototype),n}(e,arguments,u(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),p(r,e)},E(e)}function T(e){return e instanceof ArrayBuffer||"undefined"!=typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer}var S="utf-16",A="utf-16be",I="utf-16le",L="utf-8";function R(e,t){if(e<0)return-R(-e,t);var r=Math.pow(10,t);if(Math.abs(e*r%1-.5)<Number.EPSILON){var i=Math.floor(e*r);return(i%2==0?i:i+1)/r}return Math.round(e*r)/r}function b(){try{return crypto.randomUUID()}catch(i){try{var e=URL.createObjectURL(new Blob),t=e.toString();return URL.revokeObjectURL(e),t.slice(t.lastIndexOf("/")+1)}catch(e){var r=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(e){var t=(r+16*Math.random())%16|0;return r=Math.floor(r/16),("x"==e?t:3&t|8).toString(16)}))}}}function k(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var D,P,_={exports:{}},C=(D||(D=1,function(e){var t=Object.prototype.hasOwnProperty,r="~";function i(){}function n(e,t,r){this.fn=e,this.context=t,this.once=r||!1}function a(e,t,i,a,s){if("function"!=typeof i)throw new TypeError("The listener must be a function");var o=new n(i,a||e,s),l=r?r+t:t;return e._events[l]?e._events[l].fn?e._events[l]=[e._events[l],o]:e._events[l].push(o):(e._events[l]=o,e._eventsCount++),e}function s(e,t){0==--e._eventsCount?e._events=new i:delete e._events[t]}function o(){this._events=new i,this._eventsCount=0}Object.create&&(i.prototype=Object.create(null),(new i).__proto__||(r=!1)),o.prototype.eventNames=function(){var e,i,n=[];if(0===this._eventsCount)return n;for(i in e=this._events)t.call(e,i)&&n.push(r?i.slice(1):i);return Object.getOwnPropertySymbols?n.concat(Object.getOwnPropertySymbols(e)):n},o.prototype.listeners=function(e){var t=r?r+e:e,i=this._events[t];if(!i)return[];if(i.fn)return[i.fn];for(var n=0,a=i.length,s=new Array(a);n<a;n++)s[n]=i[n].fn;retLine truncated
|
||||
//# sourceMappingURL=hls.min.js.map
|
||||
@@ -1,593 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EuerChat - euer.tv</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<div id="app" class="app-container">
|
||||
<!-- Top Header Bar (Optionen) -->
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<div class="header-brand">
|
||||
<img class="header-logo" src="logo.png" alt="">
|
||||
<span class="header-brand-title">EuerChat</span>
|
||||
</div>
|
||||
<!-- Nur sichtbar, wenn ein Lua-Skript (.luac) geladen und aktiv ist -->
|
||||
<button class="btn-modi-switch hidden" id="btnModiToggle" title="Zwischen normalem Hörer und Radio-Moderator wechseln">
|
||||
<span class="modi-dot" id="modiDot"></span>
|
||||
<span id="modiToggleLabel">📻 Zum Modi wechseln</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- Options Button -->
|
||||
<button class="btn-icon" id="btnSettingsModal" title="Optionen & Einstellungen">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
<span>Optionen</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-layout">
|
||||
<!-- Chat Box (Expanded full width on the left) -->
|
||||
<section class="chat-area">
|
||||
<div class="messages-container" id="messagesContainer">
|
||||
<!-- Messages populated dynamically -->
|
||||
</div>
|
||||
|
||||
<!-- Chat Input Bar -->
|
||||
<div class="chat-input-wrapper">
|
||||
<div class="chat-input-box">
|
||||
<input type="text" id="chatInput" placeholder="Nachricht an #erde... (Enter zum Senden)" autocomplete="off">
|
||||
<div class="input-actions">
|
||||
<button class="btn-emote" id="btnEmotePicker" title="Emotes & GIFs öffnen">
|
||||
😊
|
||||
</button>
|
||||
<button class="btn-send" id="btnSend">
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar: Everything related to Chat (Compact Status, Tabs, User identity) -->
|
||||
<div class="bottom-bar">
|
||||
<div class="bottom-chat-left">
|
||||
<div class="connection-status-compact" id="connStatus" title="IRC Verbindungsstatus">
|
||||
<span class="status-dot disconnected" id="statusDot"></span>
|
||||
<span class="status-ping hidden" id="statusPing">-- ms</span>
|
||||
</div>
|
||||
<button class="btn-connect-header" id="btnHeaderConnect" title="Verbindung herstellen oder trennen">
|
||||
Verbinden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="channel-tabs-container">
|
||||
<div class="channel-tabs" id="channelList">
|
||||
<div class="channel-tab active" data-target="#erde">
|
||||
<span class="chan-prefix">#</span>
|
||||
<span class="chan-name">erde</span>
|
||||
<span class="chan-badge unread-badge hidden">0</span>
|
||||
</div>
|
||||
<div class="channel-tab" data-target="*server*">
|
||||
<span class="chan-prefix">ℹ</span>
|
||||
<span class="chan-name">Server</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-tab-add" id="btnAddChan" title="Kanal beitreten">+</button>
|
||||
</div>
|
||||
|
||||
<div class="bottom-user-status">
|
||||
<span class="bottom-user-avatar" id="userAvatar">?</span>
|
||||
<div class="bottom-user-info">
|
||||
<span class="bottom-user-nick" id="displayNick">Hoerer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Draggable Splitter to manually resize the user list & video player width -->
|
||||
<div class="sidebar-resizer" id="sidebarResizer" title="Ziehen zum Vergrößern/Verkleinern der Leiste"></div>
|
||||
|
||||
<!-- Right Sidebar: User List & Embedded Video Player below it -->
|
||||
<aside class="sidebar-users">
|
||||
<div class="sidebar-section-title">
|
||||
<span>BENUTZER</span>
|
||||
<span class="user-count" id="userCount">0</span>
|
||||
</div>
|
||||
<div class="user-list" id="userList">
|
||||
<!-- Users populated dynamically -->
|
||||
</div>
|
||||
|
||||
<!-- Media Area: Interpret/Titel drüber, Video Player in der Mitte, Hörer & Grußbox unten -->
|
||||
<div class="sidebar-media-area" id="sidebarMediaArea">
|
||||
<!-- Track Info: Interpret / Titel DIREKT ÜBER DEM PLAYER (ohne Cover) -->
|
||||
<div class="sidebar-track-header" id="sidebarTrackHeader" title="Aktueller Titel & Interpret">
|
||||
<div class="sidebar-track-info" id="sidebarTrackInfo">
|
||||
<div class="sidebar-artist" id="sidebarArtist">euer.tv Radio</div>
|
||||
<div class="sidebar-track" id="sidebarTrack">Lade aktuellen Titel...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video Player Box -->
|
||||
<div class="sidebar-video-box" id="sidebarVideoBox">
|
||||
<div class="sidebar-video-wrapper">
|
||||
<!-- Video element: MUTED AT START! -->
|
||||
<video id="streamVideo" playsinline muted autoplay></video>
|
||||
</div>
|
||||
|
||||
<!-- Video Player Controls: Mute, Volume, Quality (1080p, 720p, 360p), Resync -->
|
||||
<!-- Video Player Controls: Mute, Lautstärke, Hörer, Grußbox, Qualität, Resync in EINER Zeile! -->
|
||||
<div class="sidebar-player-controls" id="sidebarPlayerControls">
|
||||
<div class="player-ctrls-left">
|
||||
<button class="btn-video-mute is-muted" id="btnStreamMute" title="Ton stummschalten oder aktivieren">
|
||||
<span id="streamMuteIcon">🔇</span>
|
||||
</button>
|
||||
<input type="range" id="streamVolSlider" min="0" max="1" step="0.05" value="0.1" class="mini-volume-slider" title="Lautstärke">
|
||||
</div>
|
||||
|
||||
<div class="sidebar-listeners-display twitch-viewer-badge" id="sidebarListenersDisplay" title="Aktuelle Zuhörer*innen">
|
||||
<svg class="twitch-viewer-icon" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="10" cy="6.2" r="3.2"></circle>
|
||||
<path d="M4.5 16.5c0-2.8 2.5-4.8 5.5-4.8s5.5 2 5.5 4.8"></path>
|
||||
</svg>
|
||||
<span class="stat-val" id="statTotalListeners">0</span>
|
||||
</div>
|
||||
<button class="btn-grussbox-compact" id="btnOpenGrussbox" title="Grußbox öffnen & Nachricht direkt ins Studio senden">
|
||||
✉ Grußbox
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="player-ctrls-right">
|
||||
<!-- Quality selector: 1080p, 720p, 360p -->
|
||||
<select id="videoQualitySelect" class="video-quality-select" title="Stream-Qualität wählen">
|
||||
<option value="1080p" selected>1080p</option>
|
||||
<option value="720p">720p</option>
|
||||
<option value="360p">360p</option>
|
||||
</select>
|
||||
<button class="btn-video-resync" id="btnResyncStream" title="Livestream neu synchronisieren">🔄</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<!-- Emote Picker Popover -->
|
||||
<div class="emote-picker hidden" id="emotePicker">
|
||||
<div class="emote-picker-header">
|
||||
<input type="text" id="emoteSearch" placeholder="Emote suchen (z.B. CATJAM, pepe, :) )">
|
||||
<button class="btn-close-mini" id="btnCloseEmote">✕</button>
|
||||
</div>
|
||||
<div class="emote-picker-tabs">
|
||||
<button class="emote-tab active" data-tab="all">Alle</button>
|
||||
<button class="emote-tab" data-tab="gifs">GIFs</button>
|
||||
<button class="emote-tab" data-tab="smilies">Smilies</button>
|
||||
</div>
|
||||
<div class="emote-grid" id="emoteGrid">
|
||||
<!-- Emotes rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Complete Unified Options Modal -->
|
||||
<div class="modal-overlay hidden" id="settingsModal">
|
||||
<div class="modal-card modal-card-wide">
|
||||
<div class="modal-header">
|
||||
<h2>Optionen & Einstellungen</h2>
|
||||
<button class="modal-close" id="btnCloseSettingsModal">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-tabs">
|
||||
<button class="tab-btn active" data-tab-target="secDisplay">Darstellung</button>
|
||||
<button class="tab-btn" data-tab-target="secServer">Server & ZNC</button>
|
||||
<button class="tab-btn hidden" data-tab-target="secRadio" id="tabBtnRadio">Radio & Modi</button>
|
||||
<button class="tab-btn" data-tab-target="secLua" id="tabBtnLua">Lua Addons</button>
|
||||
<button class="tab-btn" data-tab-target="secUpdate" id="tabBtnUpdate">Updates</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body settings-body">
|
||||
<!-- 1. Display Section -->
|
||||
<div class="settings-section-pane active" id="secDisplay">
|
||||
<div class="settings-grid">
|
||||
<div class="settings-col">
|
||||
<h3 class="settings-section-title">Schrift & Text</h3>
|
||||
<div class="form-group">
|
||||
<label for="optFontFamily">Schriftart (System & Windows)</label>
|
||||
<select id="optFontFamily" class="form-select">
|
||||
<option value="system">Standard Sans (Segoe UI, System)</option>
|
||||
<option value="monospace">Monospace / Konsole (Consolas, Courier)</option>
|
||||
<option value="segoe">Segoe UI</option>
|
||||
<option value="arial">Arial</option>
|
||||
<option value="calibri">Calibri</option>
|
||||
<option value="verdana">Verdana</option>
|
||||
<option value="comic">Comic Sans MS</option>
|
||||
<option value="trebuchet">Trebuchet MS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 4px;">
|
||||
<label for="optFontCustom" style="font-size:11.5px;color:var(--text-dim);">Eigene installierte Schriftart manuell eingeben:</label>
|
||||
<input type="text" id="optFontCustom" class="form-input" placeholder="z. B. Cascadia Code, Bahnschrift, Fira Code..." style="font-size:12px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border-color);border-radius:var(--radius-sm);color:#fff;width:100%;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-with-value">
|
||||
<label for="optFontSize">Schriftgröße</label>
|
||||
<span class="range-val-badge" id="valFontSize">14px</span>
|
||||
</div>
|
||||
<input type="range" id="optFontSize" min="11" max="22" step="1" value="14" class="form-range">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="optLineHeight">Zeilenabstand & Dichte</label>
|
||||
<select id="optLineHeight" class="form-select">
|
||||
<option value="compact">Kompakt (eng wie mIRC / HexChat)</option>
|
||||
<option value="normal" selected>Normal (ausgewogen)</option>
|
||||
<option value="cozy">Komfortabel (mehr Abstand)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="optTimestamp">Zeitstempel-Format</label>
|
||||
<select id="optTimestamp" class="form-select">
|
||||
<option value="hm" selected>HH:mm (z. B. 21:13)</option>
|
||||
<option value="hms">HH:mm:ss (z. B. 21:13:45)</option>
|
||||
<option value="none">Ausblenden</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="optUiScale">Skalierung der Oberfläche</label>
|
||||
<select id="optUiScale" class="form-select">
|
||||
<option value="75">75 %</option>
|
||||
<option value="90">90 %</option>
|
||||
<option value="100" selected>100 % (Standard)</option>
|
||||
<option value="110">110 %</option>
|
||||
<option value="125">125 %</option>
|
||||
<option value="150">150 %</option>
|
||||
<option value="175">175 %</option>
|
||||
<option value="200">200 %</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-col">
|
||||
<h3 class="settings-section-title">Chat & Emotes</h3>
|
||||
<div class="form-group">
|
||||
<label for="optEmoteSize">Emote-Größe</label>
|
||||
<select id="optEmoteSize" class="form-select">
|
||||
<option value="20">Klein (20px)</option>
|
||||
<option value="26" selected>Normal (26px)</option>
|
||||
<option value="34">Groß (34px)</option>
|
||||
<option value="42">Riesig (42px)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-checkboxes">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" id="optIrcColors" checked>
|
||||
<span>mIRC Farbcodes & Formatierung anzeigen</span>
|
||||
</label>
|
||||
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" id="optEmotes" checked>
|
||||
<span>Emotes & animierte GIFs ersetzen</span>
|
||||
</label>
|
||||
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" id="optHighlight" checked>
|
||||
<span>Eigenen Nicknamen hervorheben</span>
|
||||
</label>
|
||||
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" id="optAutoScroll" checked>
|
||||
<span>Automatisch nach unten scrollen</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-checkboxes" style="margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border-color);">
|
||||
<span class="preview-title" style="display:block; margin-bottom: 6px; font-size:10.5px; color:var(--text-dim); font-weight:700;">SICHTBARKEIT & LAYOUT</span>
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" id="optShowMediaArea" checked>
|
||||
<span>Livestream-Player, Hörer & Grußbox anzeigen</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Preview -->
|
||||
<div class="settings-preview-wrapper">
|
||||
<span class="preview-title">VORSCHAU</span>
|
||||
<div class="settings-preview" id="settingsPreview">
|
||||
<div class="chat-msg">
|
||||
<span class="msg-time">21:13</span>
|
||||
<span class="msg-nick" style="color:#48dbfb;">DJ_Radio</span>
|
||||
<span class="msg-text">Hallo zusammen! Viel Spaß mit der Musik <img class="chat-emote" src="https://chat.euer.tv/static/emocache/catjam.gif" alt="CATJAM"></span>
|
||||
</div>
|
||||
<div class="chat-msg action-msg">
|
||||
<span class="msg-time">21:14</span>
|
||||
<span class="msg-text"><span class="msg-action-star">*</span> <span class="msg-nick" style="color:#1dd1a1;">Kellnerin</span> <span style="color:#facc15;">Jetzt läuft:</span> <span style="color:#38bdf8;">Usher Feat. Pitbull</span> - <span style="color:#a855f7;">Dj Got Us Fallin' In Love</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Server & ZNC Section -->
|
||||
<div class="settings-section-pane hidden" id="secServer">
|
||||
<div class="server-type-selector">
|
||||
<button class="server-type-btn active" id="btnTypeDirect">Direkt (irc.euer.tv)</button>
|
||||
<button class="server-type-btn" id="btnTypeZnc">ZNC Bouncer</button>
|
||||
</div>
|
||||
|
||||
<!-- Direct Form -->
|
||||
<form id="formDirect" class="form-sub-section active">
|
||||
<p class="section-desc">Verbindet direkt mit dem Radio-Chat <strong>irc.euer.tv</strong> (Port 6697, SSL-verschlüsselt) im Raum <strong>#erde</strong>.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="directNick">Name (Nickname)</label>
|
||||
<input type="text" id="directNick" placeholder="Dein Name im Chat" maxlength="30" autocomplete="off" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="directPass">Passwort (optional, nur mit registriertem Account)</label>
|
||||
<input type="password" id="directPass" placeholder="Leer lassen, wenn dein Name nicht registriert ist" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<input type="checkbox" id="directAutoConnect">
|
||||
<label for="directAutoConnect">Automatisch beim Start direkt verbinden</label>
|
||||
</div>
|
||||
|
||||
<div class="server-connect-action">
|
||||
<button type="button" class="btn-success" id="btnSaveDirect" title="Direkt-Einstellungen speichern">💾 Direkt Speichern</button>
|
||||
<button type="button" class="btn-primary" id="btnConnectNow" title="Mit diesen Daten direkt verbinden">⚡ Jetzt Verbinden</button>
|
||||
<button type="button" class="btn-secondary" id="btnClearDirect" title="Alle Direkt-Felder leeren">🗑️ Leeren</button>
|
||||
</div>
|
||||
<div class="config-save-feedback hidden" id="directSaveFeedback"></div>
|
||||
</form>
|
||||
|
||||
<!-- ZNC Form -->
|
||||
<form id="formZnc" class="form-sub-section hidden">
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-2">
|
||||
<label>ZNC Host / IP</label>
|
||||
<input type="text" id="zncHost" placeholder="z. B. meinserver.example.org" required>
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<label>ZNC Port</label>
|
||||
<input type="number" id="zncPort" value="8250" placeholder="8250" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label>ZNC Benutzer (z.B. user oder user/network)</label>
|
||||
<input type="text" id="zncUser" placeholder="z. B. benutzer oder benutzer/netzwerk" required>
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<label>ZNC Passwort</label>
|
||||
<input type="password" id="zncPass" placeholder="ZNC Passwort" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label>Nickname</label>
|
||||
<input type="text" id="zncNick" placeholder="Dein Nickname">
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<label>Standard-Raum</label>
|
||||
<input type="text" id="zncChannel" value="#erde">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<input type="checkbox" id="zncSsl">
|
||||
<label for="zncSsl">SSL / TLS Verschlüsselung aktivieren</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<input type="checkbox" id="zncAutoConnect">
|
||||
<label for="zncAutoConnect">Automatisch beim Start über ZNC verbinden</label>
|
||||
</div>
|
||||
|
||||
<div class="server-connect-action">
|
||||
<button type="button" class="btn-success" id="btnSaveZnc" title="ZNC-Einstellungen speichern">💾 ZNC Speichern</button>
|
||||
<button type="button" class="btn-primary" id="btnConnectZncNow" title="Über ZNC verbinden und als Standard speichern">⚡ Über ZNC Verbinden</button>
|
||||
<button type="button" class="btn-secondary" id="btnClearZnc" title="Alle ZNC-Felder leeren">🗑️ Leeren</button>
|
||||
</div>
|
||||
<div class="config-save-feedback hidden" id="zncSaveFeedback"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 3. Radio & Modi Section -->
|
||||
<div class="settings-section-pane hidden" id="secRadio">
|
||||
<h3 class="settings-section-title">Radio & Sende-Modus (Modi-Verwaltung)</h3>
|
||||
<p class="section-desc">
|
||||
Konfiguriere hier deinen normalen Hörer-Nick und deinen Moderatoren-Nick. Ein Klick auf den Modi-Button oben schaltet automatisch um:
|
||||
</p>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label for="cfgNormalNick">Normaler Nickname (Hörer)</label>
|
||||
<input type="text" id="cfgNormalNick" placeholder="Dein normaler Nickname">
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<label for="cfgModiNick">Modi-Nickname (Auf Sendung)</label>
|
||||
<input type="text" id="cfgModiNick" placeholder="z. B. DJ_Name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cfgInternPass">Passwort für Raum #intern</label>
|
||||
<input type="password" id="cfgInternPass" placeholder="Passwort (leer lassen, falls kein Schlüssel nötig)" autocomplete="off">
|
||||
<small class="field-hint">Wird beim Wechsel zum Modi automatisch für <code>JOIN #intern <passwort></code> verwendet.</small>
|
||||
</div>
|
||||
|
||||
<div class="modi-status-preview-box">
|
||||
<div class="modi-preview-item">
|
||||
<span class="preview-badge normal">🎧 Normal</span>
|
||||
<span>Nur im Raum <strong>#erde</strong> unterwegs.</span>
|
||||
</div>
|
||||
<div class="modi-preview-item">
|
||||
<span class="preview-badge modi">📻 Modi Aktiv</span>
|
||||
<span>Nick wechselt zu Moderatoren-Name & betritt automatisch <strong>#intern</strong>.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Lua Addons Section (Clean & Uncluttered by default) -->
|
||||
<div class="settings-section-pane hidden" id="secLua">
|
||||
<h3 class="settings-section-title">LUA ERWEITERUNGEN (*.LUA, *.LUAC)</h3>
|
||||
<p class="section-desc" style="font-size:12.5px;color:var(--text-muted);margin-bottom:12px;">
|
||||
Hier können optionale Skripte geladen werden.
|
||||
</p>
|
||||
|
||||
<div class="script-upload-box" id="luaPickerBox">
|
||||
<input type="file" id="luaFileInput" accept=".lua,.luac" style="display:none">
|
||||
<button type="button" class="btn-secondary" id="btnSelectLua">
|
||||
📁 Externe .luac / .lua Datei auswählen...
|
||||
</button>
|
||||
<span class="selected-file-name" id="luaFileName">Keine Datei ausgewählt</span>
|
||||
</div>
|
||||
|
||||
<div class="loaded-scripts-list" id="loadedScriptsList">
|
||||
<div class="script-item" id="scriptStatusText" style="color:var(--text-muted);font-size:12.5px;padding:6px;">
|
||||
○ Inaktiv (Kein Skript geladen)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radio Moderations-Details: NUR SICHTBAR WENN EIN SKRIPT GELADEN WURDE! -->
|
||||
<div class="lua-modi-details hidden" id="luaModiDetails" style="margin-top:14px;border-top:1px solid var(--border-color);padding-top:14px;">
|
||||
<div class="form-checkbox" style="margin-bottom:14px;">
|
||||
<input type="checkbox" id="chkEnableLua">
|
||||
<label for="chkEnableLua"><strong>EuerService Moderations-Addon aktiv</strong> (schaltet Modi-Umschalter frei)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. Updates Section -->
|
||||
<div class="settings-section-pane hidden" id="secUpdate">
|
||||
<h3 class="settings-section-title">Updates</h3>
|
||||
<p class="section-desc">Installierte Version: <strong id="updCurrentVersion">–</strong></p>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<input type="checkbox" id="optAutoUpdate" checked>
|
||||
<label for="optAutoUpdate">Beim Start automatisch nach Updates suchen</label>
|
||||
</div>
|
||||
|
||||
<div class="server-connect-action">
|
||||
<button type="button" class="btn-primary" id="btnCheckUpdate">Auf Updates prüfen</button>
|
||||
</div>
|
||||
<div class="update-status" id="updStatusText"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="btnResetSettings">Standard</button>
|
||||
<button class="btn-primary" id="btnSaveSettings">Übernehmen & Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grußbox Modal -->
|
||||
<div class="modal-overlay hidden" id="grussboxModal">
|
||||
<div class="modal-card modal-card-sm">
|
||||
<div class="modal-header">
|
||||
<h2>✉ Grußbox - Radio euer.tv</h2>
|
||||
<button class="modal-close" id="btnCloseGrussbox">✕</button>
|
||||
</div>
|
||||
<div class="modal-body grussbox-body">
|
||||
<p class="grussbox-desc">Sende deinen Gruß oder Musikwunsch direkt live ins Studio zum aktuellen Moderator!</p>
|
||||
<div class="form-group">
|
||||
<label for="gbNameInput">Dein Name / Nickname</label>
|
||||
<input type="text" id="gbNameInput" class="form-input" placeholder="Dein Name" maxlength="40">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gbTextInput">Dein Gruß / Musikwunsch</label>
|
||||
<textarea id="gbTextInput" class="form-textarea" rows="4" placeholder="Schreib hier deinen Gruß oder Musikwunsch..." maxlength="500"></textarea>
|
||||
</div>
|
||||
<div class="grussbox-feedback hidden" id="gbFeedback"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="btnCancelGruss">Abbrechen</button>
|
||||
<button class="btn-primary btn-submit-gruss" id="btnSubmitGruss">🚀 Gruß senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Modal -->
|
||||
<div class="modal-overlay hidden" id="updateModal">
|
||||
<div class="modal-card modal-card-sm">
|
||||
<div class="modal-header">
|
||||
<h2>⬆ Update verfügbar</h2>
|
||||
<button class="modal-close" id="btnCloseUpdate">✕</button>
|
||||
</div>
|
||||
<div class="modal-body update-body">
|
||||
<p class="update-summary" id="updSummary"></p>
|
||||
<span class="preview-title">WAS IST NEU?</span>
|
||||
<div class="update-notes" id="updNotes"></div>
|
||||
<div class="update-progress hidden" id="updProgress"><div class="update-progress-bar" id="updProgressBar"></div></div>
|
||||
<div class="update-status" id="updModalStatus"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="btnUpdateLater">Später</button>
|
||||
<button class="btn-primary" id="btnUpdateInstall">Jetzt installieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Context Menu Popover (Einfacher Klick auf Nicknamen) -->
|
||||
<div class="user-context-menu hidden" id="userContextMenu">
|
||||
<div class="ctx-header">
|
||||
<span class="ctx-prefix" id="ctxUserPrefix"></span>
|
||||
<span class="ctx-nick" id="ctxUserNick">Nick</span>
|
||||
</div>
|
||||
<div class="ctx-list">
|
||||
<button class="ctx-item" data-action="query">
|
||||
<span class="ctx-icon">💬</span> Privatnachricht (PN)
|
||||
</button>
|
||||
<button class="ctx-item" data-action="whois">
|
||||
<span class="ctx-icon">ℹ</span> WHOIS Info
|
||||
</button>
|
||||
<button class="ctx-item" data-action="grussbox">
|
||||
<span class="ctx-icon">✉</span> In Grußbox erwähnen
|
||||
</button>
|
||||
<button class="ctx-item" data-action="copy">
|
||||
<span class="ctx-icon">📋</span> Nickname kopieren
|
||||
</button>
|
||||
<div class="ctx-divider"></div>
|
||||
<div class="ctx-subhead">Kanal-Moderation</div>
|
||||
<button class="ctx-item" data-action="op">
|
||||
<span class="ctx-icon">⚡</span> OP geben (+o)
|
||||
</button>
|
||||
<button class="ctx-item" data-action="deop">
|
||||
<span class="ctx-icon">⚡</span> OP entziehen (-o)
|
||||
</button>
|
||||
<button class="ctx-item" data-action="voice">
|
||||
<span class="ctx-icon">🎤</span> Voice geben (+v)
|
||||
</button>
|
||||
<button class="ctx-item" data-action="devoice">
|
||||
<span class="ctx-icon">🎤</span> Voice entziehen (-v)
|
||||
</button>
|
||||
<div class="ctx-divider"></div>
|
||||
<button class="ctx-item ctx-danger" data-action="kick">
|
||||
<span class="ctx-icon">👢</span> Kicken
|
||||
</button>
|
||||
<button class="ctx-item ctx-danger" data-action="ban">
|
||||
<span class="ctx-icon">⛔</span> Kicken & Bannen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="hls.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 17 KiB |