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:english_learning/core/services/repositories/constants.dart';
|
||||
|
||||
|
|
@ -10,24 +12,6 @@ class DioClient {
|
|||
_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 {
|
||||
try {
|
||||
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(
|
||||
String id, FormData formData, String token) async {
|
||||
try {
|
||||
|
|
@ -268,7 +270,7 @@ class DioClient {
|
|||
);
|
||||
print('getExercises response: ${response.data}');
|
||||
return response;
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
print(
|
||||
'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}');
|
||||
rethrow;
|
||||
|
|
@ -336,4 +338,65 @@ class DioClient {
|
|||
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/features/learning/modules/feedback/models/feedback_model.dart';
|
||||
|
||||
class ExerciseRepository {
|
||||
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(
|
||||
List<Map<String, dynamic>> answers,
|
||||
String studentLearningId,
|
||||
|
|
@ -38,4 +53,25 @@ class ExerciseRepository {
|
|||
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) {
|
||||
final Map<String, dynamic> responseData = response.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<Level> levels =
|
||||
levelsData.map((json) => Level.fromJson(json)).toList();
|
||||
final Map<String, dynamic>? lastCompletedLevel =
|
||||
data['lastCompletedLevel'];
|
||||
data['lastCompletedLevel'] ?? {};
|
||||
|
||||
return {
|
||||
'levels': levels,
|
||||
|
|
@ -32,4 +32,20 @@ class LevelRepository {
|
|||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getStudentAnswers(
|
||||
String stdLearningId, String token) async {
|
||||
try {
|
||||
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return response.data['data'];
|
||||
} else {
|
||||
throw Exception(
|
||||
'Failed to fetch student answers: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch student answers: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import 'package:intl/intl.dart';
|
|||
|
||||
class LearningHistory {
|
||||
final int score;
|
||||
final dynamic currentLevel;
|
||||
final dynamic nextLevel;
|
||||
final DateTime studentFinish;
|
||||
final String currentLevel;
|
||||
final String? nextLevel;
|
||||
final DateTime? studentFinish;
|
||||
final String topicName;
|
||||
final String sectionName;
|
||||
|
||||
|
|
@ -19,16 +19,20 @@ class LearningHistory {
|
|||
|
||||
factory LearningHistory.fromJson(Map<String, dynamic> json) {
|
||||
return LearningHistory(
|
||||
score: json['SCORE'],
|
||||
currentLevel: json['CURRENT_LEVEL'],
|
||||
score: json['SCORE'] ?? 0,
|
||||
currentLevel: json['CURRENT_LEVEL'] ?? 'Unknown',
|
||||
nextLevel: json['NEXT_LEVEL'],
|
||||
studentFinish: DateTime.parse(json['STUDENT_FINISH']),
|
||||
topicName: json['TOPIC_NAME'],
|
||||
sectionName: json['SECTION_NAME'],
|
||||
studentFinish: json['STUDENT_FINISH'] != null
|
||||
? DateTime.parse(json['STUDENT_FINISH'])
|
||||
: null,
|
||||
topicName: json['TOPIC_NAME'] ?? 'Unknown Topic',
|
||||
sectionName: json['SECTION_NAME'] ?? 'Unknown Section',
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (level is int) {
|
||||
return level;
|
||||
} else if (level is String) {
|
||||
if (level.toLowerCase() == 'pretest') {
|
||||
return 0; // Treat Pretest as level 0
|
||||
}
|
||||
return int.tryParse(level.replaceAll('Level ', ''));
|
||||
int? _parseLevel(String? level) {
|
||||
if (level == null) return null;
|
||||
if (level.toLowerCase() == 'pretest') {
|
||||
return 0; // Treat Pretest as level 0
|
||||
}
|
||||
return null;
|
||||
return int.tryParse(level.replaceAll('Level ', '')) ?? -1;
|
||||
}
|
||||
|
||||
Future<void> refreshData(String token) async {
|
||||
await _sectionProvider.fetchSections(token);
|
||||
if (_sectionProvider.sections.isEmpty) {
|
||||
await _sectionProvider.fetchSections(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/features/auth/provider/user_provider.dart';
|
||||
import 'package:english_learning/features/history/provider/history_provider.dart';
|
||||
|
|
@ -18,95 +19,141 @@ class HistoryScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HistoryScreenState extends State<HistoryScreen> {
|
||||
@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!);
|
||||
});
|
||||
});
|
||||
bool isNotFoundError(String error) {
|
||||
return error.toLowerCase().contains('no learning history found') ||
|
||||
error.toLowerCase().contains('not found');
|
||||
}
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// final historyProvider =
|
||||
// Provider.of<HistoryProvider>(context, listen: false);
|
||||
// final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
|
||||
// historyProvider.refreshData(userProvider.jwtToken!);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgSoftColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 30.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your Exercise History!',
|
||||
style: AppTextStyles.blueTextStyle.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
return Consumer<HistoryProvider>(
|
||||
builder: (context, historyProvider, chiild) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgSoftColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 30.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your Exercise History!',
|
||||
style: AppTextStyles.blueTextStyle.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Track your progress with a personalized overview of all your workouts.',
|
||||
style: AppTextStyles.greyTextStyle.copyWith(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Track your progress with a personalized overview of all your workouts.',
|
||||
style: AppTextStyles.greyTextStyle.copyWith(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const CustomTabBar(),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Consumer<HistoryProvider>(
|
||||
builder: (context, historyProvider, child) {
|
||||
if (historyProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (historyProvider.error != null) {
|
||||
return Center(child: Text(historyProvider.error!));
|
||||
} else if (historyProvider.learningHistory.isEmpty) {
|
||||
return _buildEmptyState(context);
|
||||
} else {
|
||||
return ListView.builder(
|
||||
itemCount: historyProvider.learningHistory.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Column(
|
||||
children: [
|
||||
ExerciseHistoryCard(
|
||||
exercise:
|
||||
historyProvider.learningHistory[index],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
const SizedBox(height: 8),
|
||||
const CustomTabBar(),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Consumer<HistoryProvider>(
|
||||
builder: (context, historyProvider, child) {
|
||||
if (historyProvider.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator());
|
||||
} else if (historyProvider.error != null) {
|
||||
if (isNotFoundError(historyProvider.error!)) {
|
||||
return _buildEmptyState(context);
|
||||
} else {
|
||||
return _buildErrorState(historyProvider.error!);
|
||||
}
|
||||
} else if (historyProvider.learningHistory.isEmpty) {
|
||||
return _buildEmptyState(context);
|
||||
} else {
|
||||
return ListView.builder(
|
||||
itemCount: historyProvider.learningHistory.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Column(
|
||||
children: [
|
||||
ExerciseHistoryCard(
|
||||
exercise:
|
||||
historyProvider.learningHistory[index],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CustomTabBar extends StatefulWidget {
|
||||
const CustomTabBar({super.key});
|
||||
|
||||
@override
|
||||
_CustomTabBarState createState() => _CustomTabBarState();
|
||||
}
|
||||
|
||||
class _CustomTabBarState extends State<CustomTabBar> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
class CustomTabBar extends StatelessWidget {
|
||||
const CustomTabBar({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -27,12 +14,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
|
|||
final sectionProvider = Provider.of<SectionProvider>(context);
|
||||
final selectedPageIndex = historyProvider.selectedPageIndex;
|
||||
|
||||
if (sectionProvider.sections.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: sectionProvider.sections.asMap().entries.map((entry) {
|
||||
|
|
@ -56,12 +38,6 @@ class _CustomTabBarState extends State<CustomTabBar> {
|
|||
Provider.of<HistoryProvider>(context, listen: false);
|
||||
historyProvider.setSelectedPageIndex(index);
|
||||
|
||||
_scrollController.animateTo(
|
||||
index * 100.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
historyProvider.fetchLearningHistory(userProvider.jwtToken!);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,84 +28,71 @@ class ExerciseHistoryCard extends StatelessWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${exercise.topicName} /',
|
||||
),
|
||||
TextSpan(
|
||||
text: ' ${exercise.sectionName}',
|
||||
style: AppTextStyles.blackTextStyle)
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: AppTextStyles.greyTextStyle.copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${exercise.currentLevel} → ',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${exercise.nextLevel}',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Submission: ${exercise.formattedDate}',
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
exercise.topicName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 26.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: color,
|
||||
const SizedBox(height: 8),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: AppTextStyles.greyTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${exercise.currentLevel} → ',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${exercise.nextLevel}',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
'${exercise.score}/100',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Submission: ${exercise.formattedDate}',
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 26.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: color,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Text(
|
||||
'${exercise.score}/100',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
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: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/history/provider/history_provider.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/provider/completed_topics_provider.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/learning/screens/learning_screen.dart';
|
||||
|
|
@ -14,6 +17,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:google_nav_bar/google_nav_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
|
@ -78,15 +82,19 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
iconSize: 20,
|
||||
gap: 8,
|
||||
selectedIndex: _selectedIndex,
|
||||
onTabChange: (index) {
|
||||
onTabChange: (index) async {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
_pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut, // Animasi ketika berpindah tab
|
||||
);
|
||||
_pageController.jumpToPage(index);
|
||||
});
|
||||
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(
|
||||
horizontal: 16,
|
||||
|
|
@ -127,164 +135,294 @@ class HomeContent extends StatefulWidget {
|
|||
class _HomeContentState extends State<HomeContent> {
|
||||
final CardData cardData = CardData();
|
||||
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
|
||||
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';
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.gradientTheme,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.gradientTheme,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60.0,
|
||||
left: 18.34,
|
||||
right: 16.0,
|
||||
bottom: 34.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'lib/features/home/assets/images/Logo.svg',
|
||||
width: 31,
|
||||
),
|
||||
const SizedBox(width: 4.34),
|
||||
Text(
|
||||
'SEALS',
|
||||
style: AppTextStyles.logoTextStyle.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60.0,
|
||||
left: 18.34,
|
||||
right: 16.0,
|
||||
bottom: 34.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'lib/features/home/assets/images/Logo.svg',
|
||||
width: 31,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const EditProfileScreen(),
|
||||
const SizedBox(width: 4.34),
|
||||
Text(
|
||||
'SEALS',
|
||||
style:
|
||||
AppTextStyles.logoTextStyle.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(
|
||||
BootstrapIcons.person_circle,
|
||||
size: 28,
|
||||
color: AppColors.whiteColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 17),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Hi, ',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: userName,
|
||||
style: AppTextStyles.yellowTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '!',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const EditProfileScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(
|
||||
BootstrapIcons.person_circle,
|
||||
size: 28,
|
||||
color: AppColors.whiteColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 17),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Hi, ',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: userName,
|
||||
style:
|
||||
AppTextStyles.yellowTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '!',
|
||||
style:
|
||||
AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Let\'s evolve together',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CarouselSlider.builder(
|
||||
itemCount: cardData.cardData.length,
|
||||
itemBuilder: (context, index, realIndex) {
|
||||
return WelcomeCard(cardModel: cardData.cardData[index]);
|
||||
},
|
||||
options: CarouselOptions(
|
||||
height: 168,
|
||||
viewportFraction: 0.9,
|
||||
enlargeCenterPage: true,
|
||||
autoPlay: true,
|
||||
autoPlayInterval: const Duration(seconds: 3),
|
||||
autoPlayAnimationDuration: const Duration(milliseconds: 800),
|
||||
autoPlayCurve: Curves.fastOutSlowIn,
|
||||
onPageChanged: (index, reason) {
|
||||
setState(
|
||||
() {
|
||||
_currentPage = index;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SliderWidget(
|
||||
currentPage: _currentPage,
|
||||
itemCount: cardData.cardData.length,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(height: 8),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Let\'s evolve together',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
'Your Last Journey.',
|
||||
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
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
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CarouselSlider.builder(
|
||||
itemCount: cardData.cardData.length,
|
||||
itemBuilder: (context, index, realIndex) {
|
||||
return WelcomeCard(cardModel: cardData.cardData[index]);
|
||||
},
|
||||
options: CarouselOptions(
|
||||
height: 168,
|
||||
viewportFraction: 0.9,
|
||||
enlargeCenterPage: true,
|
||||
autoPlay: true,
|
||||
autoPlayInterval: const Duration(seconds: 3),
|
||||
autoPlayAnimationDuration: const Duration(milliseconds: 800),
|
||||
autoPlayCurve: Curves.fastOutSlowIn,
|
||||
onPageChanged: (index, reason) {
|
||||
setState(
|
||||
() {
|
||||
_currentPage = index;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SliderWidget(
|
||||
currentPage: _currentPage,
|
||||
itemCount: cardData.cardData.length,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ProgressCard(),
|
||||
),
|
||||
// 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
|
||||
// ),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
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';
|
||||
|
||||
class ProgressBar extends StatelessWidget {
|
||||
final int currentProgress;
|
||||
final int totalProgress;
|
||||
final int completedTopics;
|
||||
final int totalTopics;
|
||||
|
||||
const ProgressBar({
|
||||
super.key,
|
||||
required this.currentProgress,
|
||||
required this.totalProgress,
|
||||
required this.completedTopics,
|
||||
required this.totalTopics,
|
||||
});
|
||||
|
||||
@override
|
||||
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(
|
||||
builder: (context, constraints) {
|
||||
final barWidth = constraints.maxWidth - 40;
|
||||
final barWidth = constraints.maxWidth - 30;
|
||||
|
||||
return Row(
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: barWidth,
|
||||
width: double.infinity,
|
||||
child: Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Stack(
|
||||
|
|
@ -44,12 +47,25 @@ class ProgressBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'$currentProgress/$totalProgress',
|
||||
style: AppTextStyles.blueTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
// const Spacer(),
|
||||
SizedBox(
|
||||
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(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,91 +1,103 @@
|
|||
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/features/home/models/completed_topics_model.dart';
|
||||
import 'package:english_learning/features/home/widgets/progress_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
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,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.disableColor,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Listening',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Topic 8: Entertaining | Level 3',
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const ProgressBar(
|
||||
currentProgress: 8,
|
||||
totalProgress: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
...completedTopic.asMap().entries.map(
|
||||
(entry) {
|
||||
CompletedTopic topic = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildTopicItem(topic),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopicItem(CompletedTopic topic) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
topic.nameSection,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
topic.descriptionSection,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ProgressBar(
|
||||
completedTopics: topic.completedTopics,
|
||||
totalTopics: topic.totalTopics,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ class ExerciseModel {
|
|||
final String? image;
|
||||
final DateTime timeAdminExc;
|
||||
final dynamic choices;
|
||||
final String? answerStudent;
|
||||
final bool? isCorrect;
|
||||
|
||||
ExerciseModel({
|
||||
required this.idAdminExercise,
|
||||
|
|
@ -21,6 +23,8 @@ class ExerciseModel {
|
|||
this.image,
|
||||
required this.timeAdminExc,
|
||||
required this.choices,
|
||||
this.answerStudent,
|
||||
this.isCorrect,
|
||||
});
|
||||
|
||||
factory ExerciseModel.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -57,6 +61,21 @@ class ExerciseModel {
|
|||
choices: choices,
|
||||
);
|
||||
}
|
||||
|
||||
factory ExerciseModel.fromReviewJson(Map<String, dynamic> json) {
|
||||
final exerciseDetails = json['exerciseDetails'];
|
||||
return ExerciseModel(
|
||||
idAdminExercise: exerciseDetails['ID_ADMIN_EXERCISE'],
|
||||
idLevel: json['ID_LEVEL'] ?? '', // This might be available in parent data
|
||||
title: exerciseDetails['TITLE'],
|
||||
question: exerciseDetails['QUESTION'],
|
||||
questionType: exerciseDetails['QUESTION_TYPE'],
|
||||
timeAdminExc: DateTime.parse(json['TIME_STUDENT_EXC']),
|
||||
answerStudent: json['ANSWER_STUDENT'],
|
||||
isCorrect: json['IS_CORRECT'] == 1,
|
||||
choices: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleChoice {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/services/repositories/exercise_repository.dart';
|
||||
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
||||
|
|
@ -23,6 +24,7 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
String _nameLevel = '';
|
||||
String? _activeLeftOption;
|
||||
String? _studentLearningId;
|
||||
bool _isReview = false;
|
||||
|
||||
// Constants
|
||||
final List<Color> _pairColors = [
|
||||
|
|
@ -42,6 +44,7 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
String get nameLevel => _nameLevel;
|
||||
String? get activeLeftOption => _activeLeftOption;
|
||||
String? get studentLearningId => _studentLearningId;
|
||||
bool get isReview => _isReview;
|
||||
|
||||
// Initialization methods
|
||||
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) {
|
||||
if (_isReview) return;
|
||||
if (index >= 0 && index < _answers.length) {
|
||||
if (_exercises[index].choices is MatchingPair) {
|
||||
_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 {
|
||||
_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 {
|
||||
print('submitAnswersAndGetScore called');
|
||||
try {
|
||||
|
|
@ -256,10 +315,17 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
}).map((entry) {
|
||||
final index = entry.key;
|
||||
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 {
|
||||
'ID_STUDENT_LEARNING': _studentLearningId!,
|
||||
'ID_ADMIN_EXERCISE': exercise.idAdminExercise,
|
||||
'ANSWER_STUDENT': _answers[index],
|
||||
'ANSWER_STUDENT': formattedAnswer,
|
||||
};
|
||||
}).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() {
|
||||
return _answers.any((answer) => answer.isNotEmpty);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ import 'package:provider/provider.dart';
|
|||
class ExerciseScreen extends StatefulWidget {
|
||||
final String? levelId;
|
||||
final String studentLearningId;
|
||||
final bool isReview;
|
||||
|
||||
const ExerciseScreen({
|
||||
super.key,
|
||||
required this.levelId,
|
||||
required this.studentLearningId,
|
||||
this.isReview = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -29,7 +31,11 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
|||
_scrollController = ScrollController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = context.read<ExerciseProvider>();
|
||||
provider.fetchExercises(widget.levelId!);
|
||||
if (widget.isReview) {
|
||||
provider.fetchReviewExercises(widget.studentLearningId);
|
||||
} else {
|
||||
provider.fetchExercises(widget.levelId!);
|
||||
}
|
||||
provider.setStudentLearningId(widget.studentLearningId);
|
||||
});
|
||||
}
|
||||
|
|
@ -98,6 +104,8 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (provider.isReview)
|
||||
Text('Review Mode', style: TextStyle(fontSize: 24)),
|
||||
const SizedBox(height: 16),
|
||||
const ExerciseContent(),
|
||||
const SizedBox(height: 24),
|
||||
|
|
|
|||
|
|
@ -31,10 +31,11 @@ class ExerciseNavigator extends StatelessWidget {
|
|||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ResultScreen(
|
||||
currentLevel: result['CURRENT_LEVEL_NAME'],
|
||||
nextLevel: result['NEXT_LEARNING_NAME'],
|
||||
score: int.parse(result['SCORE'].toString()),
|
||||
currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
|
||||
nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
|
||||
score: int.tryParse(result['SCORE'].toString()) ?? 0,
|
||||
isCompleted: result['IS_PASS'] == 1,
|
||||
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// multiple_choice_question.dart
|
||||
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
|
|
@ -31,7 +30,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
|||
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) {
|
||||
final optionLabels = List.generate(
|
||||
options.length,
|
||||
(index) => String.fromCharCode(65 + index),
|
||||
(index) =>
|
||||
String.fromCharCode(65 + index), // Generate labels "A", "B", etc.
|
||||
);
|
||||
|
||||
return ListView.builder(
|
||||
|
|
@ -41,7 +41,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
|||
itemBuilder: (context, i) {
|
||||
final option = options[i];
|
||||
final isSelected =
|
||||
provider.answers[provider.currentExerciseIndex] == option;
|
||||
provider.answers[provider.currentExerciseIndex] == optionLabels[i];
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
|
|
@ -69,10 +69,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14.0,
|
||||
vertical: 10.0,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ class TrueFalseQuestion extends StatelessWidget {
|
|||
itemCount: options.length,
|
||||
itemBuilder: (context, i) {
|
||||
final option = options[i];
|
||||
final isSelected =
|
||||
provider.answers[provider.currentExerciseIndex] == option;
|
||||
final isSelected = provider.answers[provider.currentExerciseIndex] ==
|
||||
(i == 0 ? '1' : '0');
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
provider.answerQuestion(provider.currentExerciseIndex, option),
|
||||
// onTap: () =>
|
||||
// provider.answerQuestion(provider.currentExerciseIndex, option),
|
||||
onTap: () => provider.answerQuestion(
|
||||
provider.currentExerciseIndex, i == 0 ? '1' : '0'),
|
||||
|
||||
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/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:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FeedbackScreen extends StatefulWidget {
|
||||
const FeedbackScreen({super.key});
|
||||
final String stdLearningId;
|
||||
|
||||
const FeedbackScreen({
|
||||
super.key,
|
||||
required this.stdLearningId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FeedbackScreen> createState() => _FeedbackScreenState();
|
||||
|
|
@ -33,6 +40,58 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
|
|||
_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
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
|
|
@ -121,23 +180,7 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
|
|||
const SizedBox(height: 24),
|
||||
GlobalButton(
|
||||
text: 'Send',
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return FeedbackDialog(
|
||||
onSubmit: () {
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => const LevelListScreen(),
|
||||
// ),
|
||||
// );
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
onPressed: _submitFeedback,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GlobalButton(
|
||||
|
|
@ -149,9 +192,13 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
|
|||
// Navigator.push(
|
||||
// context,
|
||||
// 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(),
|
||||
// ),
|
||||
// );
|
||||
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) {
|
||||
return Level(
|
||||
idLevel: json['ID_LEVEL'],
|
||||
idTopic: json['ID_TOPIC'],
|
||||
idSection: json['ID_SECTION'],
|
||||
nameSection: json['NAME_SECTION'],
|
||||
nameTopic: json['NAME_TOPIC'],
|
||||
nameLevel: json['NAME_LEVEL'],
|
||||
content: json['CONTENT'],
|
||||
idLevel: json['ID_LEVEL'] ?? '',
|
||||
idTopic: json['ID_TOPIC'] ?? '',
|
||||
idSection: json['ID_SECTION'] ?? '',
|
||||
nameSection: json['NAME_SECTION'] ?? '',
|
||||
nameTopic: json['NAME_TOPIC'] ?? '',
|
||||
nameLevel: json['NAME_LEVEL'] ?? '',
|
||||
content: json['CONTENT'] ?? '',
|
||||
audio: json['AUDIO'],
|
||||
image: json['IMAGE'],
|
||||
video: json['VIDEO'],
|
||||
isPretest: json['IS_PRETEST'],
|
||||
isPretest: json['IS_PRETEST'] ?? 0,
|
||||
timeLevel: json['TIME_LEVEL'],
|
||||
idStudentLearning: json['ID_STUDENT_LEARNING'],
|
||||
score: json['SCORE'],
|
||||
|
|
|
|||
|
|
@ -6,13 +6,17 @@ class LevelProvider with ChangeNotifier {
|
|||
final LevelRepository _levelRepository = LevelRepository();
|
||||
List<Level> _levels = [];
|
||||
Map<String, dynamic>? _lastCompletedLevel;
|
||||
List<String> _unlockedLevels = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
Map<String, dynamic>? _studentAnswers;
|
||||
|
||||
List<Level> get levels => _levels;
|
||||
Map<String, dynamic>? get lastCompletedLevel => _lastCompletedLevel;
|
||||
List<String> get unlockedLevels => _unlockedLevels;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
Map<String, dynamic>? get studentAnswers => _studentAnswers;
|
||||
|
||||
Future<void> fetchLevels(String topicId, String token) async {
|
||||
_isLoading = true;
|
||||
|
|
@ -23,6 +27,9 @@ class LevelProvider with ChangeNotifier {
|
|||
final result = await _levelRepository.getLevels(topicId, token);
|
||||
_levels = result['levels'];
|
||||
_lastCompletedLevel = result['lastCompletedLevel'];
|
||||
_unlockedLevels =
|
||||
List<String>.from(_lastCompletedLevel?['UNLOCKED_LEVELS'] ?? []);
|
||||
|
||||
if (_levels.isEmpty) {
|
||||
_error = 'No levels found for this topic';
|
||||
}
|
||||
|
|
@ -39,37 +46,90 @@ class LevelProvider with ChangeNotifier {
|
|||
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) {
|
||||
final level = _levels.firstWhere((level) => level.idLevel == levelId,
|
||||
orElse: () => const Level(
|
||||
idLevel: '',
|
||||
idTopic: '',
|
||||
idSection: '',
|
||||
nameSection: '',
|
||||
nameTopic: '',
|
||||
nameLevel: '',
|
||||
content: '',
|
||||
isPretest: 0,
|
||||
timeLevel: '',
|
||||
));
|
||||
orElse: () => throw Exception('Null'));
|
||||
return level.score ?? 0;
|
||||
}
|
||||
|
||||
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;
|
||||
return _unlockedLevels.contains(
|
||||
_levels.firstWhere((level) => level.idLevel == levelId).nameLevel);
|
||||
}
|
||||
|
||||
bool isLevelCompleted(String levelId) {
|
||||
return _levels.any(
|
||||
(level) => level.idLevel == levelId && level.idStudentLearning != null);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// if (levelIndex == 0) return true; // Pretest is always allowed
|
||||
// if (_lastCompletedLevel == null)
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ class _LevelListScreenState extends State<LevelListScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final levelProvider = Provider.of<LevelProvider>(context, listen: false);
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!);
|
||||
});
|
||||
_fetchLevels();
|
||||
}
|
||||
|
||||
Future<void> _fetchLevels() async {
|
||||
final levelProvider = Provider.of<LevelProvider>(context, listen: false);
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
await levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -69,16 +71,23 @@ class _LevelListScreenState extends State<LevelListScreen> {
|
|||
if (levelProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (levelProvider.error != null) {
|
||||
return Center(child: Text('No levels available'));
|
||||
return const Center(child: Text('No levels available'));
|
||||
} else {
|
||||
final pretest = levelProvider.getPretest();
|
||||
final otherLevels = levelProvider.levels
|
||||
.where((level) => level.isPretest == 0)
|
||||
.toList();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
PretestCard(
|
||||
pretest: levelProvider.getPretest(),
|
||||
score: levelProvider
|
||||
.getLevelScore(levelProvider.getPretest().idLevel),
|
||||
pretest: pretest,
|
||||
score: levelProvider.getPretestScore(pretest.idLevel),
|
||||
isCompleted:
|
||||
levelProvider.isPretestFinished(pretest.idLevel),
|
||||
isAllowed: levelProvider.isLevelAllowed(pretest.idLevel),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
|
|
@ -90,14 +99,17 @@ class _LevelListScreenState extends State<LevelListScreen> {
|
|||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: levelProvider.levels.length - 1,
|
||||
itemCount: otherLevels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final level = levelProvider.levels[index + 1];
|
||||
final level = otherLevels[index];
|
||||
|
||||
return LevelCard(
|
||||
level: level,
|
||||
isAllowed:
|
||||
levelProvider.isLevelAllowed(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 {
|
||||
final Level level;
|
||||
final bool isAllowed;
|
||||
// final int level;
|
||||
// final bool isAllowed;
|
||||
// final bool isFinished;
|
||||
final int score;
|
||||
// final VoidCallback? onPressed;
|
||||
final bool isCompleted;
|
||||
|
||||
const LevelCard({
|
||||
super.key,
|
||||
required this.level,
|
||||
required this.isAllowed,
|
||||
// required this.level,
|
||||
// required this.isAllowed,
|
||||
// required this.isFinished,
|
||||
required this.score,
|
||||
// this.onPressed,
|
||||
required this.isCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isCompleted = level.idStudentLearning != null;
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
|
@ -128,14 +122,18 @@ class LevelCard extends StatelessWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: CustomButton(
|
||||
text: isAllowed ? 'Learn Now' : 'Not Allowed',
|
||||
text: isCompleted
|
||||
? 'Finished'
|
||||
: (isAllowed ? 'Learn Now' : 'Locked'),
|
||||
textStyle:
|
||||
isAllowed ? null : AppTextStyles.disableTextStyle,
|
||||
width: double.infinity,
|
||||
height: 36,
|
||||
color: isAllowed
|
||||
? AppColors.yellowButtonColor
|
||||
: AppColors.cardButtonColor,
|
||||
color: isCompleted
|
||||
? Colors.green
|
||||
: (isAllowed
|
||||
? AppColors.yellowButtonColor
|
||||
: AppColors.cardButtonColor),
|
||||
onPressed: isAllowed
|
||||
? () {
|
||||
Navigator.push(
|
||||
|
|
|
|||
|
|
@ -2,124 +2,160 @@ import 'package:bootstrap_icons/bootstrap_icons.dart';
|
|||
import 'package:english_learning/core/utils/styles/theme.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/providers/level_provider.dart';
|
||||
import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PretestCard extends StatelessWidget {
|
||||
final Level pretest;
|
||||
final int? score;
|
||||
final VoidCallback? onPressed;
|
||||
final bool isCompleted;
|
||||
final bool isAllowed;
|
||||
|
||||
const PretestCard({
|
||||
super.key,
|
||||
required this.pretest,
|
||||
this.score,
|
||||
this.onPressed,
|
||||
required this.isCompleted,
|
||||
required this.isAllowed,
|
||||
});
|
||||
|
||||
@override
|
||||
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(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: AppColors.gradientTheme,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isCompleted ? Colors.green : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: AppColors.whiteColor,
|
||||
width: 1.5,
|
||||
elevation: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: AppColors.gradientTheme,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color:
|
||||
isCompleted ? Colors.green : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: AppColors.whiteColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
pretest.nameLevel,
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (isCompleted) ...[
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
BootstrapIcons.check_all,
|
||||
color: AppColors.whiteColor,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
pretest.nameLevel,
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (isCompleted) ...[
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
BootstrapIcons.check_all,
|
||||
color: AppColors.whiteColor,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Score $score/100',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Score $score/100',
|
||||
style: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: Image.asset(
|
||||
'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png',
|
||||
height: 95,
|
||||
fit: BoxFit.cover,
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 13),
|
||||
CustomButton(
|
||||
text: isCompleted ? 'Finished' : 'Learn Now',
|
||||
textStyle: AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
Flexible(
|
||||
child: Image.asset(
|
||||
'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png',
|
||||
height: 95,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
width: double.infinity,
|
||||
height: 36,
|
||||
color: isCompleted ? Colors.green : AppColors.yellowButtonColor,
|
||||
onPressed: () {
|
||||
if (!isCompleted) {
|
||||
const SizedBox(height: 13),
|
||||
CustomButton(
|
||||
text: isFinished ? 'Review' : 'Learn Now',
|
||||
textStyle: isFinished
|
||||
? AppTextStyles.whiteTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
)
|
||||
: AppTextStyles.blackButtonTextStyle.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
width: double.infinity,
|
||||
height: 36,
|
||||
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: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MaterialScreen(
|
||||
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 {
|
||||
final String levelId;
|
||||
final bool isReview;
|
||||
|
||||
const MaterialScreen({
|
||||
super.key,
|
||||
required this.levelId,
|
||||
this.isReview = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -74,7 +77,7 @@ class _MaterialScreenState extends State<MaterialScreen>
|
|||
print('Student Learning created: ${result['message']}');
|
||||
|
||||
// Navigate to ExerciseScreen
|
||||
Navigator.push(
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ExerciseScreen(
|
||||
|
|
@ -164,15 +167,28 @@ class _MaterialScreenState extends State<MaterialScreen>
|
|||
videoUrl: level.video!,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
GlobalButton(
|
||||
text: 'Take Pretest',
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () {
|
||||
_stopAndResetAllMedia();
|
||||
_createStudentLearning();
|
||||
},
|
||||
)
|
||||
if (!widget.isReview)
|
||||
GlobalButton(
|
||||
text: level.isPretest == 1
|
||||
? 'Take Pretest'
|
||||
: 'Take Exercises',
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () {
|
||||
_stopAndResetAllMedia();
|
||||
_createStudentLearning();
|
||||
},
|
||||
)
|
||||
else
|
||||
GlobalButton(
|
||||
text: 'Start Review',
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () {
|
||||
_stopAndResetAllMedia();
|
||||
_createStudentLearning();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|||
VideoPlayerController? _videoController;
|
||||
YoutubePlayerController? _youtubeController;
|
||||
FlickManager? _flickManager;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
|
|
@ -29,6 +31,18 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|||
_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() {
|
||||
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) {
|
||||
_youtubeController = YoutubePlayerController(
|
||||
|
|
@ -44,18 +58,32 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|||
showVideoProgressIndicator: true,
|
||||
onReady: () {
|
||||
_youtubeController!.addListener(_youtubeListener);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_videoController =
|
||||
VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
|
||||
_flickManager = FlickManager(
|
||||
videoPlayerController: _videoController!,
|
||||
autoPlay: false,
|
||||
);
|
||||
_videoWidget = FlickVideoPlayer(
|
||||
flickManager: _flickManager!,
|
||||
_videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(_getPlayableUrl(widget.videoUrl)),
|
||||
);
|
||||
_videoController!.initialize().then((_) {
|
||||
_flickManager = FlickManager(
|
||||
videoPlayerController: _videoController!,
|
||||
autoPlay: false,
|
||||
);
|
||||
_videoWidget = FlickVideoPlayer(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (_error != null) {
|
||||
return Center(child: Text(_error!));
|
||||
}
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AspectRatio(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ class ResultScreen extends StatelessWidget {
|
|||
final String currentLevel;
|
||||
final String nextLevel;
|
||||
final int score;
|
||||
final bool? isCompleted;
|
||||
final bool isCompleted;
|
||||
final String? stdLearningId;
|
||||
|
||||
const ResultScreen({
|
||||
super.key,
|
||||
|
|
@ -16,6 +17,7 @@ class ResultScreen extends StatelessWidget {
|
|||
required this.nextLevel,
|
||||
required this.score,
|
||||
required this.isCompleted,
|
||||
this.stdLearningId,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -30,20 +32,23 @@ class ResultScreen extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (isCompleted!)
|
||||
if (isCompleted)
|
||||
CompleteResultWidget(
|
||||
currentLevel: currentLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
)
|
||||
else if (nextLevel != currentLevel)
|
||||
JumpResultWidget(
|
||||
nextLevel: nextLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
)
|
||||
else
|
||||
DownResultWidget(
|
||||
nextLevel: nextLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import 'package:english_learning/core/utils/styles/theme.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_svg/flutter_svg.dart';
|
||||
|
||||
class CompleteResultWidget extends StatelessWidget {
|
||||
final String? currentLevel;
|
||||
final int? score;
|
||||
final String stdLearningId;
|
||||
|
||||
const CompleteResultWidget({
|
||||
super.key,
|
||||
required this.currentLevel,
|
||||
required this.score,
|
||||
required this.stdLearningId,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -49,12 +52,14 @@ class CompleteResultWidget extends StatelessWidget {
|
|||
GlobalButton(
|
||||
text: 'Discover More',
|
||||
onPressed: () {
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => const FeedbackScreen(),
|
||||
// ),
|
||||
// );
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => FeedbackScreen(
|
||||
stdLearningId: stdLearningId,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import 'package:english_learning/core/utils/styles/theme.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_svg/flutter_svg.dart';
|
||||
|
||||
class DownResultWidget extends StatelessWidget {
|
||||
final String? nextLevel;
|
||||
final int? score;
|
||||
final String stdLearningId;
|
||||
|
||||
const DownResultWidget({
|
||||
super.key,
|
||||
required this.nextLevel,
|
||||
required this.score,
|
||||
required this.stdLearningId,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -58,12 +62,14 @@ class DownResultWidget extends StatelessWidget {
|
|||
GlobalButton(
|
||||
text: 'Continue',
|
||||
onPressed: () {
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => const FeedbackScreen(),
|
||||
// ),
|
||||
// );
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => FeedbackScreen(
|
||||
stdLearningId: stdLearningId,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import 'package:flutter_svg/svg.dart';
|
|||
class JumpResultWidget extends StatelessWidget {
|
||||
final String? nextLevel;
|
||||
final int? score;
|
||||
final String stdLearningId;
|
||||
|
||||
const JumpResultWidget({
|
||||
super.key,
|
||||
required this.nextLevel,
|
||||
required this.score,
|
||||
});
|
||||
const JumpResultWidget(
|
||||
{super.key,
|
||||
required this.nextLevel,
|
||||
required this.score,
|
||||
required this.stdLearningId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -60,10 +61,12 @@ class JumpResultWidget extends StatelessWidget {
|
|||
GlobalButton(
|
||||
text: 'Continue',
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const FeedbackScreen(),
|
||||
builder: (context) => FeedbackScreen(
|
||||
stdLearningId: stdLearningId,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
class Topic {
|
||||
final String id;
|
||||
final String sectionId;
|
||||
final String name;
|
||||
final String description;
|
||||
final bool isCompleted;
|
||||
|
||||
Topic({
|
||||
required this.id,
|
||||
required this.sectionId,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.isCompleted,
|
||||
});
|
||||
|
||||
factory Topic.fromJson(Map<String, dynamic> json) {
|
||||
return Topic(
|
||||
id: json['ID_TOPIC'],
|
||||
sectionId: json['ID_SECTION'],
|
||||
name: json['NAME_TOPIC'],
|
||||
description: json['DESCRIPTION_TOPIC'],
|
||||
id: json['ID_TOPIC'] ?? '',
|
||||
name: json['NAME_TOPIC'] ?? '',
|
||||
description: json['DESCRIPTION_TOPIC'] ?? '',
|
||||
isCompleted: json['IS_COMPLETED'] == 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchTopics();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_fetchTopics();
|
||||
});
|
||||
}
|
||||
|
||||
String _getFullImageUrl(String thumbnail) {
|
||||
|
|
@ -39,10 +41,14 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
|||
final token = await userProvider.getValidToken();
|
||||
|
||||
if (token != null) {
|
||||
await Provider.of<TopicProvider>(context, listen: false)
|
||||
.fetchTopics(widget.sectionId, token);
|
||||
try {
|
||||
await Provider.of<TopicProvider>(context, listen: false)
|
||||
.fetchTopics(widget.sectionId, token);
|
||||
print('Topics fetched successfully');
|
||||
} catch (e) {
|
||||
print('Error fetching topics: $e');
|
||||
}
|
||||
} 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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +106,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
|||
width: 90,
|
||||
height: 104,
|
||||
color: Colors.grey[300],
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -142,8 +148,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
|||
return TopicCard(
|
||||
title: topic.name,
|
||||
description: topic.description,
|
||||
isCompleted:
|
||||
false, // You might want to implement completion tracking
|
||||
isCompleted: topic.isCompleted,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ class TopicCard extends StatelessWidget {
|
|||
isCompleted
|
||||
? Icons.check_circle
|
||||
: 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;
|
||||
String? get error => _error;
|
||||
|
||||
Future<void> fetchSections(String token) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
Future<List<Section>> fetchSections(String token) async {
|
||||
try {
|
||||
_sections = await _repository.getSections(token);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return _sections;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class LearningCard extends StatelessWidget {
|
|||
width: 90,
|
||||
height: 104,
|
||||
color: Colors.grey[300],
|
||||
child: Icon(
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
color: Colors.grey,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
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/history_repository.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/validator_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/level/providers/level_provider.dart';
|
||||
import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart';
|
||||
|
|
@ -18,7 +20,7 @@ void main() {
|
|||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -38,6 +40,9 @@ class MyApp extends StatelessWidget {
|
|||
ProxyProvider<DioClient, HistoryRepository>(
|
||||
update: (_, dioClient, __) => HistoryRepository(dioClient),
|
||||
),
|
||||
ProxyProvider<DioClient, CompletedTopicsRepository>(
|
||||
update: (_, dioClient, __) => CompletedTopicsRepository(dioClient),
|
||||
),
|
||||
ChangeNotifierProxyProvider2<HistoryRepository, SectionProvider,
|
||||
HistoryProvider>(
|
||||
create: (context) => HistoryProvider(
|
||||
|
|
@ -62,6 +67,14 @@ class MyApp extends StatelessWidget {
|
|||
userProvider,
|
||||
)..updateFrom(previous),
|
||||
),
|
||||
ChangeNotifierProxyProvider<CompletedTopicsRepository,
|
||||
CompletedTopicsProvider>(
|
||||
create: (context) => CompletedTopicsProvider(
|
||||
context.read<CompletedTopicsRepository>(),
|
||||
),
|
||||
update: (context, completedTopicsRepository, previous) =>
|
||||
CompletedTopicsProvider(completedTopicsRepository),
|
||||
),
|
||||
],
|
||||
child: Consumer<UserProvider>(
|
||||
builder: (context, userProvider, _) {
|
||||
|
|
|
|||
18
pubspec.lock
18
pubspec.lock
|
|
@ -287,7 +287,7 @@ packages:
|
|||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
|
|
@ -358,14 +358,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -600,14 +592,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ dependencies:
|
|||
flick_video_player: ^0.9.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_logger_plus: ^5.0.0
|
||||
flutter_secure_storage: ^9.2.2
|
||||
flutter_svg: ^2.0.10+1
|
||||
google_fonts: ^6.2.1
|
||||
|
|
@ -46,7 +44,6 @@ dependencies:
|
|||
image_picker: ^1.1.2
|
||||
intl: ^0.19.0
|
||||
jwt_decoder: ^2.0.1
|
||||
logger: ^2.4.0
|
||||
provider: ^6.1.2
|
||||
shared_preferences: ^2.3.2
|
||||
shimmer: ^3.0.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user