25ed765a03
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.) als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
387 lines
14 KiB
Markdown
387 lines
14 KiB
Markdown
# 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` |
|