This commit is contained in:
Gaurav Kumar
2025-09-09 08:45:46 +05:30
parent 01be9df2ed
commit f9043173c0
25 changed files with 3632 additions and 4 deletions

View File

@@ -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();
},
),
)),
],
);
}
}

View 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;
}

View 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)),
));
}
}

View File

@@ -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,
),
),
),
],
);
}
}

View File

@@ -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,
);
}
}

View 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

View File

@@ -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)}';
}
}

View File

@@ -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,
// );
// }
// }

View 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,
);
}
}

View File

@@ -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();
},
),
)),
],
);
}
}

View File

@@ -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(),
),
],
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
};
}

View 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,
);
}
}

View 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(),
),
],
);
}
}

View File

@@ -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();
},
),
],
);
}
}

View 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,
);
}
}

View File

@@ -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();
},
),
)),
],
);
}
}