reusable_editor 1.3.4
reusable_editor: ^1.3.4 copied to clipboard
A modular form and file editor package for Flutter with support for Form widgets. Includes Cubit-based state management, form field validation, and UI input widgets.
example/main.dart
import 'dart:io';
import 'package:cross_file/cross_file.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:i_validator/i_validator.dart';
import 'package:intl/intl.dart';
import 'package:reusable_editor/reusable_editor.dart';
void main() {
runApp(const MyApp());
}
/// Define a sample Enum for the multi-select demo
enum UserRole { admin, editor, viewer, guest }
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Reusable Editor Example',
home: FormEditorDemo(),
);
}
}
class FormEditorDemo extends StatefulWidget {
const FormEditorDemo({super.key});
@override
State<FormEditorDemo> createState() => _FormEditorDemoState();
}
class _FormEditorDemoState extends State<FormEditorDemo> {
/// --- Cubit Instances ---
final EnumOptionCubit<FileDataSourceType> _sourceCubit = EnumOptionCubit();
/// Multi-select Cubit
late final MultiEnumOptionCubit<UserRole> _rolesCubit;
final FieldCubit<String> _genderCubit = FieldCubit<String>(
initialValue: 'Male',
validator: RequiredFieldValidator().call,
);
final ImageFieldCubit _imageCubit = ImageFieldCubit();
final ListImageFieldCubit _listImageCubit = ListImageFieldCubit();
final AudioFieldCubit _audioCubit = AudioFieldCubit();
final VideoFieldCubit _videoCubit = VideoFieldCubit();
final DocFieldCubit _docCubit = DocFieldCubit();
final ToggleCubit _checkboxCubit = ToggleCubit(
initialValue: false,
validator: (val) => val == true ? null : 'You must accept the terms.',
);
final TextFieldCubit _textFieldCubit = TextFieldCubit(
initialValue: '',
validator: MinLengthValidator(3).call,
);
final FieldCubit<DateTime> _datePickerCubit = FieldCubit<DateTime>(
validator: RequiredFieldValidator().call,
);
final FieldCubit<TimeOfDay> _timePickerCubit = FieldCubit<TimeOfDay>(
validator: RequiredFieldValidator().call,
);
final FieldCubit<double> _sliderCubit = FieldCubit<double>(initialValue: 0.5);
final ToggleCubit _switchCubit = ToggleCubit(
initialValue: false,
validator: (val) => val == true ? null : 'Feature must be enabled.',
);
@override
void initState() {
super.initState();
/// MultiEnumOptionCubit no longer needs options
_rolesCubit = MultiEnumOptionCubit<UserRole>();
}
@override
void dispose() {
_checkboxCubit.close();
_textFieldCubit.close();
_datePickerCubit.close();
_timePickerCubit.close();
_sliderCubit.close();
_switchCubit.close();
_sourceCubit.close();
_imageCubit.close();
_listImageCubit.close();
_audioCubit.close();
_videoCubit.close();
_docCubit.close();
_rolesCubit.close();
_genderCubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final dropdownItems = [
EnumOptionEntity(
type: FileDataSourceType.fireStorage,
icon: Icons.cloud,
label: 'Firebase',
),
EnumOptionEntity(
type: FileDataSourceType.server,
icon: Icons.storage,
label: 'Server',
),
];
final roleOptions = [
EnumOptionEntity(
type: UserRole.admin,
label: 'Administrator',
icon: Icons.security,
),
EnumOptionEntity(
type: UserRole.editor,
label: 'Content Editor',
icon: Icons.edit,
),
EnumOptionEntity(
type: UserRole.viewer,
label: 'Viewer Only',
icon: Icons.visibility,
),
EnumOptionEntity(
type: UserRole.guest,
label: 'Guest',
icon: Icons.person_outline,
),
];
return Scaffold(
appBar: AppBar(title: const Text('Reusable Editor Demo')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Single Selection (Dropdown)',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
EnumOptionDropDownMenuFormField<FileDataSourceType>(
selectedValue: _sourceCubit.state.selectedOption,
onChanged: _sourceCubit.selectOption,
hint: 'Choose data source',
dropdownItems: dropdownItems,
),
const SizedBox(height: 24),
Text(
'Multi-Selection (Checkboxes)',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: EnumMultiOptionCheckboxGroup<UserRole>(
cubit: _rolesCubit,
options: roleOptions,
label: 'User Roles',
),
),
),
const SizedBox(height: 24),
Text(
'Image Picker',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Wrap(
spacing: 10,
children: [
ElevatedButton.icon(
onPressed: () async {
final XFile? pickedFile = await 'assets/sample.png'
.loadAsXFile();
if (pickedFile != null) {
_imageCubit.selectImage(pickedFile);
}
},
icon: const Icon(Icons.image),
label: const Text('Single Pick'),
),
ElevatedButton.icon(
onPressed: () async {
// Simulating multiple file selection
final results = await Future.wait([
'assets/sample.png'.loadAsXFile(),
'assets/sample.png'.loadAsXFile(),
]);
final pickedFiles = results.whereType<XFile>().toList();
_listImageCubit.selectImages(pickedFiles);
},
icon: const Icon(Icons.photo_library),
label: const Text('Multi Pick'),
),
ElevatedButton.icon(
onPressed: () async {
final XFile? pickedFile = await 'assets/sample.mp3'
.loadAsXFile();
if (pickedFile != null) {
_audioCubit.selectAudio(pickedFile);
}
},
icon: const Icon(Icons.audiotrack),
label: const Text('Audio Pick'),
),
ElevatedButton.icon(
onPressed: () async {
final XFile? pickedFile = await 'assets/sample.mp4'
.loadAsXFile();
if (pickedFile != null) {
_videoCubit.selectVideo(pickedFile);
}
},
icon: const Icon(Icons.video_collection),
label: const Text('Video Pick'),
),
ElevatedButton.icon(
onPressed: () async {
final XFile? pickedFile = await 'assets/sample.pdf'
.loadAsXFile();
if (pickedFile != null) {
_docCubit.selectDoc(pickedFile);
}
},
icon: const Icon(Icons.description),
label: const Text('Doc Pick'),
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 20,
runSpacing: 20,
children: [
_buildUploadPreview(
cubit: _imageCubit,
label: 'Image',
builder: (state) {
final XFile? pickedFile = state.pickedFile;
if (pickedFile != null) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(pickedFile.path),
height: 80,
width: 80,
fit: BoxFit.cover,
),
);
}
return const SizedBox(
width: 80,
height: 80,
child: Center(child: Text('No image')),
);
},
),
_buildUploadPreview(
cubit: _audioCubit,
label: 'Audio',
builder: (state) {
return Icon(
state.pickedFile != null
? Icons.music_note
: Icons.mic_off,
size: 80,
color: state.pickedFile != null
? Colors.blue
: Colors.grey,
);
},
),
_buildUploadPreview(
cubit: _videoCubit,
label: 'Video',
builder: (state) {
return Icon(
state.pickedFile != null
? Icons.movie
: Icons.videocam_off,
size: 80,
color: state.pickedFile != null
? Colors.red
: Colors.grey,
);
},
),
_buildUploadPreview(
cubit: _docCubit,
label: 'Document',
builder: (state) {
return Icon(
state.pickedFile != null
? Icons.insert_drive_file
: Icons.file_present,
size: 80,
color: state.pickedFile != null
? Colors.green
: Colors.grey,
);
},
),
],
),
const SizedBox(height: 16),
BlocBuilder<ListImageFieldCubit, ListImageFieldState>(
bloc: _listImageCubit,
builder: (context, state) {
final pickedFiles = state.pickedFiles;
if (pickedFiles.isNotEmpty) {
return Column(
children: [
Wrap(
spacing: 4,
children: pickedFiles
.map(
(file) => ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.file(
File(file.path),
height: 40,
width: 40,
fit: BoxFit.cover,
),
),
)
.toList(),
),
const Text(
'Multi Images',
style: TextStyle(fontSize: 10),
),
],
);
}
return const Text('No multi images');
},
),
const Divider(height: 40),
Text(
'Standard Form Fields',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
AppRadioGroup<String>(
cubit: _genderCubit,
label: 'Select Gender',
values: const ['Male', 'Female', 'Other'],
labels: const ['Male', 'Female', 'Other'],
activeColor: Colors.deepPurple,
),
const SizedBox(height: 16),
AppCheckbox(
cubit: _checkboxCubit,
label: 'Accept Terms and Conditions',
),
const SizedBox(height: 16),
AppTextField(
bloc: _textFieldCubit,
label: 'Name',
hintText: 'Enter your name',
),
const SizedBox(height: 16),
AppDatePicker(
cubit: _datePickerCubit,
labelText: 'Select Date',
placeholderText: 'Tap to pick a date',
formatter: DateFormat('yyyy-MM-dd'),
),
const SizedBox(height: 16),
AppTimePicker(
cubit: _timePickerCubit,
labelText: 'Select Time',
hintText: 'Tap to pick a time',
),
const SizedBox(height: 16),
AppSlider(
cubit: _sliderCubit,
min: 0.0,
max: 1.0,
divisions: 10,
labelText: 'Intensity Level',
displayValue: (val) => '${(val * 100).toInt()}%',
),
const SizedBox(height: 16),
AppSwitch(cubit: _switchCubit, label: 'Enable Notifications'),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _validateForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
child: const Text('Validate & Submit'),
),
),
const SizedBox(height: 40),
],
),
),
);
}
void _validateForm() {
final isValid = [
_genderCubit.validate(),
_checkboxCubit.validate(),
_textFieldCubit.validate(),
_datePickerCubit.validate(),
_timePickerCubit.validate(),
_switchCubit.validate(),
_imageCubit.validate(),
_audioCubit.validate(),
_videoCubit.validate(),
_docCubit.validate(),
].every((error) => error == null);
final selectedRoles = _rolesCubit.selectedTypes
.map((e) => e.name)
.join(', ');
final message = isValid
? 'Valid! Gender: ${_genderCubit.state.value}, Roles: ${selectedRoles.isEmpty ? 'None' : selectedRoles}'
: 'Please correct the highlighted errors.';
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
Widget _buildUploadPreview({
required dynamic cubit,
required String label,
required Widget Function(dynamic state) builder,
}) {
return Column(
children: [
BlocBuilder(
bloc: cubit,
builder: (context, state) {
return builder(state);
},
),
Text(label, style: const TextStyle(fontSize: 10)),
],
);
}
}