refactor: call_ui/voice_channel.dart (1123 Zeilen) in 5 thematische part-Dateien unter view/ geteilt (M2, zeichenidentisch verifiziert)

This commit is contained in:
Bernd Steckmeister
2026-07-06 17:15:12 +02:00
parent b131f56b72
commit 95d9b07dbd
7 changed files with 1122 additions and 1109 deletions
+214
View File
@@ -0,0 +1,214 @@
part of '../voice_channel.dart';
class _UnifiedCallView extends StatefulWidget {
final List<_ParticipantData> participants;
final bool isLiveKit;
final String displayName;
final bool isConnecting;
final VoidCallback onHangUp;
final VoidCallback onMinimize;
final VoidCallback onToggleMic;
final VoidCallback onToggleDeafen;
final VoidCallback onToggleCam;
final VoidCallback onToggleScreen;
final bool isMuted;
final bool isDeafened;
final bool isCameraOff;
final bool isScreenSharing;
final Function(Offset)? onQualityMenu;
final PyramidTheme pt;
const _UnifiedCallView({
required this.participants,
required this.isLiveKit,
required this.displayName,
required this.isConnecting,
required this.onHangUp,
required this.onMinimize,
required this.onToggleMic,
required this.onToggleDeafen,
required this.onToggleCam,
required this.onToggleScreen,
required this.isMuted,
required this.isDeafened,
required this.isCameraOff,
required this.isScreenSharing,
this.onQualityMenu,
required this.pt,
});
@override
State<_UnifiedCallView> createState() => _UnifiedCallViewState();
}
class _UnifiedCallViewState extends State<_UnifiedCallView> {
String? _heroId;
bool _isGridView = false;
bool _stripCollapsed = false;
@override
void initState() {
super.initState();
_autoSelectHero();
}
@override
void didUpdateWidget(_UnifiedCallView oldWidget) {
super.didUpdateWidget(oldWidget);
_autoSelectHero();
}
void _autoSelectHero() {
if (_isGridView) return;
final screenshare = widget.participants.firstWhereOrNull((p) => p.isScreenShare);
if (screenshare != null && (_heroId == null || !widget.participants.any((p) => p.id == _heroId && p.hasVideo))) {
_heroId = screenshare.id;
} else if (_heroId == null && widget.participants.isNotEmpty) {
final firstVideo = widget.participants.firstWhereOrNull((p) => p.hasVideo);
if (firstVideo != null) _heroId = firstVideo.id;
}
}
List<_ParticipantData> _sorted(List<_ParticipantData> all) {
int rank(_ParticipantData p) {
if (p.isScreenShare) return 0;
if (p.hasVideo) return 1;
if (p.isSpeaking) return 2;
return 3;
}
return [...all]..sort((a, b) => rank(a).compareTo(rank(b)));
}
@override
Widget build(BuildContext context) {
final pt = widget.pt;
final all = _sorted(widget.participants);
final hero = all.firstWhereOrNull((p) => p.id == _heroId) ?? (all.isNotEmpty ? all.first : null);
if (widget.isConnecting) {
return Scaffold(
backgroundColor: pt.bg1,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const PyramidLoader(size: 80),
const SizedBox(height: 20),
Text('Verbinde…',
style: TextStyle(color: pt.fgMuted, fontSize: 14)),
const SizedBox(height: 16),
TextButton.icon(
style: TextButton.styleFrom(foregroundColor: pt.danger),
onPressed: widget.onHangUp,
icon: const Icon(Icons.call_end_rounded, size: 16),
label: const Text('Abbrechen'),
),
],
),
),
);
}
return Scaffold(
backgroundColor: pt.bg1,
body: Column(
children: [
_CallHeader(
displayName: widget.displayName,
isGridView: _isGridView,
onToggleView: () => setState(() => _isGridView = !_isGridView),
onMinimize: widget.onMinimize,
pt: pt,
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _isGridView || hero == null
? _ParticipantGrid(participants: all, pt: pt, onSelect: (p) => setState(() { _heroId = p.id; _isGridView = false; }))
: _HeroLayout(
hero: hero,
others: all.where((p) => p.id != hero.id).toList(),
stripCollapsed: _stripCollapsed,
onToggleStrip: () => setState(() => _stripCollapsed = !_stripCollapsed),
onSetStripCollapsed: (v) => setState(() => _stripCollapsed = v),
onSelectOther: (p) => setState(() => _heroId = p.id),
onQualityMenu: widget.onQualityMenu,
pt: pt,
),
),
),
_CallControls(
isMuted: widget.isMuted,
isDeafened: widget.isDeafened,
isCameraOff: widget.isCameraOff,
isScreenSharing: widget.isScreenSharing,
onToggleMic: widget.onToggleMic,
onToggleDeafen: widget.onToggleDeafen,
onToggleCam: widget.onToggleCam,
onToggleScreen: widget.onToggleScreen,
onHangUp: widget.onHangUp,
pt: pt,
),
],
),
);
}
}
class _CallHeader extends StatelessWidget {
final String displayName;
final bool isGridView;
final VoidCallback onToggleView;
final VoidCallback onMinimize;
final PyramidTheme pt;
const _CallHeader({required this.displayName, required this.isGridView, required this.onToggleView, required this.onMinimize, required this.pt});
@override
Widget build(BuildContext context) {
return Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: pt.border))),
child: Row(
children: [
Icon(Icons.headphones_rounded, size: 16, color: pt.fgMuted),
const SizedBox(width: 8),
Text(displayName, style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
const SizedBox(width: 8),
_LiveBadge(pt: pt),
const Spacer(),
PyrIconBtn(
icon: Icon(isGridView ? Icons.grid_view_rounded : Icons.featured_video_rounded, color: isGridView ? pt.accent : pt.fg),
tooltip: isGridView ? 'Hero mode' : 'Grid mode',
onPressed: onToggleView,
),
const SizedBox(width: 4),
PyrIconBtn(
icon: Icon(Icons.close_fullscreen_rounded, color: pt.fg),
tooltip: 'Minimieren',
onPressed: onMinimize,
),
],
),
);
}
}
class _LiveBadge extends StatelessWidget {
final PyramidTheme pt;
const _LiveBadge({required this.pt});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(999), color: pt.online.withAlpha(30)),
child: Row(children: [
Container(width: 6, height: 6, decoration: BoxDecoration(color: pt.online, shape: BoxShape.circle)),
const SizedBox(width: 4),
Text('Live', style: TextStyle(color: pt.online, fontSize: 11, fontWeight: FontWeight.w600)),
]),
);
}
}