modern
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'base_field.dart';
|
||||
import '../utils/entity_field_store.dart';
|
||||
|
||||
class AudioUploadField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
AudioUploadField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Select audio file',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) {
|
||||
final items =
|
||||
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
const [];
|
||||
if (isRequired && items.isEmpty) return '$label is required';
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
<UploadItem>[];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await FilePicker.platform.pickFiles(type: FileType.audio);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
final f = result.files.single;
|
||||
final Uint8List? bytes = f.bytes ??
|
||||
(f.path != null
|
||||
? await File(f.path!).readAsBytes()
|
||||
: null);
|
||||
if (bytes != null) {
|
||||
final updated = List<UploadItem>.from(items)
|
||||
..add(UploadItem(fileName: f.name, bytes: bytes));
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.audiotrack_rounded),
|
||||
label: const Text('Add Audio'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...items.map((u) => ListTile(
|
||||
leading: const Icon(Icons.music_note_rounded),
|
||||
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () {
|
||||
final updated = List<UploadItem>.from(items)..remove(u);
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
33
base_project/lib/BuilderField/shared/fields/base_field.dart
Normal file
33
base_project/lib/BuilderField/shared/fields/base_field.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Base interface for all field types in the Entity system
|
||||
/// This allows UI to be independent of field types and enables reusability
|
||||
abstract class BaseField {
|
||||
/// Unique identifier for the field (e.g., 'name', 'phone')
|
||||
String get fieldKey;
|
||||
|
||||
/// Display label for the field (e.g., 'Name', 'Phone Number')
|
||||
String get label;
|
||||
|
||||
/// Placeholder text for the field
|
||||
String get hint;
|
||||
|
||||
/// Whether the field is required for validation
|
||||
bool get isRequired;
|
||||
|
||||
/// Validation function for the field
|
||||
String? Function(String?)? get validator;
|
||||
|
||||
/// Main method - each field type provides its own implementation
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
});
|
||||
|
||||
/// Optional: Field-specific styling properties
|
||||
Map<String, dynamic>? get customProperties => null;
|
||||
|
||||
/// Optional: Field-specific validation rules
|
||||
String? Function(String?)? get customValidator => null;
|
||||
}
|
||||
162
base_project/lib/BuilderField/shared/fields/captcha_field.dart
Normal file
162
base_project/lib/BuilderField/shared/fields/captcha_field.dart
Normal file
@@ -0,0 +1,162 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
/// Generic Captcha field: shows generated code and asks user to type it
|
||||
class CaptchaField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final int length;
|
||||
|
||||
CaptchaField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Enter CAPTCHA',
|
||||
this.isRequired = true,
|
||||
this.length = 6,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return _CaptchaFieldWidget(
|
||||
key: ValueKey(fieldKey),
|
||||
label: label,
|
||||
hint: hint,
|
||||
length: length,
|
||||
colorScheme: colorScheme,
|
||||
controller: controller,
|
||||
baseValidator: validator,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
String _generateCaptcha(int length) {
|
||||
final random = Random();
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
return String.fromCharCodes(Iterable.generate(
|
||||
length,
|
||||
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _CaptchaFieldWidget extends StatefulWidget {
|
||||
final String label;
|
||||
final String hint;
|
||||
final int length;
|
||||
final ColorScheme colorScheme;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?)? baseValidator;
|
||||
final VoidCallback? onChanged;
|
||||
|
||||
const _CaptchaFieldWidget({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
required this.length,
|
||||
required this.colorScheme,
|
||||
required this.controller,
|
||||
required this.baseValidator,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CaptchaFieldWidget> createState() => _CaptchaFieldWidgetState();
|
||||
}
|
||||
|
||||
class _CaptchaFieldWidgetState extends State<_CaptchaFieldWidget> {
|
||||
late String _captcha;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_captcha = _generateCaptcha(widget.length);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border:
|
||||
Border.all(color: widget.colorScheme.primary.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_captcha,
|
||||
style: TextStyle(
|
||||
color: widget.colorScheme.primary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 3,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Refresh',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_captcha = _generateCaptcha(widget.length);
|
||||
});
|
||||
if (widget.onChanged != null) widget.onChanged!();
|
||||
},
|
||||
icon: Icon(Icons.refresh_rounded,
|
||||
color: widget.colorScheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ModernTextField(
|
||||
controller: widget.controller,
|
||||
hint: widget.hint,
|
||||
validator: (value) {
|
||||
final base = widget.baseValidator;
|
||||
final err = base != null ? base(value) : null;
|
||||
if (err != null) return err;
|
||||
if ((value ?? '').trim() != _captcha) {
|
||||
return 'CAPTCHA does not match';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged:
|
||||
widget.onChanged != null ? (_) => widget.onChanged!() : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _generateCaptcha(int length) {
|
||||
final random = Random();
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
return String.fromCharCodes(Iterable.generate(
|
||||
length,
|
||||
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
class CheckboxField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
CheckboxField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = '',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final bool checked = controller.text.toLowerCase() == 'true';
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: checked,
|
||||
onChanged: (value) {
|
||||
controller.text = (value ?? false).toString();
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// Custom text input field implementation (preferred over legacy TextField)
|
||||
class CustomTextField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final int? maxLength;
|
||||
final TextInputType? keyboardType;
|
||||
final Map<String, dynamic>? customProperties;
|
||||
final String? Function(String?)? customValidator;
|
||||
|
||||
CustomTextField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.maxLength,
|
||||
this.keyboardType,
|
||||
this.customProperties,
|
||||
this.customValidator,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
// Custom validator (e.g., range checks)
|
||||
if (customValidator != null) {
|
||||
final err = customValidator!(value);
|
||||
if (err != null) return err;
|
||||
}
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (maxLength != null && value != null && value.length > maxLength!) {
|
||||
return '$label must be less than $maxLength characters';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final bool isPassword = customProperties?['isPassword'] == true;
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
maxLength: maxLength,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: isPassword,
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
76
base_project/lib/BuilderField/shared/fields/date_field.dart
Normal file
76
base_project/lib/BuilderField/shared/fields/date_field.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
/// Date input field implementation with native date picker
|
||||
class DateField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final DateTime? initialDate;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
DateField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.initialDate,
|
||||
DateTime? firstDate,
|
||||
DateTime? lastDate,
|
||||
}) : firstDate = firstDate ?? DateTime(2000),
|
||||
lastDate = lastDate ?? DateTime(2101);
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint.isNotEmpty ? hint : label,
|
||||
readOnly: true,
|
||||
validator: validator,
|
||||
suffixIcon: const Icon(Icons.calendar_today_rounded),
|
||||
onTap: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final now = DateTime.now();
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initialDate ?? now,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
helpText: label,
|
||||
);
|
||||
if (picked != null) {
|
||||
final String value = _formatDate(picked);
|
||||
controller.text = value;
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
String two(int n) => n < 10 ? '0$n' : '$n';
|
||||
return '${date.year}-${two(date.month)}-${two(date.day)}';
|
||||
}
|
||||
}
|
||||
|
||||
// No global navigator key required; Builder provides local context
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// DateTime input field implementation with native date & time pickers
|
||||
class DateTimeField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final DateTime? initialDateTime;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
DateTimeField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.initialDateTime,
|
||||
DateTime? firstDate,
|
||||
DateTime? lastDate,
|
||||
}) : firstDate = firstDate ?? DateTime(2000),
|
||||
lastDate = lastDate ?? DateTime(2101);
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint.isNotEmpty ? hint : label,
|
||||
readOnly: true,
|
||||
validator: validator,
|
||||
suffixIcon: const Icon(Icons.access_time_rounded),
|
||||
onTap: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final now = DateTime.now();
|
||||
final datePicked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: (initialDateTime ?? now),
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
helpText: label,
|
||||
);
|
||||
if (datePicked == null) return;
|
||||
final timePicked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(initialDateTime ?? now),
|
||||
helpText: label,
|
||||
);
|
||||
if (timePicked == null) return;
|
||||
final combined = DateTime(
|
||||
datePicked.year,
|
||||
datePicked.month,
|
||||
datePicked.day,
|
||||
timePicked.hour,
|
||||
timePicked.minute,
|
||||
);
|
||||
controller.text = _formatDateTime(combined);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime dt) {
|
||||
String two(int n) => n < 10 ? '0$n' : '$n';
|
||||
return '${dt.year}-${two(dt.month)}-${two(dt.day)} ${two(dt.hour)}:${two(dt.minute)}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'base_field.dart';
|
||||
// import '../../../Reuseable/reusable_dropdown_field.dart';
|
||||
|
||||
// /// Dropdown selection field implementation
|
||||
// class DropdownField extends BaseField {
|
||||
// final String fieldKey;
|
||||
// final String label;
|
||||
// final String hint;
|
||||
// final bool isRequired;
|
||||
// final List<Map<String, dynamic>> options;
|
||||
// final String valueKey;
|
||||
// final String displayKey;
|
||||
|
||||
// DropdownField({
|
||||
// required this.fieldKey,
|
||||
// required this.label,
|
||||
// required this.hint,
|
||||
// this.isRequired = false,
|
||||
// required this.options,
|
||||
// this.valueKey = 'id',
|
||||
// this.displayKey = 'name',
|
||||
// });
|
||||
|
||||
// @override
|
||||
// String? Function(String?)? get validator => (value) {
|
||||
// if (isRequired && (value == null || value.isEmpty)) {
|
||||
// return '$label is required';
|
||||
// }
|
||||
// return null;
|
||||
// };
|
||||
|
||||
// @override
|
||||
// Widget buildField({
|
||||
// required TextEditingController controller,
|
||||
// required ColorScheme colorScheme,
|
||||
// VoidCallback? onChanged,
|
||||
// }) {
|
||||
// return ReusableDropdownField(
|
||||
// controller: controller,
|
||||
// label: label,
|
||||
// hint: hint,
|
||||
// items: options,
|
||||
// valueKey: valueKey,
|
||||
// displayKey: displayKey,
|
||||
// onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
// validator: validator,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
48
base_project/lib/BuilderField/shared/fields/email_field.dart
Normal file
48
base_project/lib/BuilderField/shared/fields/email_field.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// Email input field implementation
|
||||
class EmailField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
EmailField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(value)) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'base_field.dart';
|
||||
import '../utils/entity_field_store.dart';
|
||||
|
||||
/// Multi-file upload field; stores files in EntityFieldStore under fieldKey
|
||||
class FileUploadField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
FileUploadField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Select files',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) {
|
||||
final items =
|
||||
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
const [];
|
||||
if (isRequired && items.isEmpty) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
<UploadItem>[];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await FilePicker.platform.pickFiles(allowMultiple: true);
|
||||
if (result != null) {
|
||||
final List<UploadItem> updated = List<UploadItem>.from(items);
|
||||
for (final f in result.files) {
|
||||
final Uint8List? bytes = f.bytes;
|
||||
if (bytes == null) continue;
|
||||
updated.add(UploadItem(fileName: f.name, bytes: bytes));
|
||||
}
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.attach_file_rounded),
|
||||
label: const Text('Add Files'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...items.map((u) => ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file_rounded),
|
||||
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () {
|
||||
final updated = List<UploadItem>.from(items)..remove(u);
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'base_field.dart';
|
||||
import '../utils/entity_field_store.dart';
|
||||
|
||||
class ImageUploadField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
ImageUploadField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Select images',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) {
|
||||
final items =
|
||||
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
const [];
|
||||
if (isRequired && items.isEmpty) return '$label is required';
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
<UploadItem>[];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? picked =
|
||||
await picker.pickImage(source: ImageSource.gallery);
|
||||
if (picked != null) {
|
||||
final Uint8List bytes = await picked.readAsBytes();
|
||||
final updated = List<UploadItem>.from(items)
|
||||
..add(UploadItem(fileName: picked.name, bytes: bytes));
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.image_rounded),
|
||||
label: const Text('Add Image'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: items
|
||||
.map((u) => Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.memory(u.bytes,
|
||||
width: 72, height: 72, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final updated = List<UploadItem>.from(items)
|
||||
..remove(u);
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// Number input field implementation
|
||||
class NumberField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final double? min;
|
||||
final double? max;
|
||||
final int? decimalPlaces;
|
||||
|
||||
NumberField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.min,
|
||||
this.max,
|
||||
this.decimalPlaces,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final number = double.tryParse(value);
|
||||
if (number == null) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
if (min != null && number < min!) {
|
||||
return '$label must be at least $min';
|
||||
}
|
||||
if (max != null && number > max!) {
|
||||
return '$label must be at most $max';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: decimalPlaces != null && decimalPlaces! > 0,
|
||||
),
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// Reusable password field supporting dynamic pairing with confirm password
|
||||
/// via a shared groupId. Use two instances with the same groupId, one with
|
||||
/// isConfirm=true. EntityForm will handle cross-field validation and exclude
|
||||
/// confirm entries from submission.
|
||||
class PasswordField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final int? maxLength;
|
||||
final String? groupId; // Pair password/confirm dynamically
|
||||
final bool isConfirm; // Mark this instance as a confirm field
|
||||
|
||||
PasswordField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.maxLength,
|
||||
this.groupId,
|
||||
this.isConfirm = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (maxLength != null && value != null && value.length > maxLength!) {
|
||||
return '$label must be less than $maxLength characters';
|
||||
}
|
||||
// Cross-field match is handled centrally in EntityForm using groupId
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
maxLength: maxLength,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? get customProperties => {
|
||||
if (groupId != null) 'groupId': groupId,
|
||||
'isConfirm': isConfirm,
|
||||
'isPassword': true,
|
||||
};
|
||||
}
|
||||
51
base_project/lib/BuilderField/shared/fields/phone_field.dart
Normal file
51
base_project/lib/BuilderField/shared/fields/phone_field.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// Phone number input field implementation
|
||||
class PhoneField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final String? countryCode;
|
||||
|
||||
PhoneField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.countryCode,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty) {
|
||||
// Phone number validation
|
||||
final phoneRegex = RegExp(r'^\+?[\d\s\-\(\)]+$');
|
||||
if (!phoneRegex.hasMatch(value)) {
|
||||
return 'Please enter a valid phone number';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
keyboardType: TextInputType.phone,
|
||||
prefixIcon: countryCode != null ? Text(countryCode!) : null,
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
65
base_project/lib/BuilderField/shared/fields/radio_field.dart
Normal file
65
base_project/lib/BuilderField/shared/fields/radio_field.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
class RadioField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final List<String> options; // e.g., ['yes','no']
|
||||
|
||||
RadioField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = '',
|
||||
this.isRequired = false,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty && !options.contains(value)) {
|
||||
return 'Invalid selection';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final String current = controller.text;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
children: options.map((opt) {
|
||||
return ChoiceChip(
|
||||
label: Text(opt),
|
||||
selected: current == opt,
|
||||
selectedColor: colorScheme.primary.withOpacity(0.2),
|
||||
onSelected: (_) {
|
||||
controller.text = opt;
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
/// Boolean switch field implementation
|
||||
class SwitchField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
SwitchField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = '',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) => null;
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final bool current = (controller.text.toLowerCase() == 'true');
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: current,
|
||||
activeColor: colorScheme.onPrimary,
|
||||
activeTrackColor: colorScheme.primary,
|
||||
onChanged: (value) {
|
||||
controller.text = value.toString();
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
48
base_project/lib/BuilderField/shared/fields/url_field.dart
Normal file
48
base_project/lib/BuilderField/shared/fields/url_field.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// URL input field implementation using ModernTextField
|
||||
class UrlField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
UrlField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || !(uri.hasScheme && uri.hasAuthority)) {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
keyboardType: TextInputType.url,
|
||||
prefixIcon: const Icon(Icons.link_rounded),
|
||||
validator: validator,
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'base_field.dart';
|
||||
import '../utils/entity_field_store.dart';
|
||||
|
||||
class VideoUploadField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
VideoUploadField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Select video file',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) {
|
||||
final items =
|
||||
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
const [];
|
||||
if (isRequired && items.isEmpty) return '$label is required';
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
<UploadItem>[];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await FilePicker.platform.pickFiles(type: FileType.video);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
final f = result.files.single;
|
||||
final Uint8List? bytes = f.bytes ??
|
||||
(f.path != null
|
||||
? await File(f.path!).readAsBytes()
|
||||
: null);
|
||||
if (bytes != null) {
|
||||
final updated = List<UploadItem>.from(items)
|
||||
..add(UploadItem(fileName: f.name, bytes: bytes));
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.video_library_rounded),
|
||||
label: const Text('Add Video'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...items.map((u) => ListTile(
|
||||
leading: const Icon(Icons.videocam_rounded),
|
||||
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () {
|
||||
final updated = List<UploadItem>.from(items)..remove(u);
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user