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
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:english_learning/core/services/repositories/constants.dart';
|
import 'package:english_learning/core/services/constants.dart';
|
||||||
|
|
||||||
class DioClient {
|
class DioClient {
|
||||||
final Dio _dio = Dio();
|
final Dio _dio = Dio();
|
||||||
|
|
@ -393,7 +393,7 @@ class DioClient {
|
||||||
Future<Response> getStudentAnswers(String stdLearningId, String token) async {
|
Future<Response> getStudentAnswers(String stdLearningId, String token) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get(
|
final response = await _dio.get(
|
||||||
'/studentAnswer/$stdLearningId',
|
'/studentAnswers/$stdLearningId',
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer $token',
|
'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 {
|
try {
|
||||||
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
|
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return response.data['data'];
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load student answers');
|
throw Exception('Failed to load student answers');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,20 +32,4 @@ class LevelRepository {
|
||||||
rethrow;
|
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 Color? borderColor;
|
||||||
final double borderWidth;
|
final double borderWidth;
|
||||||
final bool transparentBackground;
|
final bool transparentBackground;
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
const GlobalButton({
|
const GlobalButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -31,6 +32,7 @@ class GlobalButton extends StatelessWidget {
|
||||||
this.borderColor,
|
this.borderColor,
|
||||||
this.borderWidth = 1.0,
|
this.borderWidth = 1.0,
|
||||||
this.transparentBackground = false,
|
this.transparentBackground = false,
|
||||||
|
this.isLoading = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -61,49 +63,66 @@ class GlobalButton extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: spaceBetween && icon != null
|
child: isLoading ? _buildLoadingIndicator() : _buildButtonContent(),
|
||||||
? 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
String? _jwtToken;
|
||||||
Map<String, dynamic>? _userData;
|
Map<String, dynamic>? _userData;
|
||||||
File? _selectedImage;
|
File? _selectedImage;
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
bool get isLoggedIn => _isLoggedIn;
|
bool get isLoggedIn => _isLoggedIn;
|
||||||
String? get jwtToken => _jwtToken;
|
String? get jwtToken => _jwtToken;
|
||||||
Map<String, dynamic>? get userData => _userData;
|
Map<String, dynamic>? get userData => _userData;
|
||||||
|
|
@ -19,6 +21,11 @@ class UserProvider with ChangeNotifier {
|
||||||
_loadLoginStatus();
|
_loadLoginStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setLoading(bool value) {
|
||||||
|
_isLoading = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadLoginStatus() async {
|
Future<void> _loadLoginStatus() async {
|
||||||
try {
|
try {
|
||||||
_jwtToken = await _userRepository.getToken();
|
_jwtToken = await _userRepository.getToken();
|
||||||
|
|
@ -60,6 +67,7 @@ class UserProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> login({required String email, required String password}) async {
|
Future<bool> login({required String email, required String password}) async {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
final response = await _userRepository.loginUser({
|
final response = await _userRepository.loginUser({
|
||||||
'EMAIL': email,
|
'EMAIL': email,
|
||||||
|
|
@ -84,6 +92,8 @@ class UserProvider with ChangeNotifier {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Login error: $e');
|
print('Login error: $e');
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,53 +128,58 @@ class SigninScreen extends StatelessWidget {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Login',
|
text: 'Login',
|
||||||
onPressed: () async {
|
isLoading: userProvider.isLoading,
|
||||||
// Validate email and password fields
|
onPressed: userProvider.isLoading
|
||||||
validatorProvider.validateField(
|
? null
|
||||||
'email',
|
: () async {
|
||||||
_emailController.text,
|
// Validate email and password fields
|
||||||
validator: validatorProvider.emailValidator,
|
validatorProvider.validateField(
|
||||||
);
|
'email',
|
||||||
validatorProvider.validateField(
|
_emailController.text,
|
||||||
'password',
|
validator: validatorProvider.emailValidator,
|
||||||
_passwordController.text,
|
);
|
||||||
validator: validatorProvider.passwordValidator,
|
validatorProvider.validateField(
|
||||||
);
|
'password',
|
||||||
|
_passwordController.text,
|
||||||
|
validator: validatorProvider.passwordValidator,
|
||||||
|
);
|
||||||
|
|
||||||
// If no errors, proceed with login
|
// If no errors, proceed with login
|
||||||
if (validatorProvider.getError('email') == null &&
|
if (validatorProvider.getError('email') == null &&
|
||||||
validatorProvider.getError('password') == null) {
|
validatorProvider.getError('password') ==
|
||||||
final isSuccess = await userProvider.login(
|
null) {
|
||||||
email: _emailController.text,
|
final isSuccess = await userProvider.login(
|
||||||
password: _passwordController.text,
|
email: _emailController.text,
|
||||||
);
|
password: _passwordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
// Navigate to HomeScreen after successful login
|
// Navigate to HomeScreen after successful login
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const HomeScreen()),
|
builder: (context) =>
|
||||||
(Route<dynamic> route) =>
|
const HomeScreen()),
|
||||||
false, // Remove all previous routes
|
(Route<dynamic> route) =>
|
||||||
).then((_) {
|
false, // Remove all previous routes
|
||||||
// Reset the fields after login
|
).then((_) {
|
||||||
validatorProvider.resetFields();
|
// Reset the fields after login
|
||||||
_emailController.clear();
|
validatorProvider.resetFields();
|
||||||
_passwordController.clear();
|
_emailController.clear();
|
||||||
});
|
_passwordController.clear();
|
||||||
} else {
|
});
|
||||||
// Show error message
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
// Show error message
|
||||||
const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(
|
const SnackBar(
|
||||||
'Login failed, please check your credentials'),
|
content: Text(
|
||||||
backgroundColor: Colors.red,
|
'Login failed, please check your credentials'),
|
||||||
),
|
backgroundColor: Colors.red,
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -187,6 +192,7 @@ class SigninScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
context.read<ValidatorProvider>().resetFields();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
|
|
||||||
|
|
@ -227,13 +227,12 @@ class SignupScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
context.read<ValidatorProvider>().resetFields();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => SigninScreen()),
|
builder: (context) => SigninScreen()),
|
||||||
).then((_) {
|
);
|
||||||
context.read<ValidatorProvider>().resetFields();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
' Login Here',
|
' Login Here',
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ class HistoryProvider with ChangeNotifier {
|
||||||
_error = null;
|
_error = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = 'Error fetching learning history: ${e.toString()}';
|
_error = 'Error fetching learning history: ${e.toString()}';
|
||||||
|
_learningHistory = [];
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
@ -91,10 +92,23 @@ class HistoryProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshData(String token) async {
|
Future<void> refreshData(String token) async {
|
||||||
if (_sectionProvider.sections.isEmpty) {
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
await _sectionProvider.fetchSections(token);
|
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 {
|
Future<void> loadHistoryData(String token) async {
|
||||||
|
|
@ -103,29 +117,52 @@ class HistoryProvider with ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch sections and learning history in parallel
|
await _sectionProvider.fetchSections(token);
|
||||||
final sectionsResult = _sectionProvider.fetchSections(token);
|
if (_sectionProvider.sections.isNotEmpty) {
|
||||||
|
String firstSectionId = _sectionProvider.sections.first.id;
|
||||||
// Use the first section ID for initial history fetch
|
_learningHistory =
|
||||||
final firstSectionId = await sectionsResult
|
await _repository.getLearningHistory(firstSectionId, token);
|
||||||
.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 {
|
} else {
|
||||||
_error = 'No sections available';
|
_error = 'No sections available';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = 'Error loading data: ${e.toString()}';
|
_error = 'Error loading data: ${e.toString()}';
|
||||||
|
_learningHistory = []; // Clear the list in case of error
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
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:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class HistoryScreen extends StatefulWidget {
|
class HistoryScreen extends StatelessWidget {
|
||||||
const HistoryScreen({
|
const HistoryScreen({super.key});
|
||||||
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!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isNotFoundError(String error) {
|
bool isNotFoundError(String error) {
|
||||||
return error.toLowerCase().contains('no learning history found') ||
|
return error.toLowerCase().contains('no learning history found') ||
|
||||||
error.toLowerCase().contains('not 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<HistoryProvider>(
|
return Consumer<HistoryProvider>(
|
||||||
builder: (context, historyProvider, chiild) {
|
builder: (context, historyProvider, child) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.bgSoftColor,
|
backgroundColor: AppColors.bgSoftColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16.0,
|
||||||
vertical: 30.0,
|
vertical: 30.0,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(),
|
_buildHeader(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const CustomTabBar(),
|
const CustomTabBar(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildContent(historyProvider),
|
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) {
|
if (historyProvider.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +61,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
if (historyProvider.error != null) {
|
if (historyProvider.error != null) {
|
||||||
return isNotFoundError(historyProvider.error!)
|
return isNotFoundError(historyProvider.error!)
|
||||||
? _buildEmptyState(context)
|
? _buildEmptyState(context)
|
||||||
: _buildErrorState(historyProvider.error!);
|
: _buildErrorState(context, historyProvider.error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historyProvider.learningHistory.isEmpty) {
|
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(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
|
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
|
||||||
|
|
@ -403,20 +419,27 @@ class _HomeContentState extends State<HomeContent> {
|
||||||
? _buildShimmerEffect()
|
? _buildShimmerEffect()
|
||||||
: completedTopicsProvider.completedTopics.isEmpty
|
: completedTopicsProvider.completedTopics.isEmpty
|
||||||
? _buildNoDataWidget()
|
? _buildNoDataWidget()
|
||||||
: ListView.builder(
|
: _buildCompletedTopicsContent(
|
||||||
shrinkWrap: true,
|
completedTopicsProvider),
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
// completedTopicsProvider.isLoading
|
||||||
horizontal: 16.0,
|
// ? _buildShimmerEffect()
|
||||||
),
|
// : completedTopicsProvider.completedTopics.isEmpty
|
||||||
itemCount: 1,
|
// ? _buildNoDataWidget()
|
||||||
itemBuilder: (context, index) {
|
// : ListView.builder(
|
||||||
return ProgressCard(
|
// shrinkWrap: true,
|
||||||
completedTopic: completedTopicsProvider
|
// physics: const NeverScrollableScrollPhysics(),
|
||||||
.completedTopics, // Kirim seluruh list
|
// 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/constants.dart';
|
||||||
import 'package:english_learning/core/services/repositories/constants.dart';
|
|
||||||
import 'package:english_learning/core/utils/styles/theme.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/models/completed_topics_model.dart';
|
||||||
import 'package:english_learning/features/home/widgets/progress_bar.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 {
|
class ExerciseModel {
|
||||||
final String idAdminExercise;
|
final String idAdminExercise;
|
||||||
final String idLevel;
|
final String idLevel;
|
||||||
|
|
@ -61,21 +63,6 @@ class ExerciseModel {
|
||||||
choices: choices,
|
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 {
|
class MultipleChoice {
|
||||||
|
|
@ -138,6 +125,13 @@ class Pair {
|
||||||
final String right;
|
final String right;
|
||||||
|
|
||||||
Pair({required this.left, required this.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 {
|
class MatchingPair {
|
||||||
|
|
@ -147,17 +141,24 @@ class MatchingPair {
|
||||||
|
|
||||||
factory MatchingPair.fromJsonList(List? jsonList) {
|
factory MatchingPair.fromJsonList(List? jsonList) {
|
||||||
if (jsonList == null) {
|
if (jsonList == null) {
|
||||||
return MatchingPair(
|
return MatchingPair(pairs: []);
|
||||||
pairs: []); // Return an empty list if jsonList is null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Pair> pairs = [];
|
List<Pair> pairs = jsonList.map((item) => Pair.fromJson(item)).toList();
|
||||||
for (var pair in jsonList) {
|
return MatchingPair(pairs: pairs);
|
||||||
pairs.add(Pair(
|
}
|
||||||
left: pair['LEFT_PAIR'] ?? '', // Use empty string as default if null
|
|
||||||
right: pair['RIGHT_PAIR'] ?? '',
|
// 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);
|
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:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
|
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
|
||||||
|
|
@ -14,6 +15,8 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
List<ExerciseModel> _exercises = [];
|
List<ExerciseModel> _exercises = [];
|
||||||
|
List<ReviewExerciseDetail> _reviewExercises = [];
|
||||||
|
ReviewExerciseModel? _reviewData;
|
||||||
Map<int, List<Map<String, String>>> _matchingAnswers = {};
|
Map<int, List<Map<String, String>>> _matchingAnswers = {};
|
||||||
List<String> _answers = [];
|
List<String> _answers = [];
|
||||||
List<List<Color>> _leftColors = [];
|
List<List<Color>> _leftColors = [];
|
||||||
|
|
@ -24,7 +27,6 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
String _nameLevel = '';
|
String _nameLevel = '';
|
||||||
String? _activeLeftOption;
|
String? _activeLeftOption;
|
||||||
String? _studentLearningId;
|
String? _studentLearningId;
|
||||||
bool _isReview = false;
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
final List<Color> _pairColors = [
|
final List<Color> _pairColors = [
|
||||||
|
|
@ -44,7 +46,7 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
String get nameLevel => _nameLevel;
|
String get nameLevel => _nameLevel;
|
||||||
String? get activeLeftOption => _activeLeftOption;
|
String? get activeLeftOption => _activeLeftOption;
|
||||||
String? get studentLearningId => _studentLearningId;
|
String? get studentLearningId => _studentLearningId;
|
||||||
bool get isReview => _isReview;
|
List<ReviewExerciseDetail> get reviewExercises => _reviewExercises;
|
||||||
|
|
||||||
// Initialization methods
|
// Initialization methods
|
||||||
void initializeAnswers() {
|
void initializeAnswers() {
|
||||||
|
|
@ -79,7 +81,6 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
void answerQuestion(int index, String answer) {
|
void answerQuestion(int index, String answer) {
|
||||||
if (_isReview) return;
|
|
||||||
if (index >= 0 && index < _answers.length) {
|
if (index >= 0 && index < _answers.length) {
|
||||||
if (_exercises[index].choices is MatchingPair) {
|
if (_exercises[index].choices is MatchingPair) {
|
||||||
_handleMatchingPairAnswer(index, answer);
|
_handleMatchingPairAnswer(index, answer);
|
||||||
|
|
@ -208,20 +209,30 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
void goToExercise(int index) {
|
void goToExercise(int index) {
|
||||||
if (index >= 0 && index < _exercises.length) {
|
if (index >= 0 && index < _exercises.length) {
|
||||||
_currentExerciseIndex = index;
|
_currentExerciseIndex = index;
|
||||||
|
print('Going to exercise: $_currentExerciseIndex'); // Debug print
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void nextQuestion() {
|
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++;
|
_currentExerciseIndex++;
|
||||||
|
print('Moving to next question: $_currentExerciseIndex'); // Debug print
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void previousQuestion() {
|
void previousQuestion() {
|
||||||
|
print(
|
||||||
|
'Current index before previous: $_currentExerciseIndex'); // Debug print
|
||||||
if (_currentExerciseIndex > 0) {
|
if (_currentExerciseIndex > 0) {
|
||||||
_currentExerciseIndex--;
|
_currentExerciseIndex--;
|
||||||
|
print(
|
||||||
|
'Moving to previous question: $_currentExerciseIndex'); // Debug print
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,6 +256,7 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
// API methods
|
// API methods
|
||||||
Future<void> fetchExercises(String levelId) async {
|
Future<void> fetchExercises(String levelId) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
_currentExerciseIndex = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -270,33 +282,93 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Di dalam ExerciseProvider class
|
||||||
Future<void> fetchReviewExercises(String stdLearningId) async {
|
Future<void> fetchReviewExercises(String stdLearningId) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_isReview = true;
|
_currentExerciseIndex = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final token = await _userProvider.getValidToken();
|
final token = await _userProvider.getValidToken();
|
||||||
if (token == null) {
|
if (token == null) throw Exception('No valid token found');
|
||||||
throw Exception('No valid token found');
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = await _repository.getStudentAnswers(stdLearningId, token);
|
final response =
|
||||||
_nameTopic = data['NAME_TOPIC'];
|
await _repository.getStudentAnswers(stdLearningId, token);
|
||||||
_nameLevel = data['NAME_LEVEL'];
|
|
||||||
final exercisesData = data['stdExercises'];
|
// Parse the response data
|
||||||
_exercises = exercisesData
|
final payload = response['payload'];
|
||||||
.map<ExerciseModel>((json) => ExerciseModel.fromReviewJson(json))
|
_reviewData = ReviewExerciseModel.fromJson(payload);
|
||||||
.toList();
|
|
||||||
_answers = _exercises.map((e) => e.answerStudent ?? '').toList();
|
// 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) {
|
} catch (e) {
|
||||||
print('Error fetching review exercises: $e');
|
print('Error fetching review exercises: $e');
|
||||||
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
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 {
|
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
|
||||||
print('submitAnswersAndGetScore called');
|
print('submitAnswersAndGetScore called');
|
||||||
try {
|
try {
|
||||||
|
|
@ -398,6 +470,7 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
_nameTopic = previous._nameTopic;
|
_nameTopic = previous._nameTopic;
|
||||||
_nameLevel = previous._nameLevel;
|
_nameLevel = previous._nameLevel;
|
||||||
_studentLearningId = previous._studentLearningId;
|
_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/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_navigator.dart';
|
||||||
import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_progress.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';
|
import 'package:english_learning/features/learning/modules/exercises/widgets/instruction_dialog.dart';
|
||||||
|
|
@ -31,6 +32,7 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
||||||
_scrollController = ScrollController();
|
_scrollController = ScrollController();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final provider = context.read<ExerciseProvider>();
|
final provider = context.read<ExerciseProvider>();
|
||||||
|
|
||||||
if (widget.isReview) {
|
if (widget.isReview) {
|
||||||
provider.fetchReviewExercises(widget.studentLearningId);
|
provider.fetchReviewExercises(widget.studentLearningId);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -62,11 +64,32 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
||||||
body: Center(child: CircularProgressIndicator()),
|
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(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.bgSoftColor,
|
backgroundColor: AppColors.bgSoftColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
automaticallyImplyLeading: false,
|
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),
|
iconTheme: const IconThemeData(color: AppColors.whiteColor),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
@ -81,38 +104,47 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
||||||
gradient: AppColors.gradientTheme,
|
gradient: AppColors.gradientTheme,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: widget.isReview
|
||||||
IconButton(
|
? [
|
||||||
icon: const Icon(Icons.info_outline, color: AppColors.whiteColor),
|
IconButton(
|
||||||
onPressed: () => _showInstructions(context),
|
icon: const Icon(
|
||||||
),
|
BootstrapIcons.info_circle,
|
||||||
],
|
color: AppColors.whiteColor,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
onPressed: () => _showInstructions(context),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
if (!widget.isReview)
|
||||||
padding: EdgeInsets.all(16.0),
|
const Padding(
|
||||||
child: ExerciseProgress(),
|
padding: EdgeInsets.all(16.0),
|
||||||
),
|
child: ExerciseProgress(),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: SingleChildScrollView(
|
||||||
onRefresh: () => provider.fetchExercises(widget.levelId!),
|
controller: _scrollController,
|
||||||
child: SingleChildScrollView(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
controller: _scrollController,
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
child: Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
children: [
|
||||||
child: Column(
|
const SizedBox(height: 16),
|
||||||
children: [
|
ExerciseContent(
|
||||||
if (provider.isReview)
|
key: ValueKey(provider.currentExerciseIndex),
|
||||||
Text('Review Mode', style: TextStyle(fontSize: 24)),
|
exercise: currentExercise,
|
||||||
const SizedBox(height: 16),
|
isReview: widget.isReview,
|
||||||
const ExerciseContent(),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ExerciseNavigator(onScrollToTop: _scrollToTop),
|
ExerciseNavigator(
|
||||||
const SizedBox(height: 32),
|
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/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/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/complete_submission.dart';
|
||||||
import 'package:english_learning/features/learning/modules/exercises/widgets/incomplete_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 {
|
class ExerciseNavigator extends StatelessWidget {
|
||||||
final VoidCallback? onScrollToTop;
|
final VoidCallback? onScrollToTop;
|
||||||
|
final bool isReview;
|
||||||
|
|
||||||
const ExerciseNavigator({
|
const ExerciseNavigator({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onScrollToTop,
|
required this.onScrollToTop,
|
||||||
|
this.isReview = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer2<ExerciseProvider, UserProvider>(
|
return Consumer2<ExerciseProvider, UserProvider>(
|
||||||
builder: (context, exerciseProvider, userProvider, _) {
|
builder: (context, exerciseProvider, userProvider, _) {
|
||||||
final currentExerciseIndex = exerciseProvider.currentExerciseIndex;
|
final currentExerciseIndex = exerciseProvider.currentExerciseIndex;
|
||||||
final isFirstExercise = currentExerciseIndex == 0;
|
final totalExercises = isReview
|
||||||
final isLastExercise =
|
? exerciseProvider.reviewExercises.length
|
||||||
currentExerciseIndex == exerciseProvider.exercises.length - 1;
|
: exerciseProvider.exercises.length;
|
||||||
|
final isFirstExercise = currentExerciseIndex == 0;
|
||||||
|
final isLastExercise = currentExerciseIndex == totalExercises - 1;
|
||||||
|
|
||||||
Future<void> submitAnswers() async {
|
Future<void> submitAnswers() async {
|
||||||
try {
|
try {
|
||||||
final result = await exerciseProvider.submitAnswersAndGetScore();
|
final result = await exerciseProvider.submitAnswersAndGetScore();
|
||||||
print('Submit result: $result'); // Logging
|
print('Submit result: $result');
|
||||||
|
|
||||||
Navigator.of(context).pushReplacement(
|
if (context.mounted) {
|
||||||
MaterialPageRoute(
|
Navigator.of(context).pushReplacement(
|
||||||
builder: (context) => ResultScreen(
|
MaterialPageRoute(
|
||||||
currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
|
builder: (context) => ResultScreen(
|
||||||
nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
|
currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
|
||||||
score: int.tryParse(result['SCORE'].toString()) ?? 0,
|
nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
|
||||||
isCompleted: result['IS_PASS'] == 1,
|
score: int.tryParse(result['SCORE'].toString()) ?? 0,
|
||||||
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
|
isCompleted: result['IS_PASS'] == 1,
|
||||||
),
|
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
print('Error submitting answers: $e'); // Logging
|
}
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
} catch (e) {
|
||||||
SnackBar(content: Text('Error: $e')),
|
print('Error submitting answers: $e');
|
||||||
);
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (!isFirstExercise)
|
if (!isFirstExercise)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
flex: 3,
|
||||||
onPressed: () {
|
child: GlobalButton(
|
||||||
exerciseProvider.previousQuestion();
|
text: 'Previous',
|
||||||
onScrollToTop?.call();
|
height: 45,
|
||||||
},
|
onPressed: () {
|
||||||
style: ElevatedButton.styleFrom(
|
exerciseProvider.previousQuestion();
|
||||||
shadowColor: Colors.transparent,
|
onScrollToTop?.call();
|
||||||
|
},
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
borderColor: AppColors.blueColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
textColor: AppColors.blueColor,
|
||||||
side: const BorderSide(
|
transparentBackground: true,
|
||||||
color: AppColors.blueColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
||||||
child: Text(
|
|
||||||
'Previous',
|
|
||||||
style: AppTextStyles.blueTextStyle.copyWith(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8),
|
||||||
if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8),
|
if (!isLastExercise)
|
||||||
if (!isLastExercise)
|
Expanded(
|
||||||
Expanded(
|
flex: 3,
|
||||||
child: ElevatedButton(
|
child: GlobalButton(
|
||||||
onPressed: () {
|
text: 'Next',
|
||||||
exerciseProvider.nextQuestion();
|
height: 45,
|
||||||
onScrollToTop?.call();
|
onPressed: () {
|
||||||
},
|
exerciseProvider.nextQuestion();
|
||||||
style: ElevatedButton.styleFrom(
|
onScrollToTop?.call();
|
||||||
shadowColor: Colors.transparent,
|
},
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
borderColor: AppColors.blueColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
textColor: AppColors.blueColor,
|
||||||
side: const BorderSide(
|
transparentBackground: true,
|
||||||
color: AppColors.blueColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
||||||
child: Text(
|
|
||||||
'Next',
|
|
||||||
style: AppTextStyles.blueTextStyle.copyWith(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (isLastExercise && !isFirstExercise) const SizedBox(width: 8),
|
||||||
if (isLastExercise && !isFirstExercise) const SizedBox(width: 8),
|
if (isLastExercise)
|
||||||
if (isLastExercise)
|
Expanded(
|
||||||
Expanded(
|
flex: 5,
|
||||||
child: ElevatedButton(
|
child: GlobalButton(
|
||||||
onPressed: () {
|
text: isReview ? 'Back to Level List' : 'Submit',
|
||||||
if (exerciseProvider.hasAnsweredQuestions()) {
|
height: 45,
|
||||||
if (exerciseProvider.hasUnansweredQuestions()) {
|
onPressed: () {
|
||||||
// Ada pertanyaan yang belum dijawab
|
if (isReview) {
|
||||||
showDialog(
|
Navigator.of(context).pop();
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return IncompleteSubmission(
|
|
||||||
onCheckAgain: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
onSubmit: () async {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
submitAnswers();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Semua pertanyaan sudah dijawab
|
if (exerciseProvider.hasAnsweredQuestions()) {
|
||||||
showDialog(
|
if (exerciseProvider.hasUnansweredQuestions()) {
|
||||||
context: context,
|
showDialog(
|
||||||
builder: (BuildContext context) {
|
context: context,
|
||||||
return CompleteSubmission(
|
builder: (BuildContext context) {
|
||||||
onCheckAgain: () {
|
return IncompleteSubmission(
|
||||||
Navigator.of(context).pop();
|
onCheckAgain: () => Navigator.of(context).pop(),
|
||||||
},
|
onSubmit: () async {
|
||||||
onSubmit: () async {
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
submitAnswers();
|
||||||
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
|
gradient: AppColors.gradientTheme,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:flutter/material.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||||
|
|
||||||
|
|
@ -11,7 +13,6 @@ class InstructionsDialog extends StatelessWidget {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.whiteColor,
|
color: AppColors.whiteColor,
|
||||||
shape: BoxShape.rectangle,
|
shape: BoxShape.rectangle,
|
||||||
|
|
@ -24,47 +25,52 @@ class InstructionsDialog extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Padding(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
children: [
|
child: Column(
|
||||||
Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
'Instructions',
|
children: [
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
Text(
|
||||||
fontSize: 22,
|
'Instructions',
|
||||||
fontWeight: FontWeight.bold,
|
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),
|
|
||||||
),
|
),
|
||||||
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}) {
|
Widget _buildInstructionItem({required IconData icon, required String text}) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: AppColors.blueColor),
|
Icon(
|
||||||
const SizedBox(width: 12),
|
icon,
|
||||||
|
color: AppColors.blueColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
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:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.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';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class MatchingPairsQuestion extends StatelessWidget {
|
class MatchingPairsQuestion extends StatelessWidget {
|
||||||
final ExerciseModel exercise;
|
final dynamic exercise;
|
||||||
|
final bool isReview;
|
||||||
|
|
||||||
const MatchingPairsQuestion({
|
const MatchingPairsQuestion({
|
||||||
super.key,
|
super.key,
|
||||||
required this.exercise,
|
required this.exercise,
|
||||||
|
this.isReview = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final provider = Provider.of<ExerciseProvider>(context);
|
final provider = Provider.of<ExerciseProvider>(context);
|
||||||
final matchingPair = exercise.choices as MatchingPair;
|
List<dynamic> pairs = [];
|
||||||
|
Map<String, String> studentAnswers = {};
|
||||||
final currentIndex = provider.currentExerciseIndex;
|
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(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
...matchingPair.pairs.asMap().entries.map((entry) {
|
...pairs.asMap().entries.map((entry) {
|
||||||
int pairIndex = entry.key;
|
int pairIndex = entry.key;
|
||||||
Pair pair = entry.value;
|
dynamic pair = entry.value;
|
||||||
return Row(
|
String left = pair is Pair ? pair.left : pair.leftPair;
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
String right = pair is Pair ? pair.right : pair.rightPair;
|
||||||
children: [
|
|
||||||
Expanded(
|
// Get the corresponding color for this pair
|
||||||
child: _buildOptionItem(
|
Color pairColor = _getPairColor(pairIndex);
|
||||||
context,
|
|
||||||
pair.left,
|
// Find if this item is matched in student answers
|
||||||
provider,
|
String? studentMatchedRight = studentAnswers[left];
|
||||||
currentIndex,
|
String? matchedLeftForRight = reverseStudentAnswers[right];
|
||||||
pairIndex,
|
|
||||||
true,
|
// 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),
|
||||||
const SizedBox(width: 10),
|
Expanded(
|
||||||
Expanded(
|
child: _buildOptionItem(
|
||||||
child: _buildOptionItem(
|
context,
|
||||||
context,
|
right,
|
||||||
pair.right,
|
provider,
|
||||||
provider,
|
currentIndex,
|
||||||
currentIndex,
|
matchedPairIndex ?? pairIndex,
|
||||||
pairIndex,
|
false,
|
||||||
false,
|
matchedLeftForRight != null,
|
||||||
|
studentAnswers,
|
||||||
|
matchedPairIndex != null
|
||||||
|
? _getPairColor(matchedPairIndex)
|
||||||
|
: pairColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).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(
|
Widget _buildOptionItem(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String option,
|
String option,
|
||||||
|
|
@ -61,61 +138,79 @@ class MatchingPairsQuestion extends StatelessWidget {
|
||||||
int exerciseIndex,
|
int exerciseIndex,
|
||||||
int pairIndex,
|
int pairIndex,
|
||||||
bool isLeft,
|
bool isLeft,
|
||||||
|
bool isMatched,
|
||||||
|
Map<String, String> studentAnswers,
|
||||||
|
Color pairColor,
|
||||||
) {
|
) {
|
||||||
Color? color = isLeft
|
Color? color;
|
||||||
? provider.getLeftColor(exerciseIndex, pairIndex)
|
bool isSelected = false;
|
||||||
: provider.getRightColor(exerciseIndex, pairIndex);
|
bool isActive = false;
|
||||||
final isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
|
|
||||||
final isActive = isLeft && provider.activeLeftOption == option;
|
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(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: isReview
|
||||||
if (!isLeft && provider.activeLeftOption == null) {
|
? null
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
: () {
|
||||||
const SnackBar(content: Text('Please select a left option first.')),
|
if (!isLeft && provider.activeLeftOption == null) {
|
||||||
);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
} else {
|
const SnackBar(
|
||||||
provider.answerQuestion(exerciseIndex, option);
|
content: Text('Please select a left option first.'),
|
||||||
}
|
duration: Duration(seconds: 1),
|
||||||
},
|
),
|
||||||
child: Padding(
|
);
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
} else {
|
||||||
child: Container(
|
provider.answerQuestion(exerciseIndex, option);
|
||||||
decoration: BoxDecoration(
|
}
|
||||||
color:
|
},
|
||||||
isSelected ? color : (isActive ? color! : AppColors.whiteColor),
|
child: Container(
|
||||||
borderRadius: const BorderRadius.only(
|
decoration: BoxDecoration(
|
||||||
topRight: Radius.circular(20),
|
color: isSelected
|
||||||
bottomRight: Radius.circular(20),
|
? color ?? pairColor
|
||||||
bottomLeft: Radius.circular(20),
|
: (isActive ? pairColor : AppColors.whiteColor),
|
||||||
),
|
borderRadius: const BorderRadius.only(
|
||||||
border: Border.all(
|
topRight: Radius.circular(20),
|
||||||
color: isSelected
|
bottomRight: Radius.circular(20),
|
||||||
? color ?? AppColors.cardDisabledColor
|
bottomLeft: Radius.circular(20),
|
||||||
: 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,
|
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
border: Border.all(
|
||||||
child: Text(
|
color: isSelected
|
||||||
option,
|
? color ?? AppColors.cardDisabledColor
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
: AppColors.cardDisabledColor,
|
||||||
fontWeight: FontWeight.w900,
|
width: isSelected ? 2 : 1,
|
||||||
color: isSelected || isActive
|
),
|
||||||
? AppColors.whiteColor
|
boxShadow: isActive && !isReview
|
||||||
: AppColors.blackColor,
|
? [
|
||||||
),
|
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:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.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';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class MultipleChoiceQuestion extends StatelessWidget {
|
class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
final ExerciseModel exercise;
|
final dynamic exercise;
|
||||||
|
final bool isReview;
|
||||||
|
|
||||||
const MultipleChoiceQuestion({
|
const MultipleChoiceQuestion({
|
||||||
super.key,
|
super.key,
|
||||||
required this.exercise,
|
required this.exercise,
|
||||||
|
this.isReview = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final provider = Provider.of<ExerciseProvider>(context);
|
final provider = Provider.of<ExerciseProvider>(context);
|
||||||
final multipleChoice = exercise.choices as MultipleChoice;
|
List<String> options = [];
|
||||||
final options = [
|
String? studentAnswer;
|
||||||
multipleChoice.optionA,
|
|
||||||
multipleChoice.optionB,
|
|
||||||
multipleChoice.optionC,
|
|
||||||
multipleChoice.optionD,
|
|
||||||
multipleChoice.optionE,
|
|
||||||
];
|
|
||||||
|
|
||||||
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) {
|
Widget _buildOptionsList(
|
||||||
final optionLabels = List.generate(
|
BuildContext context,
|
||||||
options.length,
|
List<String> options,
|
||||||
(index) =>
|
ExerciseProvider provider,
|
||||||
String.fromCharCode(65 + index), // Generate labels "A", "B", etc.
|
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(
|
bool isSelected = false;
|
||||||
shrinkWrap: true,
|
if (isReview) {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
isSelected = studentAnswer == optionLabel;
|
||||||
itemCount: options.length,
|
} else {
|
||||||
itemBuilder: (context, i) {
|
isSelected =
|
||||||
final option = options[i];
|
provider.answers[provider.currentExerciseIndex] == optionLabel;
|
||||||
final isSelected =
|
}
|
||||||
provider.answers[provider.currentExerciseIndex] == optionLabels[i];
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
onTap: isReview
|
||||||
provider.answerQuestion(provider.currentExerciseIndex, option),
|
? null
|
||||||
child: _buildOptionItem(optionLabels[i], option, isSelected),
|
: () {
|
||||||
|
provider.answerQuestion(
|
||||||
|
provider.currentExerciseIndex, option);
|
||||||
|
},
|
||||||
|
child: _buildOptionItem(optionLabel, option, isSelected),
|
||||||
);
|
);
|
||||||
},
|
}).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOptionItem(String label, String option, bool isSelected) {
|
Widget _buildOptionItem(String label, String option, bool isSelected) {
|
||||||
|
final backgroundColor =
|
||||||
|
isSelected ? AppColors.blueColor : AppColors.whiteColor;
|
||||||
|
final textColor = isSelected ? AppColors.whiteColor : AppColors.blackColor;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -60,7 +92,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(25),
|
borderRadius: BorderRadius.circular(25),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
|
|
@ -68,16 +100,13 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
: AppColors.cardDisabledColor,
|
: AppColors.cardDisabledColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
padding:
|
||||||
padding:
|
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||||
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
child: Text(
|
||||||
child: Text(
|
label,
|
||||||
label,
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
fontWeight: FontWeight.w900,
|
||||||
fontWeight: FontWeight.w900,
|
color: textColor,
|
||||||
color:
|
|
||||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -85,9 +114,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8.0),
|
margin: const EdgeInsets.only(bottom: 8.0),
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
color: backgroundColor,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topRight: Radius.circular(20),
|
topRight: Radius.circular(20),
|
||||||
bottomRight: Radius.circular(20),
|
bottomRight: Radius.circular(20),
|
||||||
|
|
@ -104,8 +132,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
option,
|
option,
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
color:
|
color: textColor,
|
||||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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: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: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 {
|
class TrueFalseQuestion extends StatelessWidget {
|
||||||
final ExerciseModel exercise;
|
final dynamic exercise;
|
||||||
|
final bool isReview;
|
||||||
|
|
||||||
const TrueFalseQuestion({
|
const TrueFalseQuestion({
|
||||||
super.key,
|
super.key,
|
||||||
required this.exercise,
|
required this.exercise,
|
||||||
|
this.isReview = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final provider = Provider.of<ExerciseProvider>(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) {
|
String? _getStudentAnswer() {
|
||||||
return ListView.builder(
|
if (isReview && exercise is ReviewExerciseDetail) {
|
||||||
shrinkWrap: true,
|
return exercise.answerStudent;
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
}
|
||||||
itemCount: options.length,
|
return null;
|
||||||
itemBuilder: (context, i) {
|
}
|
||||||
final option = options[i];
|
|
||||||
final isSelected = provider.answers[provider.currentExerciseIndex] ==
|
Widget _buildOptionsList(
|
||||||
(i == 0 ? '1' : '0');
|
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(
|
return GestureDetector(
|
||||||
// onTap: () =>
|
onTap: isReview
|
||||||
// provider.answerQuestion(provider.currentExerciseIndex, option),
|
? null
|
||||||
onTap: () => provider.answerQuestion(
|
: () {
|
||||||
provider.currentExerciseIndex, i == 0 ? '1' : '0'),
|
provider.answerQuestion(provider.currentExerciseIndex, value);
|
||||||
|
},
|
||||||
child: _buildOptionItem(option, isSelected),
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -51,7 +92,7 @@ class TrueFalseQuestion extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(25),
|
borderRadius: BorderRadius.circular(25),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
|
|
@ -59,18 +100,13 @@ class TrueFalseQuestion extends StatelessWidget {
|
||||||
: AppColors.cardDisabledColor,
|
: AppColors.cardDisabledColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
padding:
|
||||||
padding: const EdgeInsets.symmetric(
|
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||||
horizontal: 14.0,
|
child: Text(
|
||||||
vertical: 10.0,
|
label,
|
||||||
),
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
child: Text(
|
fontWeight: FontWeight.w900,
|
||||||
option,
|
color: textColor,
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
color:
|
|
||||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -78,9 +114,8 @@ class TrueFalseQuestion extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8.0),
|
margin: const EdgeInsets.only(bottom: 8.0),
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? AppColors.blueColor : AppColors.whiteColor,
|
color: backgroundColor,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topRight: Radius.circular(20),
|
topRight: Radius.circular(20),
|
||||||
bottomRight: Radius.circular(20),
|
bottomRight: Radius.circular(20),
|
||||||
|
|
@ -95,10 +130,9 @@ class TrueFalseQuestion extends StatelessWidget {
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
option,
|
text,
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
color:
|
color: textColor,
|
||||||
isSelected ? AppColors.whiteColor : AppColors.blackColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -71,22 +71,6 @@ class LevelProvider with ChangeNotifier {
|
||||||
// _lastCompletedLevel!['ID_LEVEL'] == levelId;
|
// _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) {
|
bool isPretestFinished(String levelId) {
|
||||||
return _levels.any(
|
return _levels.any(
|
||||||
(level) => level.idLevel == levelId && level.idStudentLearning != null);
|
(level) => level.idLevel == levelId && level.idStudentLearning != null);
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,32 @@ class PretestCard extends StatelessWidget {
|
||||||
final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
|
final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
|
||||||
final score = levelProvider.getPretestScore(pretest.idLevel);
|
final score = levelProvider.getPretestScore(pretest.idLevel);
|
||||||
// final isAllowed = levelProvider.isLevelAllowed(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(
|
return Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|
@ -140,17 +166,7 @@ class PretestCard extends StatelessWidget {
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
// : () {},
|
// : () {},
|
||||||
onPressed: () {
|
onPressed: navigateToMaterial,
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => MaterialScreen(
|
|
||||||
levelId: pretest.idLevel,
|
|
||||||
isReview: isFinished,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:english_learning/core/services/dio_client.dart';
|
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/services/repositories/student_learning_repository.dart';
|
||||||
import 'package:english_learning/core/widgets/global_button.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/auth/provider/user_provider.dart';
|
||||||
|
|
@ -15,11 +15,13 @@ import 'package:provider/provider.dart';
|
||||||
class MaterialScreen extends StatefulWidget {
|
class MaterialScreen extends StatefulWidget {
|
||||||
final String levelId;
|
final String levelId;
|
||||||
final bool isReview;
|
final bool isReview;
|
||||||
|
final String? studentLearningId;
|
||||||
|
|
||||||
const MaterialScreen({
|
const MaterialScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.levelId,
|
required this.levelId,
|
||||||
this.isReview = false,
|
this.isReview = false,
|
||||||
|
this.studentLearningId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -59,6 +61,11 @@ class _MaterialScreenState extends State<MaterialScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _createStudentLearning() async {
|
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(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
|
@ -77,15 +84,7 @@ class _MaterialScreenState extends State<MaterialScreen>
|
||||||
print('Student Learning created: ${result['message']}');
|
print('Student Learning created: ${result['message']}');
|
||||||
|
|
||||||
// Navigate to ExerciseScreen
|
// Navigate to ExerciseScreen
|
||||||
Navigator.pushReplacement(
|
_navigateToExercise(result['payload']['ID_STUDENT_LEARNING']);
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ExerciseScreen(
|
|
||||||
levelId: widget.levelId,
|
|
||||||
studentLearningId: result['payload']['ID_STUDENT_LEARNING'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Show error message
|
// Show error message
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<LevelProvider>(builder: (context, levelProvider, child) {
|
return Consumer<LevelProvider>(builder: (context, levelProvider, child) {
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,48 @@ class VideoPlayerWidget extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
late Widget _videoWidget;
|
|
||||||
VideoPlayerController? _videoController;
|
VideoPlayerController? _videoController;
|
||||||
YoutubePlayerController? _youtubeController;
|
YoutubePlayerController? _youtubeController;
|
||||||
FlickManager? _flickManager;
|
FlickManager? _flickManager;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
bool _isYoutubeReady = false;
|
||||||
bool get wantKeepAlive => true;
|
String? _youtubeId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_youtubeId = _extractYoutubeId(widget.videoUrl);
|
||||||
_initializeVideoPlayerWidget();
|
_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) {
|
String _getPlayableUrl(String url) {
|
||||||
if (url.contains('drive.google.com')) {
|
if (url.contains('drive.google.com')) {
|
||||||
final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)');
|
final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)');
|
||||||
|
|
@ -43,54 +70,66 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeVideoPlayerWidget() {
|
Future<void> _initializeVideoPlayerWidget() async {
|
||||||
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) {
|
if (_youtubeId != null) {
|
||||||
_youtubeController = YoutubePlayerController(
|
try {
|
||||||
initialVideoId: YoutubePlayer.convertUrlToId(widget.videoUrl)!,
|
_youtubeController = YoutubePlayerController(
|
||||||
flags: const YoutubePlayerFlags(
|
initialVideoId: _youtubeId!,
|
||||||
autoPlay: false,
|
flags: const YoutubePlayerFlags(
|
||||||
mute: false,
|
autoPlay: false,
|
||||||
),
|
mute: false,
|
||||||
);
|
hideControls: false,
|
||||||
|
controlsVisibleAtStart: true,
|
||||||
|
enableCaption: true,
|
||||||
|
useHybridComposition: true,
|
||||||
|
forceHD: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
_videoWidget = YoutubePlayer(
|
if (mounted) {
|
||||||
controller: _youtubeController!,
|
setState(() {}); // Trigger rebuild with controller
|
||||||
showVideoProgressIndicator: true,
|
}
|
||||||
onReady: () {
|
} catch (e) {
|
||||||
_youtubeController!.addListener(_youtubeListener);
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_error = "Error initializing YouTube player: $e";
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
);
|
}
|
||||||
} else {
|
} else {
|
||||||
_videoController = VideoPlayerController.networkUrl(
|
try {
|
||||||
Uri.parse(_getPlayableUrl(widget.videoUrl)),
|
_videoController = VideoPlayerController.networkUrl(
|
||||||
);
|
Uri.parse(_getPlayableUrl(widget.videoUrl)),
|
||||||
_videoController!.initialize().then((_) {
|
);
|
||||||
|
|
||||||
|
await _videoController!.initialize();
|
||||||
|
|
||||||
_flickManager = FlickManager(
|
_flickManager = FlickManager(
|
||||||
videoPlayerController: _videoController!,
|
videoPlayerController: _videoController!,
|
||||||
autoPlay: false,
|
autoPlay: false,
|
||||||
);
|
);
|
||||||
_videoWidget = FlickVideoPlayer(
|
|
||||||
flickManager: _flickManager!,
|
if (mounted) {
|
||||||
);
|
setState(() {
|
||||||
setState(() {
|
_isLoading = false;
|
||||||
_isLoading = false;
|
});
|
||||||
});
|
}
|
||||||
}).catchError((error) {
|
} catch (e) {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_error = "Error loading video: $error";
|
setState(() {
|
||||||
_isLoading = false;
|
_error = "Error initializing video player: $e";
|
||||||
});
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _youtubeListener() {
|
void _youtubeListener() {
|
||||||
if (_youtubeController!.value.playerState == PlayerState.ended) {
|
if (_youtubeController?.value.playerState == PlayerState.ended) {
|
||||||
_youtubeController!.seekTo(Duration.zero);
|
_youtubeController?.seekTo(Duration.zero);
|
||||||
_youtubeController!.pause();
|
_youtubeController?.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,20 +143,105 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading) {
|
if (_youtubeId != null && _youtubeController != null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return ClipRRect(
|
||||||
} else if (_error != null) {
|
borderRadius: BorderRadius.circular(16),
|
||||||
return Center(child: Text(_error!));
|
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(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
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() {
|
void stopAndResetVideo() {
|
||||||
if (_youtubeController != null) {
|
if (_youtubeController != null) {
|
||||||
_youtubeController!.seekTo(Duration.zero);
|
_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/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/level/screens/level_list_screen.dart';
|
||||||
import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.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/provider/section_provider.dart';
|
||||||
import 'package:english_learning/features/learning/widgets/section_card.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/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:flutter/material.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
|
||||||
|
|
||||||
class LearningScreen extends StatefulWidget {
|
class LearningScreen extends StatefulWidget {
|
||||||
const LearningScreen({
|
const LearningScreen({
|
||||||
|
|
@ -102,9 +102,7 @@ class _LearningScreenState extends State<LearningScreen> {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: 5, // Misalnya, kita menampilkan 5 shimmer items
|
itemCount: 5, // Misalnya, kita menampilkan 5 shimmer items
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Shimmer.fromColors(
|
return ShimmerWidget(
|
||||||
baseColor: Colors.grey[300]!,
|
|
||||||
highlightColor: Colors.grey[100]!,
|
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
child: Row(
|
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:english_learning/features/learning/modules/model/section_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||||
|
|
||||||
class LearningCard extends StatelessWidget {
|
class LearningCard extends StatefulWidget {
|
||||||
final Section section;
|
final Section section;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
|
@ -13,6 +13,12 @@ class LearningCard extends StatelessWidget {
|
||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LearningCard> createState() => _LearningCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LearningCardState extends State<LearningCard>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
String _getFullImageUrl(String thumbnail) {
|
String _getFullImageUrl(String thumbnail) {
|
||||||
if (thumbnail.startsWith('http')) {
|
if (thumbnail.startsWith('http')) {
|
||||||
return thumbnail;
|
return thumbnail;
|
||||||
|
|
@ -24,7 +30,7 @@ class LearningCard extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: widget.onTap,
|
||||||
child: Card(
|
child: Card(
|
||||||
color: AppColors.whiteColor,
|
color: AppColors.whiteColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|
@ -40,7 +46,7 @@ class LearningCard extends StatelessWidget {
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
_getFullImageUrl(section.thumbnail),
|
_getFullImageUrl(widget.section.thumbnail),
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 104,
|
height: 104,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|
@ -72,7 +78,7 @@ class LearningCard extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
section.name,
|
widget.section.name,
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
|
|
@ -80,7 +86,7 @@ class LearningCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
section.description,
|
widget.section.description,
|
||||||
style: AppTextStyles.disableTextStyle.copyWith(
|
style: AppTextStyles.disableTextStyle.copyWith(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class ShimmerWidget extends StatelessWidget {
|
class ShimmerWidget extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const ShimmerWidget({
|
const ShimmerWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'dart:io';
|
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/utils/styles/theme.dart';
|
||||||
import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart';
|
import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart';
|
||||||
import 'package:english_learning/core/widgets/global_button.dart';
|
import 'package:english_learning/core/widgets/global_button.dart';
|
||||||
|
|
@ -60,7 +60,9 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
userProvider.setLoading(true);
|
||||||
await userProvider.updateUserProfile(updatedData);
|
await userProvider.updateUserProfile(updatedData);
|
||||||
|
userProvider.setLoading(false);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -186,7 +188,10 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Save Changes',
|
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: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/provider/user_provider.dart';
|
||||||
import 'package:english_learning/features/auth/screens/signin/signin_screen.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';
|
import 'package:english_learning/features/settings/modules/change_password/screens/change_password_screen.dart';
|
||||||
|
|
|
||||||
|
|
@ -676,10 +676,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.5"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user