diff --git a/lib/app/app_extension.dart b/lib/app/app_extension.dart index 5ae765a..665872c 100644 --- a/lib/app/app_extension.dart +++ b/lib/app/app_extension.dart @@ -2,4 +2,8 @@ import 'package:jiffy/jiffy.dart'; String formatDateJiffy(String serverDate) { return Jiffy.parse(serverDate).format(pattern: 'dd-MM-yyyy'); +} + +String formatDateJiffyDMYHIS(String serverDate) { + return Jiffy.parse(serverDate).format(pattern: 'dd-MM-yyyy HH:mm:ss'); } \ No newline at end of file diff --git a/lib/model/edit_voice_to_text_model.dart b/lib/model/edit_voice_to_text_model.dart new file mode 100644 index 0000000..a39f168 --- /dev/null +++ b/lib/model/edit_voice_to_text_model.dart @@ -0,0 +1,51 @@ +class EditVoiceToTextModel { + final String Voice2text_ID; + final String Voice2text_Note; + final String Voice2text_Url; + final String Voice2text_Text; + final String Voice2text_User_ID; + final String Voice2text_JsonData; + final String Voice2text_Created; + final String Voice2text_Updated; + final String Voice2text_IsActive; + + EditVoiceToTextModel({ + required this.Voice2text_ID, + required this.Voice2text_Note, + required this.Voice2text_Url, + required this.Voice2text_Text, + required this.Voice2text_User_ID, + required this.Voice2text_JsonData, + required this.Voice2text_Created, + required this.Voice2text_Updated, + required this.Voice2text_IsActive, + }); + + factory EditVoiceToTextModel.fromJson(Map json) { + return EditVoiceToTextModel( + Voice2text_ID: json['Voice2text_ID'] ?? "", + Voice2text_Note: json['Voice2text_Note'] ?? "", + Voice2text_Url: json['Voice2text_Url'] ?? "", + Voice2text_Text: json['Voice2text_Text'] ?? "", + Voice2text_User_ID: json['Voice2text_User_ID'] ?? "", + Voice2text_JsonData: json['Voice2text_JsonData'] ?? "", + Voice2text_Created: json['Voice2text_Created'] ?? "", + Voice2text_Updated: json['Voice2text_Updated'] ?? "", + Voice2text_IsActive: json['Voice2text_IsActive'] ?? "", + ); + } + + Map toJson() { + return { + "Voice2text_ID": Voice2text_ID, + "Voice2text_Note": Voice2text_Note, + "Voice2text_Url": Voice2text_Url, + "Voice2text_Text": Voice2text_Text, + "Voice2text_User_ID": Voice2text_User_ID, + "Voice2text_JsonData": Voice2text_JsonData, + "Voice2text_Created": Voice2text_Created, + "Voice2text_Updated": Voice2text_Updated, + "Voice2text_IsActive": Voice2text_IsActive, + }; + } +} diff --git a/lib/model/voice_to_text_model.dart b/lib/model/voice_to_text_model.dart new file mode 100644 index 0000000..73733de --- /dev/null +++ b/lib/model/voice_to_text_model.dart @@ -0,0 +1,51 @@ +class VoiceToTextModel { + final String Voice2text_ID; + final String Voice2text_Note; + final String Voice2text_Url; + final String Voice2text_Text; + final String Voice2text_User_ID; + final String Voice2text_JsonData; + final String Voice2text_Created; + final String Voice2text_Updated; + final String Voice2text_IsActive; + + VoiceToTextModel({ + required this.Voice2text_ID, + required this.Voice2text_Note, + required this.Voice2text_Url, + required this.Voice2text_Text, + required this.Voice2text_User_ID, + required this.Voice2text_JsonData, + required this.Voice2text_Created, + required this.Voice2text_Updated, + required this.Voice2text_IsActive, + }); + + factory VoiceToTextModel.fromJson(Map json) { + return VoiceToTextModel( + Voice2text_ID: json['Voice2text_ID'] ?? "", + Voice2text_Note: json['Voice2text_Note'] ?? "", + Voice2text_Url: json['Voice2text_Url'] ?? "", + Voice2text_Text: json['Voice2text_Text'] ?? "", + Voice2text_User_ID: json['Voice2text_User_ID'] ?? "", + Voice2text_JsonData: json['Voice2text_JsonData'] ?? "", + Voice2text_Created: json['Voice2text_Created'] ?? "", + Voice2text_Updated: json['Voice2text_Updated'] ?? "", + Voice2text_IsActive: json['Voice2text_IsActive'] ?? "", + ); + } + + Map toJson() { + return { + "Voice2text_ID": Voice2text_ID, + "Voice2text_Note": Voice2text_Note, + "Voice2text_Url": Voice2text_Url, + "Voice2text_Text": Voice2text_Text, + "Voice2text_User_ID": Voice2text_User_ID, + "Voice2text_JsonData": Voice2text_JsonData, + "Voice2text_Created": Voice2text_Created, + "Voice2text_Updated": Voice2text_Updated, + "Voice2text_IsActive": Voice2text_IsActive, + }; + } +} diff --git a/lib/provider/voice_to_text_provider.dart b/lib/provider/voice_to_text_provider.dart new file mode 100644 index 0000000..3b2a34b --- /dev/null +++ b/lib/provider/voice_to_text_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../model/edit_voice_to_text_model.dart'; +import '../../model/voice_to_text_model.dart'; + +final listRekamanRwt = StateProvider>( + (ref) => List.empty(growable: true), +); + +final selectedVoiceIdx = StateProvider((ref) => "0"); +final selectedEdit = StateProvider( + (ref) => EditVoiceToTextModel( + Voice2text_ID: "", + Voice2text_Note: "", + Voice2text_Url: "", + Voice2text_Created: "", + Voice2text_IsActive: "", + Voice2text_JsonData: "", + Voice2text_Text: "", + Voice2text_Updated: "", + Voice2text_User_ID: "", + ), +); \ No newline at end of file diff --git a/lib/repository/voice_to_text_repository.dart b/lib/repository/voice_to_text_repository.dart new file mode 100644 index 0000000..ba19bf7 --- /dev/null +++ b/lib/repository/voice_to_text_repository.dart @@ -0,0 +1,23 @@ +import '../model/voice_to_text_model.dart'; +import 'base_repository.dart'; + +class VoiceToTextRepository extends BaseRepository { + VoiceToTextRepository({required super.dio}); + + Future> listRiwayatRekamanRepo({ + required String host, + required String userId, + }) async { + // final service = "${Constant.baseUrl}xauth/login"; + 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); + resp['data'].forEach((e) { + final model = VoiceToTextModel.fromJson(e); + result.add(model); + }); + + return result; + } +} \ No newline at end of file diff --git a/lib/screen/home/card_riwayat_rekaman.dart b/lib/screen/home/card_riwayat_rekaman.dart new file mode 100644 index 0000000..8c05588 --- /dev/null +++ b/lib/screen/home/card_riwayat_rekaman.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../model/edit_voice_to_text_model.dart'; +import '../../model/voice_to_text_model.dart'; +import '../../app/app_extension.dart'; + +import '../../app/constant.dart'; +import '../../provider/voice_to_text_provider.dart'; + +class CardRiwayatRekaman extends HookConsumerWidget { + final VoiceToTextModel data; + const CardRiwayatRekaman({ + super.key, + required this.data, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ + SystemUiOverlay.bottom, + ]); + + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus!.unfocus(); + }, + child: Card( + elevation: 2, + color: Constant.textWhite, + child: Padding( + padding: EdgeInsets.all( + Constant.getActualYPhone(context: context, y: 12), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + formatDateJiffyDMYHIS( + data.Voice2text_Created, + ), + style: Constant.titleInputan600(context: context).copyWith( + color: Constant.textBlack, + ), + ), + InkWell( + onTap: () { + // print('id : ${data.personID}'); + ref.read(selectedVoiceIdx.notifier).state = + data.Voice2text_ID; + + ref.read(selectedEdit.notifier).state = EditVoiceToTextModel( + Voice2text_ID: data.Voice2text_ID, + Voice2text_Note: data.Voice2text_Note, + Voice2text_Url: data.Voice2text_Url, + Voice2text_Created: data.Voice2text_Created, + Voice2text_IsActive: data.Voice2text_IsActive, + Voice2text_JsonData: data.Voice2text_JsonData, + Voice2text_Text: data.Voice2text_Text, + Voice2text_Updated: data.Voice2text_Updated, + Voice2text_User_ID: data.Voice2text_User_ID, + ); + // Navigator.of(context).pushNamed( + // editRekamanRoute, + // ); + }, + child: Icon( + Icons.edit, + color: Constant.bgButton, + size: 20, + ), + ), + ], + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 12, + ), + ), + // row note + Row( + children: [ + Icon( + Icons.note_outlined, + color: Constant.bgIcon, + size: 20, + ), + SizedBox( + width: Constant.getActualXPhone( + context: context, + x: 8, + ), + ), + Text( + data.Voice2text_Note, + style: Constant.cardText(context: context).copyWith( + color: Constant.textCardGrey, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen/home/home_screen.dart b/lib/screen/home/home_screen.dart index ab34012..8d93372 100644 --- a/lib/screen/home/home_screen.dart +++ b/lib/screen/home/home_screen.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import '../../screen/home/card_riwayat_rekaman.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:toastification/toastification.dart'; import '../../app/constant.dart'; import '../../app/route.dart'; import '../../provider/current_user_provider.dart'; +import '../../provider/voice_to_text_provider.dart'; +import 'list_riwayat_rekaman_provider.dart'; class HomeScreen extends HookConsumerWidget { const HomeScreen({super.key}); @@ -17,10 +21,10 @@ class HomeScreen extends HookConsumerWidget { ]); final currentUser = ref.watch(currentUserProvider); - // final username = currentUser?.model.username ?? "-"; final host = currentUser?.host ?? ""; final isLoading = useState(false); final userId = currentUser?.model.userId ?? ""; + final listRekamanArr = ref.watch(listRekamanRwt); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { @@ -35,7 +39,60 @@ class HomeScreen extends HookConsumerWidget { return () {}; }, [currentUser]); + 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, + ); + } else if (typeToast == "error") { + toastification.show( + context: context, + title: Text(title), + description: Text(message), + autoCloseDuration: waktu, + type: ToastificationType.error, + style: ToastificationStyle.fillColored, + ); + } + } + // listRiwayProvider + ref.listen(listRiwayatRekamanProvider, (prev, next) { + if (next is ListRiwayatRekamanStateLoading) { + isLoading.value = true; + } else if (next is ListRiwayatRekamanStateError) { + isLoading.value = false; + // errorMessage.value = next.message; + showLongToast( + 'Error', + next.message, + 'error', + Duration(seconds: 3), + ); + } else if (next is ListRiwayatRekamanStateDone) { + isLoading.value = false; + } + }); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timestap) async { + ref.read(listRiwayatRekamanProvider.notifier).listRiwayatRekaman( + host: host, + userId: userId, + ); + }); + return () {}; + }, []); + return GestureDetector( onTap: () { FocusManager.instance.primaryFocus!.unfocus(); @@ -57,7 +114,192 @@ class HomeScreen extends HookConsumerWidget { y: 34, ), ), - Text(currentUser?.model.username ?? ""), + // rekam audio baru + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: SizedBox( + width: double.infinity, + height: Constant.getActualYPhone( + context: context, + y: 48, + ), + child: ElevatedButton( + onPressed: () { + // Navigator.of(context).pushNamed(scanRoute); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Constant.bgButton, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 8, + shadowColor: Constant.bgButton.withOpacity(0.24), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.mic, + size: Constant.getActualXPhone( + context: context, + x: 20, + ), + color: Constant.textWhite, + ), + SizedBox( + width: Constant.getActualXPhone( + context: context, + x: 10, + ), + ), + Text( + 'REKAM AUDIO BARU', + style: + Constant.titleButton500(context: context).copyWith( + color: Constant.textWhite, + ), + ), + ], + ), + ), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 56, + ), + ), + // judul + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Riwayat', + style: Constant.titleRiwayat(context: context).copyWith( + color: Constant.textBlack, + ), + ), + Text( + '20 Audio Terakhir', + style: + Constant.titleInputan500(context: context).copyWith( + color: Constant.inputanGrey, + ), + ), + ], + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 24, + ), + ), + ], + ), + ), + // loading + if (isLoading.value) + Expanded( + child: Center( + child: CircularProgressIndicator( + color: Constant.bgButton, + ), + ), + ) + // belum ada riwayat + else if (listRekamanArr.isEmpty) + Expanded( + child: RefreshIndicator( + color: Constant.bgButton, + onRefresh: () async { + ref + .read(listRiwayatRekamanProvider.notifier) + .listRiwayatRekaman( + host: host, + userId: userId, + ); + }, + child: ListView( + physics: + AlwaysScrollableScrollPhysics(), // Agar bisa di-refresh meski kosong + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * + 0.3), // Atur tinggi agar teks di tengah + Align( + alignment: Alignment.center, + child: Text( + 'Belum Ada Audio', + style: Constant.titleInputan500(context: context), + ), + ), + ], + ), + ), + ) + + // list card + else + Expanded( + child: RefreshIndicator( + color: Constant.bgButton, + onRefresh: () async { + ref + .read(listRiwayatRekamanProvider.notifier) + .listRiwayatRekaman( + host: host, + userId: userId, + ); + }, + child: Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone(context: context, x: 13), + right: Constant.getActualXPhone(context: context, x: 13), + bottom: Constant.getActualYPhone(context: context, y: 20), + ), + child: ListView.builder( + physics: AlwaysScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: listRekamanArr.length, + itemBuilder: (context, i) { + final obj = listRekamanArr[i]; + return Padding( + padding: EdgeInsets.only( + top: Constant.getActualYPhone( + context: context, y: 15), + ), + child: CardRiwayatRekaman( + data: obj, + ), + ); + }, + ), + ), + ), + ), ], ), ), diff --git a/lib/screen/home/list_riwayat_rekaman_provider.dart b/lib/screen/home/list_riwayat_rekaman_provider.dart new file mode 100644 index 0000000..289c49f --- /dev/null +++ b/lib/screen/home/list_riwayat_rekaman_provider.dart @@ -0,0 +1,70 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttervoice2text/model/voice_to_text_model.dart'; +import '../../provider/dio_provider.dart'; +import '../../provider/voice_to_text_provider.dart'; +import '../../repository/base_repository.dart'; +import '../../repository/voice_to_text_repository.dart'; + +// 3. state provider +final listRiwayatRekamanProvider = + StateNotifierProvider( + (ref) => ListRiwayatRekamanNotifier(ref: ref)); + +// 2. notifier +class ListRiwayatRekamanNotifier extends StateNotifier { + final Ref ref; + ListRiwayatRekamanNotifier({required this.ref}) + : super(ListRiwayatRekamanStateInit()); + void listRiwayatRekaman({ + required String host, + required String userId, + }) async { + try { + state = ListRiwayatRekamanStateLoading(); + final resp = await VoiceToTextRepository( + dio: ref.read(dioProvider), + ).listRiwayatRekamanRepo(host: host, userId: userId); + + // print(resp); + state = ListRiwayatRekamanStateDone(model: resp); + ref.read(listRekamanRwt.notifier).state = resp; + } catch (e) { + if (e is BaseRepositoryException) { + state = ListRiwayatRekamanStateError(message: e.message); + } else { + state = ListRiwayatRekamanStateError(message: e.toString()); + } + } + } +} + +// 1. state +abstract class ListRiwayatRekamanState extends Equatable { + final DateTime date; + const ListRiwayatRekamanState(this.date); + @override + List get props => [date]; +} + +class ListRiwayatRekamanStateInit extends ListRiwayatRekamanState { + ListRiwayatRekamanStateInit() : super(DateTime.now()); +} + +class ListRiwayatRekamanStateLoading extends ListRiwayatRekamanState { + ListRiwayatRekamanStateLoading() : super(DateTime.now()); +} + +class ListRiwayatRekamanStateError extends ListRiwayatRekamanState { + final String message; + ListRiwayatRekamanStateError({ + required this.message, + }) : super(DateTime.now()); +} + +class ListRiwayatRekamanStateDone extends ListRiwayatRekamanState { + final List model; + ListRiwayatRekamanStateDone({ + required this.model, + }) : super(DateTime.now()); +} \ No newline at end of file diff --git a/lib/screen/login/login_screen.dart b/lib/screen/login/login_screen.dart index e22a1d5..9fe7778 100644 --- a/lib/screen/login/login_screen.dart +++ b/lib/screen/login/login_screen.dart @@ -33,8 +33,6 @@ class LoginScreen extends HookConsumerWidget { final isLoading = useState(false); final isSuccess = useState(false); - ToastificationItem? toastItem; - void showLongToast( String title, String message, @@ -42,7 +40,7 @@ class LoginScreen extends HookConsumerWidget { Duration waktu, ) { if (typeToast == "success") { - toastItem = toastification.show( + toastification.show( context: context, title: Text(title), description: Text(message), @@ -50,7 +48,7 @@ class LoginScreen extends HookConsumerWidget { type: ToastificationType.success, ); } else if (typeToast == "error") { - toastItem = toastification.show( + toastification.show( context: context, title: Text(title), description: Text(message), @@ -61,12 +59,6 @@ class LoginScreen extends HookConsumerWidget { } } - void hideToast() { - if (toastItem != null) { - toastification.dismissAll(); - } - } - // proses login ref.listen(loginProvider, (prev, next) { if (next is LoginStateLoading) { @@ -81,7 +73,6 @@ class LoginScreen extends HookConsumerWidget { Duration(seconds: 3), ); } else if (next is LoginStateDone) { - hideToast(); isLoading.value = false; isSuccess.value = true; ref.read(currentUserProvider.notifier).state = next.model;