diff --git a/lib/core/endpoint/api_endpoint.dart b/lib/core/endpoint/api_endpoint.dart index 0aa29d4..0b2528c 100644 --- a/lib/core/endpoint/api_endpoint.dart +++ b/lib/core/endpoint/api_endpoint.dart @@ -1,6 +1,6 @@ class APIEndpoint { - // static const String baseUrl = "http://192.168.1.9:5000"; - static const String baseUrl = "http://103.193.178.121:5000"; + static const String baseUrl = "http://192.168.1.9:5000"; + // static const String baseUrl = "http://103.193.178.121:5000"; static const String api = "$baseUrl/api"; static const String login = "/login"; diff --git a/lib/feature/quiz_play/view/quiz_play_view.dart b/lib/feature/quiz_play/view/quiz_play_view.dart index 929f277..73e538f 100644 --- a/lib/feature/quiz_play/view/quiz_play_view.dart +++ b/lib/feature/quiz_play/view/quiz_play_view.dart @@ -13,16 +13,12 @@ class QuizPlayView extends GetView { Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF9FAFB), - // appBar: _buildAppBar(), body: SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Obx(() { if (!controller.isStarting.value) { - return Text( - context.tr('ready_in', namedArgs: {'second': controller.prepareDuration.toString()}), - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ); + return _buildCountdownScreen(context); } return Column( @@ -36,8 +32,7 @@ class QuizPlayView extends GetView { const SizedBox(height: 12), _buildQuestionText(), const SizedBox(height: 30), - _buildAnswerSection(context), - const Spacer(), + Expanded(child: _buildAnswerSection(context)), _buildNextButton(context), ], ); @@ -47,23 +42,123 @@ class QuizPlayView extends GetView { ); } - Widget _buildCustomAppBar(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: const BoxDecoration( - color: Colors.transparent, - ), - child: Row( + Widget _buildCountdownScreen(BuildContext context) { + return Center( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 800), + builder: (context, value, child) { + return Transform.scale( + scale: 0.5 + (value * 0.5), + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primaryBlue.withOpacity(0.1), + border: Border.all( + color: AppColors.primaryBlue, + width: 4, + ), + ), + child: Center( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Text( + controller.prepareDuration.toString(), + key: ValueKey(controller.prepareDuration.value), + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: AppColors.primaryBlue, + ), + ), + ), + ), + ), + ); + }, + ), + const SizedBox(height: 32), + Text( + context.tr('get_ready'), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 16), + Text( + context.tr('quiz_starting_soon'), + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + Widget _buildCustomAppBar(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.arrow_back_ios, color: Colors.black87), + ), Text( context.tr('quiz_play_title'), - style: TextStyle( + style: const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 20, ), ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColors.primaryBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Icon( + Icons.timer_outlined, + size: 16, + color: AppColors.primaryBlue, + ), + const SizedBox(width: 4), + Obx(() => Text( + '${controller.timeLeft.value}s', + style: TextStyle( + color: AppColors.primaryBlue, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + )), + ], + ), + ), ], ), ); @@ -71,38 +166,141 @@ class QuizPlayView extends GetView { Widget _buildProgressBar() { final question = controller.currentQuestion; - return LinearProgressIndicator( - value: controller.timeLeft.value / question.duration, - minHeight: 8, - backgroundColor: Colors.grey[300], - valueColor: const AlwaysStoppedAnimation(Color(0xFF2563EB)), + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Progress', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + Text( + '${((controller.currentIndex.value + 1) / controller.quizData.questionListings.length * 100).toInt()}%', + style: TextStyle( + fontSize: 14, + color: AppColors.primaryBlue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.grey[300], + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: (controller.currentIndex.value + 1) / controller.quizData.questionListings.length, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + gradient: LinearGradient( + colors: [AppColors.primaryBlue, AppColors.primaryBlue.withOpacity(0.7)], + ), + ), + ), + ), + ), + const SizedBox(height: 12), + // Time progress bar + Obx(() => AnimatedContainer( + duration: const Duration(milliseconds: 100), + height: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: Colors.grey[300], + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: controller.timeLeft.value / question.duration, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: controller.timeLeft.value > question.duration * 0.3 + ? Colors.green + : controller.timeLeft.value > question.duration * 0.1 + ? Colors.orange + : Colors.red, + ), + ), + ), + )), + ], ); } Widget _buildQuestionIndicator(BuildContext context) { - return Text( - context.tr( - 'question_indicator', - namedArgs: { - 'current': (controller.currentIndex.value + 1).toString(), - 'total': controller.quizData.questionListings.length.toString(), - }, - ), - style: const TextStyle( - fontSize: 16, - color: Colors.grey, - fontWeight: FontWeight.w500, + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Container( + key: ValueKey(controller.currentIndex.value), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: AppColors.primaryBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + context.tr( + 'question_indicator', + namedArgs: { + 'current': (controller.currentIndex.value + 1).toString(), + 'total': controller.quizData.questionListings.length.toString(), + }, + ), + style: TextStyle( + fontSize: 14, + color: AppColors.primaryBlue, + fontWeight: FontWeight.bold, + ), + ), ), ); } Widget _buildQuestionText() { - return Text( - controller.currentQuestion.question, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + return SlideTransition( + position: Tween( + begin: const Offset(0.3, 0), + end: Offset.zero, + ).animate(animation), + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: Container( + key: ValueKey(controller.currentQuestion.question), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Text( + controller.currentQuestion.question, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + height: 1.4, + ), + ), ), ); } @@ -111,56 +309,186 @@ class QuizPlayView extends GetView { final question = controller.currentQuestion; if (question is OptionQuestion) { - return Column( - children: List.generate(question.options.length, (index) { - final option = question.options[index]; - final isSelected = controller.idxOptionSelected.value == index; - - return Container( - margin: const EdgeInsets.only(bottom: 12), - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: isSelected ? AppColors.primaryBlue : Colors.white, - foregroundColor: isSelected ? Colors.white : Colors.black, - side: const BorderSide(color: Colors.grey), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - onPressed: () => controller.selectAnswerOption(index), - child: Text(option), - ), + return AnimatedList( + initialItemCount: question.options.length, + itemBuilder: (context, index, animation) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Interval(index * 0.1, 1.0, curve: Curves.easeOut), + )), + child: _buildOptionButton(question.options[index], index), ); - }), + }, ); } else if (question.type == 'true_false') { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + return Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildTrueFalseButton(context.tr('yes'), true, controller.choosenAnswerTOF), - _buildTrueFalseButton(context.tr('no'), false, controller.choosenAnswerTOF), + Row( + children: [ + Expanded( + child: _buildTrueFalseButton( + context.tr('yes'), + true, + controller.choosenAnswerTOF, + Icons.check_circle, + Colors.green, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildTrueFalseButton( + context.tr('no'), + false, + controller.choosenAnswerTOF, + Icons.cancel, + Colors.red, + ), + ), + ], + ), ], ); } else { - return GlobalTextField(controller: controller.answerTextController); + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: GlobalTextField(controller: controller.answerTextController), + ); } } - Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer) { + Widget _buildOptionButton(String option, int index) { + return Obx(() { + final isSelected = controller.idxOptionSelected.value == index; + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 12), + width: double.infinity, + child: Material( + elevation: isSelected ? 8 : 2, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + controller.selectAnswerOption(index); + // Add haptic feedback + // HapticFeedback.lightImpact(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryBlue : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected ? AppColors.primaryBlue : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + ), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? Colors.white : Colors.transparent, + border: Border.all( + color: isSelected ? Colors.white : Colors.grey, + width: 2, + ), + ), + child: isSelected ? const Icon(Icons.check, size: 16, color: AppColors.primaryBlue) : null, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + option, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: isSelected ? Colors.white : Colors.black87, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }); + } + + Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer, IconData icon, Color color) { return Obx(() { final isSelected = (choosenAnswer.value == (value ? 1 : 2)); - return ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: isSelected ? (value ? Colors.green[100] : Colors.red[100]) : Colors.white, - foregroundColor: Colors.black, - side: const BorderSide(color: Colors.grey), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 120, + child: Material( + elevation: isSelected ? 8 : 2, + borderRadius: BorderRadius.circular(20), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => controller.onChooseTOF(value), + child: Container( + decoration: BoxDecoration( + color: isSelected ? color.withOpacity(0.1) : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? color : Colors.grey.shade300, + width: isSelected ? 3 : 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? color : Colors.grey.shade100, + ), + child: Icon( + icon, + size: 32, + color: isSelected ? Colors.white : Colors.grey, + ), + ), + const SizedBox(height: 12), + Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isSelected ? color : Colors.black87, + ), + ), + ], + ), + ), + ), ), - onPressed: () => controller.onChooseTOF(value), - icon: Icon(value ? Icons.check_circle_outline : Icons.cancel_outlined), - label: Text(label), ); }); } @@ -169,17 +497,50 @@ class QuizPlayView extends GetView { return Obx(() { final isEnabled = controller.isAnswerSelected.value; - return ElevatedButton( - onPressed: isEnabled ? controller.nextQuestion : null, - style: ElevatedButton.styleFrom( - backgroundColor: isEnabled ? const Color(0xFF2563EB) : Colors.grey, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - child: Text( - context.tr('next'), - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.only(top: 20), + child: Material( + elevation: isEnabled ? 6 : 2, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: isEnabled ? controller.nextQuestion : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: double.infinity, + height: 56, + decoration: BoxDecoration( + gradient: isEnabled + ? LinearGradient( + colors: [AppColors.primaryBlue, AppColors.primaryBlue.withOpacity(0.8)], + ) + : null, + color: !isEnabled ? Colors.grey.shade300 : null, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + context.tr('next'), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isEnabled ? Colors.white : Colors.grey, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.arrow_forward, + color: isEnabled ? Colors.white : Colors.grey, + ), + ], + ), + ), + ), + ), ), ); });