feat: implement pretest review feature and media player optimization
This commit is contained in:
parent
210c40812b
commit
64454f596e
2
lib/core/services/constants.dart
Normal file
2
lib/core/services/constants.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
const String baseUrl =
|
||||
'https://3be6-2001-448a-50a0-58e3-1c9f-405a-b360-22ed.ngrok-free.app/';
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
|
||||
class DioClient {
|
||||
final Dio _dio = Dio();
|
||||
|
|
@ -393,7 +393,7 @@ class DioClient {
|
|||
Future<Response> getStudentAnswers(String stdLearningId, String token) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'/studentAnswer/$stdLearningId',
|
||||
'/studentAnswers/$stdLearningId',
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
const String baseUrl =
|
||||
'https://70e7-2001-448a-50a0-2604-b558-2a22-54f6-65ce.ngrok-free.app/';
|
||||
|
|
@ -25,7 +25,7 @@ class ExerciseRepository {
|
|||
try {
|
||||
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
|
||||
if (response.statusCode == 200) {
|
||||
return response.data['data'];
|
||||
return response.data;
|
||||
} else {
|
||||
throw Exception('Failed to load student answers');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,20 +32,4 @@ class LevelRepository {
|
|||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getStudentAnswers(
|
||||
String stdLearningId, String token) async {
|
||||
try {
|
||||
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return response.data['data'];
|
||||
} else {
|
||||
throw Exception(
|
||||
'Failed to fetch student answers: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch student answers: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class GlobalButton extends StatelessWidget {
|
|||
final Color? borderColor;
|
||||
final double borderWidth;
|
||||
final bool transparentBackground;
|
||||
final bool isLoading;
|
||||
|
||||
const GlobalButton({
|
||||
super.key,
|
||||
|
|
@ -31,6 +32,7 @@ class GlobalButton extends StatelessWidget {
|
|||
this.borderColor,
|
||||
this.borderWidth = 1.0,
|
||||
this.transparentBackground = false,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -61,49 +63,66 @@ class GlobalButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: spaceBetween && icon != null
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: textColor ?? Colors.white,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
if (icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: textColor ?? Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: isLoading ? _buildLoadingIndicator() : _buildButtonContent(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingIndicator() {
|
||||
return SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(textColor ?? Colors.white),
|
||||
strokeWidth: 2.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButtonContent() {
|
||||
if (spaceBetween && icon != null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: textColor ?? Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
if (icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: textColor ?? Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ class UserProvider with ChangeNotifier {
|
|||
String? _jwtToken;
|
||||
Map<String, dynamic>? _userData;
|
||||
File? _selectedImage;
|
||||
bool _isLoading = false;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoggedIn => _isLoggedIn;
|
||||
String? get jwtToken => _jwtToken;
|
||||
Map<String, dynamic>? get userData => _userData;
|
||||
|
|
@ -19,6 +21,11 @@ class UserProvider with ChangeNotifier {
|
|||
_loadLoginStatus();
|
||||
}
|
||||
|
||||
void setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadLoginStatus() async {
|
||||
try {
|
||||
_jwtToken = await _userRepository.getToken();
|
||||
|
|
@ -60,6 +67,7 @@ class UserProvider with ChangeNotifier {
|
|||
}
|
||||
|
||||
Future<bool> login({required String email, required String password}) async {
|
||||
setLoading(true);
|
||||
try {
|
||||
final response = await _userRepository.loginUser({
|
||||
'EMAIL': email,
|
||||
|
|
@ -84,6 +92,8 @@ class UserProvider with ChangeNotifier {
|
|||
} catch (e) {
|
||||
print('Login error: $e');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -128,53 +128,58 @@ class SigninScreen extends StatelessWidget {
|
|||
const SizedBox(height: 24),
|
||||
GlobalButton(
|
||||
text: 'Login',
|
||||
onPressed: () async {
|
||||
// Validate email and password fields
|
||||
validatorProvider.validateField(
|
||||
'email',
|
||||
_emailController.text,
|
||||
validator: validatorProvider.emailValidator,
|
||||
);
|
||||
validatorProvider.validateField(
|
||||
'password',
|
||||
_passwordController.text,
|
||||
validator: validatorProvider.passwordValidator,
|
||||
);
|
||||
isLoading: userProvider.isLoading,
|
||||
onPressed: userProvider.isLoading
|
||||
? null
|
||||
: () async {
|
||||
// Validate email and password fields
|
||||
validatorProvider.validateField(
|
||||
'email',
|
||||
_emailController.text,
|
||||
validator: validatorProvider.emailValidator,
|
||||
);
|
||||
validatorProvider.validateField(
|
||||
'password',
|
||||
_passwordController.text,
|
||||
validator: validatorProvider.passwordValidator,
|
||||
);
|
||||
|
||||
// If no errors, proceed with login
|
||||
if (validatorProvider.getError('email') == null &&
|
||||
validatorProvider.getError('password') == null) {
|
||||
final isSuccess = await userProvider.login(
|
||||
email: _emailController.text,
|
||||
password: _passwordController.text,
|
||||
);
|
||||
// If no errors, proceed with login
|
||||
if (validatorProvider.getError('email') == null &&
|
||||
validatorProvider.getError('password') ==
|
||||
null) {
|
||||
final isSuccess = await userProvider.login(
|
||||
email: _emailController.text,
|
||||
password: _passwordController.text,
|
||||
);
|
||||
|
||||
if (isSuccess) {
|
||||
// Navigate to HomeScreen after successful login
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const HomeScreen()),
|
||||
(Route<dynamic> route) =>
|
||||
false, // Remove all previous routes
|
||||
).then((_) {
|
||||
// Reset the fields after login
|
||||
validatorProvider.resetFields();
|
||||
_emailController.clear();
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else {
|
||||
// Show error message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Login failed, please check your credentials'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
if (isSuccess) {
|
||||
// Navigate to HomeScreen after successful login
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const HomeScreen()),
|
||||
(Route<dynamic> route) =>
|
||||
false, // Remove all previous routes
|
||||
).then((_) {
|
||||
// Reset the fields after login
|
||||
validatorProvider.resetFields();
|
||||
_emailController.clear();
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else {
|
||||
// Show error message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Login failed, please check your credentials'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
|
|
@ -187,6 +192,7 @@ class SigninScreen extends StatelessWidget {
|
|||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<ValidatorProvider>().resetFields();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
|
|||
|
|
@ -227,13 +227,12 @@ class SignupScreen extends StatelessWidget {
|
|||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<ValidatorProvider>().resetFields();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SigninScreen()),
|
||||
).then((_) {
|
||||
context.read<ValidatorProvider>().resetFields();
|
||||
});
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
' Login Here',
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class HistoryProvider with ChangeNotifier {
|
|||
_error = null;
|
||||
} catch (e) {
|
||||
_error = 'Error fetching learning history: ${e.toString()}';
|
||||
_learningHistory = [];
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
|
@ -91,10 +92,23 @@ class HistoryProvider with ChangeNotifier {
|
|||
}
|
||||
|
||||
Future<void> refreshData(String token) async {
|
||||
if (_sectionProvider.sections.isEmpty) {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _sectionProvider.fetchSections(token);
|
||||
if (_sectionProvider.sections.isNotEmpty) {
|
||||
await fetchLearningHistory(token);
|
||||
} else {
|
||||
_error = 'No sections available';
|
||||
}
|
||||
} catch (e) {
|
||||
_error = 'Error refreshing data: ${e.toString()}';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
await fetchLearningHistory(token);
|
||||
}
|
||||
|
||||
Future<void> loadHistoryData(String token) async {
|
||||
|
|
@ -103,29 +117,52 @@ class HistoryProvider with ChangeNotifier {
|
|||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Fetch sections and learning history in parallel
|
||||
final sectionsResult = _sectionProvider.fetchSections(token);
|
||||
|
||||
// Use the first section ID for initial history fetch
|
||||
final firstSectionId = await sectionsResult
|
||||
.then((sections) => sections.isNotEmpty ? sections.first.id : null);
|
||||
|
||||
if (firstSectionId != null) {
|
||||
final historyResult =
|
||||
_repository.getLearningHistory(firstSectionId, token);
|
||||
|
||||
// Wait for both futures to complete
|
||||
final results = await Future.wait([sectionsResult, historyResult]);
|
||||
|
||||
_learningHistory = results[1] as List<LearningHistory>;
|
||||
await _sectionProvider.fetchSections(token);
|
||||
if (_sectionProvider.sections.isNotEmpty) {
|
||||
String firstSectionId = _sectionProvider.sections.first.id;
|
||||
_learningHistory =
|
||||
await _repository.getLearningHistory(firstSectionId, token);
|
||||
} else {
|
||||
_error = 'No sections available';
|
||||
}
|
||||
} catch (e) {
|
||||
_error = 'Error loading data: ${e.toString()}';
|
||||
_learningHistory = []; // Clear the list in case of error
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Future<void> loadHistoryData(String token) async {
|
||||
// _isLoading = true;
|
||||
// _error = null;
|
||||
// notifyListeners();
|
||||
|
||||
// try {
|
||||
// // Fetch sections and learning history in parallel
|
||||
// final sectionsResult = _sectionProvider.fetchSections(token);
|
||||
|
||||
// // Use the first section ID for initial history fetch
|
||||
// final firstSectionId = await sectionsResult
|
||||
// .then((sections) => sections.isNotEmpty ? sections.first.id : null);
|
||||
|
||||
// if (firstSectionId != null) {
|
||||
// final historyResult =
|
||||
// _repository.getLearningHistory(firstSectionId, token);
|
||||
|
||||
// // Wait for both futures to complete
|
||||
// final results = await Future.wait([sectionsResult, historyResult]);
|
||||
|
||||
// _learningHistory = results[1] as List<LearningHistory>;
|
||||
// } else {
|
||||
// _error = 'No sections available';
|
||||
// }
|
||||
// } catch (e) {
|
||||
// _error = 'Error loading data: ${e.toString()}';
|
||||
// } finally {
|
||||
// _isLoading = false;
|
||||
// notifyListeners();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,76 +10,50 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HistoryScreen extends StatefulWidget {
|
||||
const HistoryScreen({
|
||||
super.key,
|
||||
});
|
||||
@override
|
||||
State<HistoryScreen> createState() => _HistoryScreenState();
|
||||
}
|
||||
|
||||
class _HistoryScreenState extends State<HistoryScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final historyProvider =
|
||||
Provider.of<HistoryProvider>(context, listen: false);
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
historyProvider.fetchLearningHistory(userProvider.jwtToken!);
|
||||
});
|
||||
}
|
||||
class HistoryScreen extends StatelessWidget {
|
||||
const HistoryScreen({super.key});
|
||||
|
||||
bool isNotFoundError(String error) {
|
||||
return error.toLowerCase().contains('no learning history found') ||
|
||||
error.toLowerCase().contains('not found');
|
||||
}
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// final historyProvider =
|
||||
// Provider.of<HistoryProvider>(context, listen: false);
|
||||
// final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
|
||||
// historyProvider.refreshData(userProvider.jwtToken!);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<HistoryProvider>(
|
||||
builder: (context, historyProvider, chiild) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgSoftColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 30.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 8),
|
||||
const CustomTabBar(),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: _buildContent(historyProvider),
|
||||
),
|
||||
],
|
||||
builder: (context, historyProvider, child) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgSoftColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 30.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 8),
|
||||
const CustomTabBar(),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: _buildContent(context, historyProvider),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(HistoryProvider historyProvider) {
|
||||
Widget _buildContent(BuildContext context, HistoryProvider historyProvider) {
|
||||
print("isLoading: ${historyProvider.isLoading}");
|
||||
print("error: ${historyProvider.error}");
|
||||
print("historyLength: ${historyProvider.learningHistory.length}");
|
||||
if (historyProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
|
@ -87,7 +61,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
if (historyProvider.error != null) {
|
||||
return isNotFoundError(historyProvider.error!)
|
||||
? _buildEmptyState(context)
|
||||
: _buildErrorState(historyProvider.error!);
|
||||
: _buildErrorState(context, historyProvider.error!);
|
||||
}
|
||||
|
||||
if (historyProvider.learningHistory.isEmpty) {
|
||||
|
|
@ -145,7 +119,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(String error) {
|
||||
Widget _buildErrorState(BuildContext context, String error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -206,6 +206,22 @@ class _HomeContentState extends State<HomeContent> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildCompletedTopicsContent(CompletedTopicsProvider provider) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
itemCount: 1,
|
||||
itemBuilder: (context, index) {
|
||||
return ProgressCard(
|
||||
completedTopic: provider.completedTopics,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
|
||||
|
|
@ -403,20 +419,27 @@ class _HomeContentState extends State<HomeContent> {
|
|||
? _buildShimmerEffect()
|
||||
: completedTopicsProvider.completedTopics.isEmpty
|
||||
? _buildNoDataWidget()
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
itemCount: 1,
|
||||
itemBuilder: (context, index) {
|
||||
return ProgressCard(
|
||||
completedTopic: completedTopicsProvider
|
||||
.completedTopics, // Kirim seluruh list
|
||||
);
|
||||
},
|
||||
),
|
||||
: _buildCompletedTopicsContent(
|
||||
completedTopicsProvider),
|
||||
|
||||
// completedTopicsProvider.isLoading
|
||||
// ? _buildShimmerEffect()
|
||||
// : completedTopicsProvider.completedTopics.isEmpty
|
||||
// ? _buildNoDataWidget()
|
||||
// : ListView.builder(
|
||||
// shrinkWrap: true,
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16.0,
|
||||
// ),
|
||||
// itemCount: 1,
|
||||
// itemBuilder: (context, index) {
|
||||
// return ProgressCard(
|
||||
// completedTopic: completedTopicsProvider
|
||||
// .completedTopics, // Kirim seluruh list
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
||||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/features/home/models/completed_topics_model.dart';
|
||||
import 'package:english_learning/features/home/widgets/progress_bar.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart';
|
||||
|
||||
class ExerciseModel {
|
||||
final String idAdminExercise;
|
||||
final String idLevel;
|
||||
|
|
@ -61,21 +63,6 @@ class ExerciseModel {
|
|||
choices: choices,
|
||||
);
|
||||
}
|
||||
|
||||
factory ExerciseModel.fromReviewJson(Map<String, dynamic> json) {
|
||||
final exerciseDetails = json['exerciseDetails'];
|
||||
return ExerciseModel(
|
||||
idAdminExercise: exerciseDetails['ID_ADMIN_EXERCISE'],
|
||||
idLevel: json['ID_LEVEL'] ?? '', // This might be available in parent data
|
||||
title: exerciseDetails['TITLE'],
|
||||
question: exerciseDetails['QUESTION'],
|
||||
questionType: exerciseDetails['QUESTION_TYPE'],
|
||||
timeAdminExc: DateTime.parse(json['TIME_STUDENT_EXC']),
|
||||
answerStudent: json['ANSWER_STUDENT'],
|
||||
isCorrect: json['IS_CORRECT'] == 1,
|
||||
choices: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleChoice {
|
||||
|
|
@ -138,6 +125,13 @@ class Pair {
|
|||
final String right;
|
||||
|
||||
Pair({required this.left, required this.right});
|
||||
|
||||
factory Pair.fromJson(Map<String, dynamic> json) {
|
||||
return Pair(
|
||||
left: json['LEFT_PAIR'] ?? '',
|
||||
right: json['RIGHT_PAIR'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MatchingPair {
|
||||
|
|
@ -147,17 +141,24 @@ class MatchingPair {
|
|||
|
||||
factory MatchingPair.fromJsonList(List? jsonList) {
|
||||
if (jsonList == null) {
|
||||
return MatchingPair(
|
||||
pairs: []); // Return an empty list if jsonList is null
|
||||
return MatchingPair(pairs: []);
|
||||
}
|
||||
|
||||
List<Pair> pairs = [];
|
||||
for (var pair in jsonList) {
|
||||
pairs.add(Pair(
|
||||
left: pair['LEFT_PAIR'] ?? '', // Use empty string as default if null
|
||||
right: pair['RIGHT_PAIR'] ?? '',
|
||||
));
|
||||
}
|
||||
List<Pair> pairs = jsonList.map((item) => Pair.fromJson(item)).toList();
|
||||
return MatchingPair(pairs: pairs);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
List<String> get leftPairs => pairs.map((pair) => pair.left).toList();
|
||||
List<String> get rightPairs => pairs.map((pair) => pair.right).toList();
|
||||
}
|
||||
|
||||
extension ReviewMatchingPairListExtension on List<ReviewMatchingPair> {
|
||||
MatchingPair toMatchingPair() {
|
||||
List<Pair> pairs = map((review) => Pair(
|
||||
left: review.leftPair,
|
||||
right: review.rightPair,
|
||||
)).toList();
|
||||
return MatchingPair(pairs: pairs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
class ReviewExerciseModel {
|
||||
final String idStudentLearning;
|
||||
final String idLevel;
|
||||
final String nameSection;
|
||||
final String nameTopic;
|
||||
final String nameLevel;
|
||||
final int score;
|
||||
final bool isPass;
|
||||
final List<ReviewExerciseDetail> stdExercises;
|
||||
|
||||
ReviewExerciseModel({
|
||||
required this.idStudentLearning,
|
||||
required this.idLevel,
|
||||
required this.nameSection,
|
||||
required this.nameTopic,
|
||||
required this.nameLevel,
|
||||
required this.score,
|
||||
required this.isPass,
|
||||
required this.stdExercises,
|
||||
});
|
||||
|
||||
factory ReviewExerciseModel.fromJson(Map<String, dynamic> json) {
|
||||
return ReviewExerciseModel(
|
||||
idStudentLearning: json['ID_STUDENT_LEARNING'],
|
||||
idLevel: json['ID_LEVEL'],
|
||||
nameSection: json['NAME_SECTION'],
|
||||
nameTopic: json['NAME_TOPIC'],
|
||||
nameLevel: json['NAME_LEVEL'],
|
||||
score: json['SCORE'],
|
||||
isPass: json['IS_PASS'] == 1,
|
||||
stdExercises: (json['stdExercises'] as List)
|
||||
.map((e) => ReviewExerciseDetail.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReviewExerciseDetail {
|
||||
final String idStudentExercise;
|
||||
final String idAdminExercise;
|
||||
final String title;
|
||||
final String question;
|
||||
final String questionType;
|
||||
final int scoreWeight;
|
||||
final String? image;
|
||||
final String? video;
|
||||
final String? audio;
|
||||
|
||||
final String answerStudent;
|
||||
final int isCorrect;
|
||||
final double resultScoreStudent;
|
||||
final List<ReviewMultipleChoice>? multipleChoices;
|
||||
final List<ReviewMatchingPair>? matchingPairs;
|
||||
|
||||
ReviewExerciseDetail({
|
||||
required this.idStudentExercise,
|
||||
required this.idAdminExercise,
|
||||
required this.title,
|
||||
required this.question,
|
||||
required this.questionType,
|
||||
this.image,
|
||||
this.video,
|
||||
this.audio,
|
||||
required this.scoreWeight,
|
||||
required this.answerStudent,
|
||||
required this.isCorrect,
|
||||
required this.resultScoreStudent,
|
||||
this.multipleChoices,
|
||||
this.matchingPairs,
|
||||
});
|
||||
|
||||
factory ReviewExerciseDetail.fromJson(Map<String, dynamic> json) {
|
||||
return ReviewExerciseDetail(
|
||||
idStudentExercise: json['ID_STUDENT_EXERCISE'] ?? '',
|
||||
idAdminExercise: json['ID_ADMIN_EXERCISE'] ?? '',
|
||||
title: json['TITLE'] ?? '',
|
||||
question: json['QUESTION'] ?? '',
|
||||
questionType: json['QUESTION_TYPE'] ?? '',
|
||||
image: json['IMAGE'],
|
||||
video: json['VIDEO'],
|
||||
audio: json['AUDIO'],
|
||||
scoreWeight: json['SCORE_WEIGHT'] ?? 0,
|
||||
answerStudent: json['ANSWER_STUDENT'] ?? '',
|
||||
isCorrect: json['IS_CORRECT'] ?? 0,
|
||||
resultScoreStudent:
|
||||
double.tryParse(json['RESULT_SCORE_STUDENT'].toString()) ?? 0.0,
|
||||
multipleChoices: json['multipleChoices'] != null
|
||||
? (json['multipleChoices'] as List)
|
||||
.map((e) => ReviewMultipleChoice.fromJson(e))
|
||||
.toList()
|
||||
: null,
|
||||
matchingPairs: json['matchingPairs'] != null
|
||||
? (json['matchingPairs'] as List)
|
||||
.map((e) => ReviewMatchingPair.fromJson(e))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
List<ReviewMatchingPair> get matchingPairsOrEmpty {
|
||||
return matchingPairs ?? [];
|
||||
}
|
||||
|
||||
List<ReviewMultipleChoice> get multipleChoicesOrEmpty {
|
||||
return multipleChoices ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
class ReviewMultipleChoice {
|
||||
final String optionA;
|
||||
final String optionB;
|
||||
final String optionC;
|
||||
final String optionD;
|
||||
final String optionE;
|
||||
|
||||
ReviewMultipleChoice({
|
||||
required this.optionA,
|
||||
required this.optionB,
|
||||
required this.optionC,
|
||||
required this.optionD,
|
||||
required this.optionE,
|
||||
});
|
||||
|
||||
factory ReviewMultipleChoice.fromJson(Map<String, dynamic> json) {
|
||||
return ReviewMultipleChoice(
|
||||
optionA: json['OPTION_A'] ?? '',
|
||||
optionB: json['OPTION_B'] ?? '',
|
||||
optionC: json['OPTION_C'] ?? '',
|
||||
optionD: json['OPTION_D'] ?? '',
|
||||
optionE: json['OPTION_E'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReviewMatchingPair {
|
||||
final String leftPair;
|
||||
final String rightPair;
|
||||
|
||||
ReviewMatchingPair({
|
||||
required this.leftPair,
|
||||
required this.rightPair,
|
||||
});
|
||||
|
||||
factory ReviewMatchingPair.fromJson(Map<String, dynamic> json) {
|
||||
return ReviewMatchingPair(
|
||||
leftPair: json['LEFT_PAIR'],
|
||||
rightPair: json['RIGHT_PAIR'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart';
|
||||
import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
|
||||
|
|
@ -14,6 +15,8 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
|
||||
// State variables
|
||||
List<ExerciseModel> _exercises = [];
|
||||
List<ReviewExerciseDetail> _reviewExercises = [];
|
||||
ReviewExerciseModel? _reviewData;
|
||||
Map<int, List<Map<String, String>>> _matchingAnswers = {};
|
||||
List<String> _answers = [];
|
||||
List<List<Color>> _leftColors = [];
|
||||
|
|
@ -24,7 +27,6 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
String _nameLevel = '';
|
||||
String? _activeLeftOption;
|
||||
String? _studentLearningId;
|
||||
bool _isReview = false;
|
||||
|
||||
// Constants
|
||||
final List<Color> _pairColors = [
|
||||
|
|
@ -44,7 +46,7 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
String get nameLevel => _nameLevel;
|
||||
String? get activeLeftOption => _activeLeftOption;
|
||||
String? get studentLearningId => _studentLearningId;
|
||||
bool get isReview => _isReview;
|
||||
List<ReviewExerciseDetail> get reviewExercises => _reviewExercises;
|
||||
|
||||
// Initialization methods
|
||||
void initializeAnswers() {
|
||||
|
|
@ -79,7 +81,6 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
// }
|
||||
// }
|
||||
void answerQuestion(int index, String answer) {
|
||||
if (_isReview) return;
|
||||
if (index >= 0 && index < _answers.length) {
|
||||
if (_exercises[index].choices is MatchingPair) {
|
||||
_handleMatchingPairAnswer(index, answer);
|
||||
|
|
@ -208,20 +209,30 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
void goToExercise(int index) {
|
||||
if (index >= 0 && index < _exercises.length) {
|
||||
_currentExerciseIndex = index;
|
||||
print('Going to exercise: $_currentExerciseIndex'); // Debug print
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void nextQuestion() {
|
||||
if (_currentExerciseIndex < _answers.length - 1) {
|
||||
print('Current index before next: $_currentExerciseIndex'); // Debug print
|
||||
if (_currentExerciseIndex <
|
||||
(_exercises.isNotEmpty
|
||||
? _exercises.length - 1
|
||||
: _reviewExercises.length - 1)) {
|
||||
_currentExerciseIndex++;
|
||||
print('Moving to next question: $_currentExerciseIndex'); // Debug print
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void previousQuestion() {
|
||||
print(
|
||||
'Current index before previous: $_currentExerciseIndex'); // Debug print
|
||||
if (_currentExerciseIndex > 0) {
|
||||
_currentExerciseIndex--;
|
||||
print(
|
||||
'Moving to previous question: $_currentExerciseIndex'); // Debug print
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
@ -245,6 +256,7 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
// API methods
|
||||
Future<void> fetchExercises(String levelId) async {
|
||||
_isLoading = true;
|
||||
_currentExerciseIndex = 0;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
|
|
@ -270,33 +282,93 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
// Di dalam ExerciseProvider class
|
||||
Future<void> fetchReviewExercises(String stdLearningId) async {
|
||||
_isLoading = true;
|
||||
_isReview = true;
|
||||
_currentExerciseIndex = 0;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final token = await _userProvider.getValidToken();
|
||||
if (token == null) {
|
||||
throw Exception('No valid token found');
|
||||
}
|
||||
if (token == null) throw Exception('No valid token found');
|
||||
|
||||
final data = await _repository.getStudentAnswers(stdLearningId, token);
|
||||
_nameTopic = data['NAME_TOPIC'];
|
||||
_nameLevel = data['NAME_LEVEL'];
|
||||
final exercisesData = data['stdExercises'];
|
||||
_exercises = exercisesData
|
||||
.map<ExerciseModel>((json) => ExerciseModel.fromReviewJson(json))
|
||||
.toList();
|
||||
_answers = _exercises.map((e) => e.answerStudent ?? '').toList();
|
||||
final response =
|
||||
await _repository.getStudentAnswers(stdLearningId, token);
|
||||
|
||||
// Parse the response data
|
||||
final payload = response['payload'];
|
||||
_reviewData = ReviewExerciseModel.fromJson(payload);
|
||||
|
||||
// Sort the exercises based on the title number
|
||||
_reviewExercises = (_reviewData?.stdExercises ?? [])
|
||||
..sort((a, b) {
|
||||
// Extract numbers from titles (e.g., "Soal 1" -> 1)
|
||||
int aNumber = int.tryParse(a.title.split(' ').last) ?? 0;
|
||||
int bNumber = int.tryParse(b.title.split(' ').last) ?? 0;
|
||||
return aNumber.compareTo(bNumber);
|
||||
});
|
||||
|
||||
_nameTopic = _reviewData?.nameTopic ?? '';
|
||||
_nameLevel = _reviewData?.nameLevel ?? '';
|
||||
|
||||
// Reset current index
|
||||
_currentExerciseIndex = 0;
|
||||
} catch (e) {
|
||||
print('Error fetching review exercises: $e');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
dynamic get currentExercise {
|
||||
if (_reviewExercises.isNotEmpty) {
|
||||
return _reviewExercises[_currentExerciseIndex];
|
||||
} else if (_exercises.isNotEmpty) {
|
||||
return _exercises[_currentExerciseIndex];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String getAnswerFormatted(ReviewExerciseDetail exercise) {
|
||||
switch (exercise.questionType) {
|
||||
case 'MCQ':
|
||||
if (exercise.multipleChoices != null) {
|
||||
final choice = exercise.answerStudent;
|
||||
final choices = exercise.multipleChoices!.first;
|
||||
switch (choice) {
|
||||
case 'A':
|
||||
return choices.optionA;
|
||||
case 'B':
|
||||
return choices.optionB;
|
||||
case 'C':
|
||||
return choices.optionC;
|
||||
case 'D':
|
||||
return choices.optionD;
|
||||
case 'E':
|
||||
return choices.optionE;
|
||||
default:
|
||||
return exercise.answerStudent;
|
||||
}
|
||||
}
|
||||
return exercise.answerStudent;
|
||||
case 'TFQ':
|
||||
return exercise.answerStudent == '1' ? 'True' : 'False';
|
||||
case 'MPQ':
|
||||
if (exercise.answerStudent.isEmpty) return '';
|
||||
return exercise.answerStudent.split(', ').map((pair) {
|
||||
final parts = pair.split('-');
|
||||
if (parts.length == 2) {
|
||||
return '${parts[0]} ➜ ${parts[1]}';
|
||||
}
|
||||
return pair;
|
||||
}).join('\n');
|
||||
default:
|
||||
return exercise.answerStudent;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
|
||||
print('submitAnswersAndGetScore called');
|
||||
try {
|
||||
|
|
@ -398,6 +470,7 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
_nameTopic = previous._nameTopic;
|
||||
_nameLevel = previous._nameLevel;
|
||||
_studentLearningId = previous._studentLearningId;
|
||||
_reviewData = previous._reviewData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_content.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/content/exercise_content.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_navigator.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_progress.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/instruction_dialog.dart';
|
||||
|
|
@ -31,6 +32,7 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
|||
_scrollController = ScrollController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = context.read<ExerciseProvider>();
|
||||
|
||||
if (widget.isReview) {
|
||||
provider.fetchReviewExercises(widget.studentLearningId);
|
||||
} else {
|
||||
|
|
@ -62,11 +64,32 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
|||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final currentExercise = provider.currentExercise;
|
||||
final hasExercises = widget.isReview
|
||||
? provider.reviewExercises.isNotEmpty
|
||||
: provider.exercises.isNotEmpty;
|
||||
|
||||
if (!hasExercises || currentExercise == null) {
|
||||
return const Scaffold(
|
||||
body: Center(child: Text('No exercises available')),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgSoftColor,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: widget.isReview
|
||||
? IconButton(
|
||||
icon: const Icon(
|
||||
BootstrapIcons.arrow_left,
|
||||
color: AppColors.whiteColor,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
)
|
||||
: null, // Show back button only in review mode
|
||||
iconTheme: const IconThemeData(color: AppColors.whiteColor),
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
|
|
@ -81,38 +104,47 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
|||
gradient: AppColors.gradientTheme,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline, color: AppColors.whiteColor),
|
||||
onPressed: () => _showInstructions(context),
|
||||
),
|
||||
],
|
||||
actions: widget.isReview
|
||||
? [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
BootstrapIcons.info_circle,
|
||||
color: AppColors.whiteColor,
|
||||
size: 22,
|
||||
),
|
||||
onPressed: () => _showInstructions(context),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: ExerciseProgress(),
|
||||
),
|
||||
if (!widget.isReview)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: ExerciseProgress(),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => provider.fetchExercises(widget.levelId!),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (provider.isReview)
|
||||
Text('Review Mode', style: TextStyle(fontSize: 24)),
|
||||
const SizedBox(height: 16),
|
||||
const ExerciseContent(),
|
||||
const SizedBox(height: 24),
|
||||
ExerciseNavigator(onScrollToTop: _scrollToTop),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
ExerciseContent(
|
||||
key: ValueKey(provider.currentExerciseIndex),
|
||||
exercise: currentExercise,
|
||||
isReview: widget.isReview,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ExerciseNavigator(
|
||||
onScrollToTop: _scrollToTop,
|
||||
isReview: widget.isReview,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/question/true_false_question.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/see_progress_modal.dart';
|
||||
import 'package:english_learning/features/learning/modules/material/widgets/audio_player_widget.dart';
|
||||
import 'package:english_learning/features/learning/modules/material/widgets/image_widget.dart';
|
||||
import 'package:english_learning/features/learning/modules/material/widgets/video_player_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ExerciseContent extends StatefulWidget {
|
||||
final dynamic exercise;
|
||||
final bool isReview;
|
||||
|
||||
const ExerciseContent({
|
||||
super.key,
|
||||
required this.exercise,
|
||||
this.isReview = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExerciseContent> createState() => _ExerciseContentState();
|
||||
}
|
||||
|
||||
class _ExerciseContentState extends State<ExerciseContent>
|
||||
with WidgetsBindingObserver {
|
||||
final GlobalKey<VideoPlayerWidgetState> _videoPlayerKey = GlobalKey();
|
||||
final GlobalKey<AudioPlayerWidgetState> _audioPlayerKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopAndResetAllMedia();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused) {
|
||||
_stopAndResetAllMedia();
|
||||
}
|
||||
}
|
||||
|
||||
void _stopAndResetAllMedia() {
|
||||
_videoPlayerKey.currentState?.stopAndResetVideo();
|
||||
_audioPlayerKey.currentState?.stopAndResetAudio();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ExerciseProvider>(builder: (context, provider, child) {
|
||||
final exercises =
|
||||
widget.isReview ? provider.reviewExercises : provider.exercises;
|
||||
|
||||
if (exercises.isEmpty) {
|
||||
return const Center(child: Text('No exercises available'));
|
||||
}
|
||||
|
||||
final currentIndex = provider.currentExerciseIndex;
|
||||
final currentExercise = exercises[currentIndex];
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.05, 0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
key: ValueKey(currentIndex),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Question ${currentIndex + 1}',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
if (!widget.isReview)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: AppColors.whiteColor,
|
||||
context: context,
|
||||
builder: (context) => const SeeProgressModal(),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'See Progress',
|
||||
style: AppTextStyles.blueTextStyle.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColors.blueColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.isReview)
|
||||
_buildStatusIndicator(
|
||||
currentExercise as ReviewExerciseDetail),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(
|
||||
color: AppColors.disableColor,
|
||||
thickness: 1,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_getQuestionText(currentExercise),
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (widget.exercise.image != null &&
|
||||
widget.exercise.image!.isNotEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: ImageWidget(
|
||||
imageFileName: widget.exercise.image!,
|
||||
baseUrl: '${baseUrl}uploads/exercise/image/',
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.exercise.audio != null &&
|
||||
widget.exercise.audio!.isNotEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: AudioPlayerWidget(
|
||||
key: _audioPlayerKey,
|
||||
audioFileName: widget.exercise.audio!,
|
||||
baseUrl: '${baseUrl}uploads/exercise/audio/',
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.exercise.video != null &&
|
||||
widget.exercise.video!.isNotEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: VideoPlayerWidget(
|
||||
key: _videoPlayerKey,
|
||||
videoUrl: widget.exercise.video!,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildQuestionWidget(
|
||||
widget.exercise,
|
||||
widget.isReview,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _getQuestionText(dynamic exercise) {
|
||||
if (exercise is ReviewExerciseDetail) {
|
||||
return exercise.question;
|
||||
} else if (exercise is ExerciseModel) {
|
||||
return exercise.question;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Widget _buildQuestionWidget(
|
||||
dynamic exercise,
|
||||
bool isReview,
|
||||
) {
|
||||
String questionType = '';
|
||||
|
||||
if (exercise is ReviewExerciseDetail) {
|
||||
questionType = exercise.questionType;
|
||||
} else if (exercise is ExerciseModel) {
|
||||
questionType = exercise.questionType;
|
||||
}
|
||||
|
||||
switch (questionType) {
|
||||
case 'MCQ':
|
||||
return MultipleChoiceQuestion(
|
||||
exercise: exercise,
|
||||
isReview: isReview,
|
||||
);
|
||||
case 'TFQ':
|
||||
return TrueFalseQuestion(
|
||||
exercise: exercise,
|
||||
isReview: isReview,
|
||||
);
|
||||
case 'MPQ':
|
||||
return MatchingPairsQuestion(
|
||||
exercise: exercise,
|
||||
isReview: isReview,
|
||||
);
|
||||
default:
|
||||
return const Text('Unsupported question type');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicator(ReviewExerciseDetail exercise) {
|
||||
if (exercise.questionType == 'MPQ') {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
exercise.resultScoreStudent > 0
|
||||
? BootstrapIcons.check_circle_fill
|
||||
: BootstrapIcons.x_circle_fill,
|
||||
color: exercise.resultScoreStudent > 0 ? Colors.green : Colors.red,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${exercise.resultScoreStudent}/${exercise.scoreWeight}',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
color:
|
||||
exercise.resultScoreStudent > 0 ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
exercise.isCorrect == 1
|
||||
? BootstrapIcons.check_circle_fill
|
||||
: BootstrapIcons.x_circle_fill,
|
||||
color: exercise.isCorrect == 1 ? Colors.green : Colors.red,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
exercise.isCorrect == 1 ? 'Correct' : 'Incorrect',
|
||||
style: TextStyle(
|
||||
color: exercise.isCorrect == 1 ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/question/true_false_question.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/see_progress_modal.dart';
|
||||
import 'package:english_learning/features/learning/modules/material/widgets/image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ExerciseContent extends StatelessWidget {
|
||||
const ExerciseContent({super.key});
|
||||
|
||||
Widget _buildQuestionWidget(
|
||||
ExerciseModel exercise,
|
||||
ExerciseProvider provider,
|
||||
) {
|
||||
switch (exercise.questionType) {
|
||||
case 'MCQ':
|
||||
return MultipleChoiceQuestion(
|
||||
exercise: exercise,
|
||||
);
|
||||
case 'TFQ':
|
||||
return TrueFalseQuestion(
|
||||
exercise: exercise,
|
||||
);
|
||||
case 'MPQ':
|
||||
return MatchingPairsQuestion(
|
||||
exercise: exercise,
|
||||
);
|
||||
default:
|
||||
return const Text('Unsupported question type');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ExerciseProvider>(builder: (context, provider, child) {
|
||||
if (provider.exercises.isEmpty) {
|
||||
return const Center(child: Text('No exercises available'));
|
||||
}
|
||||
final exercise = provider.exercises[provider.currentExerciseIndex];
|
||||
|
||||
return TweenAnimationBuilder(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
builder: (context, double value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(50 * (1 - value), 0),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
key: ValueKey(provider.currentExerciseIndex),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Question ${provider.currentExerciseIndex + 1}',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: AppColors.whiteColor,
|
||||
context: context,
|
||||
builder: (context) => const SeeProgressModal(),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'See Progress',
|
||||
style: AppTextStyles.blueTextStyle.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColors.blueColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(
|
||||
color: AppColors.disableColor,
|
||||
thickness: 1,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
exercise.question,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (exercise.image != null && exercise.image!.isNotEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: ImageWidget(
|
||||
imageFileName: exercise.image!,
|
||||
baseUrl: '${baseUrl}uploads/exercise/image/',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildQuestionWidget(
|
||||
exercise,
|
||||
provider,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/core/widgets/global_button.dart';
|
||||
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/complete_submission.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/widgets/incomplete_submission.dart';
|
||||
|
|
@ -9,184 +10,145 @@ import 'package:provider/provider.dart';
|
|||
|
||||
class ExerciseNavigator extends StatelessWidget {
|
||||
final VoidCallback? onScrollToTop;
|
||||
final bool isReview;
|
||||
|
||||
const ExerciseNavigator({
|
||||
super.key,
|
||||
required this.onScrollToTop,
|
||||
this.isReview = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer2<ExerciseProvider, UserProvider>(
|
||||
builder: (context, exerciseProvider, userProvider, _) {
|
||||
final currentExerciseIndex = exerciseProvider.currentExerciseIndex;
|
||||
final isFirstExercise = currentExerciseIndex == 0;
|
||||
final isLastExercise =
|
||||
currentExerciseIndex == exerciseProvider.exercises.length - 1;
|
||||
builder: (context, exerciseProvider, userProvider, _) {
|
||||
final currentExerciseIndex = exerciseProvider.currentExerciseIndex;
|
||||
final totalExercises = isReview
|
||||
? exerciseProvider.reviewExercises.length
|
||||
: exerciseProvider.exercises.length;
|
||||
final isFirstExercise = currentExerciseIndex == 0;
|
||||
final isLastExercise = currentExerciseIndex == totalExercises - 1;
|
||||
|
||||
Future<void> submitAnswers() async {
|
||||
try {
|
||||
final result = await exerciseProvider.submitAnswersAndGetScore();
|
||||
print('Submit result: $result'); // Logging
|
||||
Future<void> submitAnswers() async {
|
||||
try {
|
||||
final result = await exerciseProvider.submitAnswersAndGetScore();
|
||||
print('Submit result: $result');
|
||||
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ResultScreen(
|
||||
currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
|
||||
nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
|
||||
score: int.tryParse(result['SCORE'].toString()) ?? 0,
|
||||
isCompleted: result['IS_PASS'] == 1,
|
||||
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error submitting answers: $e'); // Logging
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ResultScreen(
|
||||
currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
|
||||
nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
|
||||
score: int.tryParse(result['SCORE'].toString()) ?? 0,
|
||||
isCompleted: result['IS_PASS'] == 1,
|
||||
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error submitting answers: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!isFirstExercise)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
exerciseProvider.previousQuestion();
|
||||
onScrollToTop?.call();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
shadowColor: Colors.transparent,
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!isFirstExercise)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: GlobalButton(
|
||||
text: 'Previous',
|
||||
height: 45,
|
||||
onPressed: () {
|
||||
exerciseProvider.previousQuestion();
|
||||
onScrollToTop?.call();
|
||||
},
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(
|
||||
color: AppColors.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Text(
|
||||
'Previous',
|
||||
style: AppTextStyles.blueTextStyle.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
borderColor: AppColors.blueColor,
|
||||
textColor: AppColors.blueColor,
|
||||
transparentBackground: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8),
|
||||
if (!isLastExercise)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
exerciseProvider.nextQuestion();
|
||||
onScrollToTop?.call();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
shadowColor: Colors.transparent,
|
||||
if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8),
|
||||
if (!isLastExercise)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: GlobalButton(
|
||||
text: 'Next',
|
||||
height: 45,
|
||||
onPressed: () {
|
||||
exerciseProvider.nextQuestion();
|
||||
onScrollToTop?.call();
|
||||
},
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(
|
||||
color: AppColors.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Text(
|
||||
'Next',
|
||||
style: AppTextStyles.blueTextStyle.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
borderColor: AppColors.blueColor,
|
||||
textColor: AppColors.blueColor,
|
||||
transparentBackground: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isLastExercise && !isFirstExercise) const SizedBox(width: 8),
|
||||
if (isLastExercise)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (exerciseProvider.hasAnsweredQuestions()) {
|
||||
if (exerciseProvider.hasUnansweredQuestions()) {
|
||||
// Ada pertanyaan yang belum dijawab
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return IncompleteSubmission(
|
||||
onCheckAgain: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onSubmit: () async {
|
||||
Navigator.of(context).pop();
|
||||
submitAnswers();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (isLastExercise && !isFirstExercise) const SizedBox(width: 8),
|
||||
if (isLastExercise)
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: GlobalButton(
|
||||
text: isReview ? 'Back to Level List' : 'Submit',
|
||||
height: 45,
|
||||
onPressed: () {
|
||||
if (isReview) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
// Semua pertanyaan sudah dijawab
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CompleteSubmission(
|
||||
onCheckAgain: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onSubmit: () async {
|
||||
Navigator.of(context).pop();
|
||||
submitAnswers();
|
||||
if (exerciseProvider.hasAnsweredQuestions()) {
|
||||
if (exerciseProvider.hasUnansweredQuestions()) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return IncompleteSubmission(
|
||||
onCheckAgain: () => Navigator.of(context).pop(),
|
||||
onSubmit: () async {
|
||||
Navigator.of(context).pop();
|
||||
submitAnswers();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CompleteSubmission(
|
||||
onCheckAgain: () => Navigator.of(context).pop(),
|
||||
onSubmit: () async {
|
||||
Navigator.of(context).pop();
|
||||
submitAnswers();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Please answer at least one question before submitting.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Tidak ada pertanyaan yang dijawab
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Please answer at least one question before submitting.')),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.gradientTheme,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: exerciseProvider.nextQuestion,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Submit',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
gradient: AppColors.gradientTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
||||
import 'package:english_learning/core/widgets/global_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
|
||||
|
|
@ -11,7 +13,6 @@ class InstructionsDialog extends StatelessWidget {
|
|||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
shape: BoxShape.rectangle,
|
||||
|
|
@ -24,47 +25,52 @@ class InstructionsDialog extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Instructions',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInstructionItem(
|
||||
icon: Icons.question_answer,
|
||||
text: 'Answer all questions to complete the exercise.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInstructionItem(
|
||||
icon: Icons.compare_arrows,
|
||||
text:
|
||||
'For matching pairs, select a left option first, then match it with a right option.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInstructionItem(
|
||||
icon: Icons.edit,
|
||||
text:
|
||||
'You can change your answers at any time before submitting.',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
child: const Text('Got it!'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.blueColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Instructions',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
const Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 32, left: 32, right: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildInstructionItem(
|
||||
icon: BootstrapIcons.chat_square_text,
|
||||
text: 'Answer all questions to complete the exercise.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInstructionItem(
|
||||
icon: BootstrapIcons.arrow_left_right,
|
||||
text:
|
||||
'For matching pairs, select a left option first, then match it with a right option.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInstructionItem(
|
||||
icon: BootstrapIcons.pencil,
|
||||
text:
|
||||
'You can change your answers at any time before submitting.',
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
GlobalButton(
|
||||
text: 'Got It',
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -73,12 +79,16 @@ class InstructionsDialog extends StatelessWidget {
|
|||
Widget _buildInstructionItem({required IconData icon, required String text}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.blueColor),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
icon,
|
||||
color: AppColors.blueColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(fontSize: 16),
|
||||
style: AppTextStyles.greyTextStyle.copyWith(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
|
|
@ -5,55 +6,131 @@ import 'package:english_learning/features/learning/modules/exercises/models/exer
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
class MatchingPairsQuestion extends StatelessWidget {
|
||||
final ExerciseModel exercise;
|
||||
final dynamic exercise;
|
||||
final bool isReview;
|
||||
|
||||
const MatchingPairsQuestion({
|
||||
super.key,
|
||||
required this.exercise,
|
||||
this.isReview = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<ExerciseProvider>(context);
|
||||
final matchingPair = exercise.choices as MatchingPair;
|
||||
List<dynamic> pairs = [];
|
||||
Map<String, String> studentAnswers = {};
|
||||
final currentIndex = provider.currentExerciseIndex;
|
||||
|
||||
if (exercise is ExerciseModel) {
|
||||
pairs = (exercise.choices as MatchingPair).pairs;
|
||||
} else if (exercise is ReviewExerciseDetail) {
|
||||
if (exercise.matchingPairs != null) {
|
||||
pairs = exercise.matchingPairs!.map((reviewPair) {
|
||||
return Pair(
|
||||
left: reviewPair.leftPair,
|
||||
right: reviewPair.rightPair,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// Parse student answers
|
||||
if (exercise.answerStudent.isNotEmpty) {
|
||||
final answerPairs = exercise.answerStudent.split(', ');
|
||||
for (var pair in answerPairs) {
|
||||
final parts = pair.split('-');
|
||||
if (parts.length == 2) {
|
||||
studentAnswers[parts[0]] = parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a reverse mapping for easier lookup of left pair from right pair
|
||||
Map<String, String> reverseStudentAnswers = {};
|
||||
studentAnswers.forEach((left, right) {
|
||||
reverseStudentAnswers[right] = left;
|
||||
});
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
...matchingPair.pairs.asMap().entries.map((entry) {
|
||||
...pairs.asMap().entries.map((entry) {
|
||||
int pairIndex = entry.key;
|
||||
Pair pair = entry.value;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildOptionItem(
|
||||
context,
|
||||
pair.left,
|
||||
provider,
|
||||
currentIndex,
|
||||
pairIndex,
|
||||
true,
|
||||
dynamic pair = entry.value;
|
||||
String left = pair is Pair ? pair.left : pair.leftPair;
|
||||
String right = pair is Pair ? pair.right : pair.rightPair;
|
||||
|
||||
// Get the corresponding color for this pair
|
||||
Color pairColor = _getPairColor(pairIndex);
|
||||
|
||||
// Find if this item is matched in student answers
|
||||
String? studentMatchedRight = studentAnswers[left];
|
||||
String? matchedLeftForRight = reverseStudentAnswers[right];
|
||||
|
||||
// Find the color index for the matched pair
|
||||
int? matchedPairIndex;
|
||||
if (isReview && matchedLeftForRight != null) {
|
||||
matchedPairIndex = pairs.indexWhere((p) {
|
||||
if (p is Pair) {
|
||||
return p.left == matchedLeftForRight;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildOptionItem(
|
||||
context,
|
||||
left,
|
||||
provider,
|
||||
currentIndex,
|
||||
pairIndex,
|
||||
true,
|
||||
studentMatchedRight != null,
|
||||
studentAnswers,
|
||||
pairColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildOptionItem(
|
||||
context,
|
||||
pair.right,
|
||||
provider,
|
||||
currentIndex,
|
||||
pairIndex,
|
||||
false,
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildOptionItem(
|
||||
context,
|
||||
right,
|
||||
provider,
|
||||
currentIndex,
|
||||
matchedPairIndex ?? pairIndex,
|
||||
false,
|
||||
matchedLeftForRight != null,
|
||||
studentAnswers,
|
||||
matchedPairIndex != null
|
||||
? _getPairColor(matchedPairIndex)
|
||||
: pairColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getPairColor(int index) {
|
||||
final List<Color> colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.red,
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
Widget _buildOptionItem(
|
||||
BuildContext context,
|
||||
String option,
|
||||
|
|
@ -61,61 +138,79 @@ class MatchingPairsQuestion extends StatelessWidget {
|
|||
int exerciseIndex,
|
||||
int pairIndex,
|
||||
bool isLeft,
|
||||
bool isMatched,
|
||||
Map<String, String> studentAnswers,
|
||||
Color pairColor,
|
||||
) {
|
||||
Color? color = isLeft
|
||||
? provider.getLeftColor(exerciseIndex, pairIndex)
|
||||
: provider.getRightColor(exerciseIndex, pairIndex);
|
||||
final isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
|
||||
final isActive = isLeft && provider.activeLeftOption == option;
|
||||
Color? color;
|
||||
bool isSelected = false;
|
||||
bool isActive = false;
|
||||
|
||||
if (isReview) {
|
||||
if (isMatched) {
|
||||
color = pairColor;
|
||||
isSelected = true;
|
||||
}
|
||||
} else {
|
||||
if (isLeft) {
|
||||
color = provider.getLeftColor(exerciseIndex, pairIndex);
|
||||
isActive = provider.activeLeftOption == option;
|
||||
} else {
|
||||
color = provider.getRightColor(exerciseIndex, pairIndex);
|
||||
}
|
||||
isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!isLeft && provider.activeLeftOption == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please select a left option first.')),
|
||||
);
|
||||
} else {
|
||||
provider.answerQuestion(exerciseIndex, option);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected ? color : (isActive ? color! : AppColors.whiteColor),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
bottomLeft: Radius.circular(20),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? color ?? AppColors.cardDisabledColor
|
||||
: AppColors.cardDisabledColor,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: color?.withOpacity(0.5) ??
|
||||
Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
]
|
||||
: null,
|
||||
onTap: isReview
|
||||
? null
|
||||
: () {
|
||||
if (!isLeft && provider.activeLeftOption == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select a left option first.'),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
provider.answerQuestion(exerciseIndex, option);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? color ?? pairColor
|
||||
: (isActive ? pairColor : AppColors.whiteColor),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
bottomLeft: Radius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||
child: Text(
|
||||
option,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isSelected || isActive
|
||||
? AppColors.whiteColor
|
||||
: AppColors.blackColor,
|
||||
),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? color ?? AppColors.cardDisabledColor
|
||||
: AppColors.cardDisabledColor,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: isActive && !isReview
|
||||
? [
|
||||
BoxShadow(
|
||||
color: pairColor.withOpacity(0.5),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
]
|
||||
: null,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||
child: Text(
|
||||
option,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isSelected || isActive
|
||||
? AppColors.whiteColor
|
||||
: AppColors.blackColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
|
|
@ -5,54 +6,85 @@ import 'package:english_learning/features/learning/modules/exercises/models/exer
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
class MultipleChoiceQuestion extends StatelessWidget {
|
||||
final ExerciseModel exercise;
|
||||
final dynamic exercise;
|
||||
final bool isReview;
|
||||
|
||||
const MultipleChoiceQuestion({
|
||||
super.key,
|
||||
required this.exercise,
|
||||
this.isReview = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<ExerciseProvider>(context);
|
||||
final multipleChoice = exercise.choices as MultipleChoice;
|
||||
final options = [
|
||||
multipleChoice.optionA,
|
||||
multipleChoice.optionB,
|
||||
multipleChoice.optionC,
|
||||
multipleChoice.optionD,
|
||||
multipleChoice.optionE,
|
||||
];
|
||||
List<String> options = [];
|
||||
String? studentAnswer;
|
||||
|
||||
return _buildOptionsList(options, provider);
|
||||
if (exercise is ExerciseModel) {
|
||||
final multipleChoice = exercise.choices as MultipleChoice;
|
||||
options = [
|
||||
multipleChoice.optionA,
|
||||
multipleChoice.optionB,
|
||||
multipleChoice.optionC,
|
||||
multipleChoice.optionD,
|
||||
multipleChoice.optionE,
|
||||
];
|
||||
} else if (exercise is ReviewExerciseDetail) {
|
||||
if (exercise.multipleChoices?.isNotEmpty ?? false) {
|
||||
final choices = exercise.multipleChoices!.first;
|
||||
options = [
|
||||
choices.optionA,
|
||||
choices.optionB,
|
||||
choices.optionC,
|
||||
choices.optionD,
|
||||
choices.optionE,
|
||||
];
|
||||
studentAnswer = exercise.answerStudent;
|
||||
}
|
||||
}
|
||||
|
||||
return _buildOptionsList(context, options, provider, studentAnswer);
|
||||
}
|
||||
|
||||
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) {
|
||||
final optionLabels = List.generate(
|
||||
options.length,
|
||||
(index) =>
|
||||
String.fromCharCode(65 + index), // Generate labels "A", "B", etc.
|
||||
);
|
||||
Widget _buildOptionsList(
|
||||
BuildContext context,
|
||||
List<String> options,
|
||||
ExerciseProvider provider,
|
||||
String? studentAnswer,
|
||||
) {
|
||||
return Column(
|
||||
children: options.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final option = entry.value;
|
||||
final optionLabel = String.fromCharCode(65 + index); // A, B, C, D, E
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, i) {
|
||||
final option = options[i];
|
||||
final isSelected =
|
||||
provider.answers[provider.currentExerciseIndex] == optionLabels[i];
|
||||
bool isSelected = false;
|
||||
if (isReview) {
|
||||
isSelected = studentAnswer == optionLabel;
|
||||
} else {
|
||||
isSelected =
|
||||
provider.answers[provider.currentExerciseIndex] == optionLabel;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
provider.answerQuestion(provider.currentExerciseIndex, option),
|
||||
child: _buildOptionItem(optionLabels[i], option, isSelected),
|
||||
onTap: isReview
|
||||
? null
|
||||
: () {
|
||||
provider.answerQuestion(
|
||||
provider.currentExerciseIndex, option);
|
||||
},
|
||||
child: _buildOptionItem(optionLabel, option, isSelected),
|
||||
);
|
||||
},
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionItem(String label, String option, bool isSelected) {
|
||||
final backgroundColor =
|
||||
isSelected ? AppColors.blueColor : AppColors.whiteColor;
|
||||
final textColor = isSelected ? AppColors.whiteColor : AppColors.blackColor;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
|
|
@ -60,7 +92,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
|||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
|
|
@ -68,16 +100,13 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
|||
: AppColors.cardDisabledColor,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color:
|
||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -85,9 +114,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
||||
color: backgroundColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
|
|
@ -104,8 +132,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
|||
child: Text(
|
||||
option,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
color:
|
||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,49 +1,90 @@
|
|||
// true_false_question.dart
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart';
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
|
||||
class TrueFalseQuestion extends StatelessWidget {
|
||||
final ExerciseModel exercise;
|
||||
final dynamic exercise;
|
||||
final bool isReview;
|
||||
|
||||
const TrueFalseQuestion({
|
||||
super.key,
|
||||
required this.exercise,
|
||||
this.isReview = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<ExerciseProvider>(context);
|
||||
final options = ['True', 'False'];
|
||||
String? studentAnswer = _getStudentAnswer();
|
||||
|
||||
return _buildOptionsList(options, provider);
|
||||
final options = [
|
||||
{'label': 'A', 'value': '1', 'text': 'True'},
|
||||
{'label': 'B', 'value': '0', 'text': 'False'}
|
||||
];
|
||||
|
||||
return _buildOptionsList(context, options, provider, studentAnswer);
|
||||
}
|
||||
|
||||
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, i) {
|
||||
final option = options[i];
|
||||
final isSelected = provider.answers[provider.currentExerciseIndex] ==
|
||||
(i == 0 ? '1' : '0');
|
||||
String? _getStudentAnswer() {
|
||||
if (isReview && exercise is ReviewExerciseDetail) {
|
||||
return exercise.answerStudent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildOptionsList(
|
||||
BuildContext context,
|
||||
List<Map<String, String>> options,
|
||||
ExerciseProvider provider,
|
||||
String? studentAnswer,
|
||||
) {
|
||||
return Column(
|
||||
children: options.map((option) {
|
||||
final value = option['value']!;
|
||||
final label = option['label']!;
|
||||
final text = option['text']!;
|
||||
|
||||
bool isSelected = false;
|
||||
bool isCorrect = false;
|
||||
|
||||
if (isReview && exercise is ReviewExerciseDetail) {
|
||||
isSelected = studentAnswer == value;
|
||||
isCorrect = exercise.isCorrect == 1;
|
||||
} else {
|
||||
isSelected = provider.answers[provider.currentExerciseIndex] == value;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
// onTap: () =>
|
||||
// provider.answerQuestion(provider.currentExerciseIndex, option),
|
||||
onTap: () => provider.answerQuestion(
|
||||
provider.currentExerciseIndex, i == 0 ? '1' : '0'),
|
||||
|
||||
child: _buildOptionItem(option, isSelected),
|
||||
onTap: isReview
|
||||
? null
|
||||
: () {
|
||||
provider.answerQuestion(provider.currentExerciseIndex, value);
|
||||
},
|
||||
child: _buildOptionItem(
|
||||
label,
|
||||
text,
|
||||
isSelected,
|
||||
isReview && isSelected,
|
||||
isCorrect,
|
||||
),
|
||||
);
|
||||
},
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionItem(String option, bool isSelected) {
|
||||
Widget _buildOptionItem(
|
||||
String label,
|
||||
String text,
|
||||
bool isSelected,
|
||||
bool isReviewSelected,
|
||||
bool isCorrect,
|
||||
) {
|
||||
final backgroundColor =
|
||||
isSelected ? AppColors.blueColor : AppColors.whiteColor;
|
||||
final textColor = isSelected ? AppColors.whiteColor : AppColors.blackColor;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
|
|
@ -51,7 +92,7 @@ class TrueFalseQuestion extends StatelessWidget {
|
|||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
|
|
@ -59,18 +100,13 @@ class TrueFalseQuestion extends StatelessWidget {
|
|||
: AppColors.cardDisabledColor,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14.0,
|
||||
vertical: 10.0,
|
||||
),
|
||||
child: Text(
|
||||
option,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color:
|
||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -78,9 +114,8 @@ class TrueFalseQuestion extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
||||
color: backgroundColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
|
|
@ -95,10 +130,9 @@ class TrueFalseQuestion extends StatelessWidget {
|
|||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
option,
|
||||
text,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
color:
|
||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -71,22 +71,6 @@ class LevelProvider with ChangeNotifier {
|
|||
// _lastCompletedLevel!['ID_LEVEL'] == levelId;
|
||||
// }
|
||||
|
||||
Future<void> fetchStudentAnswers(String stdLearningId, String token) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_studentAnswers =
|
||||
await _levelRepository.getStudentAnswers(stdLearningId, token);
|
||||
} catch (e) {
|
||||
_error = 'Error fetching student answers: ${e.toString()}';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool isPretestFinished(String levelId) {
|
||||
return _levels.any(
|
||||
(level) => level.idLevel == levelId && level.idStudentLearning != null);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,32 @@ class PretestCard extends StatelessWidget {
|
|||
final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
|
||||
final score = levelProvider.getPretestScore(pretest.idLevel);
|
||||
// final isAllowed = levelProvider.isLevelAllowed(pretest.idLevel);
|
||||
void navigateToMaterial() {
|
||||
if (isFinished) {
|
||||
// Mode review untuk pretest yang sudah selesai
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MaterialScreen(
|
||||
levelId: pretest.idLevel,
|
||||
isReview: true,
|
||||
studentLearningId: pretest.idStudentLearning,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Mode normal untuk pretest yang belum dikerjakan
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MaterialScreen(
|
||||
levelId: pretest.idLevel,
|
||||
isReview: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -140,17 +166,7 @@ class PretestCard extends StatelessWidget {
|
|||
// );
|
||||
// }
|
||||
// : () {},
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MaterialScreen(
|
||||
levelId: pretest.idLevel,
|
||||
isReview: isFinished,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: navigateToMaterial,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:english_learning/core/services/dio_client.dart';
|
||||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
import 'package:english_learning/core/services/repositories/student_learning_repository.dart';
|
||||
import 'package:english_learning/core/widgets/global_button.dart';
|
||||
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
||||
|
|
@ -15,11 +15,13 @@ import 'package:provider/provider.dart';
|
|||
class MaterialScreen extends StatefulWidget {
|
||||
final String levelId;
|
||||
final bool isReview;
|
||||
final String? studentLearningId;
|
||||
|
||||
const MaterialScreen({
|
||||
super.key,
|
||||
required this.levelId,
|
||||
this.isReview = false,
|
||||
this.studentLearningId,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -59,6 +61,11 @@ class _MaterialScreenState extends State<MaterialScreen>
|
|||
}
|
||||
|
||||
Future<void> _createStudentLearning() async {
|
||||
if (widget.isReview && widget.studentLearningId != null) {
|
||||
// Jika mode review dan studentLearningId tersedia, langsung navigasi ke ExerciseScreen
|
||||
_navigateToExercise(widget.studentLearningId!);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
|
@ -77,15 +84,7 @@ class _MaterialScreenState extends State<MaterialScreen>
|
|||
print('Student Learning created: ${result['message']}');
|
||||
|
||||
// Navigate to ExerciseScreen
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ExerciseScreen(
|
||||
levelId: widget.levelId,
|
||||
studentLearningId: result['payload']['ID_STUDENT_LEARNING'],
|
||||
),
|
||||
),
|
||||
);
|
||||
_navigateToExercise(result['payload']['ID_STUDENT_LEARNING']);
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -98,6 +97,19 @@ class _MaterialScreenState extends State<MaterialScreen>
|
|||
}
|
||||
}
|
||||
|
||||
void _navigateToExercise(String studentLearningId) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ExerciseScreen(
|
||||
levelId: widget.levelId,
|
||||
studentLearningId: studentLearningId,
|
||||
isReview: widget.isReview,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<LevelProvider>(builder: (context, levelProvider, child) {
|
||||
|
|
|
|||
|
|
@ -16,21 +16,48 @@ class VideoPlayerWidget extends StatefulWidget {
|
|||
}
|
||||
|
||||
class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||
late Widget _videoWidget;
|
||||
VideoPlayerController? _videoController;
|
||||
YoutubePlayerController? _youtubeController;
|
||||
FlickManager? _flickManager;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
bool get wantKeepAlive => true;
|
||||
bool _isYoutubeReady = false;
|
||||
String? _youtubeId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_youtubeId = _extractYoutubeId(widget.videoUrl);
|
||||
_initializeVideoPlayerWidget();
|
||||
}
|
||||
|
||||
String? _extractYoutubeId(String url) {
|
||||
try {
|
||||
RegExp regExp = RegExp(
|
||||
r'^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/|shorts\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*',
|
||||
caseSensitive: false,
|
||||
multiLine: false,
|
||||
);
|
||||
|
||||
String? videoId = YoutubePlayer.convertUrlToId(url);
|
||||
if (videoId != null) return videoId;
|
||||
|
||||
Match? match = regExp.firstMatch(url);
|
||||
if (match != null && match.groupCount >= 1) {
|
||||
return match.group(1);
|
||||
}
|
||||
|
||||
if (url.contains('youtu.be/')) {
|
||||
return url.split('youtu.be/')[1].split(RegExp(r'[?&]'))[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error extracting YouTube ID: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _getPlayableUrl(String url) {
|
||||
if (url.contains('drive.google.com')) {
|
||||
final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)');
|
||||
|
|
@ -43,54 +70,66 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|||
return url;
|
||||
}
|
||||
|
||||
void _initializeVideoPlayerWidget() {
|
||||
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) {
|
||||
_youtubeController = YoutubePlayerController(
|
||||
initialVideoId: YoutubePlayer.convertUrlToId(widget.videoUrl)!,
|
||||
flags: const YoutubePlayerFlags(
|
||||
autoPlay: false,
|
||||
mute: false,
|
||||
),
|
||||
);
|
||||
Future<void> _initializeVideoPlayerWidget() async {
|
||||
if (_youtubeId != null) {
|
||||
try {
|
||||
_youtubeController = YoutubePlayerController(
|
||||
initialVideoId: _youtubeId!,
|
||||
flags: const YoutubePlayerFlags(
|
||||
autoPlay: false,
|
||||
mute: false,
|
||||
hideControls: false,
|
||||
controlsVisibleAtStart: true,
|
||||
enableCaption: true,
|
||||
useHybridComposition: true,
|
||||
forceHD: true,
|
||||
),
|
||||
);
|
||||
|
||||
_videoWidget = YoutubePlayer(
|
||||
controller: _youtubeController!,
|
||||
showVideoProgressIndicator: true,
|
||||
onReady: () {
|
||||
_youtubeController!.addListener(_youtubeListener);
|
||||
if (mounted) {
|
||||
setState(() {}); // Trigger rebuild with controller
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = "Error initializing YouTube player: $e";
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(_getPlayableUrl(widget.videoUrl)),
|
||||
);
|
||||
_videoController!.initialize().then((_) {
|
||||
try {
|
||||
_videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(_getPlayableUrl(widget.videoUrl)),
|
||||
);
|
||||
|
||||
await _videoController!.initialize();
|
||||
|
||||
_flickManager = FlickManager(
|
||||
videoPlayerController: _videoController!,
|
||||
autoPlay: false,
|
||||
);
|
||||
_videoWidget = FlickVideoPlayer(
|
||||
flickManager: _flickManager!,
|
||||
);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}).catchError((error) {
|
||||
setState(() {
|
||||
_error = "Error loading video: $error";
|
||||
_isLoading = false;
|
||||
});
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = "Error initializing video player: $e";
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _youtubeListener() {
|
||||
if (_youtubeController!.value.playerState == PlayerState.ended) {
|
||||
_youtubeController!.seekTo(Duration.zero);
|
||||
_youtubeController!.pause();
|
||||
if (_youtubeController?.value.playerState == PlayerState.ended) {
|
||||
_youtubeController?.seekTo(Duration.zero);
|
||||
_youtubeController?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,20 +143,105 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (_error != null) {
|
||||
return Center(child: Text(_error!));
|
||||
if (_youtubeId != null && _youtubeController != null) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: YoutubePlayerBuilder(
|
||||
player: YoutubePlayer(
|
||||
controller: _youtubeController!,
|
||||
showVideoProgressIndicator: true,
|
||||
progressIndicatorColor: Colors.red,
|
||||
progressColors: const ProgressBarColors(
|
||||
playedColor: Colors.red,
|
||||
handleColor: Colors.redAccent,
|
||||
),
|
||||
onReady: () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isYoutubeReady = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
_youtubeController!.addListener(_youtubeListener);
|
||||
},
|
||||
onEnded: (YoutubeMetaData metaData) {
|
||||
_youtubeController!.seekTo(Duration.zero);
|
||||
_youtubeController!.pause();
|
||||
},
|
||||
bottomActions: [
|
||||
CurrentPosition(),
|
||||
ProgressBar(isExpanded: true),
|
||||
RemainingDuration(),
|
||||
PlaybackSpeedButton(),
|
||||
],
|
||||
),
|
||||
builder: (context, player) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _isYoutubeReady
|
||||
? player
|
||||
: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: _videoWidget,
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _buildContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
_error!,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_flickManager != null) {
|
||||
return FlickVideoPlayer(flickManager: _flickManager!);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
void stopAndResetVideo() {
|
||||
if (_youtubeController != null) {
|
||||
_youtubeController!.seekTo(Duration.zero);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
||||
import 'package:english_learning/features/learning/modules/level/screens/level_list_screen.dart';
|
||||
import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart';
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import 'package:english_learning/features/auth/provider/user_provider.dart';
|
|||
import 'package:english_learning/features/learning/provider/section_provider.dart';
|
||||
import 'package:english_learning/features/learning/widgets/section_card.dart';
|
||||
import 'package:english_learning/features/learning/modules/topics/screens/topics_list_screen.dart';
|
||||
import 'package:english_learning/features/learning/widgets/section_card_shimmer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class LearningScreen extends StatefulWidget {
|
||||
const LearningScreen({
|
||||
|
|
@ -102,9 +102,7 @@ class _LearningScreenState extends State<LearningScreen> {
|
|||
return ListView.builder(
|
||||
itemCount: 5, // Misalnya, kita menampilkan 5 shimmer items
|
||||
itemBuilder: (context, index) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
return ShimmerWidget(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
import 'package:english_learning/features/learning/modules/model/section_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
|
||||
class LearningCard extends StatelessWidget {
|
||||
class LearningCard extends StatefulWidget {
|
||||
final Section section;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
|
|
@ -13,6 +13,12 @@ class LearningCard extends StatelessWidget {
|
|||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LearningCard> createState() => _LearningCardState();
|
||||
}
|
||||
|
||||
class _LearningCardState extends State<LearningCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
String _getFullImageUrl(String thumbnail) {
|
||||
if (thumbnail.startsWith('http')) {
|
||||
return thumbnail;
|
||||
|
|
@ -24,7 +30,7 @@ class LearningCard extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
onTap: widget.onTap,
|
||||
child: Card(
|
||||
color: AppColors.whiteColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -40,7 +46,7 @@ class LearningCard extends StatelessWidget {
|
|||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
_getFullImageUrl(section.thumbnail),
|
||||
_getFullImageUrl(widget.section.thumbnail),
|
||||
width: 90,
|
||||
height: 104,
|
||||
fit: BoxFit.cover,
|
||||
|
|
@ -72,7 +78,7 @@ class LearningCard extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
section.name,
|
||||
widget.section.name,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
|
|
@ -80,7 +86,7 @@ class LearningCard extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
section.description,
|
||||
widget.section.description,
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:shimmer/shimmer.dart';
|
|||
|
||||
class ShimmerWidget extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const ShimmerWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart';
|
||||
import 'package:english_learning/core/widgets/global_button.dart';
|
||||
|
|
@ -60,7 +60,9 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||
};
|
||||
|
||||
try {
|
||||
userProvider.setLoading(true);
|
||||
await userProvider.updateUserProfile(updatedData);
|
||||
userProvider.setLoading(false);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -186,7 +188,10 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||
const SizedBox(height: 24),
|
||||
GlobalButton(
|
||||
text: 'Save Changes',
|
||||
onPressed: () => _updateUserProfile(context),
|
||||
isLoading: userProvider.isLoading,
|
||||
onPressed: userProvider.isLoading
|
||||
? null
|
||||
: () => _updateUserProfile(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
||||
import 'package:english_learning/core/services/repositories/constants.dart';
|
||||
import 'package:english_learning/core/services/constants.dart';
|
||||
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
||||
import 'package:english_learning/features/auth/screens/signin/signin_screen.dart';
|
||||
import 'package:english_learning/features/settings/modules/change_password/screens/change_password_screen.dart';
|
||||
|
|
|
|||
|
|
@ -676,10 +676,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user