feat: update API endpoints, enhance error handling, and improve UI responsiveness
This commit is contained in:
parent
e6ce79528b
commit
e48c63bfac
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.27.0-0.2.pre"
|
||||
"dart.flutterSdkPath": "C:\\tools\\flutter"
|
||||
}
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
<application
|
||||
android:label="SEALS"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
const String baseUrl = 'http://54.173.167.62/';
|
||||
const String baseUrl = 'https://api.seals.id/';
|
||||
|
||||
const String mediaUrl = 'http://54.173.167.62/api/uploads/';
|
||||
const String mediaUrl = 'https://api.seals.id/api/uploads/';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class DioClient {
|
|||
DioClient() {
|
||||
_dio.options.baseUrl = baseUrl;
|
||||
_dio.options.connectTimeout = const Duration(seconds: 10);
|
||||
_dio.options.receiveTimeout = const Duration(seconds: 15);
|
||||
_dio.options.receiveTimeout = const Duration(seconds: 30);
|
||||
}
|
||||
|
||||
Future<Response> refreshAccessToken(String refreshToken) async {
|
||||
|
|
|
|||
37
lib/core/widgets/back_handler.dart
Normal file
37
lib/core/widgets/back_handler.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
mixin BackHandlerMixin<T extends StatefulWidget> on State<T> {
|
||||
DateTime? currentBackPressTime;
|
||||
|
||||
Future<bool> onWillPop() async {
|
||||
DateTime now = DateTime.now();
|
||||
|
||||
if (currentBackPressTime == null ||
|
||||
now.difference(currentBackPressTime!) > const Duration(seconds: 2)) {
|
||||
currentBackPressTime = now;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Press back again to exit'),
|
||||
duration: Duration(seconds: 2),
|
||||
backgroundColor: Colors.black87,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget wrapWithBackHandler({required Widget child}) {
|
||||
return WillPopScope(
|
||||
onWillPop: onWillPop,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ class CustomSnackBar {
|
|||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: duration,
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
dismissDirection: DismissDirection.vertical,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
|
@ -46,7 +46,7 @@ class CustomSnackBar {
|
|||
behavior: behavior,
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: duration,
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
dismissDirection: DismissDirection.vertical,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -37,8 +37,11 @@ class HistoryModel {
|
|||
}
|
||||
|
||||
String get formattedDate {
|
||||
return studentFinish != null
|
||||
? DateFormat('yyyy-MM-dd HH:mm').format(studentFinish!)
|
||||
: 'N/A';
|
||||
if (studentFinish == null) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
final wibDateTime = studentFinish!.add(const Duration(hours: 7));
|
||||
return '${DateFormat('yyyy-MM-dd HH:mm').format(wibDateTime)} WIB';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
// Tambahkan method untuk shimmer loading
|
||||
Widget _buildShimmerLoading() {
|
||||
return ListView.builder(
|
||||
itemCount: 5,
|
||||
|
|
@ -130,24 +129,20 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, HistoryProvider historyProvider) {
|
||||
// Prioritaskan initial loading
|
||||
if (_isInitialLoading || historyProvider.isLoading) {
|
||||
return _buildShimmerLoading();
|
||||
}
|
||||
|
||||
// Tangani error
|
||||
if (historyProvider.error != null) {
|
||||
return isNotFoundError(historyProvider.error!)
|
||||
? _buildEmptyState(context)
|
||||
: _buildErrorState(context, historyProvider.error!);
|
||||
}
|
||||
|
||||
// Tampilkan empty state jika tidak ada history
|
||||
if (historyProvider.historyModel.isEmpty) {
|
||||
return _buildEmptyState(context);
|
||||
}
|
||||
|
||||
// Tampilkan daftar history dengan refresh indicator
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshHistory,
|
||||
child: AnimatedSwitcher(
|
||||
|
|
@ -239,7 +234,12 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Container(
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshHistory,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppColors.whiteColor,
|
||||
|
|
@ -280,6 +280,9 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:bootstrap_icons/bootstrap_icons.dart';
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:english_learning/core/widgets/back_handler.dart';
|
||||
import 'package:english_learning/core/widgets/custom_button.dart';
|
||||
import 'package:english_learning/features/auth/provider/user_provider.dart';
|
||||
import 'package:english_learning/features/history/provider/history_provider.dart';
|
||||
|
|
@ -39,7 +40,7 @@ class HomeScreen extends StatefulWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
class _HomeScreenState extends State<HomeScreen> with BackHandlerMixin {
|
||||
final PageController _pageController = PageController();
|
||||
int _selectedIndex = 0;
|
||||
|
||||
|
|
@ -59,7 +60,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
return wrapWithBackHandler(
|
||||
child: DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
body: Container(
|
||||
|
|
@ -100,7 +102,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
selectedIndex: _selectedIndex,
|
||||
onTabChange: (index) async {
|
||||
if (index == 2 && _selectedIndex != 2) {
|
||||
// Only if switching TO history tab
|
||||
final historyProvider =
|
||||
Provider.of<HistoryProvider>(context, listen: false);
|
||||
final userProvider =
|
||||
|
|
@ -140,6 +141,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -169,10 +171,8 @@ class _HomeContentState extends State<HomeContent> {
|
|||
Provider.of<CompletedTopicsProvider>(context, listen: false);
|
||||
|
||||
try {
|
||||
// Reset data sebelum fetch
|
||||
completedTopicsProvider.resetData();
|
||||
|
||||
// Fetch completed topics
|
||||
await completedTopicsProvider
|
||||
.fetchCompletedTopics(userProvider.jwtToken!);
|
||||
} catch (e) {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 60 KiB |
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:english_learning/features/learning/modules/result/widgets/complete_result_widget.dart';
|
||||
import 'package:english_learning/features/learning/modules/result/widgets/down_result_widget.dart';
|
||||
import 'package:english_learning/features/learning/modules/result/widgets/jump_result_widget.dart';
|
||||
import 'package:english_learning/features/learning/modules/result/widgets/stay_result_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
|
||||
|
|
@ -26,8 +27,49 @@ class ResultScreen extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// final mediaQuery = MediaQuery.of(context);
|
||||
// final screenHeight = mediaQuery.size.height;
|
||||
Widget buildResultWidget() {
|
||||
if (isCompleted) {
|
||||
return CompleteResultWidget(
|
||||
currentLevel: currentLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
topicId: topicId,
|
||||
topicTitle: topicTitle,
|
||||
);
|
||||
}
|
||||
|
||||
if (currentLevel == nextLevel) {
|
||||
return StayResultWidget(
|
||||
nextLevel: nextLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
topicId: topicId,
|
||||
topicTitle: topicTitle,
|
||||
);
|
||||
}
|
||||
|
||||
// Menggunakan compareTo untuk membandingkan level
|
||||
final currentLevelNum = _extractLevelNumber(currentLevel);
|
||||
final nextLevelNum = _extractLevelNumber(nextLevel);
|
||||
|
||||
if (currentLevelNum < nextLevelNum) {
|
||||
return JumpResultWidget(
|
||||
nextLevel: nextLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
topicId: topicId,
|
||||
topicTitle: topicTitle,
|
||||
);
|
||||
} else {
|
||||
return DownResultWidget(
|
||||
nextLevel: nextLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
topicId: topicId,
|
||||
topicTitle: topicTitle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgSoftColor,
|
||||
|
|
@ -36,33 +78,19 @@ class ResultScreen extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (isCompleted)
|
||||
CompleteResultWidget(
|
||||
currentLevel: currentLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
topicId: topicId, // Tambahkan ini
|
||||
topicTitle: topicTitle, // Tambahkan ini
|
||||
)
|
||||
else if (nextLevel != currentLevel)
|
||||
JumpResultWidget(
|
||||
nextLevel: nextLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
topicId: topicId, // Tambahkan ini
|
||||
topicTitle: topicTitle, // Tambahkan ini
|
||||
)
|
||||
else
|
||||
DownResultWidget(
|
||||
nextLevel: nextLevel,
|
||||
score: score,
|
||||
stdLearningId: stdLearningId ?? '',
|
||||
topicId: topicId, // Tambahkan ini
|
||||
topicTitle: topicTitle, // Tambahkan ini
|
||||
),
|
||||
buildResultWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _extractLevelNumber(String level) {
|
||||
final regex = RegExp(r'\d+');
|
||||
final match = regex.firstMatch(level);
|
||||
if (match != null) {
|
||||
return int.parse(match.group(0)!);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,23 +38,16 @@ class CompleteResultWidget extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Congratulations!',
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Way to go! You conquered LEVEL $currentLevel with a $score/100! You\'re a rock star!',
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
'You conquered LEVEL $currentLevel with a $score/100! You\'re a rock star!',
|
||||
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GlobalButton(
|
||||
text: 'Discover More',
|
||||
text: 'Topic Finished!',
|
||||
onPressed: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -55,12 +55,15 @@ class DownResultWidget extends StatelessWidget {
|
|||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'$nextLevel',
|
||||
style: AppTextStyles.redTextStyle
|
||||
.copyWith(fontSize: 20, fontWeight: FontWeight.w900),
|
||||
style: AppTextStyles.redTextStyle.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GlobalButton(
|
||||
|
|
@ -71,8 +74,8 @@ class DownResultWidget extends StatelessWidget {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => FeedbackScreen(
|
||||
stdLearningId: stdLearningId,
|
||||
topicId: topicId, // Tambahkan ini
|
||||
topicTitle: topicTitle, // Tambahkan ini
|
||||
topicId: topicId,
|
||||
topicTitle: topicTitle,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -51,10 +51,11 @@ class JumpResultWidget extends StatelessWidget {
|
|||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Great job! You can jump to ...',
|
||||
style: AppTextStyles.disableTextStyle.copyWith(
|
||||
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
|
|
@ -71,8 +72,8 @@ class JumpResultWidget extends StatelessWidget {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => FeedbackScreen(
|
||||
stdLearningId: stdLearningId,
|
||||
topicId: topicId, // Tambahkan ini
|
||||
topicTitle: topicTitle, // Tambahkan ini
|
||||
topicId: topicId,
|
||||
topicTitle: topicTitle,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:english_learning/core/utils/styles/theme.dart';
|
||||
import 'package:english_learning/core/widgets/global_button.dart';
|
||||
import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class StayResultWidget extends StatelessWidget {
|
||||
final String? nextLevel;
|
||||
final int? score;
|
||||
final String stdLearningId;
|
||||
final String topicId;
|
||||
final String topicTitle;
|
||||
|
||||
const StayResultWidget({
|
||||
super.key,
|
||||
required this.nextLevel,
|
||||
required this.score,
|
||||
required this.stdLearningId,
|
||||
required this.topicId,
|
||||
required this.topicTitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Your Result',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$score/100',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SvgPicture.asset(
|
||||
'lib/features/learning/modules/result/assets/images/result_stay_illustration.svg',
|
||||
width: 259,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Learning is a journey, let\'s explore this level further to deepen your knowledge.',
|
||||
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'$nextLevel',
|
||||
style: AppTextStyles.blackTextStyle.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GlobalButton(
|
||||
text: 'Continue',
|
||||
onPressed: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => FeedbackScreen(
|
||||
stdLearningId: stdLearningId,
|
||||
topicId: topicId, // Tambahkan ini
|
||||
topicTitle: topicTitle, // Tambahkan ini
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,8 +35,7 @@ class _LearningScreenState extends State<LearningScreen>
|
|||
Provider.of<SectionProvider>(context, listen: false);
|
||||
|
||||
try {
|
||||
// Reset data sebelum fetch
|
||||
sectionProvider.resetData(); // Tambahkan method ini di SectionProvider
|
||||
sectionProvider.resetData();
|
||||
|
||||
final token = await userProvider.getValidToken();
|
||||
if (token != null) {
|
||||
|
|
@ -61,26 +60,6 @@ class _LearningScreenState extends State<LearningScreen>
|
|||
}
|
||||
}
|
||||
|
||||
// Future<void> _refreshSections() async {
|
||||
// final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
// final sectionProvider =
|
||||
// Provider.of<SectionProvider>(context, listen: false);
|
||||
|
||||
// try {
|
||||
// final token = await userProvider.getValidToken();
|
||||
// if (token != null) {
|
||||
// await sectionProvider.fetchSections(token);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// content: Text('Failed to refresh sections: $e'),
|
||||
// backgroundColor: Colors.red,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
Widget _buildErrorWidget(String error) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -151,7 +130,6 @@ class _LearningScreenState extends State<LearningScreen>
|
|||
Expanded(
|
||||
child: Consumer<SectionProvider>(
|
||||
builder: (context, sectionProvider, _) {
|
||||
// Prioritaskan loading state
|
||||
if (_isInitialLoading || sectionProvider.isLoading) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
|
|
@ -161,12 +139,10 @@ class _LearningScreenState extends State<LearningScreen>
|
|||
);
|
||||
}
|
||||
|
||||
// Tampilkan error jika ada
|
||||
if (sectionProvider.error != null) {
|
||||
return _buildErrorWidget(sectionProvider.error!);
|
||||
}
|
||||
|
||||
// Tampilkan sections atau pesan jika kosong
|
||||
if (sectionProvider.sections.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _initializeSections,
|
||||
|
|
@ -202,7 +178,6 @@ class _LearningScreenState extends State<LearningScreen>
|
|||
);
|
||||
}
|
||||
|
||||
// Tampilkan daftar sections
|
||||
return RefreshIndicator(
|
||||
onRefresh: _initializeSections,
|
||||
child: ListView.builder(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user