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 // lib/config.dart
class BaseUrl { 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) { builder: (BuildContext context, GoRouterState state) {
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>; final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
return SubMateriPage( return SubMateriPage(
id: extra['id'],
title: extra['title'], title: extra['title'],
description: extra['description'], description: extra['description'],
videoLink: extra['videoLink'], videoLink: extra['videoLink'],
@ -52,33 +53,34 @@ final router = GoRouter(
); );
}, },
), ),
// Rute untuk halaman Latihan
GoRoute( GoRoute(
path: '/latihan', path: '/latihan',
builder: (context, state) => LatihanPage(), builder: (BuildContext context, GoRouterState state) {
),
// GoRoute(
// path: '/pelafalan',
// builder: (context, state) {
// final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
// return PelafalanPage(
// );
// },
// ),
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 Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
final int currentStep = extra['currentStep']; // Ambil currentStep return LatihanPage(
id: extra['id'],
// Kirim data ke PelafalanPage currentStep: extra['currentStep'] ?? 0, // Menambahkan parameter currentStep
return PelafalanPage(
currentStep:
currentStep, // Mengirim currentStep ke halaman PelafalanPage
); );
}, },
), ),
// 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,
);
},
),
GoRoute( GoRoute(
path: '/kemajuan', path: '/kemajuan',
builder: (context, state) { builder: (context, state) {

View File

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

View File

@ -1,40 +1,98 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:flame_audio/flame_audio.dart'; // Import Flame Audio import 'package:ta_tahsin/core/baseurl/base_url.dart';
import 'package:ta_tahsin/core/theme.dart'; import 'package:ta_tahsin/core/theme.dart';
import '../materi/model/model_data_materi.dart'; import 'package:http/http.dart' as http;
import 'model/model_data_latihan.dart'; import 'dart:convert'; // Untuk parsing JSON
import 'dart:async'; // Untuk timer
import 'package:audioplayers/audioplayers.dart'; // Tambahkan audioplayers
class LatihanPage extends StatefulWidget { 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 @override
_LatihanPageState createState() => _LatihanPageState(); _LatihanPageState createState() => _LatihanPageState();
} }
class _LatihanPageState extends State<LatihanPage> { 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 @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
context.go('/materi', extra: { context.go('/navigasi'); // Navigasi kembali ke halaman utama
'title': 'Makharijul Huruf',
'description': 'tempat keluarnya huruf',
'subMateri': materiList[0]['subMateri'],
});
}, },
), ),
actions: [ actions: [
@ -42,12 +100,12 @@ class _LatihanPageState extends State<LatihanPage> {
child: Center( child: Center(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) { children: List.generate(3, (index) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: CircleAvatar( child: CircleAvatar(
radius: 12, radius: 12,
backgroundColor: index <= currentStep backgroundColor: index <= widget.currentStep
? secondPrimaryColor ? secondPrimaryColor
: Colors.grey, : Colors.grey,
), ),
@ -58,12 +116,28 @@ class _LatihanPageState extends State<LatihanPage> {
), ),
], ],
), ),
body: Padding( 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), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
child: Center( child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SizedBox(height: 10),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -108,6 +182,7 @@ class _LatihanPageState extends State<LatihanPage> {
), ),
], ],
), ),
const SizedBox(height: 5),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 40.0), padding: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 40.0),
margin: const EdgeInsets.symmetric(vertical: 10), margin: const EdgeInsets.symmetric(vertical: 10),
@ -136,7 +211,7 @@ class _LatihanPageState extends State<LatihanPage> {
), ),
), ),
Text( Text(
arabicText, latihan['potongan_ayat'], // Menampilkan potongan ayat dari data latihan
style: const TextStyle( style: const TextStyle(
fontSize: 30, fontSize: 30,
color: Colors.red, color: Colors.red,
@ -146,7 +221,7 @@ class _LatihanPageState extends State<LatihanPage> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
latinText, latihan['latin_text'], // Menampilkan teks latin dari data latihan
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
color: Colors.black, color: Colors.black,
@ -156,8 +231,8 @@ class _LatihanPageState extends State<LatihanPage> {
const SizedBox(height: 30), const SizedBox(height: 30),
IconButton( IconButton(
onPressed: () { onPressed: () {
// Play the audio file when the icon is clicked // Memutar audio dari asset sesuai dengan URL yang ada pada `correct_audio`
FlameAudio.play(audioFile); // Play the audio using flame_audio playAudio('audio/${latihan['correct_audio']}');
}, },
icon: Icon( icon: Icon(
Icons.volume_up, Icons.volume_up,
@ -176,26 +251,28 @@ class _LatihanPageState extends State<LatihanPage> {
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
setState(() { if (!isRecording) {
if (currentStep < latihanList.length - 1) { startTimer(); // Start timer and change to stop icon
currentStep++; } else {
} stopTimer(); // Stop timer and reset the button
}); // Pass data to PelafalanPage after recording
context.go( context.go(
'/pelafalan', '/pelafalan',
extra: { extra: {
'currentStep': currentStep, 'id': widget.id,
'currentStep': widget.currentStep,
'latihanData': snapshot.data, // Mengirimkan data latihan yang sudah di-fetch
}, },
); );
}
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor, backgroundColor: secondPrimaryColor,
shape: const CircleBorder(), shape: const CircleBorder(),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
), ),
child: const Icon( child: Icon(
Icons.mic, isRecording ? Icons.stop : Icons.mic, // Toggle between mic and stop icon
size: 40, size: 40,
color: Colors.white, color: Colors.white,
), ),
@ -206,7 +283,15 @@ class _LatihanPageState extends State<LatihanPage> {
], ],
), ),
), ),
);
},
), ),
); );
} }
@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:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ta_tahsin/core/theme.dart'; import 'package:ta_tahsin/core/theme.dart';
import 'model/model_data_pelafalan.dart';
class PelafalanPage extends StatelessWidget { class PelafalanPage extends StatelessWidget {
final int id;
final int currentStep; final int currentStep;
final List<dynamic> latihanData; // Menerima semua data latihan
const PelafalanPage({ const PelafalanPage({
super.key, super.key,
required this.id,
required this.currentStep, required this.currentStep,
required this.latihanData,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Ensure that currentStep is within the bounds of pelafalanList // Ambil data latihan berdasarkan currentStep
if (currentStep >= pelafalanList.length) { final latihan = latihanData[currentStep];
return Center(child: Text("End of List"));
}
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( return Scaffold(
body: Center( body: Center(
@ -59,7 +56,7 @@ class PelafalanPage extends StatelessWidget {
const SizedBox(height: 20), const SizedBox(height: 20),
Center( Center(
child: Text( child: Text(
arabicText, latihan['potongan_ayat'],
style: const TextStyle( style: const TextStyle(
fontSize: 30, fontSize: 30,
color: Colors.red, color: Colors.red,
@ -154,7 +151,7 @@ class PelafalanPage extends StatelessWidget {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
materiDescription, latihan['materi_description'],
style: TextStyle(fontSize: 14, color: Colors.black), style: TextStyle(fontSize: 14, color: Colors.black),
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
@ -164,11 +161,17 @@ class PelafalanPage extends StatelessWidget {
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
// Update the progress and navigate back to LatihanPage if (currentStep < latihanData.length - 1) {
context.go( context.go(
'/latihan', '/latihan',
extra: {'currentStep': currentStep + 1}, // Move to next step extra: {
'id': id, // ganti sesuai kebutuhan
'currentStep': currentStep + 1,
},
); );
} else {
context.go('/navigasi');
}
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor, backgroundColor: secondPrimaryColor,
@ -179,7 +182,7 @@ class PelafalanPage extends StatelessWidget {
minimumSize: Size(double.infinity, 50), minimumSize: Size(double.infinity, 50),
), ),
child: Text( child: Text(
'Lanjut', currentStep == latihanData.length - 1 ? 'Selesai' : 'Lanjut',
style: TextStyle( style: TextStyle(
color: whiteColor, color: whiteColor,
fontSize: 16, fontSize: 16,
@ -190,7 +193,14 @@ class PelafalanPage extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
context.pop(); context.go(
'/latihan',
extra: {
'id': 1, // Ganti sesuai kebutuhan
'currentStep': currentStep,
'latihanData': latihanData, // Mengirimkan data latihan yang sesuai
},
);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: Colors.white,
@ -219,4 +229,3 @@ class PelafalanPage extends StatelessWidget {
); );
} }
} }

View File

@ -59,27 +59,34 @@ class _MateriPageState extends State<MateriPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: PreferredSize(
backgroundColor: secondPrimaryColor, 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( title: Text(
widget.title, widget.title,
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: whiteColor, color: Colors.white,
), ),
), ),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), color: whiteColor, icon: const Icon(Icons.arrow_back),
color: Colors.white,
onPressed: () { onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/navigasi'); context.go('/navigasi');
}
}, },
), ),
), ),
),
),
body: isLoading body: isLoading
? Center(child: CircularProgressIndicator()) // Tampilkan loading spinner selama proses loading ? Center(child: CircularProgressIndicator()) // Tampilkan loading spinner selama proses loading
: FutureBuilder<List<dynamic>>( : FutureBuilder<List<dynamic>>(
@ -193,6 +200,7 @@ class _MateriPageState extends State<MateriPage> {
context.push( context.push(
'/submateri', // Gantilah dengan rute yang sesuai '/submateri', // Gantilah dengan rute yang sesuai
extra: { extra: {
'id': submateri['id'],
'title': submateri['title'], 'title': submateri['title'],
'description': submateri['subtitle'], 'description': submateri['subtitle'],
'videoLink': submateri['video_url'], '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 import 'package:youtube_player_flutter/youtube_player_flutter.dart'; // Import youtube_player_flutter
class SubMateriPage extends StatefulWidget { class SubMateriPage extends StatefulWidget {
final int id;
final String title; final String title;
final String description; final String description;
final String videoLink; final String videoLink;
@ -11,6 +12,7 @@ class SubMateriPage extends StatefulWidget {
const SubMateriPage({ const SubMateriPage({
super.key, super.key,
required this.id,
required this.title, required this.title,
required this.description, required this.description,
required this.videoLink, required this.videoLink,
@ -29,36 +31,42 @@ class _SubMateriPageState extends State<SubMateriPage> {
super.initState(); super.initState();
_controller = YoutubePlayerController( _controller = YoutubePlayerController(
initialVideoId: YoutubePlayer.convertUrlToId(widget.videoLink)!, initialVideoId: YoutubePlayer.convertUrlToId(widget.videoLink)!,
flags: const YoutubePlayerFlags( flags: const YoutubePlayerFlags(autoPlay: false, mute: false),
autoPlay: false,
mute: false,
),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( 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( title: Text(
widget.title, widget.title,
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: blackColor, color: Colors.white,
), ),
), ),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
color: Colors.white,
onPressed: () { onPressed: () {
if (context.canPop()) { Navigator.pop(context); // Aksi kembali
context.pop();
} else {
context.go('/');
}
}, },
), ),
), ),
),
),
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -75,7 +83,7 @@ class _SubMateriPageState extends State<SubMateriPage> {
), ),
), ),
Container( Container(
color: secondPrimaryColor, color: secondPrimaryColor1,
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 10),
width: double.infinity, width: double.infinity,
child: Text( child: Text(
@ -83,7 +91,7 @@ class _SubMateriPageState extends State<SubMateriPage> {
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: whiteColor, color: blackColor,
), ),
), ),
), ),
@ -92,10 +100,7 @@ class _SubMateriPageState extends State<SubMateriPage> {
padding: const EdgeInsets.symmetric(horizontal: 24.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Text( child: Text(
widget.intro, widget.intro,
style: TextStyle( style: TextStyle(fontSize: 16, color: blackColor),
fontSize: 16,
color: blackColor,
),
), ),
), ),
@ -103,21 +108,39 @@ class _SubMateriPageState extends State<SubMateriPage> {
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20), padding: const EdgeInsets.symmetric(
child: ElevatedButton( horizontal: 16.0,
style: ElevatedButton.styleFrom( vertical: 20,
backgroundColor: secondPrimaryColor, ),
shape: RoundedRectangleBorder( child: Container(
width: double.infinity,
height: 50,
decoration: BoxDecoration(
color: secondPrimaryColor,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: Offset(0, -4),
blurRadius: 6,
), ),
minimumSize: Size(double.infinity, 50), ],
), ),
child: TextButton(
onPressed: () { onPressed: () {
context.go('/latihan'); context.go(
'/latihan', // Gantilah dengan rute yang sesuai
extra: {'id': widget.id}, // Mengirimkan id dari halaman sebelumnya
);
}, },
child: Text( child: Text(
"Mulai Berlatih", "Mulai Berlatih",
style: TextStyle(fontSize: 16,color: whiteColor), style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
), ),
), ),
), ),