# Pyramid – Entwicklungsprotokoll
Stand: 24.04.2026

---

## Projektübersicht

**Pyramid** ist ein Flutter-basierter Matrix-Client, modelliert nach einem FluffyChat-Fork,
mit zwei parallelen Call-Systemen:

| System | Einsatz | Datei |
|--------|---------|-------|
| **LiveKit** (`livekit_client`) | Gruppen-Sprachräume | `lib/core/livekit_call_manager.dart` |
| **Matrix VoIP** (`matrix` SDK) | 1:1 Direktnachrichten | `lib/core/voip_manager.dart` |

Server-Infrastruktur läuft auf einem Raspberry Pi (`steggi-matrix.work`), LiveKit unter
`livekit.steggi-matrix.work`.

---

## Erledigte Änderungen

### 1. LiveKit – Qualitäts-Bugs behoben (`livekit_call_manager.dart`)

**Problem:** Streamer und Zuschauer konnten die Übertragungsqualität nicht ändern.

**Ursachen:**
- `_screenShareEncoding()` hatte einen hardcodierten `switch ('balanced')` — gab immer
  halbe Bitrate zurück, unabhängig vom eingestellten Qualitäts-Key.
- `setSubscribeQuality('auto')` sprang vorzeitig raus und setzte die Qualität nie.

**Fix:**
```dart
VideoEncoding _screenShareEncoding() {
  return _screenShareEncodings[_localQualityKey] ??
      const VideoEncoding(maxBitrate: 1_500_000, maxFramerate: 15);
}
```
`setSubscribeQuality` wendet jetzt immer die Qualität an, auch bei 'auto'.

---

### 2. LiveKit – Track-Events lösen UI-Rebuild aus (`livekit_call_manager.dart`)

**Problem:** Kamera-Feed wurde nach dem Publishen nicht angezeigt, weil kein Widget-Rebuild ausgelöst wurde.

**Fix:** Handler für alle Track-Events hinzugefügt:
```dart
..on<LocalTrackPublishedEvent>((_) => notifyListeners())
..on<TrackPublishedEvent>((_) => notifyListeners())
..on<TrackUnpublishedEvent>((_) => notifyListeners())
..on<TrackSubscribedEvent>((_) => notifyListeners())
..on<TrackUnsubscribedEvent>((_) => notifyListeners())
..on<ParticipantConnectedEvent>((_) => notifyListeners())
..on<ParticipantDisconnectedEvent>((_) => notifyListeners())
```

---

### 3. LiveKit – Optimistic Mute (`livekit_call_manager.dart`)

**Problem:** Mute-Button hatte eine spürbare Verzögerung (async API-Call).

**Fix:** `notifyListeners()` wird jetzt vor dem `await` aufgerufen:
```dart
Future<void> toggleMute() async {
  isMuted = !isMuted;
  notifyListeners(); // Sofort
  await _room?.localParticipant?.setMicrophoneEnabled(!isMuted);
}
```

---

### 4. VoIP – Kamera-Fix (`voip_manager.dart`)

**Problem:** Kamera-Tracks wurden nicht korrekt zugewiesen, weil der Renderer bereits belegt
wurde bevor Video-Daten verfügbar waren.

**Fix:**
- Track-Zuweisung erfolgt jetzt erst wenn `getVideoTracks().isNotEmpty` — kein Renderer-Update
  auf leere Streams mehr.
- `_isCameraMuted` wird bei eingehenden Calls korrekt aus dem Call-Typ gesetzt:
  `_isCameraMuted = call.type == CallType.kVoice;`
- Bei `kConnected`-State wird `setLocalVideoMuted(false)` nochmals aufgerufen falls Kamera
  aktiv sein soll.
- Poll-Intervall von 1500ms auf 1000ms reduziert für schnellere Feed-Erkennung.

---

### 5. VoIP – Optimistic Mic/Camera Toggle (`voip_manager.dart`)

**Problem:** `toggleMic()` und `toggleCamera()` nutzten `safeAction()` mit 1500ms Delay.

**Fix:**
- `toggleMic()` flippt State sofort, reverted bei Fehler automatisch.
- `toggleCamera()` flippt `_isCameraMuted` sofort (optimistic), ruft `_updateRenderers()`
  mehrfach nach kurzen Delays auf.

---

### 6. VoIP – Desktop Speakerphone Fix (`voip_manager.dart`)

**Problem:** `MissingPluginException: No implementation found for method enableSpeakerphone`
auf Windows/Desktop.

**Fix:** Speakerphone-Callback wird im Konstruktor unterdrückt:
```dart
try {
  (voip as dynamic).onConfigSpeakerphone = (bool _) async {};
} catch (_) {}
```

---

### 7. SQLite-Stabilität auf Windows

**Problem:** Sporadische "Database is locked"-Fehler durch parallele Zugriffe.

**Fix:** WAL-Modus aktiviert und `busy_timeout` gesetzt, um Locking-Konflikte zu verhindern.

---

### 8. Einheitliches Call-UI (`voice_channel.dart`) – Kompletter Rewrite

Die gesamte `voice_channel.dart` wurde neu strukturiert mit einer einheitlichen Architektur
für LiveKit und VoIP.

#### 8a. Unified Participant Model

```dart
class _ParticipantData {
  final String id, name;
  final Widget? videoWidget;
  final bool isSpeaking, isMuted, hasVideo, isScreenShare;
  final Color avatarColor;
  final dynamic originalParticipant;
}
```

#### 8b. Adaptive Seitenverhältnisse (`voice_channel.dart`)

**Problem:** Video-Tiles waren auf `AspectRatio(16/9)` festgelegt — Hochkant-Videos vom
Handy wurden stark beschnitten und falsch dargestellt.

**Fix:**
- `_ParticipantData` bekommt ein `aspectRatio`-Feld (Default `16.0/9.0`).
- VoIP: echte Dimension aus `RTCVideoRenderer.videoWidth/videoHeight` ausgelesen.
- LiveKit: echte Dimension aus `(videoTrack as dynamic).currentDimensions` ausgelesen.
- Alle Renderer nutzen `VideoViewFit.contain` / `RTCVideoViewObjectFitContain` statt `cover`
  — kein Beschnitt mehr.
- `_ParticipantGrid` wechselt von `GridView(childAspectRatio: 16/9)` zu `Wrap` mit
  individuell berechneter `SizedBox(width, height)` pro Tile.
- `_ParticipantStrip` passt Tile-Breite an: `101 * p.aspectRatio` für Landscape-Tiles.

#### 8c. Hero/Spotlight-System

- **Auto-Spotlight:** Screenshares werden automatisch als Hero angezeigt
- **Manuelles Pinnen:** Klick auf Tile → wird Hero; erneuter Klick = unpin
- Gear-Icon im Hero-Overlay öffnet das Qualitätsmenü (hover-reveal)
- Fullscreen-Button öffnet eine eigene Fullscreen-Seite via `Navigator.push`

#### 8d. Aktive Sprecher

Pulsierende Rahmen-Animation (`AnimationController`) für Teilnehmer, die gerade sprechen.

#### 8e. Teilnehmer-Sortierung

```
Screensharer → Kamera-an → Sprechend → Audio-only
```

#### 8f. Responsive Layouts

**Portrait:** Hero oben (Expanded) + horizontaler Teilnehmer-Strip unten (120px)
**Landscape:** Hero links (Expanded) + vertikale Sidebar rechts (200px)

Beide Layouts sind **einklappbar** (Swipe-Geste + Pfeil-Button).

#### 8g. Zentrales Mirroring

`_MirrorWrapper` spiegelt nur die eigene Kamera (`isLocal && !isScreenShare`).
Screenshares und Remote-Teilnehmer bleiben ungespiegelt.

#### 8h. Grid-Layout mit einheitlichen Tiles

`SliverGridDelegateWithFixedCrossAxisCount`:
- < 600px → 2 Spalten
- 600–900px → 3 Spalten
- > 900px → 4 Spalten

#### 8i. Steuerungs-Reihenfolge (alle Views)

```
[ Mic ] [ Deaf ] [ Kamera ] [ Screen ] | [ Auflegen ]
```

---

### 9. Mini-Call-Widget – Steuerungs-Reihenfolge (`mini_call_widget.dart`)

Gleiche Reihenfolge wie Haupt-Call-Ansicht: Mic → Deaf → Kamera → Screen.

---

### 10. 4K Support

4K (2160p) als Qualitätsoption in LiveKit- und VoIP-Qualitätsmenüs hinzugefügt.

---

### 11. App-Icon – Pyramid-Logo (`flutter_launcher_icons.yaml`)

**Problem:** App zeigte das Flutter-Standard-Icon statt dem Pyramid-Logo.

**Fix:**
- `flutter_launcher_icons.yaml` auf `assets/logo.png` (goldene 3D-Pyramide, 1024px) umgestellt.
- macOS-Generierung explizit aktiviert (`macos: generate: true`).
- Windows ICO nach `dart run flutter_launcher_icons` mit Pillow neu generiert
  (16/32/48/64/128/256px multi-size ICO, verhindert das 48px-Limit des Packages).

---

### 12. Lade-Animation – 3D Pyramiden-Painter (`pyramid_loader.dart`)

**Problem:** Lottie-JSON zeigte inkorrekte Farben, SVG wurde nur statisch gerendert
(flutter_svg führt kein JavaScript aus).

**Lösung:** JavaScript-Animations-Logik aus dem Original-SVG exakt nach Dart portiert
als `CustomPainter`:

- `_ease(t, exp)`: kubische Ease-In/Out wie im JS-Original
- `_yawAt(t)`: Yaw-Rotation mit Hold-Phase (80% Rotation, 20% Pause bei π/4)
- `_PyramidPainter`: 4 Flächen, Y-Rotation, Z-Clipping, orthografische Projektion,
  20°-Tilt, Depth-Sorting (Painter's Algorithm)
- Fill: `darken(accent, 0.45)`, Stroke: `accent`, strokeWidth: `1.4 * (size/96)`
- `useLottie: false` ist nun der Standard — kein Lottie-Asset mehr nötig

**Loader Preview** in den Appearance Settings (zwei 120px-Boxen: Custom Painter + Lottie-Fallback).

---

### 13. Chat-Scroll-Stabilisierung (`chat_view.dart`)

**Problem:** Der Chat zuckte und zitterte je weiter man nach oben scrollte, besonders
wenn neue History nachgeladen wurde.

**Ursache:** `DateDivider` und `MessageGroup` waren in einem gemeinsamen `Column`-Widget
mit einem einzigen Key zusammengefasst. Wenn beim Nachladen von History die `showDate`-
Eigenschaft einer Gruppe sich änderte (z.B. weil sie nicht mehr die älteste Gruppe war),
änderte sich die Höhe des Items — direkt in dem Bereich, den der User gerade betrachtete.

**Fix:**
- `DateDivider` und `MessageGroup` sind jetzt separate List-Items mit unabhängigen Keys:
  - Gruppen: `ValueKey('g:<firstEventId>')`
  - Datumstrennlinien: `ValueKey('d:<YYYY-MM-DD>')`
- `findChildIndexCallback` nutzt eine vorberechnete `Map<String, int>` für O(1)-Lookup
  statt O(n)-`indexWhere`.
- 200ms Cooldown nach `requestHistory` verhindert sofortige Re-Trigger durch veraltetes
  `maxScrollExtent`.

---

### 14. Startup-Flash Fix (`app.dart`)

**Problem:** Beim App-Start flackerte kurz die Server-Eingabe-Seite auf, bevor der
korrekte Redirect zu `/app` stattfand.

**Ursache:** `isLoggedInProvider` (StreamProvider) gibt beim ersten Build `AsyncLoading`
zurück. `valueOrNull ?? false` ergibt `false` → Router leitet sofort zu `/server` weiter.
Kurz danach resolvet der Stream und der Router erstellt sich neu mit `isLoggedIn = true`.

**Fix:** `PyramidApp` prüft `isLoggedInProvider` zuerst. Solange noch `isLoading`:
vollbild `PyramidLoader` statt Router — die Pyramiden-Animation überbrückt den kurzen
Moment, bis der Auth-State bekannt ist.

---

### 15. Eigenes Profilbild (`user_panel.dart`)

**Problem:** Das eigene Profilbild wurde unten links (UserPanel) nicht angezeigt —
nur ein Farbverlauf-Platzhalter mit Initial-Buchstabe.

**Fix:**
- `_ownProfileProvider` (FutureProvider): ruft einmalig `client.getProfileFromUserId(userID)`
  auf und cached das Ergebnis.
- `UserPanel` zeigt nun `MxcAvatar` mit dem echten Profilbild.
- Anzeigename kommt aus `profile.displayName` statt nur aus der UserID.
- Farbverlauf-Initial-Avatar bleibt als Fallback (`_InitialAvatar`) während des Ladens
  oder wenn kein Bild gesetzt ist.

---

### 16. QR-Code-Verifikation (`app_shell.dart`, `settings_modal.dart`)

**Problem:** Geräteverifikation unterstützte nur Emoji-SAS — kein QR-Code-Flow.

**Fix:**
- `KeyVerificationMethod.qrShow` zu `verificationMethods` in `matrix_client.dart` hinzugefügt.
- `pubspec.yaml`: `pretty_qr_code: ^3.6.0` und `qr: ^3.0.2` ergänzt.
- Eingehender Verifizierungsdialog (`app_shell.dart`) und ausgehender Dialog (`settings_modal.dart`)
  unterstützen jetzt beide 3 neue States:
  - `askChoice` → Methodenwahl (Emoji oder QR)
  - `confirmQRScan` → QR-Code anzeigen (`QrCode.fromUint8List` + `PrettyQrView`) + "Gescannt bestätigen"-Button
  - `showQRSuccess` → Erfolgsanzeige

**Technischer Hinweis:** Matrix QR-Codes sind Binärdaten (`qrDataRawBytes: Uint8Buffer`).
Rendering über `QrCode.fromUint8List(data: rawBytes, errorCorrectLevel: QrErrorCorrectLevel.L)`.

---

### 17. Wiederherstellungsschlüssel ändern (`settings_modal.dart`)

**Problem:** Kein Feature zum Wechseln des SSSS-Wiederherstellungsschlüssels vorhanden.

**Implementierung:** `_ChangeRecoveryKeyDialog` mit Bootstrap-State-Machine:

**Flow:**
1. **Laden** — Bootstrap startet, `wipeSsss(true)` → `newSsss()` generiert neuen Schlüssel
2. **Schlüssel anzeigen** — Schlüssel selektierbar + Kopier-Button; wartet auf Nutzer
3. **Bestätigen** — Nutzer tippt Schlüssel zur Bestätigung ein (`_ConfirmKeyBody`)
4. **Aktivieren** — wartet auf Bootstrap `done` (Cross-Signing + Key Backup im Hintergrund)
5. **Fertig** — Erfolgsmeldung

**Kritischer Fix (BootstrapBadStateException):**
- `newSsss()` ruft intern `checkCrossSigning()` auf → State geht sofort zu `askWipeCrossSigning`.
- `openExistingSsss()` ist nach `newSsss()` nicht mehr erreichbar — wurde entfernt.
- Alle Post-`newSsss`-States werden ohne `_step`-Guards auto-advanced.
- Bootstrap-`done` setzt `_step` nur wenn bereits in `applying` — verhindert vorzeitigen Dialog-Schluss.

---

## Offene Punkte

### ⚠️ Screenshare auf Windows (LiveKit)

`startScreenShare()` wählt auf Windows automatisch die erste verfügbare Screen-Source.
Der `ScreenSharePicker` ist implementiert, muss noch verifiziert werden.

### ℹ️ Profilbilder in Chats – Lazy Loading

Profilbilder in der Raumliste und im Chat werden erst beim Öffnen des jeweiligen
Raums geladen (kein Preloading). Ist akzeptables Verhalten, kein Fehler.

### 📋 Noch nicht implementiert (Settings)

~~Alle erledigt in Sitzung 3 (26.04.2026): Gerät umbenennen, Alle anderen Sitzungen abmelden, Ed25519-Fingerabdruck, Blockierte Nutzer, Passwort ändern, Konto löschen, Logout-Button~~

### 💡 Nice-to-have

- **QR-Express-Login** (MSC4108): Desktop zeigt QR → Handy scannt → eingeloggt + verifiziert.
  Voraussetzung: Rendezvous-Server + SDK-Unterstützung (matrix-dart-sdk ≥ 7.x erwartet).
  Aktuell blockiert durch fehlende SDK-API in 6.2.0.

---

## Geänderte Dateien (vollständig)

| Datei | Inhalt |
|-------|--------|
| `lib/core/livekit_call_manager.dart` | Quality-Bug-Fix, Track-Events, Optimistic Mute, 4K |
| `lib/core/voip_manager.dart` | Kamera-Fix, Optimistic Toggle, Speakerphone-Fix, 4K |
| `lib/features/call/voice_channel.dart` | Kompletter Rewrite: Unified Architecture, adaptive AR, Hero, Mirroring, Grid, Sortierung, Controls |
| `lib/features/call/mini_call_widget.dart` | Steuerungs-Reihenfolge |
| `lib/core/matrix_client.dart` | WAL-Modus, busy_timeout, qrShow in verificationMethods |
| `lib/widgets/pyramid_loader.dart` | 3D CustomPainter (JS-Port), useLottie=false Standard |
| `lib/core/app.dart` | Startup-Flash-Fix: PyramidLoader während isLoggedInProvider lädt |
| `lib/features/chat/chat_view.dart` | Scroll-Stabilisierung: flache Item-Liste, O(1)-Lookup, Cooldown |
| `lib/features/rooms/user_panel.dart` | Eigenes Profilbild via _ownProfileProvider + MxcAvatar |
| `lib/widgets/settings_modal.dart` | QR-Verifikation, Wiederherstellungsschlüssel ändern, Loader-Preview |
| `lib/layout/app_shell.dart` | QR-Verifikation im eingehenden Verifizierungsdialog |
| `pubspec.yaml` | pretty_qr_code, qr hinzugefügt |
| `flutter_launcher_icons.yaml` | Pyramid-Logo für alle Plattformen |
| `windows/runner/resources/app_icon.ico` | Multi-Size ICO (16/32/48/64/128/256px) via Pillow |
| `assets/logo.png` | Pyramid-Logo (1024px) |
| `assets/pyramid-loader.json` | Lottie-Fallback (aus Ordner mit Leerzeichen verschoben) |

---

## Backups

| Datum | Pfad |
|-------|------|
| 24.04.2026 (Sitzung 1) | `E:\pyramid_backup_20260424` |
| 24.04.2026 (Sitzung 2) | `E:\pyramid_backup_20260424_2` |
| 26.04.2026 | `E:\pyramid_backup_20260426` |
