refactor(home): improve completed topics API implementation

This commit is contained in:
Naresh Pratista 2024-10-23 11:16:07 +07:00
parent e28b60ba04
commit e1e9ad37da
46 changed files with 1510 additions and 703 deletions

View File

@ -1,3 +1,5 @@
// ignore_for_file: avoid_print
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:english_learning/core/services/repositories/constants.dart'; import 'package:english_learning/core/services/repositories/constants.dart';
@ -10,24 +12,6 @@ class DioClient {
_dio.options.receiveTimeout = const Duration(seconds: 3); _dio.options.receiveTimeout = const Duration(seconds: 3);
} }
Future<Response> post(String path, {dynamic data, Options? options}) async {
try {
final response = await _dio.post(
path,
data: data,
options:
options ?? Options(headers: {'Content-Type': 'application/json'}),
);
return response;
} on DioError catch (e) {
print('DioError: ${e.response?.data ?? e.message}');
rethrow; // or handle specific DioError here
} catch (e) {
print('Unexpected error: $e');
rethrow;
}
}
Future<Response> refreshAccessToken(String refreshToken) async { Future<Response> refreshAccessToken(String refreshToken) async {
try { try {
final response = await _dio.post( final response = await _dio.post(
@ -46,6 +30,24 @@ class DioClient {
} }
} }
Future<Response> post(String path, {dynamic data, Options? options}) async {
try {
final response = await _dio.post(
path,
data: data,
options:
options ?? Options(headers: {'Content-Type': 'application/json'}),
);
return response;
} on DioException catch (e) {
print('DioError: ${e.response?.data ?? e.message}');
rethrow;
} catch (e) {
print('Unexpected error: $e');
rethrow;
}
}
Future<Response> updateUserProfile( Future<Response> updateUserProfile(
String id, FormData formData, String token) async { String id, FormData formData, String token) async {
try { try {
@ -268,7 +270,7 @@ class DioClient {
); );
print('getExercises response: ${response.data}'); print('getExercises response: ${response.data}');
return response; return response;
} on DioError catch (e) { } on DioException catch (e) {
print( print(
'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}'); 'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}');
rethrow; rethrow;
@ -336,4 +338,65 @@ class DioClient {
rethrow; rethrow;
} }
} }
Future<Response> getCompletedTopics(String token) async {
try {
final response = await _dio.get(
'/topic/complete',
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
),
);
print('getCompletedTopics response: ${response.data}');
return response;
} catch (e) {
print('GetCompletedTopics error: $e');
rethrow;
}
}
Future<Response> studentFeedback(
String stdLearningId,
String feedback,
String token,
) async {
try {
final response = await _dio.put(
'/stdLearning/$stdLearningId',
data: {'FEEDBACK_STUDENT': feedback},
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
),
);
return response;
} catch (e) {
print('Submit Feedback error: $e');
rethrow;
}
}
Future<Response> getStudentAnswers(String stdLearningId, String token) async {
try {
final response = await _dio.get(
'/studentAnswer/$stdLearningId',
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
),
);
print('studentAnswers response: ${response.data}');
return response;
} catch (e) {
print('StudentAnswers error: $e');
rethrow;
}
}
} }

View 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');
}
}
}

View File

@ -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/';

View File

@ -1,4 +1,5 @@
import 'package:english_learning/core/services/dio_client.dart'; import 'package:english_learning/core/services/dio_client.dart';
import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
class ExerciseRepository { class ExerciseRepository {
final DioClient _dioClient; final DioClient _dioClient;
@ -19,6 +20,20 @@ class ExerciseRepository {
} }
} }
Future<Map<String, dynamic>> getStudentAnswers(
String stdLearningId, String token) async {
try {
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
if (response.statusCode == 200) {
return response.data['data'];
} else {
throw Exception('Failed to load student answers');
}
} catch (e) {
throw Exception('Error fetching student answers: $e');
}
}
Future<Map<String, dynamic>> submitAnswersAndGetScore( Future<Map<String, dynamic>> submitAnswersAndGetScore(
List<Map<String, dynamic>> answers, List<Map<String, dynamic>> answers,
String studentLearningId, String studentLearningId,
@ -38,4 +53,25 @@ class ExerciseRepository {
throw Exception('Error submitting answers and getting score: $e'); throw Exception('Error submitting answers and getting score: $e');
} }
} }
Future<FeedbackModel> submitFeedback(
String stdLearningId,
String feedback,
String token,
) async {
try {
final response = await _dioClient.studentFeedback(
stdLearningId,
feedback,
token,
);
if (response.statusCode == 200) {
return FeedbackModel.fromJson(response.data['payload']);
} else {
throw Exception('Failed to submit feedback');
}
} catch (e) {
throw Exception('Error submitting feedback: $e');
}
}
} }

View File

@ -10,12 +10,12 @@ class LevelRepository {
if (response.statusCode == 200 && response.data != null) { if (response.statusCode == 200 && response.data != null) {
final Map<String, dynamic> responseData = response.data; final Map<String, dynamic> responseData = response.data;
if (responseData.containsKey('data')) { if (responseData.containsKey('data')) {
final Map<String, dynamic> data = responseData['data']; final Map<String, dynamic> data = responseData['data'] ?? {};
final List<dynamic> levelsData = data['levels'] ?? []; final List<dynamic> levelsData = data['levels'] ?? [];
final List<Level> levels = final List<Level> levels =
levelsData.map((json) => Level.fromJson(json)).toList(); levelsData.map((json) => Level.fromJson(json)).toList();
final Map<String, dynamic>? lastCompletedLevel = final Map<String, dynamic>? lastCompletedLevel =
data['lastCompletedLevel']; data['lastCompletedLevel'] ?? {};
return { return {
'levels': levels, 'levels': levels,
@ -32,4 +32,20 @@ class LevelRepository {
rethrow; rethrow;
} }
} }
Future<Map<String, dynamic>> getStudentAnswers(
String stdLearningId, String token) async {
try {
final response = await _dioClient.getStudentAnswers(stdLearningId, token);
if (response.statusCode == 200 && response.data != null) {
return response.data['data'];
} else {
throw Exception(
'Failed to fetch student answers: ${response.statusCode}');
}
} catch (e) {
throw Exception('Failed to fetch student answers: $e');
}
}
} }

View File

@ -2,9 +2,9 @@ import 'package:intl/intl.dart';
class LearningHistory { class LearningHistory {
final int score; final int score;
final dynamic currentLevel; final String currentLevel;
final dynamic nextLevel; final String? nextLevel;
final DateTime studentFinish; final DateTime? studentFinish;
final String topicName; final String topicName;
final String sectionName; final String sectionName;
@ -19,16 +19,20 @@ class LearningHistory {
factory LearningHistory.fromJson(Map<String, dynamic> json) { factory LearningHistory.fromJson(Map<String, dynamic> json) {
return LearningHistory( return LearningHistory(
score: json['SCORE'], score: json['SCORE'] ?? 0,
currentLevel: json['CURRENT_LEVEL'], currentLevel: json['CURRENT_LEVEL'] ?? 'Unknown',
nextLevel: json['NEXT_LEVEL'], nextLevel: json['NEXT_LEVEL'],
studentFinish: DateTime.parse(json['STUDENT_FINISH']), studentFinish: json['STUDENT_FINISH'] != null
topicName: json['TOPIC_NAME'], ? DateTime.parse(json['STUDENT_FINISH'])
sectionName: json['SECTION_NAME'], : null,
topicName: json['TOPIC_NAME'] ?? 'Unknown Topic',
sectionName: json['SECTION_NAME'] ?? 'Unknown Section',
); );
} }
String get formattedDate { String get formattedDate {
return DateFormat('yyyy-MM-dd HH:mm').format(studentFinish); return studentFinish != null
? DateFormat('yyyy-MM-dd HH:mm').format(studentFinish!)
: 'N/A';
} }
} }

View File

@ -67,20 +67,50 @@ class HistoryProvider with ChangeNotifier {
} }
} }
int? _parseLevel(dynamic level) { int? _parseLevel(String? level) {
if (level is int) { if (level == null) return null;
return level; if (level.toLowerCase() == 'pretest') {
} else if (level is String) { return 0; // Treat Pretest as level 0
if (level.toLowerCase() == 'pretest') {
return 0; // Treat Pretest as level 0
}
return int.tryParse(level.replaceAll('Level ', ''));
} }
return null; return int.tryParse(level.replaceAll('Level ', '')) ?? -1;
} }
Future<void> refreshData(String token) async { Future<void> refreshData(String token) async {
await _sectionProvider.fetchSections(token); if (_sectionProvider.sections.isEmpty) {
await _sectionProvider.fetchSections(token);
}
await fetchLearningHistory(token); await fetchLearningHistory(token);
} }
Future<void> loadHistoryData(String token) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
// Fetch sections and learning history in parallel
final sectionsResult = _sectionProvider.fetchSections(token);
// Use the first section ID for initial history fetch
final firstSectionId = await sectionsResult
.then((sections) => sections.isNotEmpty ? sections.first.id : null);
if (firstSectionId != null) {
final historyResult =
_repository.getLearningHistory(firstSectionId, token);
// Wait for both futures to complete
final results = await Future.wait([sectionsResult, historyResult]);
_learningHistory = results[1] as List<LearningHistory>;
} else {
_error = 'No sections available';
}
} catch (e) {
_error = 'Error loading data: ${e.toString()}';
} finally {
_isLoading = false;
notifyListeners();
}
}
} }

View File

@ -1,3 +1,4 @@
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:english_learning/core/widgets/global_button.dart'; import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/history/provider/history_provider.dart'; import 'package:english_learning/features/history/provider/history_provider.dart';
@ -18,95 +19,141 @@ class HistoryScreen extends StatefulWidget {
} }
class _HistoryScreenState extends State<HistoryScreen> { class _HistoryScreenState extends State<HistoryScreen> {
@override bool isNotFoundError(String error) {
void initState() { return error.toLowerCase().contains('no learning history found') ||
super.initState(); error.toLowerCase().contains('not found');
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(context, listen: false);
historyProvider.refreshData(userProvider.jwtToken!);
});
});
} }
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((_) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final historyProvider =
// Provider.of<HistoryProvider>(context, listen: false);
// final userProvider = Provider.of<UserProvider>(context, listen: false);
// historyProvider.refreshData(userProvider.jwtToken!);
// });
// });
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Consumer<HistoryProvider>(
backgroundColor: AppColors.bgSoftColor, builder: (context, historyProvider, chiild) {
body: SafeArea( return Scaffold(
child: Center( backgroundColor: AppColors.bgSoftColor,
child: Padding( body: SafeArea(
padding: const EdgeInsets.symmetric( child: Center(
horizontal: 16.0, child: Padding(
vertical: 30.0, padding: const EdgeInsets.symmetric(
), horizontal: 16.0,
child: Column( vertical: 30.0,
children: [ ),
Container( child: Column(
padding: const EdgeInsets.all(16.0), children: [
decoration: BoxDecoration( Container(
color: AppColors.whiteColor, padding: const EdgeInsets.all(16.0),
borderRadius: BorderRadius.circular(8), decoration: BoxDecoration(
), color: AppColors.whiteColor,
child: Column( borderRadius: BorderRadius.circular(8),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
'Your Exercise History!', children: [
style: AppTextStyles.blueTextStyle.copyWith( Text(
fontSize: 18, 'Your Exercise History!',
fontWeight: FontWeight.bold, style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 18,
fontWeight: FontWeight.bold,
),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8), Text(
Text( 'Track your progress with a personalized overview of all your workouts.',
'Track your progress with a personalized overview of all your workouts.', style: AppTextStyles.greyTextStyle.copyWith(
style: AppTextStyles.greyTextStyle.copyWith( fontSize: 13,
fontSize: 13, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, ),
), ),
), ],
], ),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8), const CustomTabBar(),
const CustomTabBar(), const SizedBox(height: 8),
const SizedBox(height: 8), Expanded(
Expanded( child: Consumer<HistoryProvider>(
child: Consumer<HistoryProvider>( builder: (context, historyProvider, child) {
builder: (context, historyProvider, child) { if (historyProvider.isLoading) {
if (historyProvider.isLoading) { return const Center(
return const Center(child: CircularProgressIndicator()); child: CircularProgressIndicator());
} else if (historyProvider.error != null) { } else if (historyProvider.error != null) {
return Center(child: Text(historyProvider.error!)); if (isNotFoundError(historyProvider.error!)) {
} else if (historyProvider.learningHistory.isEmpty) { return _buildEmptyState(context);
return _buildEmptyState(context); } else {
} else { return _buildErrorState(historyProvider.error!);
return ListView.builder( }
itemCount: historyProvider.learningHistory.length, } else if (historyProvider.learningHistory.isEmpty) {
itemBuilder: (context, index) { return _buildEmptyState(context);
return Column( } else {
children: [ return ListView.builder(
ExerciseHistoryCard( itemCount: historyProvider.learningHistory.length,
exercise: itemBuilder: (context, index) {
historyProvider.learningHistory[index], return Column(
), children: [
const SizedBox(height: 8.0), 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',
),
],
), ),
); );
} }

View File

@ -5,21 +5,8 @@ import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class CustomTabBar extends StatefulWidget { class CustomTabBar extends StatelessWidget {
const CustomTabBar({super.key}); const CustomTabBar({Key? key}) : super(key: key);
@override
_CustomTabBarState createState() => _CustomTabBarState();
}
class _CustomTabBarState extends State<CustomTabBar> {
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -27,12 +14,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
final sectionProvider = Provider.of<SectionProvider>(context); final sectionProvider = Provider.of<SectionProvider>(context);
final selectedPageIndex = historyProvider.selectedPageIndex; final selectedPageIndex = historyProvider.selectedPageIndex;
if (sectionProvider.sections.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView( return SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: sectionProvider.sections.asMap().entries.map((entry) { children: sectionProvider.sections.asMap().entries.map((entry) {
@ -56,12 +38,6 @@ class _CustomTabBarState extends State<CustomTabBar> {
Provider.of<HistoryProvider>(context, listen: false); Provider.of<HistoryProvider>(context, listen: false);
historyProvider.setSelectedPageIndex(index); historyProvider.setSelectedPageIndex(index);
_scrollController.animateTo(
index * 100.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
final userProvider = Provider.of<UserProvider>(context, listen: false); final userProvider = Provider.of<UserProvider>(context, listen: false);
historyProvider.fetchLearningHistory(userProvider.jwtToken!); historyProvider.fetchLearningHistory(userProvider.jwtToken!);
} }

View File

@ -28,84 +28,71 @@ class ExerciseHistoryCard extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0), padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Column( Text(
crossAxisAlignment: CrossAxisAlignment.start, exercise.topicName,
children: [ overflow: TextOverflow.ellipsis,
RichText( style: AppTextStyles.tetriaryTextStyle.copyWith(
text: TextSpan( fontWeight: FontWeight.w500,
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,
),
),
],
), ),
Container( const SizedBox(height: 8),
padding: const EdgeInsets.symmetric( RichText(
horizontal: 16.0, text: TextSpan(
vertical: 26.0, style: AppTextStyles.greyTextStyle.copyWith(
), fontSize: 14,
decoration: BoxDecoration( fontWeight: FontWeight.w600,
border: Border.all(
color: color,
), ),
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', const SizedBox(height: 8),
style: TextStyle( Text(
color: color, 'Submission: ${exercise.formattedDate}',
fontWeight: FontWeight.bold, 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,
),
),
),
], ],
), ),
), ),

View 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'],
);
}
}

View 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();
}
}
}

View File

@ -1,8 +1,11 @@
import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/carousel_slider.dart';
import 'package:english_learning/core/widgets/custom_button.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/history/provider/history_provider.dart';
import 'package:english_learning/features/history/screens/history_screen.dart'; import 'package:english_learning/features/history/screens/history_screen.dart';
import 'package:english_learning/features/home/data/card_data.dart'; import 'package:english_learning/features/home/data/card_data.dart';
import 'package:english_learning/features/home/provider/completed_topics_provider.dart';
import 'package:english_learning/features/home/widgets/progress_card.dart'; import 'package:english_learning/features/home/widgets/progress_card.dart';
import 'package:english_learning/features/home/widgets/welcome_card.dart'; import 'package:english_learning/features/home/widgets/welcome_card.dart';
import 'package:english_learning/features/learning/screens/learning_screen.dart'; import 'package:english_learning/features/learning/screens/learning_screen.dart';
@ -14,6 +17,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:google_nav_bar/google_nav_bar.dart'; import 'package:google_nav_bar/google_nav_bar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -78,15 +82,19 @@ class _HomeScreenState extends State<HomeScreen> {
iconSize: 20, iconSize: 20,
gap: 8, gap: 8,
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex,
onTabChange: (index) { onTabChange: (index) async {
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
_pageController.animateToPage( _pageController.jumpToPage(index);
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, // Animasi ketika berpindah tab
);
}); });
if (index == 2) {
// Index 2 adalah tab History
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final userProvider =
Provider.of<UserProvider>(context, listen: false);
await historyProvider.loadHistoryData(userProvider.jwtToken!);
}
}, },
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@ -127,164 +135,294 @@ class HomeContent extends StatefulWidget {
class _HomeContentState extends State<HomeContent> { class _HomeContentState extends State<HomeContent> {
final CardData cardData = CardData(); final CardData cardData = CardData();
int _currentPage = 0; int _currentPage = 0;
bool hasOngoingExercises = false;
@override
void initState() {
super.initState();
// Memanggil fetchCompletedTopics saat HomeContent diinisialisasi
WidgetsBinding.instance.addPostFrameCallback((_) {
final userProvider = Provider.of<UserProvider>(context, listen: false);
final completedTopicsProvider =
Provider.of<CompletedTopicsProvider>(context, listen: false);
completedTopicsProvider.fetchCompletedTopics(userProvider.jwtToken!);
});
}
Widget _buildNoDataWidget() {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Still new?',
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Begin your journey!',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 14,
),
),
const SizedBox(height: 24),
CustomButton(
text: 'Explore',
width: double.infinity,
height: 44,
color: AppColors.yellowButtonColor,
onPressed: () {},
),
],
),
);
}
Widget _buildShimmerEffect() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Column(
children: List.generate(
2,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Container(
width: double.infinity,
height: 150,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
)),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<UserProvider>(builder: (context, authProvider, child) { return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
context,
authProvider,
completedTopicsProvider,
child,
) {
final userName = authProvider.getUserName() ?? 'Guest'; final userName = authProvider.getUserName() ?? 'Guest';
return Column( return SingleChildScrollView(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Stack( children: [
children: [ Stack(
Column( children: [
mainAxisSize: MainAxisSize.min, Column(
children: [ mainAxisSize: MainAxisSize.min,
Container( children: [
width: double.infinity, Container(
decoration: BoxDecoration( width: double.infinity,
gradient: AppColors.gradientTheme, decoration: BoxDecoration(
borderRadius: const BorderRadius.only( gradient: AppColors.gradientTheme,
bottomLeft: Radius.circular(24), borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(24), bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
), ),
), child: Padding(
child: Padding( padding: const EdgeInsets.only(
padding: const EdgeInsets.only( top: 60.0,
top: 60.0, left: 18.34,
left: 18.34, right: 16.0,
right: 16.0, bottom: 34.0,
bottom: 34.0, ),
), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Row(
Row( children: [
children: [ SvgPicture.asset(
SvgPicture.asset( 'lib/features/home/assets/images/Logo.svg',
'lib/features/home/assets/images/Logo.svg', width: 31,
width: 31,
),
const SizedBox(width: 4.34),
Text(
'SEALS',
style: AppTextStyles.logoTextStyle.copyWith(
fontSize: 28,
fontWeight: FontWeight.w700,
), ),
), const SizedBox(width: 4.34),
], Text(
), 'SEALS',
GestureDetector( style:
onTap: () { AppTextStyles.logoTextStyle.copyWith(
Navigator.push( fontSize: 28,
context, fontWeight: FontWeight.w700,
MaterialPageRoute( ),
builder: (context) =>
const EditProfileScreen(),
), ),
); ],
},
child: const Icon(
BootstrapIcons.person_circle,
size: 28,
color: AppColors.whiteColor,
), ),
), GestureDetector(
], onTap: () {
), Navigator.push(
const SizedBox(height: 17), context,
RichText( MaterialPageRoute(
text: TextSpan( builder: (context) =>
text: 'Hi, ', const EditProfileScreen(),
style: AppTextStyles.whiteTextStyle.copyWith( ),
fontWeight: FontWeight.w700, );
fontSize: 18, },
), child: const Icon(
children: <TextSpan>[ BootstrapIcons.person_circle,
TextSpan( size: 28,
text: userName, color: AppColors.whiteColor,
style: AppTextStyles.yellowTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
TextSpan(
text: '!',
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
), ),
), ),
], ],
), ),
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( Text(
'Let\'s evolve together', 'Your Last Journey.',
style: AppTextStyles.whiteTextStyle.copyWith( style: AppTextStyles.tetriaryTextStyle.copyWith(
fontSize: 12, 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
// ),
],
); );
}); });
} }

View 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,
),
);
}
}

View File

@ -2,31 +2,34 @@ import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
class ProgressBar extends StatelessWidget { class ProgressBar extends StatelessWidget {
final int currentProgress; final int completedTopics;
final int totalProgress; final int totalTopics;
const ProgressBar({ const ProgressBar({
super.key, super.key,
required this.currentProgress, required this.completedTopics,
required this.totalProgress, required this.totalTopics,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final progress = totalProgress > 0 ? currentProgress / totalProgress : 0.0; final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
final progress = totalTopics > 0 ? completedTopics / totalTopics : 0.0;
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final barWidth = constraints.maxWidth - 40; final barWidth = constraints.maxWidth - 30;
return Row( return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
SizedBox( SizedBox(
width: barWidth, width: double.infinity,
child: Container( child: Container(
height: 12, height: 12,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
), ),
child: Stack( child: Stack(
@ -44,12 +47,25 @@ class ProgressBar extends StatelessWidget {
), ),
), ),
), ),
const Spacer(), // const Spacer(),
Text( SizedBox(
'$currentProgress/$totalProgress', height: screenHeight * 0.02,
style: AppTextStyles.blueTextStyle.copyWith( ),
fontSize: 14, RichText(
fontWeight: FontWeight.w500, 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,
),
)
],
), ),
), ),
], ],

View File

@ -1,91 +1,103 @@
import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:english_learning/core/services/repositories/constants.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/features/home/models/completed_topics_model.dart';
import 'package:english_learning/features/home/widgets/progress_bar.dart'; import 'package:english_learning/features/home/widgets/progress_bar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ProgressCard extends StatelessWidget { class ProgressCard extends StatelessWidget {
const ProgressCard({super.key}); final List<CompletedTopic> completedTopic;
const ProgressCard({super.key, required this.completedTopic});
String _getFullImageUrl(String thumbnail) {
return thumbnail.startsWith('http')
? thumbnail
: '${baseUrl}uploads/section/$thumbnail';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( ...completedTopic.asMap().entries.map(
width: double.infinity, (entry) {
decoration: BoxDecoration( CompletedTopic topic = entry.value;
color: AppColors.whiteColor, return Padding(
borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.only(bottom: 12),
), child: _buildTopicItem(topic),
child: Padding( );
padding: const EdgeInsets.symmetric( },
horizontal: 16.0, ),
vertical: 16.0,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
const Icon(
BootstrapIcons.info_circle,
color: AppColors.disableColor,
size: 16,
),
const SizedBox(width: 8),
Text(
'Your Last Journey!',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
],
),
const SizedBox(height: 24),
Container(
width: double.infinity,
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,
),
],
),
),
),
],
),
),
)
], ],
); );
} }
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,
),
],
),
),
],
),
),
);
}
} }

View File

@ -9,6 +9,8 @@ class ExerciseModel {
final String? image; final String? image;
final DateTime timeAdminExc; final DateTime timeAdminExc;
final dynamic choices; final dynamic choices;
final String? answerStudent;
final bool? isCorrect;
ExerciseModel({ ExerciseModel({
required this.idAdminExercise, required this.idAdminExercise,
@ -21,6 +23,8 @@ class ExerciseModel {
this.image, this.image,
required this.timeAdminExc, required this.timeAdminExc,
required this.choices, required this.choices,
this.answerStudent,
this.isCorrect,
}); });
factory ExerciseModel.fromJson(Map<String, dynamic> json) { factory ExerciseModel.fromJson(Map<String, dynamic> json) {
@ -57,6 +61,21 @@ class ExerciseModel {
choices: choices, choices: choices,
); );
} }
factory ExerciseModel.fromReviewJson(Map<String, dynamic> json) {
final exerciseDetails = json['exerciseDetails'];
return ExerciseModel(
idAdminExercise: exerciseDetails['ID_ADMIN_EXERCISE'],
idLevel: json['ID_LEVEL'] ?? '', // This might be available in parent data
title: exerciseDetails['TITLE'],
question: exerciseDetails['QUESTION'],
questionType: exerciseDetails['QUESTION_TYPE'],
timeAdminExc: DateTime.parse(json['TIME_STUDENT_EXC']),
answerStudent: json['ANSWER_STUDENT'],
isCorrect: json['IS_CORRECT'] == 1,
choices: null,
);
}
} }
class MultipleChoice { class MultipleChoice {

View File

@ -1,3 +1,4 @@
import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:english_learning/core/services/repositories/exercise_repository.dart'; import 'package:english_learning/core/services/repositories/exercise_repository.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart';
@ -23,6 +24,7 @@ class ExerciseProvider extends ChangeNotifier {
String _nameLevel = ''; String _nameLevel = '';
String? _activeLeftOption; String? _activeLeftOption;
String? _studentLearningId; String? _studentLearningId;
bool _isReview = false;
// Constants // Constants
final List<Color> _pairColors = [ final List<Color> _pairColors = [
@ -42,6 +44,7 @@ class ExerciseProvider extends ChangeNotifier {
String get nameLevel => _nameLevel; String get nameLevel => _nameLevel;
String? get activeLeftOption => _activeLeftOption; String? get activeLeftOption => _activeLeftOption;
String? get studentLearningId => _studentLearningId; String? get studentLearningId => _studentLearningId;
bool get isReview => _isReview;
// Initialization methods // Initialization methods
void initializeAnswers() { void initializeAnswers() {
@ -64,11 +67,40 @@ class ExerciseProvider extends ChangeNotifier {
}); });
} }
// Answer handling methods // // Answer handling methods
// void answerQuestion(int index, String answer) {
// if (index >= 0 && index < _answers.length) {
// if (_exercises[index].choices is MatchingPair) {
// _handleMatchingPairAnswer(index, answer);
// } else {
// _answers[index] = _answers[index] == answer ? '' : answer;
// }
// notifyListeners();
// }
// }
void answerQuestion(int index, String answer) { void answerQuestion(int index, String answer) {
if (_isReview) return;
if (index >= 0 && index < _answers.length) { if (index >= 0 && index < _answers.length) {
if (_exercises[index].choices is MatchingPair) { if (_exercises[index].choices is MatchingPair) {
_handleMatchingPairAnswer(index, answer); _handleMatchingPairAnswer(index, answer);
} else if (_exercises[index].choices is MultipleChoice) {
// Store the letter index (e.g., "A", "B", "C", etc.)
final multipleChoice = _exercises[index].choices as MultipleChoice;
final options = [
multipleChoice.optionA,
multipleChoice.optionB,
multipleChoice.optionC,
multipleChoice.optionD,
multipleChoice.optionE,
];
final optionIndex = options.indexOf(answer);
if (optionIndex != -1) {
_answers[index] = String.fromCharCode(65 + optionIndex);
}
} else if (_exercises[index].choices is TrueFalse) {
// Store "1" for true and "0" for false
// _answers[index] = answer.toLowerCase() == 'true' ? '1' : '0';
_answers[index] = answer;
} else { } else {
_answers[index] = _answers[index] == answer ? '' : answer; _answers[index] = _answers[index] == answer ? '' : answer;
} }
@ -238,6 +270,33 @@ class ExerciseProvider extends ChangeNotifier {
} }
} }
Future<void> fetchReviewExercises(String stdLearningId) async {
_isLoading = true;
_isReview = true;
notifyListeners();
try {
final token = await _userProvider.getValidToken();
if (token == null) {
throw Exception('No valid token found');
}
final data = await _repository.getStudentAnswers(stdLearningId, token);
_nameTopic = data['NAME_TOPIC'];
_nameLevel = data['NAME_LEVEL'];
final exercisesData = data['stdExercises'];
_exercises = exercisesData
.map<ExerciseModel>((json) => ExerciseModel.fromReviewJson(json))
.toList();
_answers = _exercises.map((e) => e.answerStudent ?? '').toList();
} catch (e) {
print('Error fetching review exercises: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<Map<String, dynamic>> submitAnswersAndGetScore() async { Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
print('submitAnswersAndGetScore called'); print('submitAnswersAndGetScore called');
try { try {
@ -256,10 +315,17 @@ class ExerciseProvider extends ChangeNotifier {
}).map((entry) { }).map((entry) {
final index = entry.key; final index = entry.key;
final exercise = entry.value; final exercise = entry.value;
String formattedAnswer = _answers[index];
if (exercise.choices is MultipleChoice) {
formattedAnswer = formattedAnswer;
} else if (exercise.choices is TrueFalse) {
formattedAnswer = formattedAnswer.toLowerCase() == 'true' ? '1' : '0';
}
return { return {
'ID_STUDENT_LEARNING': _studentLearningId!, 'ID_STUDENT_LEARNING': _studentLearningId!,
'ID_ADMIN_EXERCISE': exercise.idAdminExercise, 'ID_ADMIN_EXERCISE': exercise.idAdminExercise,
'ANSWER_STUDENT': _answers[index], 'ANSWER_STUDENT': formattedAnswer,
}; };
}).toList(); }).toList();
@ -286,6 +352,26 @@ class ExerciseProvider extends ChangeNotifier {
} }
} }
Future<FeedbackModel> submitFeedback(String feedback) async {
try {
final token = await _userProvider.getValidToken();
if (token == null) {
throw Exception('No valid token found');
}
if (_studentLearningId == null) {
throw Exception('Student Learning ID is not set');
}
print('Submitting feedback for stdLearningId: $_studentLearningId');
final result = await _repository.submitFeedback(
_studentLearningId!, feedback, token);
print('Feedback submitted successfully');
return result;
} catch (e) {
print('Error submitting feedback: $e');
rethrow;
}
}
bool hasAnsweredQuestions() { bool hasAnsweredQuestions() {
return _answers.any((answer) => answer.isNotEmpty); return _answers.any((answer) => answer.isNotEmpty);
} }

View File

@ -10,11 +10,13 @@ import 'package:provider/provider.dart';
class ExerciseScreen extends StatefulWidget { class ExerciseScreen extends StatefulWidget {
final String? levelId; final String? levelId;
final String studentLearningId; final String studentLearningId;
final bool isReview;
const ExerciseScreen({ const ExerciseScreen({
super.key, super.key,
required this.levelId, required this.levelId,
required this.studentLearningId, required this.studentLearningId,
this.isReview = false,
}); });
@override @override
@ -29,7 +31,11 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
_scrollController = ScrollController(); _scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<ExerciseProvider>(); final provider = context.read<ExerciseProvider>();
provider.fetchExercises(widget.levelId!); if (widget.isReview) {
provider.fetchReviewExercises(widget.studentLearningId);
} else {
provider.fetchExercises(widget.levelId!);
}
provider.setStudentLearningId(widget.studentLearningId); provider.setStudentLearningId(widget.studentLearningId);
}); });
} }
@ -98,6 +104,8 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
children: [ children: [
if (provider.isReview)
Text('Review Mode', style: TextStyle(fontSize: 24)),
const SizedBox(height: 16), const SizedBox(height: 16),
const ExerciseContent(), const ExerciseContent(),
const SizedBox(height: 24), const SizedBox(height: 24),

View File

@ -31,10 +31,11 @@ class ExerciseNavigator extends StatelessWidget {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ResultScreen( builder: (context) => ResultScreen(
currentLevel: result['CURRENT_LEVEL_NAME'], currentLevel: result['CURRENT_LEVEL_NAME'] ?? '',
nextLevel: result['NEXT_LEARNING_NAME'], nextLevel: result['NEXT_LEARNING_NAME'] ?? '',
score: int.parse(result['SCORE'].toString()), score: int.tryParse(result['SCORE'].toString()) ?? 0,
isCompleted: result['IS_PASS'] == 1, isCompleted: result['IS_PASS'] == 1,
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
), ),
), ),
); );

View File

@ -1,4 +1,3 @@
// multiple_choice_question.dart
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
@ -31,7 +30,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) { Widget _buildOptionsList(List<String> options, ExerciseProvider provider) {
final optionLabels = List.generate( final optionLabels = List.generate(
options.length, options.length,
(index) => String.fromCharCode(65 + index), (index) =>
String.fromCharCode(65 + index), // Generate labels "A", "B", etc.
); );
return ListView.builder( return ListView.builder(
@ -41,7 +41,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
itemBuilder: (context, i) { itemBuilder: (context, i) {
final option = options[i]; final option = options[i];
final isSelected = final isSelected =
provider.answers[provider.currentExerciseIndex] == option; provider.answers[provider.currentExerciseIndex] == optionLabels[i];
return GestureDetector( return GestureDetector(
onTap: () => onTap: () =>
@ -69,10 +69,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
), ),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding:
horizontal: 14.0, const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
vertical: 10.0,
),
child: Text( child: Text(
label, label,
style: AppTextStyles.blackTextStyle.copyWith( style: AppTextStyles.blackTextStyle.copyWith(

View File

@ -28,12 +28,15 @@ class TrueFalseQuestion extends StatelessWidget {
itemCount: options.length, itemCount: options.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final option = options[i]; final option = options[i];
final isSelected = final isSelected = provider.answers[provider.currentExerciseIndex] ==
provider.answers[provider.currentExerciseIndex] == option; (i == 0 ? '1' : '0');
return GestureDetector( return GestureDetector(
onTap: () => // onTap: () =>
provider.answerQuestion(provider.currentExerciseIndex, option), // provider.answerQuestion(provider.currentExerciseIndex, option),
onTap: () => provider.answerQuestion(
provider.currentExerciseIndex, i == 0 ? '1' : '0'),
child: _buildOptionItem(option, isSelected), child: _buildOptionItem(option, isSelected),
); );
}, },

View File

@ -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']),
);
}
}

View File

@ -1,10 +1,17 @@
import 'package:english_learning/core/widgets/global_button.dart'; import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:english_learning/features/learning/modules/feedback/widgets/feedback_dialog.dart'; import 'package:english_learning/features/learning/modules/feedback/widgets/feedback_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FeedbackScreen extends StatefulWidget { class FeedbackScreen extends StatefulWidget {
const FeedbackScreen({super.key}); final String stdLearningId;
const FeedbackScreen({
super.key,
required this.stdLearningId,
});
@override @override
State<FeedbackScreen> createState() => _FeedbackScreenState(); State<FeedbackScreen> createState() => _FeedbackScreenState();
@ -33,6 +40,58 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
_focusNode.unfocus(); _focusNode.unfocus();
} }
Future<void> _submitFeedback() async {
if (_controller.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter your feedback before submitting.'),
backgroundColor: Colors.red,
),
);
return;
}
final exerciseProvider =
Provider.of<ExerciseProvider>(context, listen: false);
try {
final result = await exerciseProvider.submitFeedback(_controller.text);
print('Feedback submitted successfully: $result');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Feedback submitted successfully!'),
backgroundColor: Colors.green,
),
);
// Show the dialog
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return FeedbackDialog(
onSubmit: () {
Navigator.of(dialogContext).pop(); // Close the dialog
Navigator.of(context).pop(); // Return to the previous screen
},
);
},
);
}
} catch (e) {
print('Error submitting feedback: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to submit feedback: $e'),
backgroundColor: Colors.red,
),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
@ -121,23 +180,7 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
GlobalButton( GlobalButton(
text: 'Send', text: 'Send',
onPressed: () { onPressed: _submitFeedback,
showDialog(
context: context,
builder: (BuildContext context) {
return FeedbackDialog(
onSubmit: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const LevelListScreen(),
// ),
// );
},
);
},
);
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
GlobalButton( GlobalButton(
@ -149,9 +192,13 @@ class _FeedbackScreenState extends State<FeedbackScreen> {
// Navigator.push( // Navigator.push(
// context, // context,
// MaterialPageRoute( // MaterialPageRoute(
// builder: (context) => const LevelListScreen(), // builder: (context) => LevelListScreen(
// topicId: widget.topicId!,
// topicTitle: widget.topicTitle!,
// ),
// ), // ),
// ); // );
Navigator.of(context).pop(true);
}, },
) )
], ],

View File

@ -58,6 +58,11 @@ class FeedbackDialog extends StatelessWidget {
// builder: (context) => const LevelListScreen(), // builder: (context) => const LevelListScreen(),
// ), // ),
// ); // );
onSubmit();
// Navigator.of(context).popUntil(
// (route) => route.isFirst,
// );
Navigator.of(context).pop(true);
}, },
) )
], ],

View File

@ -33,17 +33,17 @@ class Level {
factory Level.fromJson(Map<String, dynamic> json) { factory Level.fromJson(Map<String, dynamic> json) {
return Level( return Level(
idLevel: json['ID_LEVEL'], idLevel: json['ID_LEVEL'] ?? '',
idTopic: json['ID_TOPIC'], idTopic: json['ID_TOPIC'] ?? '',
idSection: json['ID_SECTION'], idSection: json['ID_SECTION'] ?? '',
nameSection: json['NAME_SECTION'], nameSection: json['NAME_SECTION'] ?? '',
nameTopic: json['NAME_TOPIC'], nameTopic: json['NAME_TOPIC'] ?? '',
nameLevel: json['NAME_LEVEL'], nameLevel: json['NAME_LEVEL'] ?? '',
content: json['CONTENT'], content: json['CONTENT'] ?? '',
audio: json['AUDIO'], audio: json['AUDIO'],
image: json['IMAGE'], image: json['IMAGE'],
video: json['VIDEO'], video: json['VIDEO'],
isPretest: json['IS_PRETEST'], isPretest: json['IS_PRETEST'] ?? 0,
timeLevel: json['TIME_LEVEL'], timeLevel: json['TIME_LEVEL'],
idStudentLearning: json['ID_STUDENT_LEARNING'], idStudentLearning: json['ID_STUDENT_LEARNING'],
score: json['SCORE'], score: json['SCORE'],

View File

@ -6,13 +6,17 @@ class LevelProvider with ChangeNotifier {
final LevelRepository _levelRepository = LevelRepository(); final LevelRepository _levelRepository = LevelRepository();
List<Level> _levels = []; List<Level> _levels = [];
Map<String, dynamic>? _lastCompletedLevel; Map<String, dynamic>? _lastCompletedLevel;
List<String> _unlockedLevels = [];
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
Map<String, dynamic>? _studentAnswers;
List<Level> get levels => _levels; List<Level> get levels => _levels;
Map<String, dynamic>? get lastCompletedLevel => _lastCompletedLevel; Map<String, dynamic>? get lastCompletedLevel => _lastCompletedLevel;
List<String> get unlockedLevels => _unlockedLevels;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
String? get error => _error; String? get error => _error;
Map<String, dynamic>? get studentAnswers => _studentAnswers;
Future<void> fetchLevels(String topicId, String token) async { Future<void> fetchLevels(String topicId, String token) async {
_isLoading = true; _isLoading = true;
@ -23,6 +27,9 @@ class LevelProvider with ChangeNotifier {
final result = await _levelRepository.getLevels(topicId, token); final result = await _levelRepository.getLevels(topicId, token);
_levels = result['levels']; _levels = result['levels'];
_lastCompletedLevel = result['lastCompletedLevel']; _lastCompletedLevel = result['lastCompletedLevel'];
_unlockedLevels =
List<String>.from(_lastCompletedLevel?['UNLOCKED_LEVELS'] ?? []);
if (_levels.isEmpty) { if (_levels.isEmpty) {
_error = 'No levels found for this topic'; _error = 'No levels found for this topic';
} }
@ -39,37 +46,90 @@ class LevelProvider with ChangeNotifier {
orElse: () => throw Exception('Pretest not found')); orElse: () => throw Exception('Pretest not found'));
} }
// int getLevelScore(String levelId) {
// final level = _levels.firstWhere((level) => level.idLevel == levelId,
// orElse: () => const Level(
// idLevel: '',
// idTopic: '',
// idSection: '',
// nameSection: '',
// nameTopic: '',
// nameLevel: '',
// content: '',
// isPretest: 0,
// timeLevel: '',
// ));
// return level.score ?? 0;
// }
// bool isLevelAllowed(String levelName) {
// return _unlockedLevels.contains(levelName);
// }
// bool isLevelCompleted(String levelId) {
// return _lastCompletedLevel != null &&
// _lastCompletedLevel!['ID_LEVEL'] == levelId;
// }
Future<void> fetchStudentAnswers(String stdLearningId, String token) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_studentAnswers =
await _levelRepository.getStudentAnswers(stdLearningId, token);
} catch (e) {
_error = 'Error fetching student answers: ${e.toString()}';
} finally {
_isLoading = false;
notifyListeners();
}
}
bool isPretestFinished(String levelId) {
return _levels.any(
(level) => level.idLevel == levelId && level.idStudentLearning != null);
}
int getPretestScore(String levelId) {
final pretest = _levels.firstWhere(
(level) => level.idLevel == levelId && level.isPretest == 1,
orElse: () => throw Exception('Null'));
return pretest.score ?? 0;
}
int getLevelScore(String levelId) { int getLevelScore(String levelId) {
final level = _levels.firstWhere((level) => level.idLevel == levelId, final level = _levels.firstWhere((level) => level.idLevel == levelId,
orElse: () => const Level( orElse: () => throw Exception('Null'));
idLevel: '',
idTopic: '',
idSection: '',
nameSection: '',
nameTopic: '',
nameLevel: '',
content: '',
isPretest: 0,
timeLevel: '',
));
return level.score ?? 0; return level.score ?? 0;
} }
bool isLevelAllowed(String levelId) { bool isLevelAllowed(String levelId) {
if (_lastCompletedLevel == null) { return _unlockedLevels.contains(
// Jika tidak ada level yang selesai, hanya pretest yang diizinkan _levels.firstWhere((level) => level.idLevel == levelId).nameLevel);
return levelId == _levels.first.idLevel;
}
String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL'];
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 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) { // bool isLevelAllowed(int levelIndex) {
// if (levelIndex == 0) return true; // Pretest is always allowed // if (levelIndex == 0) return true; // Pretest is always allowed
// if (_lastCompletedLevel == null) // if (_lastCompletedLevel == null)

View File

@ -24,11 +24,13 @@ class _LevelListScreenState extends State<LevelListScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { _fetchLevels();
final levelProvider = Provider.of<LevelProvider>(context, listen: false); }
final userProvider = Provider.of<UserProvider>(context, listen: false);
levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!); 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 @override
@ -69,16 +71,23 @@ class _LevelListScreenState extends State<LevelListScreen> {
if (levelProvider.isLoading) { if (levelProvider.isLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (levelProvider.error != null) { } else if (levelProvider.error != null) {
return Center(child: Text('No levels available')); return const Center(child: Text('No levels available'));
} else { } else {
final pretest = levelProvider.getPretest();
final otherLevels = levelProvider.levels
.where((level) => level.isPretest == 0)
.toList();
return Padding( return Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
PretestCard( PretestCard(
pretest: levelProvider.getPretest(), pretest: pretest,
score: levelProvider score: levelProvider.getPretestScore(pretest.idLevel),
.getLevelScore(levelProvider.getPretest().idLevel), isCompleted:
levelProvider.isPretestFinished(pretest.idLevel),
isAllowed: levelProvider.isLevelAllowed(pretest.idLevel),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Expanded( Expanded(
@ -90,14 +99,17 @@ class _LevelListScreenState extends State<LevelListScreen> {
crossAxisSpacing: 16, crossAxisSpacing: 16,
mainAxisSpacing: 16, mainAxisSpacing: 16,
), ),
itemCount: levelProvider.levels.length - 1, itemCount: otherLevels.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final level = levelProvider.levels[index + 1]; final level = otherLevels[index];
return LevelCard( return LevelCard(
level: level, level: level,
isAllowed: isAllowed:
levelProvider.isLevelAllowed(level.idLevel), levelProvider.isLevelAllowed(level.idLevel),
score: levelProvider.getLevelScore(level.idLevel), score: levelProvider.getLevelScore(level.idLevel),
isCompleted:
levelProvider.isLevelCompleted(level.idLevel),
); );
}, },
), ),

View File

@ -7,25 +7,19 @@ import 'package:flutter/material.dart';
class LevelCard extends StatelessWidget { class LevelCard extends StatelessWidget {
final Level level; final Level level;
final bool isAllowed; final bool isAllowed;
// final int level;
// final bool isAllowed;
// final bool isFinished;
final int score; final int score;
// final VoidCallback? onPressed; final bool isCompleted;
const LevelCard({ const LevelCard({
super.key, super.key,
required this.level, required this.level,
required this.isAllowed, required this.isAllowed,
// required this.level,
// required this.isAllowed,
// required this.isFinished,
required this.score, required this.score,
// this.onPressed, required this.isCompleted,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isCompleted = level.idStudentLearning != null;
return Card( return Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -128,14 +122,18 @@ class LevelCard extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CustomButton( child: CustomButton(
text: isAllowed ? 'Learn Now' : 'Not Allowed', text: isCompleted
? 'Finished'
: (isAllowed ? 'Learn Now' : 'Locked'),
textStyle: textStyle:
isAllowed ? null : AppTextStyles.disableTextStyle, isAllowed ? null : AppTextStyles.disableTextStyle,
width: double.infinity, width: double.infinity,
height: 36, height: 36,
color: isAllowed color: isCompleted
? AppColors.yellowButtonColor ? Colors.green
: AppColors.cardButtonColor, : (isAllowed
? AppColors.yellowButtonColor
: AppColors.cardButtonColor),
onPressed: isAllowed onPressed: isAllowed
? () { ? () {
Navigator.push( Navigator.push(

View File

@ -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/utils/styles/theme.dart';
import 'package:english_learning/core/widgets/custom_button.dart'; import 'package:english_learning/core/widgets/custom_button.dart';
import 'package:english_learning/features/learning/modules/level/models/level_model.dart'; import 'package:english_learning/features/learning/modules/level/models/level_model.dart';
import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart';
import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart'; import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class PretestCard extends StatelessWidget { class PretestCard extends StatelessWidget {
final Level pretest; final Level pretest;
final int? score; final int? score;
final VoidCallback? onPressed; final bool isCompleted;
final bool isAllowed;
const PretestCard({ const PretestCard({
super.key, super.key,
required this.pretest, required this.pretest,
this.score, this.score,
this.onPressed, required this.isCompleted,
required this.isAllowed,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isCompleted = pretest.idStudentLearning != null; return Consumer<LevelProvider>(builder: (context, levelProvider, _) {
final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
final score = levelProvider.getPretestScore(pretest.idLevel);
// final isAllowed = levelProvider.isLevelAllowed(pretest.idLevel);
return Card( return Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
gradient: AppColors.gradientTheme,
), ),
child: Column( elevation: 0,
crossAxisAlignment: CrossAxisAlignment.start, child: Container(
children: [ padding: const EdgeInsets.all(16),
Row( decoration: BoxDecoration(
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(12),
mainAxisAlignment: MainAxisAlignment.spaceBetween, gradient: AppColors.gradientTheme,
children: [ ),
Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Row(
padding: const EdgeInsets.symmetric( crossAxisAlignment: CrossAxisAlignment.start,
vertical: 4, horizontal: 8), mainAxisAlignment: MainAxisAlignment.spaceBetween,
decoration: BoxDecoration( children: [
borderRadius: BorderRadius.circular(8), Column(
color: isCompleted ? Colors.green : Colors.transparent, crossAxisAlignment: CrossAxisAlignment.start,
border: Border.all( children: [
color: AppColors.whiteColor, Container(
width: 1.5, 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( const SizedBox(height: 6),
mainAxisSize: MainAxisSize.min, Text(
children: [ 'Score $score/100',
Text( style: AppTextStyles.whiteTextStyle.copyWith(
pretest.nameLevel, fontWeight: FontWeight.w900,
style: AppTextStyles.whiteTextStyle.copyWith( fontSize: 12,
fontWeight: FontWeight.w900, ),
fontSize: 16,
),
),
if (isCompleted) ...[
const SizedBox(width: 4),
const Icon(
BootstrapIcons.check_all,
color: AppColors.whiteColor,
size: 20,
),
],
],
), ),
), const SizedBox(height: 16),
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,
), ),
), Flexible(
], child: Image.asset(
), 'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png',
const SizedBox(height: 13), height: 95,
CustomButton( fit: BoxFit.cover,
text: isCompleted ? 'Finished' : 'Learn Now', ),
textStyle: AppTextStyles.whiteTextStyle.copyWith( ),
fontWeight: FontWeight.w900, ],
), ),
width: double.infinity, const SizedBox(height: 13),
height: 36, CustomButton(
color: isCompleted ? Colors.green : AppColors.yellowButtonColor, text: isFinished ? 'Review' : 'Learn Now',
onPressed: () { textStyle: isFinished
if (!isCompleted) { ? 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( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MaterialScreen( builder: (context) => MaterialScreen(
levelId: pretest.idLevel, levelId: pretest.idLevel,
isReview: isFinished,
), ),
), ),
); );
} },
// Jika isCompleted true, tidak melakukan apa-apa ),
}, ],
), ),
],
), ),
), );
); });
} }
} }

View File

@ -14,9 +14,12 @@ import 'package:provider/provider.dart';
class MaterialScreen extends StatefulWidget { class MaterialScreen extends StatefulWidget {
final String levelId; final String levelId;
final bool isReview;
const MaterialScreen({ const MaterialScreen({
super.key, super.key,
required this.levelId, required this.levelId,
this.isReview = false,
}); });
@override @override
@ -74,7 +77,7 @@ class _MaterialScreenState extends State<MaterialScreen>
print('Student Learning created: ${result['message']}'); print('Student Learning created: ${result['message']}');
// Navigate to ExerciseScreen // Navigate to ExerciseScreen
Navigator.push( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ExerciseScreen( builder: (context) => ExerciseScreen(
@ -164,15 +167,28 @@ class _MaterialScreenState extends State<MaterialScreen>
videoUrl: level.video!, videoUrl: level.video!,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
GlobalButton( if (!widget.isReview)
text: 'Take Pretest', GlobalButton(
onPressed: _isLoading text: level.isPretest == 1
? null ? 'Take Pretest'
: () { : 'Take Exercises',
_stopAndResetAllMedia(); onPressed: _isLoading
_createStudentLearning(); ? null
}, : () {
) _stopAndResetAllMedia();
_createStudentLearning();
},
)
else
GlobalButton(
text: 'Start Review',
onPressed: _isLoading
? null
: () {
_stopAndResetAllMedia();
_createStudentLearning();
},
),
], ],
), ),
), ),

View File

@ -20,6 +20,8 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
VideoPlayerController? _videoController; VideoPlayerController? _videoController;
YoutubePlayerController? _youtubeController; YoutubePlayerController? _youtubeController;
FlickManager? _flickManager; FlickManager? _flickManager;
bool _isLoading = true;
String? _error;
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -29,6 +31,18 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
_initializeVideoPlayerWidget(); _initializeVideoPlayerWidget();
} }
String _getPlayableUrl(String url) {
if (url.contains('drive.google.com')) {
final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)');
final match = regex.firstMatch(url);
if (match != null) {
final fileId = match.group(1);
return 'https://drive.google.com/uc?export=download&id=$fileId';
}
}
return url;
}
void _initializeVideoPlayerWidget() { void _initializeVideoPlayerWidget() {
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) { if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) {
_youtubeController = YoutubePlayerController( _youtubeController = YoutubePlayerController(
@ -44,18 +58,32 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
showVideoProgressIndicator: true, showVideoProgressIndicator: true,
onReady: () { onReady: () {
_youtubeController!.addListener(_youtubeListener); _youtubeController!.addListener(_youtubeListener);
setState(() {
_isLoading = false;
});
}, },
); );
} else { } else {
_videoController = _videoController = VideoPlayerController.networkUrl(
VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); Uri.parse(_getPlayableUrl(widget.videoUrl)),
_flickManager = FlickManager(
videoPlayerController: _videoController!,
autoPlay: false,
);
_videoWidget = FlickVideoPlayer(
flickManager: _flickManager!,
); );
_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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
} else if (_error != null) {
return Center(child: Text(_error!));
}
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: AspectRatio( child: AspectRatio(

View File

@ -8,7 +8,8 @@ class ResultScreen extends StatelessWidget {
final String currentLevel; final String currentLevel;
final String nextLevel; final String nextLevel;
final int score; final int score;
final bool? isCompleted; final bool isCompleted;
final String? stdLearningId;
const ResultScreen({ const ResultScreen({
super.key, super.key,
@ -16,6 +17,7 @@ class ResultScreen extends StatelessWidget {
required this.nextLevel, required this.nextLevel,
required this.score, required this.score,
required this.isCompleted, required this.isCompleted,
this.stdLearningId,
}); });
@override @override
@ -30,20 +32,23 @@ class ResultScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (isCompleted!) if (isCompleted)
CompleteResultWidget( CompleteResultWidget(
currentLevel: currentLevel, currentLevel: currentLevel,
score: score, score: score,
stdLearningId: stdLearningId ?? '',
) )
else if (nextLevel != currentLevel) else if (nextLevel != currentLevel)
JumpResultWidget( JumpResultWidget(
nextLevel: nextLevel, nextLevel: nextLevel,
score: score, score: score,
stdLearningId: stdLearningId ?? '',
) )
else else
DownResultWidget( DownResultWidget(
nextLevel: nextLevel, nextLevel: nextLevel,
score: score, score: score,
stdLearningId: stdLearningId ?? '',
), ),
], ],
), ),

View File

@ -1,16 +1,19 @@
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/core/widgets/global_button.dart'; import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
class CompleteResultWidget extends StatelessWidget { class CompleteResultWidget extends StatelessWidget {
final String? currentLevel; final String? currentLevel;
final int? score; final int? score;
final String stdLearningId;
const CompleteResultWidget({ const CompleteResultWidget({
super.key, super.key,
required this.currentLevel, required this.currentLevel,
required this.score, required this.score,
required this.stdLearningId,
}); });
@override @override
@ -49,12 +52,14 @@ class CompleteResultWidget extends StatelessWidget {
GlobalButton( GlobalButton(
text: 'Discover More', text: 'Discover More',
onPressed: () { onPressed: () {
// Navigator.push( Navigator.pushReplacement(
// context, context,
// MaterialPageRoute( MaterialPageRoute(
// builder: (context) => const FeedbackScreen(), builder: (context) => FeedbackScreen(
// ), stdLearningId: stdLearningId,
// ); ),
),
);
}, },
) )
], ],

View File

@ -1,15 +1,19 @@
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/core/widgets/global_button.dart'; import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
class DownResultWidget extends StatelessWidget { class DownResultWidget extends StatelessWidget {
final String? nextLevel; final String? nextLevel;
final int? score; final int? score;
final String stdLearningId;
const DownResultWidget({ const DownResultWidget({
super.key, super.key,
required this.nextLevel, required this.nextLevel,
required this.score, required this.score,
required this.stdLearningId,
}); });
@override @override
@ -58,12 +62,14 @@ class DownResultWidget extends StatelessWidget {
GlobalButton( GlobalButton(
text: 'Continue', text: 'Continue',
onPressed: () { onPressed: () {
// Navigator.push( Navigator.pushReplacement(
// context, context,
// MaterialPageRoute( MaterialPageRoute(
// builder: (context) => const FeedbackScreen(), builder: (context) => FeedbackScreen(
// ), stdLearningId: stdLearningId,
// ); ),
),
);
}, },
) )
], ],

View File

@ -7,12 +7,13 @@ import 'package:flutter_svg/svg.dart';
class JumpResultWidget extends StatelessWidget { class JumpResultWidget extends StatelessWidget {
final String? nextLevel; final String? nextLevel;
final int? score; final int? score;
final String stdLearningId;
const JumpResultWidget({ const JumpResultWidget(
super.key, {super.key,
required this.nextLevel, required this.nextLevel,
required this.score, required this.score,
}); required this.stdLearningId});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -60,10 +61,12 @@ class JumpResultWidget extends StatelessWidget {
GlobalButton( GlobalButton(
text: 'Continue', text: 'Continue',
onPressed: () { onPressed: () {
Navigator.push( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const FeedbackScreen(), builder: (context) => FeedbackScreen(
stdLearningId: stdLearningId,
),
), ),
); );
}, },

View File

@ -1,22 +1,22 @@
class Topic { class Topic {
final String id; final String id;
final String sectionId;
final String name; final String name;
final String description; final String description;
final bool isCompleted;
Topic({ Topic({
required this.id, required this.id,
required this.sectionId,
required this.name, required this.name,
required this.description, required this.description,
required this.isCompleted,
}); });
factory Topic.fromJson(Map<String, dynamic> json) { factory Topic.fromJson(Map<String, dynamic> json) {
return Topic( return Topic(
id: json['ID_TOPIC'], id: json['ID_TOPIC'] ?? '',
sectionId: json['ID_SECTION'], name: json['NAME_TOPIC'] ?? '',
name: json['NAME_TOPIC'], description: json['DESCRIPTION_TOPIC'] ?? '',
description: json['DESCRIPTION_TOPIC'], isCompleted: json['IS_COMPLETED'] == 1,
); );
} }
} }

View File

@ -23,7 +23,9 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchTopics(); WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchTopics();
});
} }
String _getFullImageUrl(String thumbnail) { String _getFullImageUrl(String thumbnail) {
@ -39,10 +41,14 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
final token = await userProvider.getValidToken(); final token = await userProvider.getValidToken();
if (token != null) { if (token != null) {
await Provider.of<TopicProvider>(context, listen: false) try {
.fetchTopics(widget.sectionId, token); await Provider.of<TopicProvider>(context, listen: false)
.fetchTopics(widget.sectionId, token);
print('Topics fetched successfully');
} catch (e) {
print('Error fetching topics: $e');
}
} else { } else {
// Handle the case when token is null (user might not be logged in)
print('No valid token found. User might need to log in.'); print('No valid token found. User might need to log in.');
} }
} }
@ -100,7 +106,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
width: 90, width: 90,
height: 104, height: 104,
color: Colors.grey[300], color: Colors.grey[300],
child: Center(child: CircularProgressIndicator()), child: const Center(child: CircularProgressIndicator()),
); );
}, },
), ),
@ -142,8 +148,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
return TopicCard( return TopicCard(
title: topic.name, title: topic.name,
description: topic.description, description: topic.description,
isCompleted: isCompleted: topic.isCompleted,
false, // You might want to implement completion tracking
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,

View File

@ -63,7 +63,8 @@ class TopicCard extends StatelessWidget {
isCompleted isCompleted
? Icons.check_circle ? Icons.check_circle
: Icons.radio_button_unchecked, : Icons.radio_button_unchecked,
color: AppColors.blueColor, color:
isCompleted ? AppColors.blueColor : AppColors.greyColor,
), ),
], ],
), ),

View File

@ -12,19 +12,15 @@ class SectionProvider extends ChangeNotifier {
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
String? get error => _error; String? get error => _error;
Future<void> fetchSections(String token) async { Future<List<Section>> fetchSections(String token) async {
_isLoading = true;
_error = null;
notifyListeners();
try { try {
_sections = await _repository.getSections(token); _sections = await _repository.getSections(token);
_isLoading = false;
notifyListeners(); notifyListeners();
return _sections;
} catch (e) { } catch (e) {
_isLoading = false;
_error = e.toString(); _error = e.toString();
notifyListeners(); notifyListeners();
return [];
} }
} }
} }

View File

@ -50,7 +50,7 @@ class LearningCard extends StatelessWidget {
width: 90, width: 90,
height: 104, height: 104,
color: Colors.grey[300], color: Colors.grey[300],
child: Icon( child: const Icon(
Icons.image_not_supported, Icons.image_not_supported,
color: Colors.grey, color: Colors.grey,
), ),

View File

@ -1,10 +1,12 @@
import 'package:english_learning/core/services/dio_client.dart'; import 'package:english_learning/core/services/dio_client.dart';
import 'package:english_learning/core/services/repositories/completed_topics_repository.dart';
import 'package:english_learning/core/services/repositories/exercise_repository.dart'; import 'package:english_learning/core/services/repositories/exercise_repository.dart';
import 'package:english_learning/core/services/repositories/history_repository.dart'; import 'package:english_learning/core/services/repositories/history_repository.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/auth/provider/validator_provider.dart'; import 'package:english_learning/features/auth/provider/validator_provider.dart';
import 'package:english_learning/features/history/provider/history_provider.dart'; import 'package:english_learning/features/history/provider/history_provider.dart';
import 'package:english_learning/features/home/provider/completed_topics_provider.dart';
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart'; import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart';
import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart';
@ -18,7 +20,7 @@ void main() {
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -38,6 +40,9 @@ class MyApp extends StatelessWidget {
ProxyProvider<DioClient, HistoryRepository>( ProxyProvider<DioClient, HistoryRepository>(
update: (_, dioClient, __) => HistoryRepository(dioClient), update: (_, dioClient, __) => HistoryRepository(dioClient),
), ),
ProxyProvider<DioClient, CompletedTopicsRepository>(
update: (_, dioClient, __) => CompletedTopicsRepository(dioClient),
),
ChangeNotifierProxyProvider2<HistoryRepository, SectionProvider, ChangeNotifierProxyProvider2<HistoryRepository, SectionProvider,
HistoryProvider>( HistoryProvider>(
create: (context) => HistoryProvider( create: (context) => HistoryProvider(
@ -62,6 +67,14 @@ class MyApp extends StatelessWidget {
userProvider, userProvider,
)..updateFrom(previous), )..updateFrom(previous),
), ),
ChangeNotifierProxyProvider<CompletedTopicsRepository,
CompletedTopicsProvider>(
create: (context) => CompletedTopicsProvider(
context.read<CompletedTopicsRepository>(),
),
update: (context, completedTopicsRepository, previous) =>
CompletedTopicsProvider(completedTopicsRepository),
),
], ],
child: Consumer<UserProvider>( child: Consumer<UserProvider>(
builder: (context, userProvider, _) { builder: (context, userProvider, _) {

View File

@ -287,7 +287,7 @@ packages:
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_cache_manager: flutter_cache_manager:
dependency: "direct main" dependency: transitive
description: description:
name: flutter_cache_manager name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
@ -358,14 +358,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
flutter_logger_plus:
dependency: "direct main"
description:
name: flutter_logger_plus
sha256: c01751b8074384d116a4c1e4b549233053ea6b78fcc07b5503248227b06b923c
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -600,14 +592,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
logger:
dependency: "direct main"
description:
name: logger
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -37,8 +37,6 @@ dependencies:
flick_video_player: ^0.9.0 flick_video_player: ^0.9.0
flutter: flutter:
sdk: flutter sdk: flutter
flutter_cache_manager: ^3.4.1
flutter_logger_plus: ^5.0.0
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
google_fonts: ^6.2.1 google_fonts: ^6.2.1
@ -46,7 +44,6 @@ dependencies:
image_picker: ^1.1.2 image_picker: ^1.1.2
intl: ^0.19.0 intl: ^0.19.0
jwt_decoder: ^2.0.1 jwt_decoder: ^2.0.1
logger: ^2.4.0
provider: ^6.1.2 provider: ^6.1.2
shared_preferences: ^2.3.2 shared_preferences: ^2.3.2
shimmer: ^3.0.0 shimmer: ^3.0.0