Refresh GUI documentation and profile synchronization
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
|---|---|
|
||||
| `TrayApp` | `ApplicationContext`; hält Tray-Icon, öffnet ConfigForm, verarbeitet Board-Events |
|
||||
| `SerialManager` | Verbindungsverwaltung, WMI-Erkennung, Lese-Thread, Sende-Methoden |
|
||||
| `ConfigForm` | Hauptfenster (Grid + Encoder-Panel + Footer); öffnet ActionDialog |
|
||||
| `ConfigForm` | Hauptfenster (Profilauswahl, Grid, Encoder-Panel und Footer); öffnet ActionDialog |
|
||||
| `ActionDialog` | Modaler Dialog zum Bearbeiten einer Aktion + LED-Einstellungen |
|
||||
| `DeviceConfig` | C#-Spiegel von `SDeviceConfig`; Validierung und Serialisierung (740 B) |
|
||||
| `MacroTable` | C#-Spiegel von `SMacroTable`; Validierung und Serialisierung (512 B) |
|
||||
@@ -46,7 +46,7 @@ Benutzer → ConfigForm → ActionDialog
|
||||
UI-Thread : TrayApp, ConfigForm, ActionDialog, alle WinForms-Controls
|
||||
BG-Thread : SerialManager.ReadLoop() – blockiert auf ReadByte()
|
||||
Timer-Thread : SerialManager._reconnectTimer → TryConnect() alle 3 s
|
||||
Sende-Task : ConfigForm.OnSave() → Task.Run() (blockiert ~400 ms für Transfer)
|
||||
Sende-Task : ConfigForm.OnSave() → Task.Run() (blockiert bis ACK/NACK oder Timeout)
|
||||
```
|
||||
|
||||
Alle Board-Events werden per `SynchronizationContext.Post` auf den UI-Thread gepostet. Controls dürfen nie vom BG-Thread angefasst werden.
|
||||
@@ -75,3 +75,8 @@ Disconnect → ReadLoop bricht ab → Disconnected-Event → 5 s Backoff → Tim
|
||||
gemeinsamen Transfer-Lock; einzelne Pakete unter einem Write-Lock.
|
||||
- **Host-Commands**: Pakete sind vollständig definiert, die Ausführung einer
|
||||
Command-ID als Desktop-Aktion ist bewusst noch nicht implementiert.
|
||||
- **UI-Modell-Grenze**: Das Binärmodell kann alle sieben Firmware-Animationen
|
||||
lesen und schreiben. Der ActionDialog bietet aktuell vier davon zur Auswahl.
|
||||
|
||||
Bewusst offene Punkte stehen gesammelt in
|
||||
[`07_known_limitations.md`](07_known_limitations.md).
|
||||
|
||||
@@ -84,7 +84,10 @@ _configAckOk / _macroAckOk – volatile bool (true = ACK, false = NACK)
|
||||
| `SendConfig(cfg)` | bool | BEGIN(0x10) → 124×DATA(0x11) → COMMIT(0x12), 5 ms zwischen Chunks, wartet auf ACK/NACK |
|
||||
| `SendMacros(macros)` | bool | BEGIN(0x20) → 86×DATA(0x21) → COMMIT(0x22), 5 ms zwischen Chunks, wartet auf ACK/NACK |
|
||||
|
||||
`SendConfig` und `SendMacros` blockieren ~1,5 s (Chunks + NVM-Zeit) → werden in `Task.Run()` aus `ConfigForm.OnSave()` aufgerufen.
|
||||
Die reine Chunk-Pausierung beträgt nominal etwa 640 ms für Config und 450 ms
|
||||
für Makros, jeweils zuzüglich Port-, NVM- und ACK-Zeit. Nach jedem COMMIT wird
|
||||
höchstens 3 s auf ACK/NACK gewartet. Deshalb laufen beide Methoden aus
|
||||
`ConfigForm.OnSave()` in `Task.Run()` und niemals auf dem UI-Thread.
|
||||
|
||||
Vor `BEGIN` wird das zugehörige ACK-Gate geleert. Beide Blob-Sender teilen
|
||||
einen Transfer-Lock, prüfen jeden `Send()`-Rückgabewert und brechen bei
|
||||
|
||||
@@ -25,6 +25,7 @@ Sie muessen deshalb synchron zu `VersaMCU/src/config/nvm_config.h` und `macro_co
|
||||
| `ActiveProfileIndex` | aktives Profil 0..2 |
|
||||
| `GlobalBrightness` | globale LED-Helligkeit |
|
||||
| `Profiles[3]` | komplette Profil-Daten |
|
||||
| `EncSensitivity[4]` | im Vertrag enthalten; Firmware nutzt den Wert derzeit nicht |
|
||||
|
||||
Fuer die GUI gibt es zusaetzlich Komfortzugriffe auf das aktive Profil:
|
||||
|
||||
@@ -62,6 +63,32 @@ Jedes Profil belegt 236 Byte:
|
||||
20 x led_period_ms
|
||||
```
|
||||
|
||||
### Action-Daten
|
||||
|
||||
| Typ | Datenbereich |
|
||||
|---|---|
|
||||
| `None` | Daten werden ignoriert |
|
||||
| `HidKey` | Low-Byte Keycode `0x00..0x65`, High-Byte Modifier |
|
||||
| `HidConsumer` | Usage `0x0000..0x03FF` |
|
||||
| `HostCommand` | frei definierte Command-ID `0x0000..0xFFFF` |
|
||||
| `Macro` | Slot `0..31` |
|
||||
| `ProfileSwitch` | Profil `0..2`; `0x00FF` oder `0xFFFF` bedeuten „nächstes Profil“ |
|
||||
|
||||
### LED-Animationen im Binärformat
|
||||
|
||||
| Wert | Enum |
|
||||
|---|---|
|
||||
| `0` | `Static` |
|
||||
| `1` | `Blink` |
|
||||
| `2` | `Pulse` |
|
||||
| `3` | `FadeIn` |
|
||||
| `4` | `FadeOut` |
|
||||
| `5` | `ColorCycle` |
|
||||
| `6` | `ColorFade` |
|
||||
|
||||
`DeviceConfig` akzeptiert alle Werte `0..6`. Der aktuelle ActionDialog bietet
|
||||
nur `Static`, `Blink`, `Pulse` und `ColorCycle` an.
|
||||
|
||||
## CRC
|
||||
|
||||
CRC16-CCITT:
|
||||
@@ -109,10 +136,20 @@ Ein Step besteht aus:
|
||||
- `keycode`
|
||||
- `modifier`
|
||||
|
||||
`keycode == 0` beendet die Ausführung des Slots. Belegte Steps nach der ersten
|
||||
Lücke werden daher zwar serialisiert, von der Firmware aber nicht ausgeführt.
|
||||
|
||||
Es gibt kein Magic und keine CRC fuer die Makrotabelle.
|
||||
Beim Transfer prüft die GUI trotzdem exakte Größe und alle HID-Keycodes; die
|
||||
Firmware prüft zusätzlich die vollständige Chunkmenge.
|
||||
|
||||
## Validierung
|
||||
|
||||
`ToBytes()` verweigert ungültige Objektzustände. `FromBytes()` verändert das
|
||||
bestehende Objekt erst, nachdem Größe, Magic, Version, CRC, aktives Profil,
|
||||
Action-Werte, LED-Enums und die Mindestperiode von 2 ms für `Pulse` geprüft
|
||||
wurden. Für die Makrotabelle gelten exakte 512 Byte und Keycodes bis `0x65`.
|
||||
|
||||
## Wichtig fuer Aenderungen
|
||||
|
||||
Wenn sich Firmware-Layout, Magic, Version, Profilzahl oder Makrogroesse aendern, muessen mindestens diese Stellen zusammen angepasst werden:
|
||||
|
||||
+10
-4
@@ -38,8 +38,11 @@ Icon und Tooltip spiegeln den Verbindungsstatus:
|
||||
| `EvtMacroAck` | `serial.SignalMacroAck()` — gibt SendMacros()-Thread frei |
|
||||
| `EvtMacroNack` | `serial.SignalMacroNack()` — gibt SendMacros()-Thread frei (Fehler) |
|
||||
| `EvtPong` | MessageBox "Ping OK" |
|
||||
| `EvtKeyDown/Up` | Command-ID in Byte 2/3 empfangen |
|
||||
| `EvtEncCw/Ccw` | Command-ID und Encoderrichtung empfangen |
|
||||
| `EvtKeyDown/Up` | Host-Command-ID in Byte 2/3 empfangen |
|
||||
| `EvtEncCw/Ccw` | Host-Command-ID und Encoderrichtung empfangen |
|
||||
|
||||
Diese vier Host-Events sendet die Firmware nur für Actions vom Typ
|
||||
`HostCommand`. `TrayApp` dekodiert die 16-Bit-ID, führt sie aber noch nicht aus.
|
||||
|
||||
ACK/NACK-Events zeigen keine eigene MessageBox mehr — das Ergebnis wird nach Abschluss beider Transfers gebündelt in `ConfigForm.OnSave()` angezeigt.
|
||||
|
||||
@@ -56,8 +59,11 @@ OnDisconnected():
|
||||
Icon + Text + Menü-Item aktualisieren
|
||||
```
|
||||
|
||||
Ein unvollständiger oder ungültiger Dump wird einmal wiederholt. Schlägt auch
|
||||
der zweite Versuch fehl, zeigt das Tray-Icon eine Warnung.
|
||||
Ein mit `END` abgeschlossener, aber unvollständiger oder ungültiger Dump wird
|
||||
einmal wiederholt. Schlägt auch der zweite Versuch fehl, zeigt das Tray-Icon
|
||||
eine Warnung. Bleibt `END` vollständig aus, gibt es derzeit keinen
|
||||
Empfangs-Timeout; siehe
|
||||
[`07_known_limitations.md`](07_known_limitations.md).
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
|
||||
+19
-5
@@ -4,7 +4,11 @@
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Hauptkonfigurationsfenster: zeigt alle 20 MX-Buttons und 4 Encoder, öffnet `ActionDialog` bei Klick, speichert Config + Makros auf das Board.
|
||||
Hauptkonfigurationsfenster: wählt eines von drei Profilen, zeigt dessen 20
|
||||
MX-Buttons und 4 Encoder, öffnet `ActionDialog` bei Klick und speichert Config
|
||||
plus Makros auf das Board.
|
||||
|
||||
Oberhalb des Tasten-Grids befindet sich die Profilauswahl für Profil 1 bis 3.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -45,15 +49,23 @@ Entspricht `key_id - 5` in der Firmware. Im TableLayoutPanel: Spalte=col, Zeile=
|
||||
|
||||
```csharp
|
||||
Task.Run(() => {
|
||||
_serial.SendConfig(_config); // ~300 ms
|
||||
Thread.Sleep(50);
|
||||
_serial.SendMacros(_macros); // ~250 ms
|
||||
bool cfgOk = _serial.SendConfig(_config); // wartet auf ACK/NACK
|
||||
bool macroOk = _serial.SendMacros(_macros);
|
||||
InvokeOnUi(() => { /* Button-Text + Enabled zurücksetzen */ });
|
||||
});
|
||||
```
|
||||
|
||||
Save-Button wird während der Übertragung deaktiviert, Text wechselt zu "Wird gesendet...".
|
||||
Save-Button ist nur aktiviert wenn Board verbunden (`_serial.IsConnected`).
|
||||
Config und Makros werden sequenziell gesendet; ein gemeinsamer Transfer-Lock
|
||||
verhindert parallele Blob-Transfers.
|
||||
|
||||
## Profile
|
||||
|
||||
Die Profilauswahl setzt `ActiveProfileIndex` und schaltet alle Komfortzugriffe
|
||||
von `DeviceConfig` auf das gewählte Profil um. `RefreshAll()` synchronisiert
|
||||
die Auswahl nach Board-Reload oder JSON-Import und zeichnet anschließend die
|
||||
20 MX- und 12 Encoder-Schaltflächen neu.
|
||||
|
||||
## Import / Export
|
||||
|
||||
@@ -64,7 +76,9 @@ Fehler (IO, JSON-Parse, falsche Version) werden per `MessageBox` angezeigt.
|
||||
|
||||
## RefreshAll
|
||||
|
||||
Wird von `TrayApp` nach erfolgreicher Config vom Board aufgerufen (über `_configForm?.RefreshAll()`). Aktualisiert alle 20 MX-Buttons und 12 Encoder-Buttons ohne Dialog.
|
||||
Wird von `TrayApp` nach erfolgreicher Config oder Makrotabelle vom Board sowie
|
||||
nach JSON-Import aufgerufen. Aktualisiert Profilauswahl, alle 20 MX-Buttons und
|
||||
12 Encoder-Buttons ohne Dialog.
|
||||
|
||||
## Extensions (in derselben Datei)
|
||||
|
||||
|
||||
+19
-5
@@ -39,8 +39,9 @@ LED-Panels (`_colorPanel`, `_animPanel`) erscheinen zusätzlich wenn `showColor=
|
||||
// Index 2: "Profil 2" → Data = 1
|
||||
// Index 3: "Profil 3" → Data = 2
|
||||
|
||||
// Initialbelegung:
|
||||
_profileCombo.SelectedIndex = action.Data == 0xFFFF ? 0 : action.Data + 1;
|
||||
// Initialbelegung: beide von der Firmware akzeptierten Zykluswerte erkennen
|
||||
_profileCombo.SelectedIndex =
|
||||
action.Data is 0x00FF or 0xFFFF ? 0 : action.Data + 1;
|
||||
|
||||
// In OnOk():
|
||||
data = _profileCombo.SelectedIndex == 0
|
||||
@@ -48,7 +49,9 @@ data = _profileCombo.SelectedIndex == 0
|
||||
: (ushort)(_profileCombo.SelectedIndex - 1);
|
||||
```
|
||||
|
||||
Im Board wird `0xFFFF` als `(uint8_t)0xFF` gespeichert und in der Firmware als "nächstes Profil" interpretiert.
|
||||
`SAction.data` ist ein 16-Bit-Feld. Die GUI schreibt für „nächstes Profil“
|
||||
kanonisch `0xFFFF`; Firmware und Deserialisierung akzeptieren zusätzlich den
|
||||
älteren Wert `0x00FF`.
|
||||
|
||||
## Tasten-Capture (HID-Modus)
|
||||
|
||||
@@ -62,7 +65,10 @@ WinForms behandelt Pfeil- und Enter-Tasten als "Dialog Keys" in `ProcessDialogKe
|
||||
|
||||
## Makro-Capture
|
||||
|
||||
Jeder der 8 Steps hat einen eigenen Capture-Button. `_captureStep` (0–7, -1 = inaktiv) zeigt welcher Step gerade aufnimmt. Capture-Logik identisch mit HID-Modus, schreibt in `_stepKeycodes[captureStep]`.
|
||||
Jeder der 8 Steps hat einen eigenen Capture-Button. `_captureStep` (0–7,
|
||||
-1 = inaktiv) zeigt, welcher Step gerade aufnimmt. Capture-Logik identisch mit
|
||||
HID-Modus, schreibt in `_stepKeycodes[captureStep]`. Der erste leere Step
|
||||
beendet die Makrosequenz; deshalb dürfen belegte Steps keine Lücke enthalten.
|
||||
|
||||
## Schlüssellookup (layout-unabhängig)
|
||||
|
||||
@@ -107,6 +113,14 @@ Scan-Code → HID Usage via s_scanToHid[sc]
|
||||
Nur wenn `showColor=true` (MX-Buttons). Enthält:
|
||||
- **Farbpicker**: `ColorDialog` → `_colorBtn.BackColor`
|
||||
- **Animations-Dropdown**: Statisch / Blinken / Pulsieren / Regenbogen
|
||||
- **Periode-Dropdown**: Presets von "Sehr langsam (8 s)" bis "Sehr schnell (250 ms)"
|
||||
- **Periode-Dropdown**: 500 ms, 1 s, 2 s oder 4 s
|
||||
|
||||
Bei "Regenbogen" wird der Farbpicker ausgeblendet (Farbe irrelevant).
|
||||
|
||||
Das Binärmodell kennt außerdem `FadeIn`, `FadeOut` und `ColorFade`; diese drei
|
||||
Werte sind im Dialog noch nicht auswählbar. Per-LED-Helligkeit wird im
|
||||
Binärformat und JSON erhalten, besitzt hier aber ebenfalls kein Bedienelement.
|
||||
Beim Öffnen einer nicht angebotenen Animation fällt die Auswahl derzeit auf
|
||||
`Static` zurück. Unbekannte, aber formal gültige Consumer-Usages fallen auf
|
||||
„Play / Pause“ zurück. Details stehen in
|
||||
[`07_known_limitations.md`](07_known_limitations.md).
|
||||
|
||||
+14
-2
@@ -14,7 +14,14 @@ Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als St
|
||||
{
|
||||
"index": 0,
|
||||
"action": { "type": "HidKey", "data": 260 },
|
||||
"led": { "r": 80, "g": 40, "b": 0, "anim": "ColorCycle", "period_ms": 4000 }
|
||||
"led": {
|
||||
"r": 80,
|
||||
"g": 40,
|
||||
"b": 0,
|
||||
"brightness": 255,
|
||||
"anim": "ColorCycle",
|
||||
"period_ms": 4000
|
||||
}
|
||||
},
|
||||
...
|
||||
],
|
||||
@@ -32,7 +39,9 @@ Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als St
|
||||
|
||||
## Serialisierung
|
||||
|
||||
`ConfigJson.Serialize(cfg)` → JSON-String. Exportiert alle 20 Buttons + 4 Encoder vollständig.
|
||||
`ConfigJson.Serialize(cfg)` → JSON-String. Exportiert alle 20 Buttons und vier
|
||||
Encoder des aktuell aktiven Profils vollständig, einschließlich
|
||||
Per-LED-Helligkeit.
|
||||
|
||||
## Deserialisierung
|
||||
|
||||
@@ -47,6 +56,9 @@ Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als St
|
||||
## Anmerkungen
|
||||
|
||||
- `MacroTable` wird **nicht** exportiert (kein JSON-Format für Makros definiert)
|
||||
- globale Helligkeit und Encoder-Sensitivität werden nicht exportiert
|
||||
- `profile` wählt das Zielprofil aus und macht es gleichzeitig zum aktiven
|
||||
Profil; die beiden anderen Profile bleiben unverändert
|
||||
- `data` enthält den `ushort`-Wert direkt (für HidKey z.B. `Keycode | (Modifier << 8)`)
|
||||
- Die Datei ist kein Binärformat und kann manuell bearbeitet werden
|
||||
- Ein ungültiger Wert wird vor dem Anwenden mit `InvalidDataException`
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Bekannte Einschränkungen
|
||||
|
||||
Diese Seite trennt bewusst offene Punkte von bereits implementierten
|
||||
Verträgen. Sie ist vor Änderungen an Verbindungslogik, ActionDialog oder
|
||||
Binärformat zu lesen.
|
||||
|
||||
## Host-Commands
|
||||
|
||||
Firmware und GUI übertragen für `HostCommand`:
|
||||
|
||||
- Key- beziehungsweise Encoder-ID
|
||||
- Richtung über die Event-ID
|
||||
- 16-Bit-Command-ID in Byte 2/3
|
||||
|
||||
`TrayApp` führt die Command-ID noch nicht als Desktop-Aktion aus. Eine spätere
|
||||
Implementierung benötigt ein explizites Allowlist-Mapping; beliebige
|
||||
Shellbefehle aus Config-Daten sind nicht zulässig.
|
||||
|
||||
## LED- und globale Einstellungen
|
||||
|
||||
Das Binärformat und `DeviceConfig` unterstützen sieben Animationen:
|
||||
`Static`, `Blink`, `Pulse`, `FadeIn`, `FadeOut`, `ColorCycle` und `ColorFade`.
|
||||
Der `ActionDialog` bietet aktuell nur die ersten drei sowie `ColorCycle` an.
|
||||
Beim Öffnen einer nicht angebotenen Animation fällt die Auswahl auf `Static`
|
||||
zurück und würde beim Bestätigen entsprechend gespeichert.
|
||||
|
||||
Per-LED-Helligkeit wird binär und im JSON erhalten, besitzt aber kein
|
||||
Bedienelement. Globale Helligkeit und Encoder-Sensitivität werden binär
|
||||
erhalten, sind weder im Hauptfenster editierbar noch Teil des JSON-Exports.
|
||||
Die Firmware verwendet Encoder-Sensitivität derzeit ebenfalls nicht.
|
||||
|
||||
## ActionDialog
|
||||
|
||||
- Die Consumer-Liste enthält zwölf vordefinierte Usages. Andere formal gültige
|
||||
Usages bis `0x03FF` können aus dem Board gelesen werden, fallen beim Öffnen
|
||||
des Dialogs aber auf „Play / Pause“ zurück.
|
||||
- Das Periodenfeld bietet nur 500, 1000, 2000 und 4000 ms. Andere geladene
|
||||
Perioden fallen im Dialog auf 4000 ms zurück.
|
||||
- Ein Makro endet am ersten Step mit `keycode == 0`; Lücken vor späteren
|
||||
belegten Steps werden nicht ausgeführt.
|
||||
|
||||
## Transfers und Verbindung
|
||||
|
||||
- Das CDC-Protokoll besitzt kein Byte-Framing, keine Paket-CRC und keine
|
||||
Transfer-ID. Verliert der Stream ein Byte, kann die 8-Byte-Grenze bis zum
|
||||
Reconnect verschoben bleiben.
|
||||
- Chunkzahl, eindeutige Indizes, Vollständigkeit und END-Metadaten werden
|
||||
geprüft. Bleibt ein Dump jedoch ohne `END`, existiert noch kein
|
||||
Empfangs-Timeout oder Watchdog; die automatische Wiederholung startet nur
|
||||
nach einem als ungültig erkannten `END`.
|
||||
- `RequestConfig()` und `RequestMacros()` melden einen unmittelbar
|
||||
fehlgeschlagenen `Send()` nicht an `TrayApp`.
|
||||
- Während `SendConfig()` und `SendMacros()` ist nur die Speichern-Schaltfläche
|
||||
deaktiviert. Andere Editoraktionen werden nicht global gesperrt.
|
||||
|
||||
## Testabdeckung
|
||||
|
||||
Die Console-Contract-Tests prüfen:
|
||||
|
||||
- Configgröße, CRC und Roundtrip
|
||||
- ausgewählte ungültige Configfelder
|
||||
- Makrogröße und HID-Keycode-Grenze
|
||||
- Chunkvollständigkeit und doppelte Chunks
|
||||
- 16-Bit-Host-Command-Payload
|
||||
|
||||
Nicht automatisiert geprüft werden WinForms-Interaktionen, WMI-Porterkennung,
|
||||
echte SerialPort-Fehler, Reconnects, NVM-Schreibzeiten, HID-Ausgabe und
|
||||
Hardwareverhalten. Dafür ist weiterhin ein Test mit angeschlossenem VersaPad
|
||||
erforderlich.
|
||||
+3
-1
@@ -15,6 +15,7 @@ gegen `../../VersaMCU` geprüft werden.
|
||||
| [04_config_form.md](04_config_form.md) | Grid, Speichern, Import/Export |
|
||||
| [05_action_dialog.md](05_action_dialog.md) | Key-Capture, Action-Auswahl, LED-Einstellungen |
|
||||
| [06_config_json.md](06_config_json.md) | JSON-Format und Grenzen |
|
||||
| [07_known_limitations.md](07_known_limitations.md) | Bewusst offene GUI-, Protokoll- und Testgrenzen |
|
||||
|
||||
## Schnellreferenz
|
||||
|
||||
@@ -23,5 +24,6 @@ gegen `../../VersaMCU` geprüft werden.
|
||||
- DTR und Connect-Pfad -> [00_architecture.md](00_architecture.md), [01_serial_manager.md](01_serial_manager.md)
|
||||
- Makro-Slot-Konvention -> [02_device_config.md](02_device_config.md)
|
||||
- Host-Command-Status -> [03_tray_app.md](03_tray_app.md)
|
||||
- bekannte Einschränkungen -> [07_known_limitations.md](07_known_limitations.md)
|
||||
- automatisierte Binär-/Transferverträge ->
|
||||
`../tests/VersaGUI.ContractTests/`
|
||||
[`../tests/VersaGUI.ContractTests/`](../tests/VersaGUI.ContractTests/)
|
||||
|
||||
Reference in New Issue
Block a user