diff --git a/lib/app/route.dart b/lib/app/route.dart index b340316..459a8a8 100644 --- a/lib/app/route.dart +++ b/lib/app/route.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../screen/rekaman/edit_rekam_screen.dart'; import '../screen/rekaman/rekam_screen.dart'; import '../screen/home/home_screen.dart'; @@ -9,6 +10,7 @@ const splashRoute = "/splashRoute"; const loginRoute = "/loginRoute"; const homeRoute = "/homeRoute"; const rekamRoute = "/rekamRoute"; +const editRekamRoute = "/editRekamRoute"; class AppRoute { static Route generateRoute(RouteSettings settings) { @@ -56,6 +58,17 @@ class AppRoute { }); } + // edit screen + if (settings.name == editRekamRoute) { + return MaterialPageRoute(builder: (context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(1.0), padding: EdgeInsets.all(0)), + child: EditRekamScreen(), + ); + }); + } + return MaterialPageRoute(builder: (context) { return MediaQuery( data: MediaQuery.of(context).copyWith( diff --git a/lib/repository/base_repository.dart b/lib/repository/base_repository.dart index 82a55dd..19be4ff 100644 --- a/lib/repository/base_repository.dart +++ b/lib/repository/base_repository.dart @@ -9,16 +9,12 @@ abstract class BaseRepository { // POST audio Future> postAudio({ - required String filePath, + // required String filePath, + required FormData formData, required String service, String? token, }) async { try { - FormData formData = FormData.fromMap({ - "audio": - await MultipartFile.fromFile(filePath, filename: "rekaman.mp3"), - }); - final response = await dio.post( // Constant.baseUrl + service, service, diff --git a/lib/repository/voice_to_text_repository.dart b/lib/repository/voice_to_text_repository.dart index ba19bf7..250d6c0 100644 --- a/lib/repository/voice_to_text_repository.dart +++ b/lib/repository/voice_to_text_repository.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; + import '../model/voice_to_text_model.dart'; import 'base_repository.dart'; @@ -9,7 +13,8 @@ class VoiceToTextRepository extends BaseRepository { required String userId, }) async { // final service = "${Constant.baseUrl}xauth/login"; - final service = "http://${host}/one-api/scan-ktp/Voicetotext/listRiwayatRekaman"; + final service = + "http://${host}/one-api/scan-ktp/Voicetotext/listRiwayatRekaman"; final resp = await post(param: {"userId": userId}, service: service); final result = List.empty(growable: true); @@ -20,4 +25,37 @@ class VoiceToTextRepository extends BaseRepository { return result; } -} \ No newline at end of file + + // proses upload + Future> prosesUpload({ + required String host, + required String userId, + required String filePath, + required String qrCodeStr, + }) async { + // final service = "${Constant.baseUrl}xauth/login"; + final service = "http://${host}/one-api/scan-ktp/Voicetotext/uploadRekaman"; + + FormData formData = FormData.fromMap({ + "audio": await MultipartFile.fromFile( + filePath, + filename: "rekaman.mp3", + ), + "userId": userId, + "qrCodeStr":qrCodeStr, + }); + + final resp = await postAudio( + service: service, + formData: formData, + ); + + final result = List.empty(growable: true); + resp['data'].forEach((e) { + final model = VoiceToTextModel.fromJson(e); + result.add(model); + }); + + return result; + } +} diff --git a/lib/screen/home/home_screen.dart b/lib/screen/home/home_screen.dart index a5bb747..c6eda0d 100644 --- a/lib/screen/home/home_screen.dart +++ b/lib/screen/home/home_screen.dart @@ -52,6 +52,7 @@ class HomeScreen extends HookConsumerWidget { description: Text(message), autoCloseDuration: waktu, type: ToastificationType.success, + style: ToastificationStyle.fillColored, ); } else if (typeToast == "error") { toastification.show( diff --git a/lib/screen/rekaman/edit_rekam_screen.dart b/lib/screen/rekaman/edit_rekam_screen.dart new file mode 100644 index 0000000..f3426f8 --- /dev/null +++ b/lib/screen/rekaman/edit_rekam_screen.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../app/constant.dart'; + +class EditRekamScreen extends HookConsumerWidget { + const EditRekamScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus!.unfocus(); + }, + child: Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: Constant.bgGrey, + body: Column( + children: [ + // atas + Image.asset( + 'images/vektoratas.png', + width: double.infinity, + fit: BoxFit.cover, + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen/rekaman/rekam_screen.dart b/lib/screen/rekaman/rekam_screen.dart index bb583b2..9ae0128 100644 --- a/lib/screen/rekaman/rekam_screen.dart +++ b/lib/screen/rekaman/rekam_screen.dart @@ -2,15 +2,21 @@ import 'package:ffmpeg_kit_flutter_audio/ffmpeg_kit.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fluttervoice2text/app/route.dart'; +import 'package:fluttervoice2text/model/edit_voice_to_text_model.dart'; import 'package:path_provider/path_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:toastification/toastification.dart'; import 'dart:io'; import '../../app/constant.dart'; +import '../../provider/current_user_provider.dart'; import '../../provider/voice_to_text_provider.dart'; +import '../home/list_riwayat_rekaman_provider.dart'; +import 'upload_rekam_provider.dart'; class RekamScreen extends HookConsumerWidget { const RekamScreen({super.key}); @@ -24,11 +30,16 @@ class RekamScreen extends HookConsumerWidget { final isRekam = useState(false); final judulTombol = useState("MULAI REKAM"); - final qrCodeStr = useState(""); + final qrCodeStr = useState("Scan QRCode untuk dapat merekam"); final isSelesaiRekam = useState(false); final audioPath = useState(""); final recorder = useState(FlutterSoundRecorder()); final player = useState(FlutterSoundPlayer()); + final isLoadingUpload = useState(false); + final currentUser = ref.watch(currentUserProvider); + final host = currentUser?.host ?? ""; + final userId = currentUser?.model.userId ?? ""; + final awalan = useState("Info :"); Future requestPermissions() async { await Permission.microphone.request(); @@ -57,6 +68,7 @@ class RekamScreen extends HookConsumerWidget { if (scannedBarcode.isNotEmpty) { ref.read(barcodeX.notifier).state = barcodes.barcodes.first; qrCodeStr.value = scannedBarcode; + awalan.value = "QrCode : "; } } } @@ -95,27 +107,6 @@ class RekamScreen extends HookConsumerWidget { } } - Future berhentiRekaman() async { - try { - await recorder.value.stopRecorder(); - isSelesaiRekam.value = false; - isRekam.value = false; - - // panggil fungsi untuk kirim ke BE - if (audioPath.value.isNotEmpty) { - String? mp3Path = await convertToMp3(audioPath.value); - if (mp3Path != null) { - // await uploadAudioFile(mp3Path); - print('mp3 convert $mp3Path'); - } else { - print("Konversi gagal!"); - } - } - } catch (e) { - print("Error stop record ${e.toString()}"); - } - } - Future playRecording() async { try { if (audioPath.value.isNotEmpty && File(audioPath.value).existsSync()) { @@ -128,6 +119,148 @@ class RekamScreen extends HookConsumerWidget { } } + Future berhentiRekaman() async { + try { + await recorder.value.stopRecorder(); + isSelesaiRekam.value = false; + isRekam.value = false; + + // panggil fungsi untuk kirim ke BE + if (audioPath.value.isNotEmpty) { + String? mp3Path = await convertToMp3(audioPath.value); + if (mp3Path != null) { + // await uploadAudioFile(mp3Path); + ref.read(uploadRekamProvider.notifier).uploadRekam( + host: host, + filePath: mp3Path, + userId: userId, + qrCodeStr: qrCodeStr.value, + ); + print('mp3 convert $mp3Path'); + } else { + print("Konversi gagal!"); + } + } + } catch (e) { + print("Error stop record ${e.toString()}"); + } + } + + void showLongToast( + String title, + String message, + String typeToast, + Duration waktu, + ) { + if (typeToast == "success") { + toastification.show( + context: context, + title: Text(title), + description: Text(message), + autoCloseDuration: waktu, + type: ToastificationType.success, + style: ToastificationStyle.fillColored, + ); + } else if (typeToast == "error") { + toastification.show( + context: context, + title: Text(title), + description: Text(message), + autoCloseDuration: waktu, + type: ToastificationType.error, + style: ToastificationStyle.fillColored, + ); + } else if (typeToast == "warning") { + toastification.show( + context: context, + title: Text(title), + description: Text(message), + autoCloseDuration: waktu, + type: ToastificationType.warning, + style: ToastificationStyle.fillColored, + ); + } + } + + // list Riwayat Rekam + ref.listen(listRiwayatRekamanProvider, (prev, next) { + if (next is ListRiwayatRekamanStateLoading) { + isLoadingUpload.value = true; + } else if (next is ListRiwayatRekamanStateError) { + isLoadingUpload.value = false; + // errorMessage.value = next.message; + showLongToast( + 'Error', + next.message, + 'error', + Duration(seconds: 3), + ); + } else if (next is ListRiwayatRekamanStateDone) { + isLoadingUpload.value = false; + + Navigator.of(context).pop(); + Navigator.of(context).pushNamed( + editRekamRoute, + ); + } + }); + + // listen upload rekam + ref.listen(uploadRekamProvider, (prev, next) { + if (next is UploadRekamStateLoading) { + isLoadingUpload.value = true; + } else if (next is UploadRekamStateError) { + isLoadingUpload.value = false; + // errorMessage.value = next.message; + print("Err : ${next.message}"); + showLongToast( + 'Error', + next.message, + 'error', + Duration(seconds: 3), + ); + } else if (next is UploadRekamStateDone) { + ref.read(barcodeX.notifier).state = Barcode(); + isRekam.value = false; + isSelesaiRekam.value = false; + + isLoadingUpload.value = false; + ref.read(selectedVoiceIdx.notifier).state = next.model[0].Voice2text_ID; + + ref.read(selectedEdit.notifier).state = EditVoiceToTextModel( + Voice2text_Created: next.model[0].Voice2text_Created, + Voice2text_ID: next.model[0].Voice2text_ID, + Voice2text_IsActive: next.model[0].Voice2text_IsActive, + Voice2text_JsonData: next.model[0].Voice2text_JsonData, + Voice2text_Note: next.model[0].Voice2text_Note, + Voice2text_Text: next.model[0].Voice2text_Text, + Voice2text_Updated: next.model[0].Voice2text_Updated, + Voice2text_Url: next.model[0].Voice2text_Url, + Voice2text_User_ID: next.model[0].Voice2text_User_ID, + ); + + ref.read(listRiwayatRekamanProvider.notifier).listRiwayatRekaman( + host: host, + userId: userId, + ); + } + }); + + // loading proses upload useEffect + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timestamp) { + if (isLoadingUpload.value == true) { + showLongToast( + 'Warning', + 'Sedang Upload Rekaman', + 'warning', + Duration(seconds: 3), + ); + } + }); + return () {}; + }, [isLoadingUpload.value]); + return Scaffold( backgroundColor: Constant.bgGrey, bottomNavigationBar: BottomAppBar( @@ -225,11 +358,9 @@ class RekamScreen extends HookConsumerWidget { alignment: Alignment.center, padding: EdgeInsets.symmetric(vertical: 10), color: Constant.inputanGrey, - child: Text("Info: ${qrCodeStr.value}", + child: Text("${awalan.value} ${qrCodeStr.value}", style: TextStyle(color: Colors.white)), ), - SizedBox(height: 40), - ElevatedButton(onPressed: playRecording, child: Text('Play')), ], ), ), diff --git a/lib/screen/rekaman/upload_rekam_provider.dart b/lib/screen/rekaman/upload_rekam_provider.dart new file mode 100644 index 0000000..a929b5f --- /dev/null +++ b/lib/screen/rekaman/upload_rekam_provider.dart @@ -0,0 +1,75 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttervoice2text/model/voice_to_text_model.dart'; +import 'package:fluttervoice2text/repository/voice_to_text_repository.dart'; + +import '../../provider/dio_provider.dart'; +import '../../repository/base_repository.dart'; + +// 3. state provider +final uploadRekamProvider = StateNotifierProvider( + (ref) => UploadRekamNotifier(ref: ref)); + +// 2. notifier +class UploadRekamNotifier extends StateNotifier { + final Ref ref; + UploadRekamNotifier({required this.ref}) : super(UploadRekamStateInit()); + void uploadRekam({ + required String host, + required String userId, + required String filePath, + required String qrCodeStr, + }) async { + try { + state = UploadRekamStateLoading(); + final resp = await VoiceToTextRepository( + dio: ref.read(dioProvider), + ).prosesUpload( + host: host, + filePath: filePath, + userId: userId, + qrCodeStr:qrCodeStr, + ); + + // print(resp); + state = UploadRekamStateDone(model: resp); + } catch (e) { + if (e is BaseRepositoryException) { + state = UploadRekamStateError(message: e.message); + } else { + state = UploadRekamStateError(message: e.toString()); + } + } + } +} + +// 1. state +abstract class UploadRekamState extends Equatable { + final DateTime date; + const UploadRekamState(this.date); + @override + List get props => [date]; +} + +class UploadRekamStateInit extends UploadRekamState { + UploadRekamStateInit() : super(DateTime.now()); +} + +class UploadRekamStateLoading extends UploadRekamState { + UploadRekamStateLoading() : super(DateTime.now()); +} + +class UploadRekamStateError extends UploadRekamState { + final String message; + UploadRekamStateError({ + required this.message, + }) : super(DateTime.now()); +} + +class UploadRekamStateDone extends UploadRekamState { + final List model; + UploadRekamStateDone({ + required this.model, + }) : super(DateTime.now()); +} \ No newline at end of file