refactor(home): improve completed topics API implementation
This commit is contained in:
parent
e28b60ba04
commit
e1e9ad37da
|
|
@ -1,3 +1,5 @@
|
||||||
|
// 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/repositories/constants.dart';
|
||||||
|
|
||||||
|
|
@ -10,24 +12,6 @@ class DioClient {
|
||||||
_dio.options.receiveTimeout = const Duration(seconds: 3);
|
_dio.options.receiveTimeout = const Duration(seconds: 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> post(String path, {dynamic data, Options? options}) async {
|
|
||||||
try {
|
|
||||||
final response = await _dio.post(
|
|
||||||
path,
|
|
||||||
data: data,
|
|
||||||
options:
|
|
||||||
options ?? Options(headers: {'Content-Type': 'application/json'}),
|
|
||||||
);
|
|
||||||
return response;
|
|
||||||
} on DioError catch (e) {
|
|
||||||
print('DioError: ${e.response?.data ?? e.message}');
|
|
||||||
rethrow; // or handle specific DioError here
|
|
||||||
} catch (e) {
|
|
||||||
print('Unexpected error: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Response> refreshAccessToken(String refreshToken) async {
|
Future<Response> refreshAccessToken(String refreshToken) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
|
|
@ -46,6 +30,24 @@ class DioClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Response> post(String path, {dynamic data, Options? options}) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
options:
|
||||||
|
options ?? Options(headers: {'Content-Type': 'application/json'}),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
print('DioError: ${e.response?.data ?? e.message}');
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
print('Unexpected error: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> updateUserProfile(
|
Future<Response> updateUserProfile(
|
||||||
String id, FormData formData, String token) async {
|
String id, FormData formData, String token) async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -268,7 +270,7 @@ class DioClient {
|
||||||
);
|
);
|
||||||
print('getExercises response: ${response.data}');
|
print('getExercises response: ${response.data}');
|
||||||
return response;
|
return response;
|
||||||
} on DioError catch (e) {
|
} on DioException catch (e) {
|
||||||
print(
|
print(
|
||||||
'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}');
|
'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}');
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|
@ -336,4 +338,65 @@ class DioClient {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Response> getCompletedTopics(String token) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
'/topic/complete',
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print('getCompletedTopics response: ${response.data}');
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
print('GetCompletedTopics error: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> studentFeedback(
|
||||||
|
String stdLearningId,
|
||||||
|
String feedback,
|
||||||
|
String token,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.put(
|
||||||
|
'/stdLearning/$stdLearningId',
|
||||||
|
data: {'FEEDBACK_STUDENT': feedback},
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
print('Submit Feedback error: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> getStudentAnswers(String stdLearningId, String token) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
'/studentAnswer/$stdLearningId',
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print('studentAnswers response: ${response.data}');
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
print('StudentAnswers error: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
lib/core/services/local_storage_service.dart
Normal file
0
lib/core/services/local_storage_service.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:english_learning/core/services/dio_client.dart';
|
||||||
|
import 'package:english_learning/features/home/models/completed_topics_model.dart';
|
||||||
|
|
||||||
|
class CompletedTopicsRepository {
|
||||||
|
final DioClient _dioClient;
|
||||||
|
|
||||||
|
CompletedTopicsRepository(this._dioClient);
|
||||||
|
|
||||||
|
Future<List<CompletedTopic>> getCompletedTopics(String token) async {
|
||||||
|
try {
|
||||||
|
final response = await _dioClient.getCompletedTopics(token);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> topicsData = response.data['payload'];
|
||||||
|
return topicsData.map((data) => CompletedTopic.fromJson(data)).toList();
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to load completed topics: ${response.statusMessage}');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception('Network error: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
const String baseUrl = 'https://3311-114-6-25-184.ngrok-free.app/';
|
const String baseUrl =
|
||||||
|
'https://70e7-2001-448a-50a0-2604-b558-2a22-54f6-65ce.ngrok-free.app/';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:english_learning/core/services/dio_client.dart';
|
import 'package:english_learning/core/services/dio_client.dart';
|
||||||
|
import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
|
||||||
|
|
||||||
class ExerciseRepository {
|
class ExerciseRepository {
|
||||||
final DioClient _dioClient;
|
final DioClient _dioClient;
|
||||||
|
|
@ -19,6 +20,20 @@ class ExerciseRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getStudentAnswers(
|
||||||
|
String stdLearningId, String token) async {
|
||||||
|
try {
|
||||||
|
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.data['data'];
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to load student answers');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Error fetching student answers: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitAnswersAndGetScore(
|
Future<Map<String, dynamic>> submitAnswersAndGetScore(
|
||||||
List<Map<String, dynamic>> answers,
|
List<Map<String, dynamic>> answers,
|
||||||
String studentLearningId,
|
String studentLearningId,
|
||||||
|
|
@ -38,4 +53,25 @@ class ExerciseRepository {
|
||||||
throw Exception('Error submitting answers and getting score: $e');
|
throw Exception('Error submitting answers and getting score: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<FeedbackModel> submitFeedback(
|
||||||
|
String stdLearningId,
|
||||||
|
String feedback,
|
||||||
|
String token,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final response = await _dioClient.studentFeedback(
|
||||||
|
stdLearningId,
|
||||||
|
feedback,
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return FeedbackModel.fromJson(response.data['payload']);
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to submit feedback');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Error submitting feedback: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ class LevelRepository {
|
||||||
if (response.statusCode == 200 && response.data != null) {
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
final Map<String, dynamic> responseData = response.data;
|
final Map<String, dynamic> responseData = response.data;
|
||||||
if (responseData.containsKey('data')) {
|
if (responseData.containsKey('data')) {
|
||||||
final Map<String, dynamic> data = responseData['data'];
|
final Map<String, dynamic> data = responseData['data'] ?? {};
|
||||||
final List<dynamic> levelsData = data['levels'] ?? [];
|
final List<dynamic> levelsData = data['levels'] ?? [];
|
||||||
final List<Level> levels =
|
final List<Level> levels =
|
||||||
levelsData.map((json) => Level.fromJson(json)).toList();
|
levelsData.map((json) => Level.fromJson(json)).toList();
|
||||||
final Map<String, dynamic>? lastCompletedLevel =
|
final Map<String, dynamic>? lastCompletedLevel =
|
||||||
data['lastCompletedLevel'];
|
data['lastCompletedLevel'] ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'levels': levels,
|
'levels': levels,
|
||||||
|
|
@ -32,4 +32,20 @@ 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class LearningHistory {
|
class LearningHistory {
|
||||||
final int score;
|
final int score;
|
||||||
final dynamic currentLevel;
|
final String currentLevel;
|
||||||
final dynamic nextLevel;
|
final String? nextLevel;
|
||||||
final DateTime studentFinish;
|
final DateTime? studentFinish;
|
||||||
final String topicName;
|
final String topicName;
|
||||||
final String sectionName;
|
final String sectionName;
|
||||||
|
|
||||||
|
|
@ -19,16 +19,20 @@ class LearningHistory {
|
||||||
|
|
||||||
factory LearningHistory.fromJson(Map<String, dynamic> json) {
|
factory LearningHistory.fromJson(Map<String, dynamic> json) {
|
||||||
return LearningHistory(
|
return LearningHistory(
|
||||||
score: json['SCORE'],
|
score: json['SCORE'] ?? 0,
|
||||||
currentLevel: json['CURRENT_LEVEL'],
|
currentLevel: json['CURRENT_LEVEL'] ?? 'Unknown',
|
||||||
nextLevel: json['NEXT_LEVEL'],
|
nextLevel: json['NEXT_LEVEL'],
|
||||||
studentFinish: DateTime.parse(json['STUDENT_FINISH']),
|
studentFinish: json['STUDENT_FINISH'] != null
|
||||||
topicName: json['TOPIC_NAME'],
|
? DateTime.parse(json['STUDENT_FINISH'])
|
||||||
sectionName: json['SECTION_NAME'],
|
: null,
|
||||||
|
topicName: json['TOPIC_NAME'] ?? 'Unknown Topic',
|
||||||
|
sectionName: json['SECTION_NAME'] ?? 'Unknown Section',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get formattedDate {
|
String get formattedDate {
|
||||||
return DateFormat('yyyy-MM-dd HH:mm').format(studentFinish);
|
return studentFinish != null
|
||||||
|
? DateFormat('yyyy-MM-dd HH:mm').format(studentFinish!)
|
||||||
|
: 'N/A';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,20 +67,50 @@ class HistoryProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _parseLevel(dynamic level) {
|
int? _parseLevel(String? level) {
|
||||||
if (level is int) {
|
if (level == null) return null;
|
||||||
return level;
|
|
||||||
} else if (level is String) {
|
|
||||||
if (level.toLowerCase() == 'pretest') {
|
if (level.toLowerCase() == 'pretest') {
|
||||||
return 0; // Treat Pretest as level 0
|
return 0; // Treat Pretest as level 0
|
||||||
}
|
}
|
||||||
return int.tryParse(level.replaceAll('Level ', ''));
|
return int.tryParse(level.replaceAll('Level ', '')) ?? -1;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshData(String token) async {
|
Future<void> refreshData(String token) async {
|
||||||
|
if (_sectionProvider.sections.isEmpty) {
|
||||||
await _sectionProvider.fetchSections(token);
|
await _sectionProvider.fetchSections(token);
|
||||||
|
}
|
||||||
await fetchLearningHistory(token);
|
await fetchLearningHistory(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:bootstrap_icons/bootstrap_icons.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';
|
||||||
import 'package:english_learning/features/history/provider/history_provider.dart';
|
import 'package:english_learning/features/history/provider/history_provider.dart';
|
||||||
|
|
@ -18,22 +19,28 @@ class HistoryScreen extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryScreenState extends State<HistoryScreen> {
|
class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
@override
|
bool isNotFoundError(String error) {
|
||||||
void initState() {
|
return error.toLowerCase().contains('no learning history found') ||
|
||||||
super.initState();
|
error.toLowerCase().contains('not found');
|
||||||
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
|
||||||
|
// 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>(
|
||||||
|
builder: (context, historyProvider, chiild) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.bgSoftColor,
|
backgroundColor: AppColors.bgSoftColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|
@ -79,9 +86,14 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
child: Consumer<HistoryProvider>(
|
child: Consumer<HistoryProvider>(
|
||||||
builder: (context, historyProvider, child) {
|
builder: (context, historyProvider, child) {
|
||||||
if (historyProvider.isLoading) {
|
if (historyProvider.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(
|
||||||
|
child: CircularProgressIndicator());
|
||||||
} else if (historyProvider.error != null) {
|
} else if (historyProvider.error != null) {
|
||||||
return Center(child: Text(historyProvider.error!));
|
if (isNotFoundError(historyProvider.error!)) {
|
||||||
|
return _buildEmptyState(context);
|
||||||
|
} else {
|
||||||
|
return _buildErrorState(historyProvider.error!);
|
||||||
|
}
|
||||||
} else if (historyProvider.learningHistory.isEmpty) {
|
} else if (historyProvider.learningHistory.isEmpty) {
|
||||||
return _buildEmptyState(context);
|
return _buildEmptyState(context);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -109,6 +121,41 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorState(String error) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
BootstrapIcons.exclamation_diamond_fill,
|
||||||
|
color: AppColors.blueColor,
|
||||||
|
size: 82,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Error: $error',
|
||||||
|
style: AppTextStyles.redTextStyle.copyWith(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GlobalButton(
|
||||||
|
onPressed: () {
|
||||||
|
final historyProvider =
|
||||||
|
Provider.of<HistoryProvider>(context, listen: false);
|
||||||
|
final userProvider =
|
||||||
|
Provider.of<UserProvider>(context, listen: false);
|
||||||
|
historyProvider.refreshData(userProvider.jwtToken!);
|
||||||
|
},
|
||||||
|
text: 'Try Again',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(BuildContext context) {
|
Widget _buildEmptyState(BuildContext context) {
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,8 @@ 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';
|
||||||
|
|
||||||
class CustomTabBar extends StatefulWidget {
|
class CustomTabBar extends StatelessWidget {
|
||||||
const CustomTabBar({super.key});
|
const CustomTabBar({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
_CustomTabBarState createState() => _CustomTabBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CustomTabBarState extends State<CustomTabBar> {
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -27,12 +14,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
|
||||||
final sectionProvider = Provider.of<SectionProvider>(context);
|
final sectionProvider = Provider.of<SectionProvider>(context);
|
||||||
final selectedPageIndex = historyProvider.selectedPageIndex;
|
final selectedPageIndex = historyProvider.selectedPageIndex;
|
||||||
|
|
||||||
if (sectionProvider.sections.isEmpty) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: _scrollController,
|
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: sectionProvider.sections.asMap().entries.map((entry) {
|
children: sectionProvider.sections.asMap().entries.map((entry) {
|
||||||
|
|
@ -56,12 +38,6 @@ class _CustomTabBarState extends State<CustomTabBar> {
|
||||||
Provider.of<HistoryProvider>(context, listen: false);
|
Provider.of<HistoryProvider>(context, listen: false);
|
||||||
historyProvider.setSelectedPageIndex(index);
|
historyProvider.setSelectedPageIndex(index);
|
||||||
|
|
||||||
_scrollController.animateTo(
|
|
||||||
index * 100.0,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
|
|
||||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||||
historyProvider.fetchLearningHistory(userProvider.jwtToken!);
|
historyProvider.fetchLearningHistory(userProvider.jwtToken!);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,47 +28,35 @@ class ExerciseHistoryCard extends StatelessWidget {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0),
|
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
RichText(
|
Expanded(
|
||||||
text: TextSpan(
|
child: Column(
|
||||||
style: AppTextStyles.disableTextStyle.copyWith(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
Text(
|
||||||
text: '${exercise.topicName} /',
|
exercise.topicName,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
TextSpan(
|
|
||||||
text: ' ${exercise.sectionName}',
|
|
||||||
style: AppTextStyles.blackTextStyle)
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: AppTextStyles.greyTextStyle.copyWith(
|
style: AppTextStyles.greyTextStyle.copyWith(
|
||||||
fontSize: 12,
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${exercise.currentLevel} → ',
|
text: '${exercise.currentLevel} → ',
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
style: AppTextStyles.blackTextStyle.copyWith(),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${exercise.nextLevel}',
|
text: '${exercise.nextLevel}',
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
color: color,
|
color: color,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -84,6 +72,8 @@ class ExerciseHistoryCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16.0,
|
||||||
|
|
@ -106,9 +96,6 @@ class ExerciseHistoryCard extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
lib/features/home/models/completed_topics_model.dart
Normal file
28
lib/features/home/models/completed_topics_model.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
class CompletedTopic {
|
||||||
|
final String idSection;
|
||||||
|
final String nameSection;
|
||||||
|
final String descriptionSection;
|
||||||
|
final String thumbnail;
|
||||||
|
final int totalTopics;
|
||||||
|
final int completedTopics;
|
||||||
|
|
||||||
|
CompletedTopic({
|
||||||
|
required this.idSection,
|
||||||
|
required this.nameSection,
|
||||||
|
required this.descriptionSection,
|
||||||
|
required this.thumbnail,
|
||||||
|
required this.totalTopics,
|
||||||
|
required this.completedTopics,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CompletedTopic.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CompletedTopic(
|
||||||
|
idSection: json['ID_SECTION'],
|
||||||
|
nameSection: json['NAME_SECTION'],
|
||||||
|
descriptionSection: json['DESCRIPTION_SECTION'],
|
||||||
|
thumbnail: json['THUMBNAIL'],
|
||||||
|
totalTopics: json['TOTAL_TOPICS'],
|
||||||
|
completedTopics: json['COMPLETED_TOPICS'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/features/home/provider/completed_topics_provider.dart
Normal file
34
lib/features/home/provider/completed_topics_provider.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// lib/features/home/providers/completed_topics_provider.dart
|
||||||
|
|
||||||
|
import 'package:english_learning/core/services/repositories/completed_topics_repository.dart';
|
||||||
|
import 'package:english_learning/features/home/models/completed_topics_model.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class CompletedTopicsProvider with ChangeNotifier {
|
||||||
|
final CompletedTopicsRepository _repository;
|
||||||
|
List<CompletedTopic> _completedTopics = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
CompletedTopicsProvider(this._repository);
|
||||||
|
|
||||||
|
List<CompletedTopic> get completedTopics => _completedTopics;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get error => _error;
|
||||||
|
|
||||||
|
Future<void> fetchCompletedTopics(String token) async {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_completedTopics = await _repository.getCompletedTopics(token);
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
print('Error fetching completed topics: $_error');
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
|
import 'package:english_learning/core/widgets/custom_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/history/provider/history_provider.dart';
|
||||||
import 'package:english_learning/features/history/screens/history_screen.dart';
|
import 'package:english_learning/features/history/screens/history_screen.dart';
|
||||||
import 'package:english_learning/features/home/data/card_data.dart';
|
import 'package:english_learning/features/home/data/card_data.dart';
|
||||||
|
import 'package:english_learning/features/home/provider/completed_topics_provider.dart';
|
||||||
import 'package:english_learning/features/home/widgets/progress_card.dart';
|
import 'package:english_learning/features/home/widgets/progress_card.dart';
|
||||||
import 'package:english_learning/features/home/widgets/welcome_card.dart';
|
import 'package:english_learning/features/home/widgets/welcome_card.dart';
|
||||||
import 'package:english_learning/features/learning/screens/learning_screen.dart';
|
import 'package:english_learning/features/learning/screens/learning_screen.dart';
|
||||||
|
|
@ -14,6 +17,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:google_nav_bar/google_nav_bar.dart';
|
import 'package:google_nav_bar/google_nav_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
@ -78,15 +82,19 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onTabChange: (index) {
|
onTabChange: (index) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedIndex = index;
|
_selectedIndex = index;
|
||||||
_pageController.animateToPage(
|
_pageController.jumpToPage(index);
|
||||||
index,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeInOut, // Animasi ketika berpindah tab
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
if (index == 2) {
|
||||||
|
// Index 2 adalah tab History
|
||||||
|
final historyProvider =
|
||||||
|
Provider.of<HistoryProvider>(context, listen: false);
|
||||||
|
final userProvider =
|
||||||
|
Provider.of<UserProvider>(context, listen: false);
|
||||||
|
await historyProvider.loadHistoryData(userProvider.jwtToken!);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
|
|
@ -127,14 +135,89 @@ class HomeContent extends StatefulWidget {
|
||||||
class _HomeContentState extends State<HomeContent> {
|
class _HomeContentState extends State<HomeContent> {
|
||||||
final CardData cardData = CardData();
|
final CardData cardData = CardData();
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
bool hasOngoingExercises = false;
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Memanggil fetchCompletedTopics saat HomeContent diinisialisasi
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||||
|
final completedTopicsProvider =
|
||||||
|
Provider.of<CompletedTopicsProvider>(context, listen: false);
|
||||||
|
completedTopicsProvider.fetchCompletedTopics(userProvider.jwtToken!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNoDataWidget() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Still new?',
|
||||||
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Begin your journey!',
|
||||||
|
style: AppTextStyles.disableTextStyle.copyWith(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
CustomButton(
|
||||||
|
text: 'Explore',
|
||||||
|
width: double.infinity,
|
||||||
|
height: 44,
|
||||||
|
color: AppColors.yellowButtonColor,
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShimmerEffect() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
|
child: Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey[300]!,
|
||||||
|
highlightColor: Colors.grey[100]!,
|
||||||
|
child: Column(
|
||||||
|
children: List.generate(
|
||||||
|
2,
|
||||||
|
(index) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 150,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<UserProvider>(builder: (context, authProvider, child) {
|
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
|
||||||
|
context,
|
||||||
|
authProvider,
|
||||||
|
completedTopicsProvider,
|
||||||
|
child,
|
||||||
|
) {
|
||||||
final userName = authProvider.getUserName() ?? 'Guest';
|
final userName = authProvider.getUserName() ?? 'Guest';
|
||||||
|
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Stack(
|
Stack(
|
||||||
|
|
@ -173,7 +256,8 @@ class _HomeContentState extends State<HomeContent> {
|
||||||
const SizedBox(width: 4.34),
|
const SizedBox(width: 4.34),
|
||||||
Text(
|
Text(
|
||||||
'SEALS',
|
'SEALS',
|
||||||
style: AppTextStyles.logoTextStyle.copyWith(
|
style:
|
||||||
|
AppTextStyles.logoTextStyle.copyWith(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
|
|
@ -209,14 +293,16 @@ class _HomeContentState extends State<HomeContent> {
|
||||||
children: <TextSpan>[
|
children: <TextSpan>[
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: userName,
|
text: userName,
|
||||||
style: AppTextStyles.yellowTextStyle.copyWith(
|
style:
|
||||||
|
AppTextStyles.yellowTextStyle.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '!',
|
text: '!',
|
||||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
style:
|
||||||
|
AppTextStyles.whiteTextStyle.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
|
|
@ -269,22 +355,74 @@ class _HomeContentState extends State<HomeContent> {
|
||||||
itemCount: cardData.cardData.length,
|
itemCount: cardData.cardData.length,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.only(
|
||||||
child: ProgressCard(),
|
top: 8.0,
|
||||||
|
left: 24.0,
|
||||||
|
right: 24.0,
|
||||||
|
bottom: 47.0,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.2),
|
||||||
|
spreadRadius: 2,
|
||||||
|
blurRadius: 5,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
),
|
),
|
||||||
// hasOngoingExercises
|
|
||||||
// ? const Padding(
|
|
||||||
// padding: EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
// child:
|
|
||||||
// ProgressCard(), // Display progress card if exercises are completed
|
|
||||||
// )
|
|
||||||
// : const Padding(
|
|
||||||
// padding: EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
// child:
|
|
||||||
// ExploreCard(), // Display ExploreCard if no exercises are completed
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
BootstrapIcons.info_circle,
|
||||||
|
color: AppColors.tetriaryColor,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Your Last Journey.',
|
||||||
|
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
completedTopicsProvider.isLoading
|
||||||
|
? _buildShimmerEffect()
|
||||||
|
: completedTopicsProvider.completedTopics.isEmpty
|
||||||
|
? _buildNoDataWidget()
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
itemCount: 1,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ProgressCard(
|
||||||
|
completedTopic: completedTopicsProvider
|
||||||
|
.completedTopics, // Kirim seluruh list
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
lib/features/home/widgets/no_progress_card.dart
Normal file
16
lib/features/home/widgets/no_progress_card.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||||
|
|
||||||
|
class NoProgressCard extends StatelessWidget {
|
||||||
|
const NoProgressCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,31 +2,34 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||||
|
|
||||||
class ProgressBar extends StatelessWidget {
|
class ProgressBar extends StatelessWidget {
|
||||||
final int currentProgress;
|
final int completedTopics;
|
||||||
final int totalProgress;
|
final int totalTopics;
|
||||||
|
|
||||||
const ProgressBar({
|
const ProgressBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.currentProgress,
|
required this.completedTopics,
|
||||||
required this.totalProgress,
|
required this.totalTopics,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final progress = totalProgress > 0 ? currentProgress / totalProgress : 0.0;
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final screenHeight = mediaQuery.size.height;
|
||||||
|
final progress = totalTopics > 0 ? completedTopics / totalTopics : 0.0;
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final barWidth = constraints.maxWidth - 40;
|
final barWidth = constraints.maxWidth - 30;
|
||||||
|
|
||||||
return Row(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: barWidth,
|
width: double.infinity,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 12,
|
height: 12,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade300,
|
color: Colors.grey.shade200,
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: BorderRadius.circular(7),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
|
@ -44,13 +47,26 @@ class ProgressBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
// const Spacer(),
|
||||||
Text(
|
SizedBox(
|
||||||
'$currentProgress/$totalProgress',
|
height: screenHeight * 0.02,
|
||||||
|
),
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '$completedTopics/$totalTopics ',
|
||||||
|
style: AppTextStyles.blueTextStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'Topics Completed',
|
||||||
style: AppTextStyles.blueTextStyle.copyWith(
|
style: AppTextStyles.blueTextStyle.copyWith(
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,103 @@
|
||||||
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/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/widgets/progress_bar.dart';
|
import 'package:english_learning/features/home/widgets/progress_bar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ProgressCard extends StatelessWidget {
|
class ProgressCard extends StatelessWidget {
|
||||||
const ProgressCard({super.key});
|
final List<CompletedTopic> completedTopic;
|
||||||
|
|
||||||
|
const ProgressCard({super.key, required this.completedTopic});
|
||||||
|
|
||||||
|
String _getFullImageUrl(String thumbnail) {
|
||||||
|
return thumbnail.startsWith('http')
|
||||||
|
? thumbnail
|
||||||
|
: '${baseUrl}uploads/section/$thumbnail';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
...completedTopic.asMap().entries.map(
|
||||||
width: double.infinity,
|
(entry) {
|
||||||
decoration: BoxDecoration(
|
CompletedTopic topic = entry.value;
|
||||||
color: AppColors.whiteColor,
|
return Padding(
|
||||||
borderRadius: BorderRadius.circular(12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
),
|
child: _buildTopicItem(topic),
|
||||||
child: Padding(
|
);
|
||||||
padding: const EdgeInsets.symmetric(
|
},
|
||||||
horizontal: 16.0,
|
|
||||||
vertical: 16.0,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
BootstrapIcons.info_circle,
|
|
||||||
color: AppColors.disableColor,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Your Last Journey!',
|
|
||||||
style: AppTextStyles.disableTextStyle.copyWith(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
const SizedBox(height: 24),
|
}
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
Widget _buildTopicItem(CompletedTopic topic) {
|
||||||
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
color: AppColors.disableColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
_getFullImageUrl(topic.thumbnail),
|
||||||
|
width: 90,
|
||||||
|
height: 130,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Container(
|
||||||
|
width: 90,
|
||||||
|
height: 130,
|
||||||
|
color: Colors.grey[300],
|
||||||
|
child: const Icon(
|
||||||
|
Icons.image_not_supported,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Listening',
|
topic.nameSection,
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
fontSize: 15,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Topic 8: Entertaining | Level 3',
|
topic.descriptionSection,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: AppTextStyles.disableTextStyle.copyWith(
|
style: AppTextStyles.disableTextStyle.copyWith(
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 24),
|
||||||
const ProgressBar(
|
ProgressBar(
|
||||||
currentProgress: 8,
|
completedTopics: topic.completedTopics,
|
||||||
totalProgress: 11,
|
totalTopics: topic.totalTopics,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ class ExerciseModel {
|
||||||
final String? image;
|
final String? image;
|
||||||
final DateTime timeAdminExc;
|
final DateTime timeAdminExc;
|
||||||
final dynamic choices;
|
final dynamic choices;
|
||||||
|
final String? answerStudent;
|
||||||
|
final bool? isCorrect;
|
||||||
|
|
||||||
ExerciseModel({
|
ExerciseModel({
|
||||||
required this.idAdminExercise,
|
required this.idAdminExercise,
|
||||||
|
|
@ -21,6 +23,8 @@ class ExerciseModel {
|
||||||
this.image,
|
this.image,
|
||||||
required this.timeAdminExc,
|
required this.timeAdminExc,
|
||||||
required this.choices,
|
required this.choices,
|
||||||
|
this.answerStudent,
|
||||||
|
this.isCorrect,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ExerciseModel.fromJson(Map<String, dynamic> json) {
|
factory ExerciseModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -57,6 +61,21 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
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';
|
||||||
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
||||||
|
|
@ -23,6 +24,7 @@ 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 = [
|
||||||
|
|
@ -42,6 +44,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;
|
||||||
|
|
||||||
// Initialization methods
|
// Initialization methods
|
||||||
void initializeAnswers() {
|
void initializeAnswers() {
|
||||||
|
|
@ -64,11 +67,40 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Answer handling methods
|
// // Answer handling methods
|
||||||
|
// void answerQuestion(int index, String answer) {
|
||||||
|
// if (index >= 0 && index < _answers.length) {
|
||||||
|
// if (_exercises[index].choices is MatchingPair) {
|
||||||
|
// _handleMatchingPairAnswer(index, answer);
|
||||||
|
// } else {
|
||||||
|
// _answers[index] = _answers[index] == answer ? '' : answer;
|
||||||
|
// }
|
||||||
|
// notifyListeners();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
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);
|
||||||
|
} else if (_exercises[index].choices is MultipleChoice) {
|
||||||
|
// Store the letter index (e.g., "A", "B", "C", etc.)
|
||||||
|
final multipleChoice = _exercises[index].choices as MultipleChoice;
|
||||||
|
final options = [
|
||||||
|
multipleChoice.optionA,
|
||||||
|
multipleChoice.optionB,
|
||||||
|
multipleChoice.optionC,
|
||||||
|
multipleChoice.optionD,
|
||||||
|
multipleChoice.optionE,
|
||||||
|
];
|
||||||
|
final optionIndex = options.indexOf(answer);
|
||||||
|
if (optionIndex != -1) {
|
||||||
|
_answers[index] = String.fromCharCode(65 + optionIndex);
|
||||||
|
}
|
||||||
|
} else if (_exercises[index].choices is TrueFalse) {
|
||||||
|
// Store "1" for true and "0" for false
|
||||||
|
// _answers[index] = answer.toLowerCase() == 'true' ? '1' : '0';
|
||||||
|
_answers[index] = answer;
|
||||||
} else {
|
} else {
|
||||||
_answers[index] = _answers[index] == answer ? '' : answer;
|
_answers[index] = _answers[index] == answer ? '' : answer;
|
||||||
}
|
}
|
||||||
|
|
@ -238,6 +270,33 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchReviewExercises(String stdLearningId) async {
|
||||||
|
_isLoading = true;
|
||||||
|
_isReview = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final token = await _userProvider.getValidToken();
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('No valid token found');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = await _repository.getStudentAnswers(stdLearningId, token);
|
||||||
|
_nameTopic = data['NAME_TOPIC'];
|
||||||
|
_nameLevel = data['NAME_LEVEL'];
|
||||||
|
final exercisesData = data['stdExercises'];
|
||||||
|
_exercises = exercisesData
|
||||||
|
.map<ExerciseModel>((json) => ExerciseModel.fromReviewJson(json))
|
||||||
|
.toList();
|
||||||
|
_answers = _exercises.map((e) => e.answerStudent ?? '').toList();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching review exercises: $e');
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
|
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
|
||||||
print('submitAnswersAndGetScore called');
|
print('submitAnswersAndGetScore called');
|
||||||
try {
|
try {
|
||||||
|
|
@ -256,10 +315,17 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
}).map((entry) {
|
}).map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final exercise = entry.value;
|
final exercise = entry.value;
|
||||||
|
String formattedAnswer = _answers[index];
|
||||||
|
if (exercise.choices is MultipleChoice) {
|
||||||
|
formattedAnswer = formattedAnswer;
|
||||||
|
} else if (exercise.choices is TrueFalse) {
|
||||||
|
formattedAnswer = formattedAnswer.toLowerCase() == 'true' ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'ID_STUDENT_LEARNING': _studentLearningId!,
|
'ID_STUDENT_LEARNING': _studentLearningId!,
|
||||||
'ID_ADMIN_EXERCISE': exercise.idAdminExercise,
|
'ID_ADMIN_EXERCISE': exercise.idAdminExercise,
|
||||||
'ANSWER_STUDENT': _answers[index],
|
'ANSWER_STUDENT': formattedAnswer,
|
||||||
};
|
};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
|
@ -286,6 +352,26 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<FeedbackModel> submitFeedback(String feedback) async {
|
||||||
|
try {
|
||||||
|
final token = await _userProvider.getValidToken();
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('No valid token found');
|
||||||
|
}
|
||||||
|
if (_studentLearningId == null) {
|
||||||
|
throw Exception('Student Learning ID is not set');
|
||||||
|
}
|
||||||
|
print('Submitting feedback for stdLearningId: $_studentLearningId');
|
||||||
|
final result = await _repository.submitFeedback(
|
||||||
|
_studentLearningId!, feedback, token);
|
||||||
|
print('Feedback submitted successfully');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error submitting feedback: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool hasAnsweredQuestions() {
|
bool hasAnsweredQuestions() {
|
||||||
return _answers.any((answer) => answer.isNotEmpty);
|
return _answers.any((answer) => answer.isNotEmpty);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@ import 'package:provider/provider.dart';
|
||||||
class ExerciseScreen extends StatefulWidget {
|
class ExerciseScreen extends StatefulWidget {
|
||||||
final String? levelId;
|
final String? levelId;
|
||||||
final String studentLearningId;
|
final String studentLearningId;
|
||||||
|
final bool isReview;
|
||||||
|
|
||||||
const ExerciseScreen({
|
const ExerciseScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.levelId,
|
required this.levelId,
|
||||||
required this.studentLearningId,
|
required this.studentLearningId,
|
||||||
|
this.isReview = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -29,7 +31,11 @@ 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) {
|
||||||
|
provider.fetchReviewExercises(widget.studentLearningId);
|
||||||
|
} else {
|
||||||
provider.fetchExercises(widget.levelId!);
|
provider.fetchExercises(widget.levelId!);
|
||||||
|
}
|
||||||
provider.setStudentLearningId(widget.studentLearningId);
|
provider.setStudentLearningId(widget.studentLearningId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +104,8 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (provider.isReview)
|
||||||
|
Text('Review Mode', style: TextStyle(fontSize: 24)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const ExerciseContent(),
|
const ExerciseContent(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,11 @@ class ExerciseNavigator extends StatelessWidget {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ResultScreen(
|
builder: (context) => ResultScreen(
|
||||||
currentLevel: result['CURRENT_LEVEL_NAME'],
|
currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
|
||||||
nextLevel: result['NEXT_LEARNING_NAME'],
|
nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
|
||||||
score: int.parse(result['SCORE'].toString()),
|
score: int.tryParse(result['SCORE'].toString()) ?? 0,
|
||||||
isCompleted: result['IS_PASS'] == 1,
|
isCompleted: result['IS_PASS'] == 1,
|
||||||
|
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// multiple_choice_question.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';
|
||||||
|
|
@ -31,7 +30,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) {
|
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) {
|
||||||
final optionLabels = List.generate(
|
final optionLabels = List.generate(
|
||||||
options.length,
|
options.length,
|
||||||
(index) => String.fromCharCode(65 + index),
|
(index) =>
|
||||||
|
String.fromCharCode(65 + index), // Generate labels "A", "B", etc.
|
||||||
);
|
);
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
|
@ -41,7 +41,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final option = options[i];
|
final option = options[i];
|
||||||
final isSelected =
|
final isSelected =
|
||||||
provider.answers[provider.currentExerciseIndex] == option;
|
provider.answers[provider.currentExerciseIndex] == optionLabels[i];
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
|
|
@ -69,10 +69,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding:
|
||||||
horizontal: 14.0,
|
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||||
vertical: 10.0,
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,15 @@ class TrueFalseQuestion extends StatelessWidget {
|
||||||
itemCount: options.length,
|
itemCount: options.length,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final option = options[i];
|
final option = options[i];
|
||||||
final isSelected =
|
final isSelected = provider.answers[provider.currentExerciseIndex] ==
|
||||||
provider.answers[provider.currentExerciseIndex] == option;
|
(i == 0 ? '1' : '0');
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
// onTap: () =>
|
||||||
provider.answerQuestion(provider.currentExerciseIndex, option),
|
// provider.answerQuestion(provider.currentExerciseIndex, option),
|
||||||
|
onTap: () => provider.answerQuestion(
|
||||||
|
provider.currentExerciseIndex, i == 0 ? '1' : '0'),
|
||||||
|
|
||||||
child: _buildOptionItem(option, isSelected),
|
child: _buildOptionItem(option, isSelected),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
class FeedbackModel {
|
||||||
|
final String idStudentLearning;
|
||||||
|
final String id;
|
||||||
|
final String idLevel;
|
||||||
|
final DateTime studentStart;
|
||||||
|
final DateTime studentFinish;
|
||||||
|
final int score;
|
||||||
|
final bool isPass;
|
||||||
|
final String nextLearning;
|
||||||
|
final String feedbackStudent;
|
||||||
|
final DateTime timeLearning;
|
||||||
|
|
||||||
|
FeedbackModel({
|
||||||
|
required this.idStudentLearning,
|
||||||
|
required this.id,
|
||||||
|
required this.idLevel,
|
||||||
|
required this.studentStart,
|
||||||
|
required this.studentFinish,
|
||||||
|
required this.score,
|
||||||
|
required this.isPass,
|
||||||
|
required this.nextLearning,
|
||||||
|
required this.feedbackStudent,
|
||||||
|
required this.timeLearning,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FeedbackModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return FeedbackModel(
|
||||||
|
idStudentLearning: json['ID_STUDENT_LEARNING'],
|
||||||
|
id: json['ID'],
|
||||||
|
idLevel: json['ID_LEVEL'],
|
||||||
|
studentStart: DateTime.parse(json['STUDENT_START']),
|
||||||
|
studentFinish: DateTime.parse(json['STUDENT_FINISH']),
|
||||||
|
score: json['SCORE'],
|
||||||
|
isPass: json['IS_PASS'] == 1,
|
||||||
|
nextLearning: json['NEXT_LEARNING'],
|
||||||
|
feedbackStudent: json['FEEDBACK_STUDENT'],
|
||||||
|
timeLearning: DateTime.parse(json['TIME_LEARNING']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
import 'package:english_learning/core/widgets/global_button.dart';
|
import 'package:english_learning/core/widgets/global_button.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||||
|
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||||
import 'package:english_learning/features/learning/modules/feedback/widgets/feedback_dialog.dart';
|
import 'package:english_learning/features/learning/modules/feedback/widgets/feedback_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class FeedbackScreen extends StatefulWidget {
|
class FeedbackScreen extends StatefulWidget {
|
||||||
const FeedbackScreen({super.key});
|
final String stdLearningId;
|
||||||
|
|
||||||
|
const FeedbackScreen({
|
||||||
|
super.key,
|
||||||
|
required this.stdLearningId,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FeedbackScreen> createState() => _FeedbackScreenState();
|
State<FeedbackScreen> createState() => _FeedbackScreenState();
|
||||||
|
|
@ -33,6 +40,58 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _submitFeedback() async {
|
||||||
|
if (_controller.text.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please enter your feedback before submitting.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final exerciseProvider =
|
||||||
|
Provider.of<ExerciseProvider>(context, listen: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await exerciseProvider.submitFeedback(_controller.text);
|
||||||
|
|
||||||
|
print('Feedback submitted successfully: $result');
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Feedback submitted successfully!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show the dialog
|
||||||
|
if (mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
return FeedbackDialog(
|
||||||
|
onSubmit: () {
|
||||||
|
Navigator.of(dialogContext).pop(); // Close the dialog
|
||||||
|
Navigator.of(context).pop(); // Return to the previous screen
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error submitting feedback: $e');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to submit feedback: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|
@ -121,23 +180,7 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Send',
|
text: 'Send',
|
||||||
onPressed: () {
|
onPressed: _submitFeedback,
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return FeedbackDialog(
|
|
||||||
onSubmit: () {
|
|
||||||
// Navigator.push(
|
|
||||||
// context,
|
|
||||||
// MaterialPageRoute(
|
|
||||||
// builder: (context) => const LevelListScreen(),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
|
|
@ -149,9 +192,13 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
|
||||||
// Navigator.push(
|
// Navigator.push(
|
||||||
// context,
|
// context,
|
||||||
// MaterialPageRoute(
|
// MaterialPageRoute(
|
||||||
// builder: (context) => const LevelListScreen(),
|
// builder: (context) => LevelListScreen(
|
||||||
|
// topicId: widget.topicId!,
|
||||||
|
// topicTitle: widget.topicTitle!,
|
||||||
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ class FeedbackDialog extends StatelessWidget {
|
||||||
// builder: (context) => const LevelListScreen(),
|
// builder: (context) => const LevelListScreen(),
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
|
onSubmit();
|
||||||
|
// Navigator.of(context).popUntil(
|
||||||
|
// (route) => route.isFirst,
|
||||||
|
// );
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,17 @@ class Level {
|
||||||
|
|
||||||
factory Level.fromJson(Map<String, dynamic> json) {
|
factory Level.fromJson(Map<String, dynamic> json) {
|
||||||
return Level(
|
return Level(
|
||||||
idLevel: json['ID_LEVEL'],
|
idLevel: json['ID_LEVEL'] ?? '',
|
||||||
idTopic: json['ID_TOPIC'],
|
idTopic: json['ID_TOPIC'] ?? '',
|
||||||
idSection: json['ID_SECTION'],
|
idSection: json['ID_SECTION'] ?? '',
|
||||||
nameSection: json['NAME_SECTION'],
|
nameSection: json['NAME_SECTION'] ?? '',
|
||||||
nameTopic: json['NAME_TOPIC'],
|
nameTopic: json['NAME_TOPIC'] ?? '',
|
||||||
nameLevel: json['NAME_LEVEL'],
|
nameLevel: json['NAME_LEVEL'] ?? '',
|
||||||
content: json['CONTENT'],
|
content: json['CONTENT'] ?? '',
|
||||||
audio: json['AUDIO'],
|
audio: json['AUDIO'],
|
||||||
image: json['IMAGE'],
|
image: json['IMAGE'],
|
||||||
video: json['VIDEO'],
|
video: json['VIDEO'],
|
||||||
isPretest: json['IS_PRETEST'],
|
isPretest: json['IS_PRETEST'] ?? 0,
|
||||||
timeLevel: json['TIME_LEVEL'],
|
timeLevel: json['TIME_LEVEL'],
|
||||||
idStudentLearning: json['ID_STUDENT_LEARNING'],
|
idStudentLearning: json['ID_STUDENT_LEARNING'],
|
||||||
score: json['SCORE'],
|
score: json['SCORE'],
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,17 @@ class LevelProvider with ChangeNotifier {
|
||||||
final LevelRepository _levelRepository = LevelRepository();
|
final LevelRepository _levelRepository = LevelRepository();
|
||||||
List<Level> _levels = [];
|
List<Level> _levels = [];
|
||||||
Map<String, dynamic>? _lastCompletedLevel;
|
Map<String, dynamic>? _lastCompletedLevel;
|
||||||
|
List<String> _unlockedLevels = [];
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
Map<String, dynamic>? _studentAnswers;
|
||||||
|
|
||||||
List<Level> get levels => _levels;
|
List<Level> get levels => _levels;
|
||||||
Map<String, dynamic>? get lastCompletedLevel => _lastCompletedLevel;
|
Map<String, dynamic>? get lastCompletedLevel => _lastCompletedLevel;
|
||||||
|
List<String> get unlockedLevels => _unlockedLevels;
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
String? get error => _error;
|
String? get error => _error;
|
||||||
|
Map<String, dynamic>? get studentAnswers => _studentAnswers;
|
||||||
|
|
||||||
Future<void> fetchLevels(String topicId, String token) async {
|
Future<void> fetchLevels(String topicId, String token) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
|
@ -23,6 +27,9 @@ class LevelProvider with ChangeNotifier {
|
||||||
final result = await _levelRepository.getLevels(topicId, token);
|
final result = await _levelRepository.getLevels(topicId, token);
|
||||||
_levels = result['levels'];
|
_levels = result['levels'];
|
||||||
_lastCompletedLevel = result['lastCompletedLevel'];
|
_lastCompletedLevel = result['lastCompletedLevel'];
|
||||||
|
_unlockedLevels =
|
||||||
|
List<String>.from(_lastCompletedLevel?['UNLOCKED_LEVELS'] ?? []);
|
||||||
|
|
||||||
if (_levels.isEmpty) {
|
if (_levels.isEmpty) {
|
||||||
_error = 'No levels found for this topic';
|
_error = 'No levels found for this topic';
|
||||||
}
|
}
|
||||||
|
|
@ -39,37 +46,90 @@ class LevelProvider with ChangeNotifier {
|
||||||
orElse: () => throw Exception('Pretest not found'));
|
orElse: () => throw Exception('Pretest not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// int getLevelScore(String levelId) {
|
||||||
|
// final level = _levels.firstWhere((level) => level.idLevel == levelId,
|
||||||
|
// orElse: () => const Level(
|
||||||
|
// idLevel: '',
|
||||||
|
// idTopic: '',
|
||||||
|
// idSection: '',
|
||||||
|
// nameSection: '',
|
||||||
|
// nameTopic: '',
|
||||||
|
// nameLevel: '',
|
||||||
|
// content: '',
|
||||||
|
// isPretest: 0,
|
||||||
|
// timeLevel: '',
|
||||||
|
// ));
|
||||||
|
// return level.score ?? 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// bool isLevelAllowed(String levelName) {
|
||||||
|
// return _unlockedLevels.contains(levelName);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// bool isLevelCompleted(String levelId) {
|
||||||
|
// return _lastCompletedLevel != null &&
|
||||||
|
// _lastCompletedLevel!['ID_LEVEL'] == levelId;
|
||||||
|
// }
|
||||||
|
|
||||||
|
Future<void> fetchStudentAnswers(String stdLearningId, String token) async {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_studentAnswers =
|
||||||
|
await _levelRepository.getStudentAnswers(stdLearningId, token);
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Error fetching student answers: ${e.toString()}';
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPretestFinished(String levelId) {
|
||||||
|
return _levels.any(
|
||||||
|
(level) => level.idLevel == levelId && level.idStudentLearning != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getPretestScore(String levelId) {
|
||||||
|
final pretest = _levels.firstWhere(
|
||||||
|
(level) => level.idLevel == levelId && level.isPretest == 1,
|
||||||
|
orElse: () => throw Exception('Null'));
|
||||||
|
return pretest.score ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
int getLevelScore(String levelId) {
|
int getLevelScore(String levelId) {
|
||||||
final level = _levels.firstWhere((level) => level.idLevel == levelId,
|
final level = _levels.firstWhere((level) => level.idLevel == levelId,
|
||||||
orElse: () => const Level(
|
orElse: () => throw Exception('Null'));
|
||||||
idLevel: '',
|
|
||||||
idTopic: '',
|
|
||||||
idSection: '',
|
|
||||||
nameSection: '',
|
|
||||||
nameTopic: '',
|
|
||||||
nameLevel: '',
|
|
||||||
content: '',
|
|
||||||
isPretest: 0,
|
|
||||||
timeLevel: '',
|
|
||||||
));
|
|
||||||
return level.score ?? 0;
|
return level.score ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isLevelAllowed(String levelId) {
|
bool isLevelAllowed(String levelId) {
|
||||||
if (_lastCompletedLevel == null) {
|
return _unlockedLevels.contains(
|
||||||
// Jika tidak ada level yang selesai, hanya pretest yang diizinkan
|
_levels.firstWhere((level) => level.idLevel == levelId).nameLevel);
|
||||||
return levelId == _levels.first.idLevel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL'];
|
bool isLevelCompleted(String levelId) {
|
||||||
int lastCompletedIndex =
|
return _levels.any(
|
||||||
_levels.indexWhere((level) => level.idLevel == lastCompletedLevelId);
|
(level) => level.idLevel == levelId && level.idStudentLearning != null);
|
||||||
int currentIndex = _levels.indexWhere((level) => level.idLevel == levelId);
|
|
||||||
|
|
||||||
// Level diizinkan jika indeksnya kurang dari atau sama dengan indeks terakhir yang selesai + 1
|
|
||||||
return currentIndex <= lastCompletedIndex + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bool isLevelAllowed(String levelId) {
|
||||||
|
// if (_lastCompletedLevel == null) {
|
||||||
|
// // Jika tidak ada level yang selesai, hanya pretest yang diizinkan
|
||||||
|
// return levelId == _levels.first.idLevel;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL'];
|
||||||
|
// int lastCompletedIndex =
|
||||||
|
// _levels.indexWhere((level) => level.idLevel == lastCompletedLevelId);
|
||||||
|
// int currentIndex = _levels.indexWhere((level) => level.idLevel == levelId);
|
||||||
|
|
||||||
|
// // Level diizinkan jika indeksnya kurang dari atau sama dengan indeks terakhir yang selesai + 1
|
||||||
|
// return currentIndex <= lastCompletedIndex + 1;
|
||||||
|
// }
|
||||||
|
|
||||||
// bool isLevelAllowed(int levelIndex) {
|
// bool isLevelAllowed(int levelIndex) {
|
||||||
// if (levelIndex == 0) return true; // Pretest is always allowed
|
// if (levelIndex == 0) return true; // Pretest is always allowed
|
||||||
// if (_lastCompletedLevel == null)
|
// if (_lastCompletedLevel == null)
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,13 @@ class _LevelListScreenState extends State<LevelListScreen> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_fetchLevels();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchLevels() async {
|
||||||
final levelProvider = Provider.of<LevelProvider>(context, listen: false);
|
final levelProvider = Provider.of<LevelProvider>(context, listen: false);
|
||||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||||
levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!);
|
await levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -69,16 +71,23 @@ class _LevelListScreenState extends State<LevelListScreen> {
|
||||||
if (levelProvider.isLoading) {
|
if (levelProvider.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
} else if (levelProvider.error != null) {
|
} else if (levelProvider.error != null) {
|
||||||
return Center(child: Text('No levels available'));
|
return const Center(child: Text('No levels available'));
|
||||||
} else {
|
} else {
|
||||||
|
final pretest = levelProvider.getPretest();
|
||||||
|
final otherLevels = levelProvider.levels
|
||||||
|
.where((level) => level.isPretest == 0)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
PretestCard(
|
PretestCard(
|
||||||
pretest: levelProvider.getPretest(),
|
pretest: pretest,
|
||||||
score: levelProvider
|
score: levelProvider.getPretestScore(pretest.idLevel),
|
||||||
.getLevelScore(levelProvider.getPretest().idLevel),
|
isCompleted:
|
||||||
|
levelProvider.isPretestFinished(pretest.idLevel),
|
||||||
|
isAllowed: levelProvider.isLevelAllowed(pretest.idLevel),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -90,14 +99,17 @@ class _LevelListScreenState extends State<LevelListScreen> {
|
||||||
crossAxisSpacing: 16,
|
crossAxisSpacing: 16,
|
||||||
mainAxisSpacing: 16,
|
mainAxisSpacing: 16,
|
||||||
),
|
),
|
||||||
itemCount: levelProvider.levels.length - 1,
|
itemCount: otherLevels.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final level = levelProvider.levels[index + 1];
|
final level = otherLevels[index];
|
||||||
|
|
||||||
return LevelCard(
|
return LevelCard(
|
||||||
level: level,
|
level: level,
|
||||||
isAllowed:
|
isAllowed:
|
||||||
levelProvider.isLevelAllowed(level.idLevel),
|
levelProvider.isLevelAllowed(level.idLevel),
|
||||||
score: levelProvider.getLevelScore(level.idLevel),
|
score: levelProvider.getLevelScore(level.idLevel),
|
||||||
|
isCompleted:
|
||||||
|
levelProvider.isLevelCompleted(level.idLevel),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -7,25 +7,19 @@ import 'package:flutter/material.dart';
|
||||||
class LevelCard extends StatelessWidget {
|
class LevelCard extends StatelessWidget {
|
||||||
final Level level;
|
final Level level;
|
||||||
final bool isAllowed;
|
final bool isAllowed;
|
||||||
// final int level;
|
|
||||||
// final bool isAllowed;
|
|
||||||
// final bool isFinished;
|
|
||||||
final int score;
|
final int score;
|
||||||
// final VoidCallback? onPressed;
|
final bool isCompleted;
|
||||||
|
|
||||||
const LevelCard({
|
const LevelCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.level,
|
required this.level,
|
||||||
required this.isAllowed,
|
required this.isAllowed,
|
||||||
// required this.level,
|
|
||||||
// required this.isAllowed,
|
|
||||||
// required this.isFinished,
|
|
||||||
required this.score,
|
required this.score,
|
||||||
// this.onPressed,
|
required this.isCompleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool isCompleted = level.idStudentLearning != null;
|
|
||||||
return Card(
|
return Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
|
@ -128,14 +122,18 @@ class LevelCard extends StatelessWidget {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: CustomButton(
|
child: CustomButton(
|
||||||
text: isAllowed ? 'Learn Now' : 'Not Allowed',
|
text: isCompleted
|
||||||
|
? 'Finished'
|
||||||
|
: (isAllowed ? 'Learn Now' : 'Locked'),
|
||||||
textStyle:
|
textStyle:
|
||||||
isAllowed ? null : AppTextStyles.disableTextStyle,
|
isAllowed ? null : AppTextStyles.disableTextStyle,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 36,
|
height: 36,
|
||||||
color: isAllowed
|
color: isCompleted
|
||||||
|
? Colors.green
|
||||||
|
: (isAllowed
|
||||||
? AppColors.yellowButtonColor
|
? AppColors.yellowButtonColor
|
||||||
: AppColors.cardButtonColor,
|
: AppColors.cardButtonColor),
|
||||||
onPressed: isAllowed
|
onPressed: isAllowed
|
||||||
? () {
|
? () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,31 @@ import 'package:bootstrap_icons/bootstrap_icons.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/custom_button.dart';
|
import 'package:english_learning/core/widgets/custom_button.dart';
|
||||||
import 'package:english_learning/features/learning/modules/level/models/level_model.dart';
|
import 'package:english_learning/features/learning/modules/level/models/level_model.dart';
|
||||||
|
import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart';
|
||||||
import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart';
|
import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class PretestCard extends StatelessWidget {
|
class PretestCard extends StatelessWidget {
|
||||||
final Level pretest;
|
final Level pretest;
|
||||||
final int? score;
|
final int? score;
|
||||||
final VoidCallback? onPressed;
|
final bool isCompleted;
|
||||||
|
final bool isAllowed;
|
||||||
|
|
||||||
const PretestCard({
|
const PretestCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pretest,
|
required this.pretest,
|
||||||
this.score,
|
this.score,
|
||||||
this.onPressed,
|
required this.isCompleted,
|
||||||
|
required this.isAllowed,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool isCompleted = pretest.idStudentLearning != null;
|
return Consumer<LevelProvider>(builder: (context, levelProvider, _) {
|
||||||
|
final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
|
||||||
|
final score = levelProvider.getPretestScore(pretest.idLevel);
|
||||||
|
// final isAllowed = levelProvider.isLevelAllowed(pretest.idLevel);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|
@ -47,7 +54,8 @@ class PretestCard extends StatelessWidget {
|
||||||
vertical: 4, horizontal: 8),
|
vertical: 4, horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: isCompleted ? Colors.green : Colors.transparent,
|
color:
|
||||||
|
isCompleted ? Colors.green : Colors.transparent,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColors.whiteColor,
|
color: AppColors.whiteColor,
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
|
|
@ -96,30 +104,58 @@ class PretestCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 13),
|
const SizedBox(height: 13),
|
||||||
CustomButton(
|
CustomButton(
|
||||||
text: isCompleted ? 'Finished' : 'Learn Now',
|
text: isFinished ? 'Review' : 'Learn Now',
|
||||||
textStyle: AppTextStyles.whiteTextStyle.copyWith(
|
textStyle: isFinished
|
||||||
|
? AppTextStyles.whiteTextStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
)
|
||||||
|
: AppTextStyles.blackButtonTextStyle.copyWith(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 36,
|
height: 36,
|
||||||
color: isCompleted ? Colors.green : AppColors.yellowButtonColor,
|
color: isFinished ? Colors.green : AppColors.yellowButtonColor,
|
||||||
|
// onPressed: () {
|
||||||
|
// if (!isCompleted) {
|
||||||
|
// Navigator.push(
|
||||||
|
// context,
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (context) => MaterialScreen(
|
||||||
|
// levelId: pretest.idLevel,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// // Jika isCompleted true, tidak melakukan apa-apa
|
||||||
|
// },
|
||||||
|
// onPressed: isAllowed
|
||||||
|
// ? () {
|
||||||
|
// Navigator.push(
|
||||||
|
// context,
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (context) => MaterialScreen(
|
||||||
|
// levelId: pretest.idLevel,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// : () {},
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (!isCompleted) {
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => MaterialScreen(
|
builder: (context) => MaterialScreen(
|
||||||
levelId: pretest.idLevel,
|
levelId: pretest.idLevel,
|
||||||
|
isReview: isFinished,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
// Jika isCompleted true, tidak melakukan apa-apa
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class MaterialScreen extends StatefulWidget {
|
class MaterialScreen extends StatefulWidget {
|
||||||
final String levelId;
|
final String levelId;
|
||||||
|
final bool isReview;
|
||||||
|
|
||||||
const MaterialScreen({
|
const MaterialScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.levelId,
|
required this.levelId,
|
||||||
|
this.isReview = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -74,7 +77,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.push(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ExerciseScreen(
|
builder: (context) => ExerciseScreen(
|
||||||
|
|
@ -164,8 +167,11 @@ class _MaterialScreenState extends State<MaterialScreen>
|
||||||
videoUrl: level.video!,
|
videoUrl: level.video!,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
if (!widget.isReview)
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Take Pretest',
|
text: level.isPretest == 1
|
||||||
|
? 'Take Pretest'
|
||||||
|
: 'Take Exercises',
|
||||||
onPressed: _isLoading
|
onPressed: _isLoading
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
|
|
@ -173,6 +179,16 @@ class _MaterialScreenState extends State<MaterialScreen>
|
||||||
_createStudentLearning();
|
_createStudentLearning();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
GlobalButton(
|
||||||
|
text: 'Start Review',
|
||||||
|
onPressed: _isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
_stopAndResetAllMedia();
|
||||||
|
_createStudentLearning();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
VideoPlayerController? _videoController;
|
VideoPlayerController? _videoController;
|
||||||
YoutubePlayerController? _youtubeController;
|
YoutubePlayerController? _youtubeController;
|
||||||
FlickManager? _flickManager;
|
FlickManager? _flickManager;
|
||||||
|
bool _isLoading = true;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
|
@ -29,6 +31,18 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
_initializeVideoPlayerWidget();
|
_initializeVideoPlayerWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getPlayableUrl(String url) {
|
||||||
|
if (url.contains('drive.google.com')) {
|
||||||
|
final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)');
|
||||||
|
final match = regex.firstMatch(url);
|
||||||
|
if (match != null) {
|
||||||
|
final fileId = match.group(1);
|
||||||
|
return 'https://drive.google.com/uc?export=download&id=$fileId';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
void _initializeVideoPlayerWidget() {
|
void _initializeVideoPlayerWidget() {
|
||||||
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) {
|
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) {
|
||||||
_youtubeController = YoutubePlayerController(
|
_youtubeController = YoutubePlayerController(
|
||||||
|
|
@ -44,11 +58,16 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
showVideoProgressIndicator: true,
|
showVideoProgressIndicator: true,
|
||||||
onReady: () {
|
onReady: () {
|
||||||
_youtubeController!.addListener(_youtubeListener);
|
_youtubeController!.addListener(_youtubeListener);
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_videoController =
|
_videoController = VideoPlayerController.networkUrl(
|
||||||
VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
|
Uri.parse(_getPlayableUrl(widget.videoUrl)),
|
||||||
|
);
|
||||||
|
_videoController!.initialize().then((_) {
|
||||||
_flickManager = FlickManager(
|
_flickManager = FlickManager(
|
||||||
videoPlayerController: _videoController!,
|
videoPlayerController: _videoController!,
|
||||||
autoPlay: false,
|
autoPlay: false,
|
||||||
|
|
@ -56,6 +75,15 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
_videoWidget = FlickVideoPlayer(
|
_videoWidget = FlickVideoPlayer(
|
||||||
flickManager: _flickManager!,
|
flickManager: _flickManager!,
|
||||||
);
|
);
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}).catchError((error) {
|
||||||
|
setState(() {
|
||||||
|
_error = "Error loading video: $error";
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,6 +104,11 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
} else if (_error != null) {
|
||||||
|
return Center(child: Text(_error!));
|
||||||
|
}
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ class ResultScreen extends StatelessWidget {
|
||||||
final String currentLevel;
|
final String currentLevel;
|
||||||
final String nextLevel;
|
final String nextLevel;
|
||||||
final int score;
|
final int score;
|
||||||
final bool? isCompleted;
|
final bool isCompleted;
|
||||||
|
final String? stdLearningId;
|
||||||
|
|
||||||
const ResultScreen({
|
const ResultScreen({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -16,6 +17,7 @@ class ResultScreen extends StatelessWidget {
|
||||||
required this.nextLevel,
|
required this.nextLevel,
|
||||||
required this.score,
|
required this.score,
|
||||||
required this.isCompleted,
|
required this.isCompleted,
|
||||||
|
this.stdLearningId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -30,20 +32,23 @@ class ResultScreen extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (isCompleted!)
|
if (isCompleted)
|
||||||
CompleteResultWidget(
|
CompleteResultWidget(
|
||||||
currentLevel: currentLevel,
|
currentLevel: currentLevel,
|
||||||
score: score,
|
score: score,
|
||||||
|
stdLearningId: stdLearningId ?? '',
|
||||||
)
|
)
|
||||||
else if (nextLevel != currentLevel)
|
else if (nextLevel != currentLevel)
|
||||||
JumpResultWidget(
|
JumpResultWidget(
|
||||||
nextLevel: nextLevel,
|
nextLevel: nextLevel,
|
||||||
score: score,
|
score: score,
|
||||||
|
stdLearningId: stdLearningId ?? '',
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
DownResultWidget(
|
DownResultWidget(
|
||||||
nextLevel: nextLevel,
|
nextLevel: nextLevel,
|
||||||
score: score,
|
score: score,
|
||||||
|
stdLearningId: stdLearningId ?? '',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
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/core/widgets/global_button.dart';
|
||||||
|
import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
class CompleteResultWidget extends StatelessWidget {
|
class CompleteResultWidget extends StatelessWidget {
|
||||||
final String? currentLevel;
|
final String? currentLevel;
|
||||||
final int? score;
|
final int? score;
|
||||||
|
final String stdLearningId;
|
||||||
|
|
||||||
const CompleteResultWidget({
|
const CompleteResultWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.currentLevel,
|
required this.currentLevel,
|
||||||
required this.score,
|
required this.score,
|
||||||
|
required this.stdLearningId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -49,12 +52,14 @@ class CompleteResultWidget extends StatelessWidget {
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Discover More',
|
text: 'Discover More',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Navigator.push(
|
Navigator.pushReplacement(
|
||||||
// context,
|
context,
|
||||||
// MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
// builder: (context) => const FeedbackScreen(),
|
builder: (context) => FeedbackScreen(
|
||||||
// ),
|
stdLearningId: stdLearningId,
|
||||||
// );
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
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/core/widgets/global_button.dart';
|
||||||
|
import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
class DownResultWidget extends StatelessWidget {
|
class DownResultWidget extends StatelessWidget {
|
||||||
final String? nextLevel;
|
final String? nextLevel;
|
||||||
final int? score;
|
final int? score;
|
||||||
|
final String stdLearningId;
|
||||||
|
|
||||||
const DownResultWidget({
|
const DownResultWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.nextLevel,
|
required this.nextLevel,
|
||||||
required this.score,
|
required this.score,
|
||||||
|
required this.stdLearningId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -58,12 +62,14 @@ class DownResultWidget extends StatelessWidget {
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Continue',
|
text: 'Continue',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Navigator.push(
|
Navigator.pushReplacement(
|
||||||
// context,
|
context,
|
||||||
// MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
// builder: (context) => const FeedbackScreen(),
|
builder: (context) => FeedbackScreen(
|
||||||
// ),
|
stdLearningId: stdLearningId,
|
||||||
// );
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,13 @@ import 'package:flutter_svg/svg.dart';
|
||||||
class JumpResultWidget extends StatelessWidget {
|
class JumpResultWidget extends StatelessWidget {
|
||||||
final String? nextLevel;
|
final String? nextLevel;
|
||||||
final int? score;
|
final int? score;
|
||||||
|
final String stdLearningId;
|
||||||
|
|
||||||
const JumpResultWidget({
|
const JumpResultWidget(
|
||||||
super.key,
|
{super.key,
|
||||||
required this.nextLevel,
|
required this.nextLevel,
|
||||||
required this.score,
|
required this.score,
|
||||||
});
|
required this.stdLearningId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -60,10 +61,12 @@ class JumpResultWidget extends StatelessWidget {
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Continue',
|
text: 'Continue',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const FeedbackScreen(),
|
builder: (context) => FeedbackScreen(
|
||||||
|
stdLearningId: stdLearningId,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
class Topic {
|
class Topic {
|
||||||
final String id;
|
final String id;
|
||||||
final String sectionId;
|
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
|
final bool isCompleted;
|
||||||
|
|
||||||
Topic({
|
Topic({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.sectionId,
|
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
|
required this.isCompleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Topic.fromJson(Map<String, dynamic> json) {
|
factory Topic.fromJson(Map<String, dynamic> json) {
|
||||||
return Topic(
|
return Topic(
|
||||||
id: json['ID_TOPIC'],
|
id: json['ID_TOPIC'] ?? '',
|
||||||
sectionId: json['ID_SECTION'],
|
name: json['NAME_TOPIC'] ?? '',
|
||||||
name: json['NAME_TOPIC'],
|
description: json['DESCRIPTION_TOPIC'] ?? '',
|
||||||
description: json['DESCRIPTION_TOPIC'],
|
isCompleted: json['IS_COMPLETED'] == 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_fetchTopics();
|
_fetchTopics();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getFullImageUrl(String thumbnail) {
|
String _getFullImageUrl(String thumbnail) {
|
||||||
|
|
@ -39,10 +41,14 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
||||||
final token = await userProvider.getValidToken();
|
final token = await userProvider.getValidToken();
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
|
try {
|
||||||
await Provider.of<TopicProvider>(context, listen: false)
|
await Provider.of<TopicProvider>(context, listen: false)
|
||||||
.fetchTopics(widget.sectionId, token);
|
.fetchTopics(widget.sectionId, token);
|
||||||
|
print('Topics fetched successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching topics: $e');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle the case when token is null (user might not be logged in)
|
|
||||||
print('No valid token found. User might need to log in.');
|
print('No valid token found. User might need to log in.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +106,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 104,
|
height: 104,
|
||||||
color: Colors.grey[300],
|
color: Colors.grey[300],
|
||||||
child: Center(child: CircularProgressIndicator()),
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -142,8 +148,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
||||||
return TopicCard(
|
return TopicCard(
|
||||||
title: topic.name,
|
title: topic.name,
|
||||||
description: topic.description,
|
description: topic.description,
|
||||||
isCompleted:
|
isCompleted: topic.isCompleted,
|
||||||
false, // You might want to implement completion tracking
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ class TopicCard extends StatelessWidget {
|
||||||
isCompleted
|
isCompleted
|
||||||
? Icons.check_circle
|
? Icons.check_circle
|
||||||
: Icons.radio_button_unchecked,
|
: Icons.radio_button_unchecked,
|
||||||
color: AppColors.blueColor,
|
color:
|
||||||
|
isCompleted ? AppColors.blueColor : AppColors.greyColor,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,15 @@ class SectionProvider extends ChangeNotifier {
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
String? get error => _error;
|
String? get error => _error;
|
||||||
|
|
||||||
Future<void> fetchSections(String token) async {
|
Future<List<Section>> fetchSections(String token) async {
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_sections = await _repository.getSections(token);
|
_sections = await _repository.getSections(token);
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
return _sections;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_isLoading = false;
|
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class LearningCard extends StatelessWidget {
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 104,
|
height: 104,
|
||||||
color: Colors.grey[300],
|
color: Colors.grey[300],
|
||||||
child: Icon(
|
child: const Icon(
|
||||||
Icons.image_not_supported,
|
Icons.image_not_supported,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
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/completed_topics_repository.dart';
|
||||||
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
|
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
|
||||||
import 'package:english_learning/core/services/repositories/history_repository.dart';
|
import 'package:english_learning/core/services/repositories/history_repository.dart';
|
||||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
import 'package:english_learning/core/utils/styles/theme.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/provider/validator_provider.dart';
|
import 'package:english_learning/features/auth/provider/validator_provider.dart';
|
||||||
import 'package:english_learning/features/history/provider/history_provider.dart';
|
import 'package:english_learning/features/history/provider/history_provider.dart';
|
||||||
|
import 'package:english_learning/features/home/provider/completed_topics_provider.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/level/providers/level_provider.dart';
|
import 'package:english_learning/features/learning/modules/level/providers/level_provider.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';
|
||||||
|
|
@ -18,7 +20,7 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({Key? key}) : super(key: key);
|
const MyApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -38,6 +40,9 @@ class MyApp extends StatelessWidget {
|
||||||
ProxyProvider<DioClient, HistoryRepository>(
|
ProxyProvider<DioClient, HistoryRepository>(
|
||||||
update: (_, dioClient, __) => HistoryRepository(dioClient),
|
update: (_, dioClient, __) => HistoryRepository(dioClient),
|
||||||
),
|
),
|
||||||
|
ProxyProvider<DioClient, CompletedTopicsRepository>(
|
||||||
|
update: (_, dioClient, __) => CompletedTopicsRepository(dioClient),
|
||||||
|
),
|
||||||
ChangeNotifierProxyProvider2<HistoryRepository, SectionProvider,
|
ChangeNotifierProxyProvider2<HistoryRepository, SectionProvider,
|
||||||
HistoryProvider>(
|
HistoryProvider>(
|
||||||
create: (context) => HistoryProvider(
|
create: (context) => HistoryProvider(
|
||||||
|
|
@ -62,6 +67,14 @@ class MyApp extends StatelessWidget {
|
||||||
userProvider,
|
userProvider,
|
||||||
)..updateFrom(previous),
|
)..updateFrom(previous),
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProxyProvider<CompletedTopicsRepository,
|
||||||
|
CompletedTopicsProvider>(
|
||||||
|
create: (context) => CompletedTopicsProvider(
|
||||||
|
context.read<CompletedTopicsRepository>(),
|
||||||
|
),
|
||||||
|
update: (context, completedTopicsRepository, previous) =>
|
||||||
|
CompletedTopicsProvider(completedTopicsRepository),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: Consumer<UserProvider>(
|
child: Consumer<UserProvider>(
|
||||||
builder: (context, userProvider, _) {
|
builder: (context, userProvider, _) {
|
||||||
|
|
|
||||||
18
pubspec.lock
18
pubspec.lock
|
|
@ -287,7 +287,7 @@ packages:
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_cache_manager:
|
flutter_cache_manager:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
|
|
@ -358,14 +358,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
flutter_logger_plus:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_logger_plus
|
|
||||||
sha256: c01751b8074384d116a4c1e4b549233053ea6b78fcc07b5503248227b06b923c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "5.0.0"
|
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -600,14 +592,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
logger:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: logger
|
|
||||||
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.0"
|
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,6 @@ dependencies:
|
||||||
flick_video_player: ^0.9.0
|
flick_video_player: ^0.9.0
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_cache_manager: ^3.4.1
|
|
||||||
flutter_logger_plus: ^5.0.0
|
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
flutter_svg: ^2.0.10+1
|
flutter_svg: ^2.0.10+1
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
|
|
@ -46,7 +44,6 @@ dependencies:
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
jwt_decoder: ^2.0.1
|
jwt_decoder: ^2.0.1
|
||||||
logger: ^2.4.0
|
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
shared_preferences: ^2.3.2
|
shared_preferences: ^2.3.2
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user