feat: integrate api latihan & menambahkan base_url

This commit is contained in:
vionar3 2025-05-18 12:04:53 +07:00
parent 363d216f21
commit 76710e8216
7 changed files with 388 additions and 260 deletions

View File

@ -1,4 +1,4 @@
// lib/config.dart
class BaseUrl {
static const String baseUrl = 'http://192.168.100.45:8000/api';
static const String baseUrl = 'http://192.168.100.13:8000/api';
}

View File

@ -45,6 +45,7 @@ final router = GoRouter(
builder: (BuildContext context, GoRouterState state) {
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
return SubMateriPage(
id: extra['id'],
title: extra['title'],
description: extra['description'],
videoLink: extra['videoLink'],
@ -52,33 +53,34 @@ final router = GoRouter(
);
},
),
GoRoute(
path: '/latihan',
builder: (context, state) => LatihanPage(),
),
// GoRoute(
// path: '/pelafalan',
// builder: (context, state) {
// final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
// return PelafalanPage(
// Rute untuk halaman Latihan
GoRoute(
path: '/latihan',
builder: (BuildContext context, GoRouterState state) {
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
return LatihanPage(
id: extra['id'],
currentStep: extra['currentStep'] ?? 0, // Menambahkan parameter currentStep
);
},
),
// );
// },
// ),
GoRoute(
path: '/pelafalan',
builder: (context, state) {
// Ambil data dari state.extra yang dikirim melalui context.go()
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
final int currentStep = extra['currentStep']; // Ambil currentStep
// Rute untuk halaman Pelafalan
GoRoute(
path: '/pelafalan',
builder: (context, state) {
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
final int currentStep = extra['currentStep'];
final List<dynamic> latihanData = extra['latihanData'];
return PelafalanPage(
id: extra['id'],
currentStep: currentStep,
latihanData: latihanData,
);
},
),
// Kirim data ke PelafalanPage
return PelafalanPage(
currentStep:
currentStep, // Mengirim currentStep ke halaman PelafalanPage
);
},
),
GoRoute(
path: '/kemajuan',
builder: (context, state) {

View File

@ -6,6 +6,7 @@ double spacing = 8;
Color primaryColor = const Color(0xffFE0E46);
Color secondPrimaryColor = const Color(0xff08CACA);
Color secondPrimaryColor1 = const Color.fromARGB(255, 189, 211, 211);
Color thirdPrimaryColor = const Color(0xffF2C98A);
Color blueBorderChat = const Color(0xffE0F6CA);
Color primaryColorW = const Color(0xff003299).withOpacity(0.2);

View File

@ -1,40 +1,98 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flame_audio/flame_audio.dart'; // Import Flame Audio
import 'package:go_router/go_router.dart';
import 'package:ta_tahsin/core/baseurl/base_url.dart';
import 'package:ta_tahsin/core/theme.dart';
import '../materi/model/model_data_materi.dart';
import 'model/model_data_latihan.dart';
import 'package:http/http.dart' as http;
import 'dart:convert'; // Untuk parsing JSON
import 'dart:async'; // Untuk timer
import 'package:audioplayers/audioplayers.dart'; // Tambahkan audioplayers
class LatihanPage extends StatefulWidget {
const LatihanPage({super.key});
final int id; // ID yang diteruskan dari halaman sebelumnya
final int currentStep; // Menambahkan parameter currentStep
const LatihanPage({super.key, required this.id, required this.currentStep});
@override
_LatihanPageState createState() => _LatihanPageState();
}
class _LatihanPageState extends State<LatihanPage> {
int currentStep = 0; // Start with the first step
late Future<List<dynamic>> latihanData; // Data latihan berdasarkan id_submateri
bool isRecording = false; // Menandakan apakah sedang merekam
int timer = 10; // Timer dimulai dari 10 detik
late Timer countdownTimer; // Timer untuk countdown
String timerText = "10"; // Menampilkan countdown timer dalam teks
final AudioPlayer _audioPlayer = AudioPlayer(); // Inisialisasi Audioplayer
@override
void initState() {
super.initState();
latihanData = fetchLatihanData(widget.id); // Ambil data latihan berdasarkan id_submateri yang diteruskan
}
// Fungsi untuk mengambil data latihan dari API
Future<List<dynamic>> fetchLatihanData(int id_submateri) async {
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/latihan/$id_submateri'), // Gantilah dengan URL API yang sesuai
);
if (response.statusCode == 200) {
return json.decode(response.body)['data']; // Parse data latihan dari API
} else {
throw Exception('Failed to load latihan');
}
}
// Fungsi untuk mulai countdown timer
void startTimer() {
setState(() {
isRecording = true; // Set to true when starting to record
timer = 10; // Reset timer to 10 seconds
timerText = timer.toString();
});
countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (this.timer > 0) {
setState(() {
this.timer--;
timerText = this.timer.toString();
});
} else {
countdownTimer.cancel();
setState(() {
isRecording = false; // Stop recording after timer ends
});
}
});
}
// Fungsi untuk menghentikan timer dan reset ke kondisi semula
void stopTimer() {
countdownTimer.cancel();
setState(() {
isRecording = false; // Stop recording
timer = 10; // Reset timer
timerText = timer.toString();
});
}
// Fungsi untuk memutar audio dari asset berdasarkan URL yang diterima
void playAudio(String audioUrl) async {
// Menggunakan AssetSource untuk memutar audio dari asset
await _audioPlayer.play(AssetSource(audioUrl)); // Gunakan AssetSource untuk audio lokal
print("Audio playing...");
}
@override
Widget build(BuildContext context) {
if (currentStep >= latihanList.length) {
currentStep = latihanList.length - 1; // Stay at last step if out of bounds
}
final arabicText = latihanList[currentStep]['arabicText'];
final latinText = latihanList[currentStep]['latinText'];
final audioFile = latihanList[currentStep]['audioFile']; // Get the audio file from data
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
context.go('/materi', extra: {
'title': 'Makharijul Huruf',
'description': 'tempat keluarnya huruf',
'subMateri': materiList[0]['subMateri'],
});
context.go('/navigasi'); // Navigasi kembali ke halaman utama
},
),
actions: [
@ -42,12 +100,12 @@ class _LatihanPageState extends State<LatihanPage> {
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
children: List.generate(3, (index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: CircleAvatar(
radius: 12,
backgroundColor: index <= currentStep
backgroundColor: index <= widget.currentStep
? secondPrimaryColor
: Colors.grey,
),
@ -58,155 +116,182 @@ class _LatihanPageState extends State<LatihanPage> {
),
],
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
body: FutureBuilder<List<dynamic>>(
future: latihanData,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator()); // Tampilkan loading selama data dimuat
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}')); // Menampilkan pesan error
} else if (!snapshot.hasData || snapshot.data == null || snapshot.data!.isEmpty) {
return Center(child: Text('Tidak ada latihan tersedia')); // Menampilkan pesan jika tidak ada data latihan
}
// Ambil data latihan berdasarkan currentStep
final latihanList = snapshot.data!;
final latihan = latihanList[widget.currentStep]; // Ambil data latihan untuk currentStep
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.lightbulb,
color: secondPrimaryColor,
size: 40,
),
const SizedBox(width: 5),
Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.grey,
width: 1,
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.lightbulb,
color: secondPrimaryColor,
size: 40,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dengarkan, ikuti dan rekam',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: blackColor,
const SizedBox(width: 5),
Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.grey,
width: 1,
),
textAlign: TextAlign.start,
),
Text(
'pelafalanmu terhadap lafadz ini',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: blackColor,
),
textAlign: TextAlign.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dengarkan, ikuti dan rekam',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: blackColor,
),
textAlign: TextAlign.start,
),
Text(
'pelafalanmu terhadap lafadz ini',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: blackColor,
),
textAlign: TextAlign.start,
),
],
),
),
],
),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 40.0),
margin: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 5,
blurRadius: 10,
),
],
color: Colors.white,
),
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 40.0),
child: Text(
"Ucapkan potongan kata ini",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
Text(
latihan['potongan_ayat'], // Menampilkan potongan ayat dari data latihan
style: const TextStyle(
fontSize: 30,
color: Colors.red,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Text(
latihan['latin_text'], // Menampilkan teks latin dari data latihan
style: TextStyle(
fontSize: 15,
color: Colors.black,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 30),
IconButton(
onPressed: () {
// Memutar audio dari asset sesuai dengan URL yang ada pada `correct_audio`
playAudio('audio/${latihan['correct_audio']}');
},
icon: Icon(
Icons.volume_up,
size: 50,
color: secondPrimaryColor,
),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
)
],
),
),
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton(
onPressed: () {
if (!isRecording) {
startTimer(); // Start timer and change to stop icon
} else {
stopTimer(); // Stop timer and reset the button
// Pass data to PelafalanPage after recording
context.go(
'/pelafalan',
extra: {
'id': widget.id,
'currentStep': widget.currentStep,
'latihanData': snapshot.data, // Mengirimkan data latihan yang sudah di-fetch
},
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: const CircleBorder(),
padding: const EdgeInsets.all(16),
),
child: Icon(
isRecording ? Icons.stop : Icons.mic, // Toggle between mic and stop icon
size: 40,
color: Colors.white,
),
),
),
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 40.0),
margin: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 5,
blurRadius: 10,
),
],
color: Colors.white,
),
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 40.0),
child: Text(
"Ucapkan potongan kata ini",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
Text(
arabicText,
style: const TextStyle(
fontSize: 30,
color: Colors.red,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Text(
latinText,
style: TextStyle(
fontSize: 15,
color: Colors.black,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 30),
IconButton(
onPressed: () {
// Play the audio file when the icon is clicked
FlameAudio.play(audioFile); // Play the audio using flame_audio
},
icon: Icon(
Icons.volume_up,
size: 50,
color: secondPrimaryColor,
),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
)
],
),
),
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton(
onPressed: () {
setState(() {
if (currentStep < latihanList.length - 1) {
currentStep++;
}
});
context.go(
'/pelafalan',
extra: {
'currentStep': currentStep,
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: const CircleBorder(),
padding: const EdgeInsets.all(16),
),
child: const Icon(
Icons.mic,
size: 40,
color: Colors.white,
),
),
),
),
),
],
),
),
),
);
},
),
);
}
@override
void dispose() {
_audioPlayer.dispose(); // Jangan lupa untuk melepaskan player
super.dispose();
}
}

View File

@ -1,28 +1,25 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:ta_tahsin/core/theme.dart';
import 'model/model_data_pelafalan.dart';
class PelafalanPage extends StatelessWidget {
final int id;
final int currentStep;
final List<dynamic> latihanData; // Menerima semua data latihan
const PelafalanPage({
super.key,
required this.id,
required this.currentStep,
required this.latihanData,
});
@override
Widget build(BuildContext context) {
// Ensure that currentStep is within the bounds of pelafalanList
if (currentStep >= pelafalanList.length) {
return Center(child: Text("End of List"));
}
// Ambil data latihan berdasarkan currentStep
final latihan = latihanData[currentStep];
final arabicText = pelafalanList[currentStep]['arabicText'];
final latinText = pelafalanList[currentStep]['latinText'];
final materiDescription = pelafalanList[currentStep]['materiDescription'];
final correctAudio = pelafalanList[currentStep]['correctAudio'];
final recordedAudio = pelafalanList[currentStep]['recordedAudio'];
return Scaffold(
body: Center(
@ -59,7 +56,7 @@ class PelafalanPage extends StatelessWidget {
const SizedBox(height: 20),
Center(
child: Text(
arabicText,
latihan['potongan_ayat'],
style: const TextStyle(
fontSize: 30,
color: Colors.red,
@ -154,7 +151,7 @@ class PelafalanPage extends StatelessWidget {
),
const SizedBox(height: 10),
Text(
materiDescription,
latihan['materi_description'],
style: TextStyle(fontSize: 14, color: Colors.black),
textAlign: TextAlign.left,
),
@ -164,11 +161,17 @@ class PelafalanPage extends StatelessWidget {
children: [
ElevatedButton(
onPressed: () {
// Update the progress and navigate back to LatihanPage
context.go(
'/latihan',
extra: {'currentStep': currentStep + 1}, // Move to next step
);
if (currentStep < latihanData.length - 1) {
context.go(
'/latihan',
extra: {
'id': id, // ganti sesuai kebutuhan
'currentStep': currentStep + 1,
},
);
} else {
context.go('/navigasi');
}
},
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
@ -179,7 +182,7 @@ class PelafalanPage extends StatelessWidget {
minimumSize: Size(double.infinity, 50),
),
child: Text(
'Lanjut',
currentStep == latihanData.length - 1 ? 'Selesai' : 'Lanjut',
style: TextStyle(
color: whiteColor,
fontSize: 16,
@ -190,7 +193,14 @@ class PelafalanPage extends StatelessWidget {
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
context.pop();
context.go(
'/latihan',
extra: {
'id': 1, // Ganti sesuai kebutuhan
'currentStep': currentStep,
'latihanData': latihanData, // Mengirimkan data latihan yang sesuai
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
@ -219,4 +229,3 @@ class PelafalanPage extends StatelessWidget {
);
}
}

View File

@ -59,25 +59,32 @@ class _MateriPageState extends State<MateriPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: secondPrimaryColor,
title: Text(
widget.title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: whiteColor,
appBar: PreferredSize(
preferredSize: Size.fromHeight(50), // Ukuran tinggi AppBar
child: Card(
elevation: 4, // Menambahkan shadow
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero, // Tidak ada radius, sudut tajam
),
margin: EdgeInsets.zero, // Menghilangkan margin Card
child: AppBar(
backgroundColor: secondPrimaryColor, // Warna AppBar (sesuaikan dengan secondPrimaryColor)
title: Text(
widget.title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.white,
onPressed: () {
context.go('/navigasi');
},
),
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back), color: whiteColor,
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/navigasi');
}
},
),
),
body: isLoading
@ -193,6 +200,7 @@ class _MateriPageState extends State<MateriPage> {
context.push(
'/submateri', // Gantilah dengan rute yang sesuai
extra: {
'id': submateri['id'],
'title': submateri['title'],
'description': submateri['subtitle'],
'videoLink': submateri['video_url'],

View File

@ -4,6 +4,7 @@ import 'package:ta_tahsin/core/theme.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart'; // Import youtube_player_flutter
class SubMateriPage extends StatefulWidget {
final int id;
final String title;
final String description;
final String videoLink;
@ -11,6 +12,7 @@ class SubMateriPage extends StatefulWidget {
const SubMateriPage({
super.key,
required this.id,
required this.title,
required this.description,
required this.videoLink,
@ -29,34 +31,40 @@ class _SubMateriPageState extends State<SubMateriPage> {
super.initState();
_controller = YoutubePlayerController(
initialVideoId: YoutubePlayer.convertUrlToId(widget.videoLink)!,
flags: const YoutubePlayerFlags(
autoPlay: false,
mute: false,
),
flags: const YoutubePlayerFlags(autoPlay: false, mute: false),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
appBar: PreferredSize(
preferredSize: Size.fromHeight(50), // Ukuran tinggi AppBar
child: Card(
elevation: 4, // Menambahkan shadow
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero, // Tidak ada radius, sudut tajam
),
margin: EdgeInsets.zero, // Menghilangkan margin Card
child: AppBar(
backgroundColor:
secondPrimaryColor, // Warna AppBar (sesuaikan dengan secondPrimaryColor)
title: Text(
widget.title,
style: TextStyle(
fontSize: 25,
fontSize: 24,
fontWeight: FontWeight.bold,
color: blackColor,
color: Colors.white,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/');
}
},
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.white,
onPressed: () {
Navigator.pop(context); // Aksi kembali
},
),
),
),
),
body: Column(
@ -75,7 +83,7 @@ class _SubMateriPageState extends State<SubMateriPage> {
),
),
Container(
color: secondPrimaryColor,
color: secondPrimaryColor1,
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 10),
width: double.infinity,
child: Text(
@ -83,7 +91,7 @@ class _SubMateriPageState extends State<SubMateriPage> {
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: whiteColor,
color: blackColor,
),
),
),
@ -92,32 +100,47 @@ class _SubMateriPageState extends State<SubMateriPage> {
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Text(
widget.intro,
style: TextStyle(
fontSize: 16,
color: blackColor,
),
style: TextStyle(fontSize: 16, color: blackColor),
),
),
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 20,
),
child: Container(
width: double.infinity,
height: 50,
decoration: BoxDecoration(
color: secondPrimaryColor,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: Offset(0, -4),
blurRadius: 6,
),
],
),
onPressed: () {
context.go('/latihan');
},
child: Text(
"Mulai Berlatih",
style: TextStyle(fontSize: 16,color: whiteColor),
child: TextButton(
onPressed: () {
context.go(
'/latihan', // Gantilah dengan rute yang sesuai
extra: {'id': widget.id}, // Mengirimkan id dari halaman sebelumnya
);
},
child: Text(
"Mulai Berlatih",
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),