# 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((_) => notifyListeners()) ..on((_) => notifyListeners()) ..on((_) => notifyListeners()) ..on((_) => notifyListeners()) ..on((_) => notifyListeners()) ..on((_) => notifyListeners()) ..on((_) => 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 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:')` - Datumstrennlinien: `ValueKey('d:')` - `findChildIndexCallback` nutzt eine vorberechnete `Map` 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` |