debug: input file

This commit is contained in:
Golek 2022-12-07 13:41:00 +07:00
parent c63282dfba
commit 521c54dfa8
11 changed files with 220 additions and 123 deletions

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion 33
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion 33
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {

View File

@ -59,7 +59,24 @@ List<Story> getStories(GolekThemeData theme) {
allowMultiple: k.boolean(label: 'allowMultiple', initial: false), allowMultiple: k.boolean(label: 'allowMultiple', initial: false),
onChange: (files) {}, onChange: (files) {},
files: [], files: [],
inputLabel: '', isTouched: k.boolean(label: 'isTouched', initial: false),
isError: k.boolean(label: 'isError', initial: false),
errorMessage:
k.text(label: 'messageError', initial: 'Ini pesan message error'),
uploadButtonLabel: k.text(
label: 'uploadButtonLabel',
initial: 'Upload Foto\u{002A}',
),
descriptionLabel: k.text(
label: 'descriptionLabel',
initial: 'Maks. 1 Foto, 1 Foto Maks. 2 mb',
),
guidanceLabel: k.text(
label: 'guidanceLabel',
initial:
'Tingkatkan kepercayaan partner bisnis dengan mengunggah foto tentang usaha'
' anda (Contoh foto kantor, armada karyawan dll.)',
),
), ),
), ),
]; ];

View File

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:mime/mime.dart';
import 'package:component_library/component_library.dart'; import 'package:component_library/component_library.dart';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@ -16,20 +16,24 @@ class InputFile extends StatefulWidget {
required this.onChange, required this.onChange,
this.allowMultiple = false, this.allowMultiple = false,
this.allowedExtensions = const ['jpg', 'jpeg', 'png', 'gif'], this.allowedExtensions = const ['jpg', 'jpeg', 'png', 'gif'],
required this.inputLabel, this.uploadButtonLabel,
this.guidanceLabel,
this.descriptionLabel,
this.isTouched = false, this.isTouched = false,
this.isError = false, this.isError = false,
this.messageError, this.errorMessage,
}) : super(key: key); }) : super(key: key);
final List<File> files; final List<File> files;
final void Function(List<File> files) onChange; final void Function(List<File> files) onChange;
final bool? allowMultiple; final bool? allowMultiple;
final List<String>? allowedExtensions; final List<String>? allowedExtensions;
final String inputLabel; final String? uploadButtonLabel;
final String? guidanceLabel;
final String? descriptionLabel;
final bool? isTouched; final bool? isTouched;
final bool? isError; final bool? isError;
final String? messageError; final String? errorMessage;
@override @override
State<InputFile> createState() => _InputFileState(); State<InputFile> createState() => _InputFileState();
@ -46,9 +50,8 @@ class _InputFileState extends State<InputFile> {
} }
void removeFile(File file) { void removeFile(File file) {
final filtered = files.skipWhile((f) => f.hashCode == file.hashCode); files.removeAt(files.indexOf(file));
controller.sink.add(filtered.toList()); controller.sink.add(files);
files = filtered.toList();
} }
void close() { void close() {
@ -117,6 +120,9 @@ class _InputFileState extends State<InputFile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = GolekTheme.of(context); final theme = GolekTheme.of(context);
print("INPUT FILE");
print(widget.isError!);
print(widget.isTouched!);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -132,7 +138,7 @@ class _InputFileState extends State<InputFile> {
child: IntrinsicHeight( child: IntrinsicHeight(
child: DashedRect( child: DashedRect(
color: widget.isError! && widget.isTouched! color: widget.isError! && widget.isTouched!
? theme.dangerMainColor // jika error dan touched ? theme.dangerMainColor
: theme.neutral50Color, : theme.neutral50Color,
strokeWidth: 2.0, strokeWidth: 2.0,
gap: 3.0, gap: 3.0,
@ -141,7 +147,11 @@ class _InputFileState extends State<InputFile> {
children: [ children: [
const SizedBox(height: 21), const SizedBox(height: 21),
OutlinedButton( OutlinedButton(
onPressed: () => showModalFileToPick( onPressed: () {
setState(() {
});
showModalFileToPick(
context: context, context: context,
onPickCamera: (context) async { onPickCamera: (context) async {
await pickFileFromCamera(context); await pickFileFromCamera(context);
@ -151,7 +161,8 @@ class _InputFileState extends State<InputFile> {
await pickFileFromGallery(context); await pickFileFromGallery(context);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), );
},
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: BorderSide( side: BorderSide(
width: 1, width: 1,
@ -163,14 +174,13 @@ class _InputFileState extends State<InputFile> {
), ),
), ),
child: Text( child: Text(
'Upload Foto\u{002A}', widget.uploadButtonLabel ?? 'Upload Foto\u{002A}',
style: theme.textStyle.copyWith( style: theme.textStyle.copyWith(
fontSize: FontSize.style14, fontSize: FontSize.style14,
color: theme.primaryMainColor, color: theme.primaryMainColor,
), ),
), ),
), ),
const SizedBox(height: 10),
StreamBuilder( StreamBuilder(
stream: stream, stream: stream,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -182,13 +192,26 @@ class _InputFileState extends State<InputFile> {
); );
}, },
), ),
const SizedBox(height: 5),
Text( Text(
'Maks. 1 Foto, 1 Foto Maks. 2 mb', widget.guidanceLabel ?? 'Maks. 1 Foto, 1 Foto Maks. 2 mb',
style: theme.textStyle.copyWith( style: theme.textStyle.copyWith(
fontSize: FontSize.style14, fontSize: FontSize.style14,
color: theme.primaryMainColor, color: theme.primaryMainColor,
), ),
), ),
const SizedBox(height: 2),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Text(
widget.descriptionLabel ?? '',
textAlign: TextAlign.center,
style: theme.textStyle.copyWith(
fontSize: FontSize.style10,
color: theme.neutral70Color,
),
),
),
const SizedBox(height: 38), const SizedBox(height: 38),
], ],
), ),
@ -196,8 +219,8 @@ class _InputFileState extends State<InputFile> {
), ),
), ),
ErrorMessageBuilder( ErrorMessageBuilder(
inputLabel: widget.inputLabel, inputLabel: '', // ini nanti di custom di reactive component
messageError: widget.messageError ?? '', messageError: widget.errorMessage ?? '',
) )
], ],
); );
@ -226,30 +249,13 @@ class ListFilesBuilder extends StatelessWidget {
spacing: 4, spacing: 4,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: List.generate(files.length, (index) { children: List.generate(files.length, (index) {
var file = files[index]; final file = files[index];
final mimeType = lookupMimeType(file.path);
final isImage = mimeType?.startsWith('image/') == true;
return Column(children: [ return Column(children: [
Visibility( const SizedBox(height: 10),
visible: multiple == false, if (multiple == false)
child: Container(
margin: const EdgeInsets.all(10),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: ExtendedImage.file(
file,
height: 105,
width: 150,
enableMemoryCache: false,
clearMemoryCacheIfFailed: true,
clearMemoryCacheWhenDispose: true,
),
),
),
),
Visibility(
visible: multiple == true,
child: Stack(
children: [
Container( Container(
margin: const EdgeInsets.all(10), margin: const EdgeInsets.all(10),
child: ClipRRect( child: ClipRRect(
@ -264,6 +270,33 @@ class ListFilesBuilder extends StatelessWidget {
), ),
), ),
), ),
if (multiple == true)
Stack(
children: [
if (isImage)
Container(
margin: const EdgeInsets.all(10),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: ExtendedImage.file(
file,
height: 105,
width: 150,
enableMemoryCache: false,
clearMemoryCacheIfFailed: true,
clearMemoryCacheWhenDispose: true,
),
),
),
if (!isImage)
Container(
margin: const EdgeInsets.all(10),
child: Icon(
Icons.check_circle,
color: theme.primaryMainColor,
size: 50,
),
),
GestureDetector( GestureDetector(
onTap: () => onRemoveFile(file), onTap: () => onRemoveFile(file),
child: Container( child: Container(
@ -279,7 +312,6 @@ class ListFilesBuilder extends StatelessWidget {
) )
], ],
), ),
),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text( child: Text(

View File

@ -1,5 +1,9 @@
import 'dart:io';
import 'package:component_library/component_library.dart'; import 'package:component_library/component_library.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:form_fields/form_fields.dart';
import 'package:reactive_forms/reactive_forms.dart';
import 'landing_page/landing_app_bar.dart'; import 'landing_page/landing_app_bar.dart';
class LandingPage extends StatefulWidget { class LandingPage extends StatefulWidget {
@ -27,90 +31,48 @@ class _LandingPageState extends State<LandingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = GolekTheme.of(context); final theme = GolekTheme.of(context);
final form = FormGroup({
'foto': FormControl<List<File>>(
value: [],
validators: [requiredFile],
),
});
return Scaffold( return Scaffold(
appBar: LandingAppBar(), appBar: LandingAppBar(),
body: SafeArea( body: SafeArea(
child: Stack( child: ReactiveForm(
children: [ formGroup: form,
Container(
width: double.infinity,
height: 497,
decoration: BoxDecoration(
color: theme.primaryMainColor,
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(25),
),
),
),
Container(
padding: const EdgeInsets.only(
right: 20,
left: 20,
bottom: 80,
),
width: double.infinity,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Image.asset( ReactiveValueListenableBuilder(
'assets/trucker_shipper.png', formControlName: 'foto',
package: 'features/landing_menu', builder: (context, control, child) {
width: 300, print('DEBUG DI VALUE LISTEN');
height: 200, print(control.value);
print(control.touched);
print(control.errors.entries);
return Container();
},
), ),
const SizedBox(height: 12), ReactiveInputFile(
Text( name: 'foto',
'Cari, Muat, Jalan.\nPraktis kan ?', allowMultiple: true,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: theme.textStyle.copyWith(
fontSize: FontSize.style24,
color: theme.neutral10Color,
fontWeight: FontWeight.w600,
letterSpacing: 1,
),
),
const SizedBox(height: 21),
GestureDetector(
onTap: () async {},
child: Text(
'GolekTruk memberikan kebebasan\nberlogistik menggunakan budaya\nperusahaan sendiri.',
textAlign: TextAlign.center,
style: theme.textStyle.copyWith(
fontSize: FontSize.style14,
color: theme.neutral10Color,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 21),
Row(
children: [
CardProduct(
onClickInfo: () => {},
label: 'Cari Truk',
description: 'Pengirim Muatan',
assets: 'assets/box_shipper.png',
onTap: () {},
),
const SizedBox(width: 14),
CardProduct(
onClickInfo: () => {},
label: 'Cari Muatan',
description: 'Penyedia Angkutan',
assets: 'assets/truk_trucker.png',
onTap: () async {},
), ),
ElevatedButton(
onPressed: () {
form.markAllAsTouched();
print(form.control('foto').hasErrors);
print(form.control('foto').errors);
print(form.control('foto').touched);
if (form.valid){
print("FORM IS VALID");
}
},
child: const Text('Submit'),
)
], ],
), ),
],
),
),
],
), ),
), ),
); );

View File

@ -1 +1,3 @@
export 'src/reactive_input_field.dart'; export 'src/reactive_input_field.dart';
export 'src/reactive_input_file.dart';
export 'src/validators/required_file.dart';

View File

@ -16,10 +16,15 @@ class ErrorMessages {
...messages, ...messages,
}; };
final message = mergedErrorMessages.entries.firstWhere( MapEntry<String, dynamic> message = const MapEntry('', '');
print('GET UI ERROR ');
print(control.errors);
if (control.errors.entries.isNotEmpty) {
message = mergedErrorMessages.entries.firstWhere(
(element) => element.key == control.errors.entries.first.key, (element) => element.key == control.errors.entries.first.key,
orElse: () => const MapEntry('', ''), orElse: () => const MapEntry('', ''),
); );
}
return message.value.toString().replaceFirst('{_field_}', label); return message.value.toString().replaceFirst('{_field_}', label);
} }

View File

@ -0,0 +1,51 @@
import 'dart:io';
import 'package:component_library/component_library.dart';
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';
import 'error_messages.dart';
class ReactiveInputFile extends ReactiveFormField<List<File>, List<File>> {
ReactiveInputFile({
required String name,
allowMultiple = false,
allowedExtensions = const ['jpg', 'jpeg', 'png', 'gif'],
uploadButtonLabel,
guidanceLabel,
descriptionLabel,
}) : super(
formControlName: name,
builder: (field) {
return InputFile(
files: field.value ?? [],
allowMultiple: allowMultiple,
descriptionLabel: descriptionLabel,
guidanceLabel: guidanceLabel,
uploadButtonLabel: uploadButtonLabel,
allowedExtensions: allowedExtensions,
errorMessage: ErrorMessages.getUiErrorMessage(
control: field.control,
label: '',
widgetCustomMessages: {
'requiredFile': 'Foto harus diisi'
}
),
isTouched: field.control.touched,
isError: field.control.hasErrors,
onChange: (files) {
print('ON CAHNEF');
print(field.control.touched);
print(field.control.hasErrors);
field.didChange(files);
},
);
},
);
@override
ReactiveFormFieldState<List<File>, List<File>> createState() {
return ReactiveFormFieldState<List<File>, List<File>>();
}
}

View File

@ -0,0 +1,7 @@
import 'package:reactive_forms/reactive_forms.dart';
Map<String, dynamic>? requiredFile(AbstractControl<dynamic> control) {
return control.isNotNull && control.value?.isNotEmpty == true
? null
: {'requiredFile': true};
}

View File

@ -0,0 +1,21 @@
import 'package:reactive_forms/reactive_forms.dart';
ValidatorFunction _mustMatch(String controlName, String matchingControlName) {
return (AbstractControl<dynamic> control) {
final form = control as FormGroup;
final formControl = form.control(controlName);
final matchingFormControl = form.control(matchingControlName);
if (formControl.value != matchingFormControl.value) {
matchingFormControl.setErrors({'mustMatch': true});
// force messages to show up as soon as possible
matchingFormControl.markAsTouched();
} else {
matchingFormControl.removeError('mustMatch');
}
return null;
};
}