Compare commits
5 Commits
e6ce79528b
...
7ba660a4eb
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ba660a4eb | |||
| 3182129f26 | |||
| 24b3480a50 | |||
| 8861afea72 | |||
| 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"
|
||||||
}
|
}
|
||||||
101
README.md
101
README.md
|
|
@ -1,16 +1,97 @@
|
||||||
# english_learning
|
<p align="center">
|
||||||
|
<img src="assets/logo.png" alt="SEALS Logo" width="200"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
A new Flutter project.
|
# SEALS - Smart English Adaptive Learning System
|
||||||
|
|
||||||
## Getting Started
|
## Overview
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
SEALS refers to a personalized approach to language education that adjusts to the individual learner's needs, pace, and skill level.
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
## System Requirements
|
||||||
|
|
||||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
- Flutter version 3.19.0 or higher
|
||||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
- Dart version 3.3.0 or higher
|
||||||
|
- Android Studio / VS Code
|
||||||
|
- Android SDK version 23 (Android 7.0) or higher
|
||||||
|
- Minimum 2GB RAM
|
||||||
|
- 500MB free storage space
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
## Installation
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitlab.com/profile-image/kedaireka/polinema-adapative-learning/mobile-adaptive-learning.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Navigate to project directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mobile-adaptive-learning
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
audioplayers: ^6.1.0
|
||||||
|
bootstrap_icons: ^1.11.3
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
carousel_slider: ^5.0.0
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
|
dio: ^5.7.0
|
||||||
|
flick_video_player: ^0.9.0
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_inappwebview: ^6.0.0
|
||||||
|
flutter_secure_storage: ^9.2.2
|
||||||
|
flutter_svg: ^2.0.10+1
|
||||||
|
flutter_widget_from_html: ^0.15.2
|
||||||
|
google_fonts: ^6.2.1
|
||||||
|
google_nav_bar: ^5.0.6
|
||||||
|
html: ^0.15.4
|
||||||
|
image_picker: ^1.1.2
|
||||||
|
intl: ^0.19.0
|
||||||
|
jwt_decoder: ^2.0.1
|
||||||
|
provider: ^6.1.2
|
||||||
|
shared_preferences: ^2.3.2
|
||||||
|
shimmer: ^3.0.0
|
||||||
|
video_player: ^2.9.1
|
||||||
|
youtube_player_flutter: ^9.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Release
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate Android App Bundle
|
||||||
|
flutter build appbundle --release
|
||||||
|
|
||||||
|
# Generate APK
|
||||||
|
flutter build apk --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Team
|
||||||
|
|
||||||
|
- Naresh Pratista - Mobile App Developer - [nareshpratista.contact@gmail.com](mailto:nareshpratista.contact@gmail.com)
|
||||||
|
- Diah Putri Nofianti - UI/UX Designer - [diahputrinofianti@gmail.com](mailto:diahputrinofianti@gmail.com)
|
||||||
|
- Elang Putra Adam - Backend Developer - [elangptra17@gmail.com](mailto:elangptra17@gmail.com)
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- [Flutter](https://flutter.dev/) - UI framework
|
||||||
|
- [Provider](https://pub.dev/packages/provider) - State management
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
<application
|
<application
|
||||||
android:label="SEALS"
|
android:label="SEALS"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
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() {
|
DioClient() {
|
||||||
_dio.options.baseUrl = baseUrl;
|
_dio.options.baseUrl = baseUrl;
|
||||||
_dio.options.connectTimeout = const Duration(seconds: 10);
|
_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 {
|
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,
|
behavior: SnackBarBehavior.floating,
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
dismissDirection: DismissDirection.horizontal,
|
dismissDirection: DismissDirection.vertical,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
|
@ -46,7 +46,7 @@ class CustomSnackBar {
|
||||||
behavior: behavior,
|
behavior: behavior,
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
dismissDirection: DismissDirection.horizontal,
|
dismissDirection: DismissDirection.vertical,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,11 @@ class HistoryModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
String get formattedDate {
|
String get formattedDate {
|
||||||
return studentFinish != null
|
if (studentFinish == null) {
|
||||||
? DateFormat('yyyy-MM-dd HH:mm').format(studentFinish!)
|
return 'N/A';
|
||||||
: '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() {
|
Widget _buildShimmerLoading() {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
|
|
@ -130,24 +129,20 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context, HistoryProvider historyProvider) {
|
Widget _buildContent(BuildContext context, HistoryProvider historyProvider) {
|
||||||
// Prioritaskan initial loading
|
|
||||||
if (_isInitialLoading || historyProvider.isLoading) {
|
if (_isInitialLoading || historyProvider.isLoading) {
|
||||||
return _buildShimmerLoading();
|
return _buildShimmerLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tangani error
|
|
||||||
if (historyProvider.error != null) {
|
if (historyProvider.error != null) {
|
||||||
return isNotFoundError(historyProvider.error!)
|
return isNotFoundError(historyProvider.error!)
|
||||||
? _buildEmptyState(context)
|
? _buildEmptyState(context)
|
||||||
: _buildErrorState(context, historyProvider.error!);
|
: _buildErrorState(context, historyProvider.error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tampilkan empty state jika tidak ada history
|
|
||||||
if (historyProvider.historyModel.isEmpty) {
|
if (historyProvider.historyModel.isEmpty) {
|
||||||
return _buildEmptyState(context);
|
return _buildEmptyState(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tampilkan daftar history dengan refresh indicator
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _refreshHistory,
|
onRefresh: _refreshHistory,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
|
|
@ -239,46 +234,54 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(BuildContext context) {
|
Widget _buildEmptyState(BuildContext context) {
|
||||||
return Container(
|
return RefreshIndicator(
|
||||||
decoration: BoxDecoration(
|
onRefresh: _refreshHistory,
|
||||||
borderRadius: BorderRadius.circular(12),
|
child: ListView(
|
||||||
color: AppColors.whiteColor,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
),
|
children: [
|
||||||
child: Padding(
|
Container(
|
||||||
padding: const EdgeInsets.all(16.0),
|
decoration: BoxDecoration(
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(12),
|
||||||
mainAxisSize: MainAxisSize.min,
|
color: AppColors.whiteColor,
|
||||||
children: [
|
),
|
||||||
Text(
|
child: Padding(
|
||||||
'Still New?',
|
padding: const EdgeInsets.all(16.0),
|
||||||
style: AppTextStyles.blackTextStyle.copyWith(
|
child: Column(
|
||||||
fontSize: 18,
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontWeight: FontWeight.bold,
|
children: [
|
||||||
|
Text(
|
||||||
|
'Still New?',
|
||||||
|
style: AppTextStyles.blackTextStyle.copyWith(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Begin your journey!',
|
||||||
|
style: AppTextStyles.disableTextStyle.copyWith(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SvgPicture.asset(
|
||||||
|
'lib/features/history/assets/images/is_empty_illustration.svg',
|
||||||
|
width: 160,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
GlobalButton(
|
||||||
|
text: 'Explore',
|
||||||
|
backgroundColor: AppColors.yellowButtonColor,
|
||||||
|
textColor: AppColors.blackColor,
|
||||||
|
onPressed: () {
|
||||||
|
HomeScreen.navigateToTab(context, 1);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
'Begin your journey!',
|
],
|
||||||
style: AppTextStyles.disableTextStyle.copyWith(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SvgPicture.asset(
|
|
||||||
'lib/features/history/assets/images/is_empty_illustration.svg',
|
|
||||||
width: 160,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
GlobalButton(
|
|
||||||
text: 'Explore',
|
|
||||||
backgroundColor: AppColors.yellowButtonColor,
|
|
||||||
textColor: AppColors.blackColor,
|
|
||||||
onPressed: () {
|
|
||||||
HomeScreen.navigateToTab(context, 1);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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/back_handler.dart';
|
||||||
import 'package:english_learning/core/widgets/custom_button.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/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();
|
final PageController _pageController = PageController();
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
|
|
@ -59,83 +60,84 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return wrapWithBackHandler(
|
||||||
length: 4,
|
child: DefaultTabController(
|
||||||
child: Scaffold(
|
length: 4,
|
||||||
body: Container(
|
child: Scaffold(
|
||||||
color: AppColors.bgSoftColor,
|
body: Container(
|
||||||
child: PageView(
|
color: AppColors.bgSoftColor,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
child: PageView(
|
||||||
controller: _pageController,
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: _screens,
|
controller: _pageController,
|
||||||
onPageChanged: (index) {
|
children: _screens,
|
||||||
setState(() {
|
onPageChanged: (index) {
|
||||||
_selectedIndex = index;
|
setState(() {
|
||||||
});
|
_selectedIndex = index;
|
||||||
},
|
});
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: AppColors.gradientTheme,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(20),
|
|
||||||
topRight: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 20,
|
|
||||||
bottom: 24,
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
),
|
|
||||||
child: GNav(
|
|
||||||
activeColor: AppColors.blueColor,
|
|
||||||
tabBackgroundColor: AppColors.whiteColor,
|
|
||||||
tabBorderRadius: 100,
|
|
||||||
color: AppColors.whiteColor,
|
|
||||||
iconSize: 20,
|
|
||||||
gap: 8,
|
|
||||||
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 =
|
|
||||||
Provider.of<UserProvider>(context, listen: false);
|
|
||||||
|
|
||||||
if (!historyProvider.isInitialized) {
|
|
||||||
await historyProvider
|
|
||||||
.loadInitialData(userProvider.jwtToken!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToTab(index);
|
|
||||||
},
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
horizontal: 16,
|
),
|
||||||
vertical: 8,
|
bottomNavigationBar: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: AppColors.gradientTheme,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
),
|
),
|
||||||
tabs: const [
|
),
|
||||||
GButton(
|
child: Padding(
|
||||||
icon: BootstrapIcons.house,
|
padding: const EdgeInsets.only(
|
||||||
text: 'Home',
|
top: 20,
|
||||||
|
bottom: 24,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
child: GNav(
|
||||||
|
activeColor: AppColors.blueColor,
|
||||||
|
tabBackgroundColor: AppColors.whiteColor,
|
||||||
|
tabBorderRadius: 100,
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
iconSize: 20,
|
||||||
|
gap: 8,
|
||||||
|
selectedIndex: _selectedIndex,
|
||||||
|
onTabChange: (index) async {
|
||||||
|
if (index == 2 && _selectedIndex != 2) {
|
||||||
|
final historyProvider =
|
||||||
|
Provider.of<HistoryProvider>(context, listen: false);
|
||||||
|
final userProvider =
|
||||||
|
Provider.of<UserProvider>(context, listen: false);
|
||||||
|
|
||||||
|
if (!historyProvider.isInitialized) {
|
||||||
|
await historyProvider
|
||||||
|
.loadInitialData(userProvider.jwtToken!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToTab(index);
|
||||||
|
},
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
),
|
),
|
||||||
GButton(
|
tabs: const [
|
||||||
icon: BootstrapIcons.book,
|
GButton(
|
||||||
text: 'Learning',
|
icon: BootstrapIcons.house,
|
||||||
),
|
text: 'Home',
|
||||||
GButton(
|
),
|
||||||
icon: BootstrapIcons.clock_history,
|
GButton(
|
||||||
text: 'History',
|
icon: BootstrapIcons.book,
|
||||||
),
|
text: 'Learning',
|
||||||
GButton(
|
),
|
||||||
icon: BootstrapIcons.gear,
|
GButton(
|
||||||
text: 'Settings',
|
icon: BootstrapIcons.clock_history,
|
||||||
),
|
text: 'History',
|
||||||
],
|
),
|
||||||
|
GButton(
|
||||||
|
icon: BootstrapIcons.gear,
|
||||||
|
text: 'Settings',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -169,10 +171,8 @@ class _HomeContentState extends State<HomeContent> {
|
||||||
Provider.of<CompletedTopicsProvider>(context, listen: false);
|
Provider.of<CompletedTopicsProvider>(context, listen: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Reset data sebelum fetch
|
|
||||||
completedTopicsProvider.resetData();
|
completedTopicsProvider.resetData();
|
||||||
|
|
||||||
// Fetch completed topics
|
|
||||||
await completedTopicsProvider
|
await completedTopicsProvider
|
||||||
.fetchCompletedTopics(userProvider.jwtToken!);
|
.fetchCompletedTopics(userProvider.jwtToken!);
|
||||||
} catch (e) {
|
} 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/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/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/jump_result_widget.dart';
|
||||||
|
import 'package:english_learning/features/learning/modules/result/widgets/stay_result_widget.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';
|
||||||
|
|
||||||
|
|
@ -26,8 +27,49 @@ class ResultScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// final mediaQuery = MediaQuery.of(context);
|
Widget buildResultWidget() {
|
||||||
// final screenHeight = mediaQuery.size.height;
|
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(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.bgSoftColor,
|
backgroundColor: AppColors.bgSoftColor,
|
||||||
|
|
@ -36,33 +78,19 @@ class ResultScreen extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (isCompleted)
|
buildResultWidget(),
|
||||||
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
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
'Congratulations!',
|
'You conquered LEVEL $currentLevel with a $score/100! You\'re a rock star!',
|
||||||
style: AppTextStyles.disableTextStyle.copyWith(
|
style: AppTextStyles.tetriaryTextStyle.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(
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
text: 'Discover More',
|
text: 'Topic Finished!',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,15 @@ class DownResultWidget extends StatelessWidget {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'$nextLevel',
|
'$nextLevel',
|
||||||
style: AppTextStyles.redTextStyle
|
style: AppTextStyles.redTextStyle.copyWith(
|
||||||
.copyWith(fontSize: 20, fontWeight: FontWeight.w900),
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
GlobalButton(
|
GlobalButton(
|
||||||
|
|
@ -71,8 +74,8 @@ class DownResultWidget extends StatelessWidget {
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => FeedbackScreen(
|
builder: (context) => FeedbackScreen(
|
||||||
stdLearningId: stdLearningId,
|
stdLearningId: stdLearningId,
|
||||||
topicId: topicId, // Tambahkan ini
|
topicId: topicId,
|
||||||
topicTitle: topicTitle, // Tambahkan ini
|
topicTitle: topicTitle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,11 @@ class JumpResultWidget extends StatelessWidget {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
'Great job! You can jump to ...',
|
'Great job! You can jump to ...',
|
||||||
style: AppTextStyles.disableTextStyle.copyWith(
|
style: AppTextStyles.tetriaryTextStyle.copyWith(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -71,8 +72,8 @@ class JumpResultWidget extends StatelessWidget {
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => FeedbackScreen(
|
builder: (context) => FeedbackScreen(
|
||||||
stdLearningId: stdLearningId,
|
stdLearningId: stdLearningId,
|
||||||
topicId: topicId, // Tambahkan ini
|
topicId: topicId,
|
||||||
topicTitle: topicTitle, // Tambahkan ini
|
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);
|
Provider.of<SectionProvider>(context, listen: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Reset data sebelum fetch
|
sectionProvider.resetData();
|
||||||
sectionProvider.resetData(); // Tambahkan method ini di SectionProvider
|
|
||||||
|
|
||||||
final token = await userProvider.getValidToken();
|
final token = await userProvider.getValidToken();
|
||||||
if (token != null) {
|
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) {
|
Widget _buildErrorWidget(String error) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -151,7 +130,6 @@ class _LearningScreenState extends State<LearningScreen>
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Consumer<SectionProvider>(
|
child: Consumer<SectionProvider>(
|
||||||
builder: (context, sectionProvider, _) {
|
builder: (context, sectionProvider, _) {
|
||||||
// Prioritaskan loading state
|
|
||||||
if (_isInitialLoading || sectionProvider.isLoading) {
|
if (_isInitialLoading || sectionProvider.isLoading) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
|
@ -161,12 +139,10 @@ class _LearningScreenState extends State<LearningScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tampilkan error jika ada
|
|
||||||
if (sectionProvider.error != null) {
|
if (sectionProvider.error != null) {
|
||||||
return _buildErrorWidget(sectionProvider.error!);
|
return _buildErrorWidget(sectionProvider.error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tampilkan sections atau pesan jika kosong
|
|
||||||
if (sectionProvider.sections.isEmpty) {
|
if (sectionProvider.sections.isEmpty) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _initializeSections,
|
onRefresh: _initializeSections,
|
||||||
|
|
@ -202,7 +178,6 @@ class _LearningScreenState extends State<LearningScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tampilkan daftar sections
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _initializeSections,
|
onRefresh: _initializeSections,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user