import 'package:flutter/material.dart'; import '../nav.dart'; import '../services/call_service.dart'; import '../theme.dart'; import '../utils/format.dart'; import '../widgets/avatar.dart'; /// 一对一语音通话界面:呼出 / 来电 / 通话中三种状态。 /// 界面只读 CallService 的状态,信令与房间管理都在 service 里。 class CallScreen extends StatefulWidget { const CallScreen({super.key}); @override State createState() => _CallScreenState(); } class _CallScreenState extends State { CallService get _call => CallService.instance; @override void initState() { super.initState(); // 通话状态变化时刷新界面;回到空闲态时关闭页面 _call.addListener(_onCallChanged); } @override void dispose() { _call.removeListener(_onCallChanged); super.dispose(); } void _onCallChanged() { if (!mounted) return; if (_call.phase == CallPhase.idle) { final hint = _call.takeEndHint(); Navigator.of(context).maybePop(); if (hint != null) { // 页面关闭后,用全局导航键把提示弹到底层页面上 WidgetsBinding.instance.addPostFrameCallback((_) { final ctx = navigatorKey.currentContext; if (ctx != null) { ScaffoldMessenger.maybeOf(ctx)?.showSnackBar( SnackBar(content: Text(hint), duration: const Duration(seconds: 2)), ); } }); } return; } setState(() {}); } @override Widget build(BuildContext context) { final phase = _call.phase; return Scaffold( backgroundColor: const Color(0xFF2B3038), body: SafeArea( child: Column( children: [ const SizedBox(height: AppGap.x12), NameAvatar(name: _call.peerName, size: 88), const SizedBox(height: AppGap.x4), Text( _call.peerName, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Colors.white), ), const SizedBox(height: AppGap.x2), Text( _statusText(phase), style: const TextStyle(fontSize: AppFont.sub, color: Color(0xFFB0B6BF)), ), const Spacer(), _buttons(phase), const SizedBox(height: AppGap.x12), ], ), ), ); } String _statusText(CallPhase phase) { switch (phase) { case CallPhase.outgoing: return '正在呼叫,等待对方接听…'; case CallPhase.incoming: return '邀请你语音通话'; case CallPhase.incall: return FormatUtils.callDuration(_call.callSeconds); case CallPhase.idle: return '通话已结束'; } } Widget _buttons(CallPhase phase) { switch (phase) { case CallPhase.outgoing: return _roundButton(icon: Icons.call_end, color: AppColors.danger, label: '取消', onTap: _call.leave); case CallPhase.incoming: return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _roundButton(icon: Icons.call_end, color: AppColors.danger, label: '拒绝', onTap: _call.reject), _roundButton(icon: Icons.call, color: const Color(0xFF2BA245), label: '接听', onTap: _call.accept), ], ); case CallPhase.incall: return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _toggleButton( icon: _call.micOn ? Icons.mic : Icons.mic_off, label: _call.micOn ? '静音' : '已静音', active: !_call.micOn, onTap: _call.toggleMic, ), _roundButton(icon: Icons.call_end, color: AppColors.danger, label: '挂断', onTap: _call.hangup), _toggleButton( icon: Icons.volume_up, label: _call.speakerOn ? '免提开' : '免提', active: _call.speakerOn, onTap: _call.toggleSpeaker, ), ], ); case CallPhase.idle: return const SizedBox.shrink(); } } Widget _roundButton({ required IconData icon, required Color color, required String label, required VoidCallback onTap, }) { return Column( mainAxisSize: MainAxisSize.min, children: [ InkWell( onTap: onTap, borderRadius: BorderRadius.circular(32), child: Container( width: 64, height: 64, decoration: BoxDecoration(color: color, shape: BoxShape.circle), child: Icon(icon, size: 30, color: Colors.white), ), ), const SizedBox(height: AppGap.x2), Text(label, style: const TextStyle(fontSize: AppFont.sub, color: Color(0xFFB0B6BF))), ], ); } Widget _toggleButton({ required IconData icon, required String label, required bool active, required VoidCallback onTap, }) { return Column( mainAxisSize: MainAxisSize.min, children: [ InkWell( onTap: onTap, borderRadius: BorderRadius.circular(32), child: Container( width: 64, height: 64, decoration: BoxDecoration( color: active ? Colors.white : const Color(0xFF3A4048), shape: BoxShape.circle, ), child: Icon(icon, size: 28, color: active ? AppColors.textPrimary : Colors.white), ), ), const SizedBox(height: AppGap.x2), Text(label, style: const TextStyle(fontSize: AppFont.sub, color: Color(0xFFB0B6BF))), ], ); } }