dramaling-app/apps/mobile/lib/features/dialogue/screens/dialogue_main_screen.dart

656 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../shared/widgets/voice_input_button.dart';
import '../providers/dialogue_provider.dart';
import '../widgets/dialogue_background.dart';
import '../widgets/character_avatar.dart';
import '../widgets/dialogue_bubble.dart';
import '../widgets/task_display_panel.dart';
import '../widgets/vocabulary_panel.dart';
import '../widgets/reply_assistance_panel.dart';
/// 情境對話主界面
///
/// 實現完整的AI對話功能包括
/// - 沉浸式場景背景
/// - 角色對話展示
/// - 語音和文字輸入
/// - 任務進度追蹤
/// - 指定詞彙提示
/// - 回覆輔助功能
/// - 限時挑戰模式
class DialogueMainScreen extends ConsumerStatefulWidget {
/// 場景ID
final String scenarioId;
/// 關卡ID
final String levelId;
/// 是否為限時挑戰模式
final bool isTimeChallenge;
const DialogueMainScreen({
super.key,
required this.scenarioId,
required this.levelId,
this.isTimeChallenge = false,
});
@override
ConsumerState<DialogueMainScreen> createState() => _DialogueMainScreenState();
}
class _DialogueMainScreenState extends ConsumerState<DialogueMainScreen>
with TickerProviderStateMixin {
final TextEditingController _textController = TextEditingController();
final FocusNode _textFocusNode = FocusNode();
late AnimationController _timerController;
late Animation<double> _timerAnimation;
@override
void initState() {
super.initState();
// 限時挑戰計時器動畫
_timerController = AnimationController(
duration: const Duration(seconds: 300), // 300秒 = 5分鐘
vsync: this,
);
_timerAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _timerController,
curve: Curves.linear,
));
// 如果是限時挑戰,開始計時
if (widget.isTimeChallenge) {
_timerController.forward();
_timerController.addStatusListener(_onTimerComplete);
}
// 初始化對話
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(dialogueProvider.notifier).initializeDialogue(
scenarioId: widget.scenarioId,
levelId: widget.levelId,
isTimeChallenge: widget.isTimeChallenge,
);
});
}
@override
void dispose() {
_textController.dispose();
_textFocusNode.dispose();
_timerController.dispose();
super.dispose();
}
/// 計時器完成處理
void _onTimerComplete(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_showTimeUpDialog();
}
}
@override
Widget build(BuildContext context) {
final dialogueState = ref.watch(dialogueProvider);
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
// 背景場景
DialogueBackground(
scenarioId: widget.scenarioId,
backgroundUrl: dialogueState.currentScene?.backgroundImageUrl,
),
// 主要內容
Column(
children: [
// 頂部工具列
_buildTopBar(dialogueState),
// 對話內容區域
Expanded(
child: _buildDialogueContent(dialogueState),
),
// 底部輸入區域
_buildInputArea(dialogueState),
],
),
// 任務顯示面板
if (dialogueState.currentTask != null)
Positioned(
top: 80.h,
right: 16.w,
child: TaskDisplayPanel(
task: dialogueState.currentTask!,
),
),
// 指定詞彙面板
if (dialogueState.requiredVocabulary.isNotEmpty)
Positioned(
top: 80.h,
left: 16.w,
child: VocabularyPanel(
vocabularies: dialogueState.requiredVocabulary,
usedVocabularies: dialogueState.usedVocabulary,
),
),
// 回覆輔助面板
if (dialogueState.showReplyAssistance)
Positioned.fill(
child: ReplyAssistancePanel(
suggestions: dialogueState.replySuggestions,
onSelectSuggestion: _onSelectSuggestion,
onClose: _closeReplyAssistance,
),
),
],
),
),
);
}
/// 頂部工具列
Widget _buildTopBar(DialogueState state) {
return Container(
height: 60.h,
padding: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.transparent,
],
),
),
child: Row(
children: [
// 返回按鈕
IconButton(
onPressed: _showExitConfirmation,
icon: Icon(
Icons.arrow_back_ios,
color: Colors.white,
size: 20.sp,
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 限時挑戰計時器
if (widget.isTimeChallenge)
AnimatedBuilder(
animation: _timerAnimation,
builder: (context, child) {
final remainingSeconds = (_timerAnimation.value * 300).round();
final minutes = remainingSeconds ~/ 60;
final seconds = remainingSeconds % 60;
return Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.h,
),
decoration: BoxDecoration(
color: remainingSeconds < 60
? Colors.red.withOpacity(0.8)
: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer,
color: Colors.white,
size: 16.sp,
),
SizedBox(width: 4.w),
Text(
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
],
),
),
// 資源顯示
Row(
children: [
// 命條
Row(
children: [
Icon(
Icons.favorite,
color: Colors.red,
size: 16.sp,
),
SizedBox(width: 4.w),
Text(
'${state.lifePoints}',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(width: 16.w),
// 鑽石
Row(
children: [
Icon(
Icons.diamond,
color: Colors.blue,
size: 16.sp,
),
SizedBox(width: 4.w),
Text(
'${state.diamonds}',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
],
),
);
}
/// 對話內容區域
Widget _buildDialogueContent(DialogueState state) {
return Column(
children: [
// 角色頭像和名稱
if (state.currentCharacter != null)
Padding(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: CharacterAvatar(
character: state.currentCharacter!,
showDetails: true,
),
),
// 對話氣泡
Expanded(
child: Container(
margin: EdgeInsets.symmetric(horizontal: 20.w),
child: state.currentDialogue != null
? DialogueBubble(
dialogue: state.currentDialogue!,
isUserReply: false,
)
: Center(
child: CircularProgressIndicator(
color: Theme.of(context).primaryColor,
),
),
),
),
// 用戶回覆氣泡(如果有的話)
if (state.lastUserReply != null)
Container(
margin: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
child: DialogueBubble(
dialogue: state.lastUserReply!,
isUserReply: true,
),
),
],
);
}
/// 底部輸入區域
Widget _buildInputArea(DialogueState state) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.9),
Colors.transparent,
],
),
),
child: Column(
children: [
// 功能按鈕行
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 角色詳情
_buildFunctionButton(
icon: Icons.person,
label: '角色',
onTap: _showCharacterDetails,
),
// 關鍵詞
_buildFunctionButton(
icon: Icons.key,
label: '關鍵詞',
onTap: _showKeywords,
),
// 任務提示
_buildFunctionButton(
icon: Icons.lightbulb,
label: '任務',
onTap: _showTaskHint,
disabled: state.currentTask?.isCompleted ?? true,
),
// 中翻英
_buildFunctionButton(
icon: Icons.translate,
label: '翻譯',
onTap: _showTranslation,
),
// 回覆輔助
_buildFunctionButton(
icon: Icons.help,
label: '輔助',
onTap: _showReplyAssistance,
cost: 30,
disabled: state.diamonds < 30,
),
],
),
SizedBox(height: 16.h),
// 輸入區域
Row(
children: [
// 文字輸入框
Expanded(
child: TextField(
controller: _textController,
focusNode: _textFocusNode,
maxLines: 3,
minLines: 1,
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
decoration: InputDecoration(
hintText: '請輸入你的回覆...',
hintStyle: TextStyle(
color: Colors.grey,
fontSize: 16.sp,
),
filled: true,
fillColor: Colors.black.withOpacity(0.6),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24.r),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 12.h,
),
),
),
),
SizedBox(width: 8.w),
// 語音輸入按鈕
VoiceInputButton(
size: 48,
languageId: state.currentLanguage,
onResult: _onVoiceResult,
onError: _onVoiceError,
),
SizedBox(width: 8.w),
// 發送按鈕
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _textController.text.trim().isNotEmpty
? Theme.of(context).primaryColor
: Colors.grey,
),
child: IconButton(
onPressed: _textController.text.trim().isNotEmpty
? _sendReply
: null,
icon: Icon(
Icons.send,
color: Colors.white,
size: 20.sp,
),
),
),
],
),
],
),
);
}
/// 功能按鈕
Widget _buildFunctionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
int? cost,
bool disabled = false,
}) {
return GestureDetector(
onTap: disabled ? null : onTap,
child: Container(
width: 60.w,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: disabled
? Colors.grey.withOpacity(0.3)
: Colors.white.withOpacity(0.2),
),
child: Stack(
children: [
Center(
child: Icon(
icon,
color: disabled ? Colors.grey : Colors.white,
size: 20.sp,
),
),
if (cost != null)
Positioned(
top: -2.h,
right: -2.w,
child: Container(
padding: EdgeInsets.all(2.w),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
child: Icon(
Icons.diamond,
color: Colors.white,
size: 8.sp,
),
),
),
],
),
),
SizedBox(height: 4.h),
Text(
cost != null ? '$label($cost)' : label,
style: TextStyle(
color: disabled ? Colors.grey : Colors.white,
fontSize: 12.sp,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// 語音識別結果處理
void _onVoiceResult(String text) {
setState(() {
_textController.text = text;
});
}
/// 語音識別錯誤處理
void _onVoiceError(String error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('語音識別失敗:$error'),
backgroundColor: Colors.red,
),
);
}
/// 發送回覆
void _sendReply() {
final text = _textController.text.trim();
if (text.isEmpty) return;
ref.read(dialogueProvider.notifier).sendReply(text);
_textController.clear();
}
/// 選擇建議回覆
void _onSelectSuggestion(String suggestion) {
setState(() {
_textController.text = suggestion;
});
_closeReplyAssistance();
}
/// 顯示角色詳情
void _showCharacterDetails() {
// TODO: 導航到角色詳情頁面
}
/// 顯示關鍵詞
void _showKeywords() {
// TODO: 導航到關鍵詞頁面
}
/// 顯示任務提示
void _showTaskHint() {
// TODO: 顯示任務提示對話框
}
/// 顯示翻譯
void _showTranslation() {
// TODO: 顯示翻譯對話框
}
/// 顯示回覆輔助
void _showReplyAssistance() {
ref.read(dialogueProvider.notifier).showReplyAssistance();
}
/// 關閉回覆輔助
void _closeReplyAssistance() {
ref.read(dialogueProvider.notifier).hideReplyAssistance();
}
/// 顯示退出確認
void _showExitConfirmation() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('確認離開'),
content: Text(
widget.isTimeChallenge
? '離開限時挑戰將無法繼續,確定要離開嗎?'
: '確定要離開對話嗎?當前進度將會保存。',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('取消'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Text('確定'),
),
],
),
);
}
/// 顯示時間結束對話框
void _showTimeUpDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('時間到!'),
content: Text('限時挑戰時間已結束,正在計算成績...'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: 跳轉到結果頁面
Navigator.of(context).pop();
},
child: Text('查看結果'),
),
],
),
);
}
}