Bernds Wunsch 2026-10-08: UI grundsätzlich prüfen (Navigation, Sackgassen, Anruf-Logik). Audit + Konzept in docs/UI_AUDIT.md, belegt mit neuer Screenshot-Tour integration_test/ui_audit_tour_test.dart (35 Zustände, Handy + PC, zwei Testkonten, Test-Space mit Sprachkanal). Sofort behoben: - S1/S2: Gegenseite legt auf / Anruf scheitert / Sprachkanal bricht ab → automatisch zurück zum Chat mit Hinweis (Fehler sichtbar statt still); leere Anrufansicht deutsch mit Zurück-Knopf (vorher englische Sackgasse ohne Zurück am Handy). - S4: toter „Threads“-Knopf entfernt. S5: Space betreten (PC) öffnet letzten bzw. ersten Textkanal. S6: keine doppelte Anrufsteuerung am PC. - Annehmen-Fenster mit Anrufer-Avatar und Anrufart; eigene Kachel mit eigenem Initial statt „D“; Mitgliederliste rund mit Profilbild und „(Du)“; Tastaturkürzel nur am PC; englische Texte in Anruf/Leerzuständen deutsch. - Tests: Fehlerempfänger des Testrahmens nach app.main() zurück (vorher verschwanden Fehlermeldungen), Lesebestätigungs-Test im Wegwerf-Raum statt in der DM, robustere Schritte; test.ps1 räumt liegengebliebene Test-Prozesse weg; gemeinsame Hilfen integration_test/support/. Co-Authored-By: Claude Opus 5.5 <[email protected]>
194 lines
6.7 KiB
Dart
194 lines
6.7 KiB
Dart
part of '../voice_channel.dart';
|
|
|
|
class _BigAvatar extends StatelessWidget {
|
|
final _ParticipantData data;
|
|
final PyramidTheme pt;
|
|
const _BigAvatar({required this.data, required this.pt});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Container(
|
|
width: 100, height: 100,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(colors: [data.avatarColor, data.avatarColor.withAlpha(150)], begin: Alignment.topLeft, end: Alignment.bottomRight),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Center(child: Text(data.initial, style: const TextStyle(color: Colors.white, fontSize: 40, fontWeight: FontWeight.bold))),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SmallAvatar extends StatelessWidget {
|
|
final _ParticipantData data;
|
|
final PyramidTheme pt;
|
|
const _SmallAvatar({required this.data, required this.pt});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Container(
|
|
width: 44, height: 44,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(colors: [data.avatarColor, data.avatarColor.withAlpha(150)], begin: Alignment.topLeft, end: Alignment.bottomRight),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Center(child: Text(data.initial, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold))),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CallControls extends StatelessWidget {
|
|
final bool isMuted;
|
|
final bool isDeafened;
|
|
final bool isCameraOff;
|
|
final bool isScreenSharing;
|
|
final VoidCallback onToggleMic;
|
|
final VoidCallback onToggleDeafen;
|
|
final VoidCallback onToggleCam;
|
|
final VoidCallback onToggleScreen;
|
|
final VoidCallback onHangUp;
|
|
final PyramidTheme pt;
|
|
// Handy: Lautsprecher/Hörmuschel statt „Ton aus“ (Platz in der Leiste).
|
|
final bool isSpeakerOn;
|
|
final VoidCallback? onToggleSpeaker;
|
|
|
|
const _CallControls({
|
|
required this.isMuted,
|
|
required this.isDeafened,
|
|
required this.isCameraOff,
|
|
required this.isScreenSharing,
|
|
required this.onToggleMic,
|
|
required this.onToggleDeafen,
|
|
required this.onToggleCam,
|
|
required this.onToggleScreen,
|
|
required this.onHangUp,
|
|
required this.pt,
|
|
this.isSpeakerOn = true,
|
|
this.onToggleSpeaker,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final platform = Theme.of(context).platform;
|
|
final showSpeaker = onToggleSpeaker != null &&
|
|
(platform == TargetPlatform.android || platform == TargetPlatform.iOS);
|
|
return SafeArea(
|
|
top: false,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
|
decoration: BoxDecoration(color: pt.bg1, border: Border(top: BorderSide(color: pt.border))),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_VoiceCtrlBtn(icon: isMuted ? Icons.mic_off_rounded : Icons.mic_rounded, danger: isMuted, pt: pt, onTap: onToggleMic),
|
|
const SizedBox(width: 16),
|
|
if (showSpeaker)
|
|
_VoiceCtrlBtn(
|
|
icon: isSpeakerOn ? Icons.volume_up_rounded : Icons.phone_in_talk_rounded,
|
|
active: isSpeakerOn,
|
|
pt: pt,
|
|
onTap: onToggleSpeaker!,
|
|
)
|
|
else
|
|
_VoiceCtrlBtn(icon: isDeafened ? Icons.headset_off_rounded : Icons.headphones_rounded, danger: isDeafened, pt: pt, onTap: onToggleDeafen),
|
|
const SizedBox(width: 16),
|
|
_VoiceCtrlBtn(icon: isCameraOff ? Icons.videocam_off_rounded : Icons.videocam_rounded, active: !isCameraOff, pt: pt, onTap: onToggleCam),
|
|
const SizedBox(width: 16),
|
|
_VoiceCtrlBtn(icon: Icons.screen_share_rounded, active: isScreenSharing, pt: pt, onTap: onToggleScreen),
|
|
const SizedBox(width: 24),
|
|
Container(width: 1, height: 32, color: pt.border),
|
|
const SizedBox(width: 24),
|
|
_VoiceCtrlBtn(icon: Icons.call_end_rounded, isDanger: true, pt: pt, onTap: onHangUp),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NoCall extends StatelessWidget {
|
|
final PyramidTheme pt;
|
|
final WidgetRef ref;
|
|
const _NoCall({required this.pt, required this.ref});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
color: pt.bg1,
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
height: 52,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: pt.border))),
|
|
child: Row(
|
|
children: [
|
|
PyrIconBtn(
|
|
icon: const Icon(Icons.arrow_back_rounded),
|
|
tooltip: 'Zurück',
|
|
onPressed: () => ref.read(viewModeProvider.notifier).state = ViewMode.chat,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text('Kein Anruf', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.call_end_rounded, size: 48, color: pt.fgDim),
|
|
const SizedBox(height: 12),
|
|
Text('Gerade läuft kein Anruf.', style: TextStyle(color: pt.fgMuted, fontSize: 14)),
|
|
const SizedBox(height: 16),
|
|
FilledButton(
|
|
onPressed: () => ref.read(viewModeProvider.notifier).state = ViewMode.chat,
|
|
style: FilledButton.styleFrom(backgroundColor: pt.accent, foregroundColor: pt.accentFg),
|
|
child: const Text('Zurück zum Chat'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FullscreenVideoPage extends StatelessWidget {
|
|
final _ParticipantData data;
|
|
const _FullscreenVideoPage({required this.data});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: Stack(children: [
|
|
Positioned.fill(
|
|
child: data.videoWidget != null
|
|
? _MirrorWrapper(
|
|
isLocal: data.id == 'local' || (data.originalParticipant is LocalParticipant),
|
|
isScreenShare: data.isScreenShare,
|
|
child: data.videoWidget!,
|
|
)
|
|
: _BigAvatar(data: data, pt: PyramidTheme.of(context)),
|
|
),
|
|
Positioned(
|
|
top: 0, right: 0,
|
|
child: SafeArea(
|
|
child: IconButton(
|
|
icon: const Icon(Icons.fullscreen_exit_rounded, color: Colors.white, size: 28),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
}
|