feat(ui): untere Leiste (Chats/Server/Du) als Option, Server-Uebersicht, Du-Tab, Feinschliff

- Einstellung 'Navigation: Seitenmenue | Untere Leiste' (Handy, Standard
  bleibt Seitenmenue): Tabs Chats/Server/Du, Server-Uebersicht als Karten,
  Du-Tab mit Profil + Einstellungen, Zurueck-Taste ueber die Tabs
- CallDock als eigenes Widget (Anrufleiste auch ueber der Tab-Leiste)
- Sprachkanaele am Handy als Kachel mit 'wer ist drin'
- Suchfeld mit Lupe, 'Kanaele durchsuchen' im Server, Filter-Chips am PC
  umbrechend, '#' nur bei Server-Kanaelen, englische Reste entfernt
- Anrufansicht merkt sich Bubbles/Kacheln, Bubbles mit leichtem Glanz
- UI-Tour: Gruppenanruf (echter zweiter LiveKit-Teilnehmer), Bubbles mit
  5/7 Personen, untere Leiste; Version 0.1.42

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-10 14:22:02 +02:00
co-authored by Claude Opus 5.5
parent de24053167
commit 391ebf0ffa
19 changed files with 1139 additions and 87 deletions
+4 -4
View File
@@ -70,13 +70,13 @@ class BubbleSim {
final active = ids.where((id) => bodies.containsKey(id)).toList();
double base;
if (active.length <= 1) {
base = shortSide * 0.30;
base = shortSide * 0.27;
} else {
// Kreise sollen zusammen gut 40 % der Fläche bedecken.
// Kreise sollen zusammen gut ein Drittel der Fläche bedecken.
final sumW2 = active.fold<double>(0, (s, id) => s + weight(id) * weight(id));
base = math.sqrt(0.42 * w * h / (math.pi * sumW2));
base = math.sqrt(0.36 * w * h / (math.pi * sumW2));
}
final maxR = shortSide * (active.length <= 2 ? 0.30 : 0.36);
final maxR = shortSide * (active.length <= 2 ? 0.27 : 0.36);
for (final b in bodies.values) {
if (b.leaving) {
b.targetR = 0;
@@ -164,6 +164,17 @@ class _Bubble extends StatelessWidget {
isScreenShare: data.isScreenShare,
child: data.coverVideoWidget!,
),
const IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: RadialGradient(
center: Alignment(-0.45, -0.55),
radius: 0.95,
colors: [Color(0x2EFFFFFF), Color(0x00FFFFFF)],
),
),
),
),
if (showName)
Align(
alignment: const Alignment(0, 0.62),
@@ -303,3 +314,49 @@ class _BubbleRingPainter extends CustomPainter {
@override
bool shouldRepaint(_BubbleRingPainter old) => true;
}
/// Vorführ-Ansicht der Bubbles mit erfundenen Teilnehmern – nur für
/// Screenshots/Tests (UI-Tour), damit man die Ansicht mit mehreren Personen
/// sieht, ohne dass so viele Testkonten anrufen müssen.
@visibleForTesting
class CallBubblesDemo extends StatelessWidget {
final int count;
const CallBubblesDemo({super.key, this.count = 5});
static const _names = ['Du', 'Anna', 'Ben', 'Clara', 'David', 'Ela', 'Finn'];
@override
Widget build(BuildContext context) {
final pt = PyramidTheme.of(context);
final participants = [
for (var i = 0; i < count.clamp(1, _names.length); i++)
_ParticipantData(
id: 'demo$i',
name: _names[i],
isLocal: i == 0,
isSpeaking: i == 2,
isMuted: i == 3,
avatarColor: VoiceChannelView._avatarColors[i % VoiceChannelView._avatarColors.length],
),
];
return _UnifiedCallView(
participants: participants,
client: null,
startedAt: DateTime.now().subtract(const Duration(minutes: 3, seconds: 12)),
isLiveKit: true,
displayName: 'Familie',
isConnecting: false,
onHangUp: () {},
onMinimize: () {},
onToggleMic: () {},
onToggleDeafen: () {},
onToggleCam: () {},
onToggleScreen: () {},
isMuted: false,
isDeafened: false,
isCameraOff: true,
isScreenSharing: false,
pt: pt,
);
}
}
+6 -2
View File
@@ -54,7 +54,10 @@ class _UnifiedCallView extends StatefulWidget {
}
class _UnifiedCallViewState extends State<_UnifiedCallView> {
_CallLayout _mode = _CallLayout.bubbles;
/// Zuletzt gewählte Ansicht (Bubbles/Kacheln) – bleibt beim Minimieren
/// und für den nächsten Anruf erhalten.
static _CallLayout _preferred = _CallLayout.bubbles;
_CallLayout _mode = _preferred;
String? _heroId;
bool _stripCollapsed = false;
Set<String> _knownScreens = {};
@@ -86,7 +89,7 @@ class _UnifiedCallViewState extends State<_UnifiedCallView> {
_knownScreens = screens;
if (_mode == _CallLayout.hero &&
!widget.participants.any((p) => p.id == _heroId && p.hasVideo)) {
_mode = _CallLayout.bubbles;
_mode = _preferred;
_heroId = null;
}
}
@@ -154,6 +157,7 @@ class _UnifiedCallViewState extends State<_UnifiedCallView> {
_mode = _mode == _CallLayout.bubbles
? _CallLayout.grid
: _CallLayout.bubbles;
_preferred = _mode;
_heroId = null;
}),
onMinimize: widget.onMinimize,