This commit is contained in:
string 2025-09-16 08:49:08 +05:30
parent bc02a06d56
commit 0805cf44e4
7 changed files with 968 additions and 50 deletions

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:barcode_widget/barcode_widget.dart';
import 'base_field.dart';
class BarcodeField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<String?> Function()? scanner; // returns scanned text
BarcodeField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
this.scanner,
});
@override
String? Function(String?)? get validator => (v) {
if (isRequired && (v == null || v.trim().isEmpty)) return 'Required';
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(
color: colorScheme.onSurface, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: colorScheme.outlineVariant.withOpacity(0.3)),
),
child: Column(
children: [
if (controller.text.trim().isNotEmpty) ...[
BarcodeWidget(
barcode: Barcode.code128(),
data: controller.text.trim(),
width: 220,
height: 80,
),
const SizedBox(height: 12),
],
Row(
children: [
Expanded(
child: TextFormField(
controller: controller,
decoration: InputDecoration(
labelText:
hint.isEmpty ? 'Enter or scan barcode' : hint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
filled: true,
),
validator: validator,
onChanged: (_) => onChanged?.call(),
),
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Scan Barcode',
icon: Icon(Icons.document_scanner_rounded,
color: colorScheme.primary),
onPressed: scanner == null
? null
: () async {
final val = await scanner!.call();
if (val != null) {
controller.text = val;
onChanged?.call();
}
},
),
],
),
],
),
),
],
);
}
}

View File

@ -124,13 +124,13 @@ class _CalculatedWidgetState extends State<_CalculatedWidget> {
case 'Addition':
acc += nums[i];
break;
case 'sub':
case 'Subtraction':
acc -= nums[i];
break;
case 'mul':
case 'Multiplication':
acc *= nums[i];
break;
case 'div':
case 'Division':
if (nums[i] != 0) acc /= nums[i];
break;
}

View File

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'base_field.dart';
class CurrencyField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final String currencySymbol;
final int decimalDigits;
final List<Map<String, String>>
currencyOptions; // [{code: 'INR', symbol: '₹', name: 'India (INR)'}]
CurrencyField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
this.currencySymbol = '₹',
this.decimalDigits = 2,
List<Map<String, String>>? currencyOptions,
}) : currencyOptions = currencyOptions ??
const [
{'code': 'INR', 'symbol': '₹', 'name': 'India (INR)'},
{'code': 'USD', 'symbol': '\$', 'name': 'USA (USD)'},
{'code': 'EUR', 'symbol': '', 'name': 'Euro (EUR)'},
{'code': 'GBP', 'symbol': '£', 'name': 'UK (GBP)'},
{'code': 'AUD', 'symbol': 'A\$', 'name': 'Australia (AUD)'},
{'code': 'CAD', 'symbol': 'C\$', 'name': 'Canada (CAD)'},
{'code': 'CHF', 'symbol': 'CHF', 'name': 'Switzerland (CHF)'},
{'code': 'CNY', 'symbol': 'Â¥', 'name': 'China (CNY)'},
{'code': 'HKD', 'symbol': 'HK\$', 'name': 'Hong Kong (HKD)'},
{'code': 'NZD', 'symbol': 'NZ\$', 'name': 'New Zealand (NZD)'},
{'code': 'SGD', 'symbol': 'S\$', 'name': 'Singapore (SGD)'},
{'code': 'ZAR', 'symbol': 'R', 'name': 'South Africa (ZAR)'},
{'code': 'SEK', 'symbol': 'kr', 'name': 'Sweden (SEK)'},
{'code': 'NOK', 'symbol': 'kr', 'name': 'Norway (NOK)'},
{'code': 'MXN', 'symbol': '\$', 'name': 'Mexico (MXN)'},
{'code': 'BRL', 'symbol': 'R\$', 'name': 'Brazil (BRL)'},
{'code': 'RUB', 'symbol': '', 'name': 'Russia (RUB)'},
{'code': 'KRW', 'symbol': '', 'name': 'South Korea (KRW)'},
{'code': 'TRY', 'symbol': '', 'name': 'Turkey (TRY)'},
{'code': 'JPY', 'symbol': '¥', 'name': 'Japan (JPY)'},
];
@override
String? Function(String?)? get validator => (value) {
final v = value?.trim() ?? '';
if (isRequired && v.isEmpty) return 'Required';
if (v.isNotEmpty && double.tryParse(v.replaceAll(',', '')) == null) {
return 'Invalid amount';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _CurrencyFieldWidget(
controller: controller,
colorScheme: colorScheme,
label: label,
hint: hint.isEmpty ? '0.${'0' * decimalDigits}' : hint,
isRequired: isRequired,
decimalDigits: decimalDigits,
defaultSymbol: currencySymbol,
options: currencyOptions,
onChanged: onChanged,
validator: validator,
);
}
}
class _CurrencyFieldWidget extends StatefulWidget {
final TextEditingController controller;
final ColorScheme colorScheme;
final String label;
final String hint;
final bool isRequired;
final int decimalDigits;
final String defaultSymbol;
final List<Map<String, String>> options;
final VoidCallback? onChanged;
final String? Function(String?)? validator;
const _CurrencyFieldWidget({
required this.controller,
required this.colorScheme,
required this.label,
required this.hint,
required this.isRequired,
required this.decimalDigits,
required this.defaultSymbol,
required this.options,
this.onChanged,
this.validator,
});
@override
State<_CurrencyFieldWidget> createState() => _CurrencyFieldWidgetState();
}
class _CurrencyFieldWidgetState extends State<_CurrencyFieldWidget> {
late String _selectedSymbol;
@override
void initState() {
super.initState();
_selectedSymbol = widget.defaultSymbol;
}
@override
Widget build(BuildContext context) {
final cs = widget.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.label,
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outlineVariant.withOpacity(0.4)),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedSymbol,
items: widget.options
.map((o) => DropdownMenuItem<String>(
value: o['symbol'],
child: Row(
children: [
Text(o['symbol'] ?? ''),
const SizedBox(width: 8),
Text(
o['code'] ?? '',
style: TextStyle(
color: cs.onSurface.withOpacity(0.7),
fontSize: 12),
),
],
),
))
.toList(),
onChanged: (val) {
if (val == null) return;
setState(() => _selectedSymbol = val);
widget.onChanged?.call();
},
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: widget.controller,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]')),
],
decoration: InputDecoration(
hintText: widget.hint,
prefixText: '$_selectedSymbol ',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
filled: true,
),
validator: widget.validator,
onChanged: (_) => widget.onChanged?.call(),
),
),
],
),
],
);
}
}

View File

@ -0,0 +1,319 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../ui/entity_form.dart';
import 'custom_text_field.dart';
import 'number_field.dart' as shared_number;
import 'date_field.dart' as shared_date;
import 'currency_field.dart';
import 'qr_code_field.dart';
import 'barcode_field.dart';
/// FieldGroupField
/// - Visual group that renders multiple sub-inputs inside a styled container
/// - Uses EntityForm's composite submit capability via assignByJsonPaths + paths
/// - The group controller stores a JSON string of path->value; on submit, EntityForm
/// assigns each value into the request body by its json path.
class FieldGroupField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final List<GroupSubField>? subFields;
// Schema-driven: field name -> type (e.g., 'text', 'number', 'date', 'currency', 'qrcode', 'barcode')
final String? groupPrefix; // e.g., 'primary'
final Map<String, String>? schema;
FieldGroupField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
this.subFields,
this.groupPrefix,
this.schema,
});
@override
String? Function(String?)? get validator => (v) {
if (!isRequired) return null;
final fields = _effectiveSubFields;
if (fields.any((sf) => sf.isRequired)) {
final scope = _scope;
if (scope == null) return null;
for (final sf in fields) {
if (!sf.isRequired) continue;
final c = scope.controllers[sf.path];
if (c == null || c.text.trim().isEmpty) {
return 'Please complete required fields';
}
}
}
return null;
};
static EntityFormScope? get _scope => _EntityFormScopeAccessor.scope;
@override
Map<String, dynamic>? get customProperties => {
'excludeFromSubmit': true,
'assignByJsonPaths': true,
'paths': _effectiveSubFields.map((e) => e.path).toList(),
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _EntityFormScopeAccessor(
builder: (scope) {
final fields = _effectiveSubFields;
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: colorScheme.outlineVariant.withOpacity(0.4)),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 6),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
if (hint.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
hint,
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 12,
),
),
],
const SizedBox(height: 12),
...fields.map((sf) => _buildSubField(
scope, sf, colorScheme, controller, onChanged)),
],
),
),
);
},
);
}
Widget _buildSubField(
EntityFormScope? scope,
GroupSubField sf,
ColorScheme colorScheme,
TextEditingController groupController,
VoidCallback? onChanged,
) {
final existing = scope?.controllers[sf.path];
final TextEditingController controller =
existing ?? TextEditingController();
// Ensure controller is registered in scope for inner fields
if (scope != null && existing == null) {
scope.controllers[sf.path] = controller;
}
// Prefill subfield from group's encoded initial JSON (built by EntityForm)
if (controller.text.isEmpty && groupController.text.isNotEmpty) {
try {
final decoded = const JsonDecoder().convert(groupController.text);
if (decoded is Map<String, dynamic>) {
final dynamic v = decoded[sf.path];
if (v != null && v.toString().isNotEmpty) {
controller.text = v.toString();
}
}
} catch (_) {
// Ignore decode errors; groupController may not be JSON yet
}
}
controller.addListener(() {
// Encode all subfield values into the group controller for submit mapping
final Map<String, dynamic> map = {};
for (final s in _effectiveSubFields) {
final c = scope?.controllers[s.path];
if (c != null && c.text.isNotEmpty) {
map[s.path] = c.text.trim();
}
}
groupController.text = _encode(map);
onChanged?.call();
scope?.notifyParent();
});
// Build appropriate shared field by type, but render it inline
final BaseField field = _toSharedField(sf);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (sf.label.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
sf.label,
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
field.buildField(
controller: controller,
colorScheme: colorScheme,
onChanged: onChanged,
),
],
),
);
}
String _encode(Map<String, dynamic> map) => const JsonEncoder().convert(map);
List<GroupSubField> get _effectiveSubFields {
if (subFields != null && subFields!.isNotEmpty) return subFields!;
final List<GroupSubField> out = [];
final prefix = groupPrefix ?? '';
final schemaMap = schema ?? const {};
schemaMap.forEach((name, type) {
final path = prefix.isEmpty ? name : '$prefix.$name';
out.add(GroupSubField(
path: path,
label: _capitalize(name.replaceAll('_', ' ')),
hint: '',
keyboardType: _keyboardFromType(type),
isRequired: false,
type: type,
));
});
return out;
}
TextInputType _keyboardFromType(String type) {
switch (type.toLowerCase()) {
case 'number':
case 'currency':
return TextInputType.number;
default:
return TextInputType.text;
}
}
String _capitalize(String s) {
if (s.isEmpty) return s;
return s[0].toUpperCase() + s.substring(1);
}
BaseField _toSharedField(GroupSubField sf) {
final t = (sf.type ?? 'text').toLowerCase();
switch (t) {
case 'number':
return shared_number.NumberField(
fieldKey: sf.path,
label: sf.label,
hint: sf.hint,
isRequired: sf.isRequired,
);
case 'date':
return shared_date.DateField(
fieldKey: sf.path,
label: sf.label,
hint: sf.hint,
isRequired: sf.isRequired,
);
case 'currency':
return CurrencyField(
fieldKey: sf.path,
label: sf.label,
hint: sf.hint,
isRequired: sf.isRequired,
);
case 'qrcode':
case 'qr':
return QRCodeField(
fieldKey: sf.path,
label: sf.label,
hint: sf.hint,
isRequired: sf.isRequired,
);
case 'barcode':
return BarcodeField(
fieldKey: sf.path,
label: sf.label,
hint: sf.hint,
isRequired: sf.isRequired,
);
case 'text':
default:
return CustomTextField(
fieldKey: sf.path,
label: sf.label,
hint: sf.hint,
isRequired: sf.isRequired,
);
}
}
}
class GroupSubField {
final String path; // JSON path for EntityForm to assign during submit
final String label;
final String hint;
final TextInputType keyboardType;
final bool isRequired;
final String? type; // schema type identifier
GroupSubField({
required this.path,
required this.label,
this.hint = '',
this.keyboardType = TextInputType.text,
this.isRequired = false,
this.type,
});
}
/// Helper to access EntityFormScope in a builder without changing base components
class _EntityFormScopeAccessor extends StatefulWidget {
final Widget Function(EntityFormScope? scope) builder;
const _EntityFormScopeAccessor({required this.builder});
static EntityFormScope? get scope => _lastScope;
static EntityFormScope? _lastScope;
@override
State<_EntityFormScopeAccessor> createState() =>
_EntityFormScopeAccessorState();
}
class _EntityFormScopeAccessorState extends State<_EntityFormScopeAccessor> {
@override
Widget build(BuildContext context) {
final scope = EntityFormScope.of(context);
_EntityFormScopeAccessor._lastScope = scope;
return widget.builder(scope);
}
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'base_field.dart';
class QRCodeField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<String?> Function()? scanner; // returns scanned text
QRCodeField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
this.scanner,
});
@override
String? Function(String?)? get validator => (v) {
if (isRequired && (v == null || v.trim().isEmpty)) return 'Required';
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(
color: colorScheme.onSurface, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: colorScheme.outlineVariant.withOpacity(0.3)),
),
child: Column(
children: [
if (controller.text.trim().isNotEmpty) ...[
QrImageView(
data: controller.text.trim(),
version: QrVersions.auto,
size: 160,
gapless: false,
),
const SizedBox(height: 12),
],
Row(
children: [
Expanded(
child: TextFormField(
controller: controller,
decoration: InputDecoration(
labelText:
hint.isEmpty ? 'Enter or scan QR data' : hint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
filled: true,
),
validator: validator,
onChanged: (_) => onChanged?.call(),
),
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Scan QR',
icon: Icon(Icons.qr_code_scanner_rounded,
color: colorScheme.primary),
onPressed: scanner == null
? null
: () async {
final val = await scanner!.call();
if (val != null) {
controller.text = val;
onChanged?.call();
}
},
),
],
),
],
),
),
],
);
}
}

View File

@ -70,18 +70,26 @@ class _ValueListPickerWidget extends StatefulWidget {
class _ValueListPickerWidgetState extends State<_ValueListPickerWidget> {
bool _loading = false;
String? _loadError;
Future<void> _open() async {
if (!mounted) return;
setState(() => _loading = true);
List<Map<String, dynamic>> options = const [];
try {
options = await widget.optionsLoader();
} finally {
setState(() => _loading = false);
}
// Kick off the load but render the sheet immediately with a loader
final Future<List<Map<String, dynamic>>> loadFuture = (() async {
try {
_loadError = null;
final list = await widget.optionsLoader();
return list;
} catch (e) {
_loadError = e.toString();
return const <Map<String, dynamic>>[];
}
})();
final cs = widget.colorScheme;
await showModalBottomSheet(
final selected = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: cs.surface,
@ -90,48 +98,257 @@ class _ValueListPickerWidgetState extends State<_ValueListPickerWidget> {
),
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: options.isEmpty
? Center(
child: Text('No data',
style: TextStyle(color: cs.onSurfaceVariant)))
: ListView.separated(
itemCount: options.length,
separatorBuilder: (_, __) =>
Divider(height: 1, color: cs.outline.withOpacity(0.08)),
itemBuilder: (context, index) {
final o = options[index];
return ListTile(
leading: CircleAvatar(
child: Text(
((o['name'] ?? o['title'] ?? 'N') as String)
.substring(0, 1)
.toUpperCase())),
title: Text((o['name'] ?? o['title'] ?? '').toString()),
subtitle: Text(
(o['description'] ?? o['email'] ?? o['phone'] ?? '')
.toString()),
onTap: () {
final scope = EntityFormScope.of(context);
if (scope != null) {
widget.fillMappings
.forEach((sourceKey, targetFieldKey) {
final v = o[sourceKey];
final c = scope.controllers[targetFieldKey];
if (c != null) c.text = v?.toString() ?? '';
});
scope.notifyParent();
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header with close button
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(
child: Text(
widget.label,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
IconButton(
tooltip: 'Close',
icon:
Icon(Icons.close_rounded, color: cs.onSurfaceVariant),
onPressed: () => Navigator.of(context).pop(),
)
],
),
),
const SizedBox(height: 4),
FutureBuilder<List<Map<String, dynamic>>>(
future: loadFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Padding(
padding: const EdgeInsets.all(24),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: cs.primary),
),
const SizedBox(width: 12),
Text('Loading...',
style: TextStyle(color: cs.onSurfaceVariant)),
],
),
);
}
if (snapshot.hasError || _loadError != null) {
final String msg = snapshot.error?.toString() ??
_loadError ??
'Failed to load';
return Padding(
padding: const EdgeInsets.all(16),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: cs.errorContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.error.withOpacity(0.2)),
),
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.error_outline, color: cs.error),
const SizedBox(width: 8),
Expanded(
child: Text(msg,
style: TextStyle(color: cs.onSurfaceVariant)),
),
],
),
),
);
}
final options =
snapshot.data ?? const <Map<String, dynamic>>[];
if (options.isEmpty) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Icon(Icons.inbox_rounded,
color: cs.onSurfaceVariant, size: 36),
const SizedBox(height: 8),
Text('No data found',
style: TextStyle(color: cs.onSurfaceVariant)),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close_rounded),
label: const Text('Close'),
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
),
)
],
),
);
}
return Flexible(
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: options.length,
separatorBuilder: (_, __) => Divider(
height: 1, color: cs.outline.withOpacity(0.08)),
itemBuilder: (context, index) {
final o = options[index];
dynamic _readByPath(
Map<String, dynamic> m, String path) {
if (m.isEmpty || path.isEmpty) return null;
if (path.contains('.')) {
dynamic current = m;
for (final seg in path.split('.')) {
if (current is Map && current.containsKey(seg)) {
current = current[seg];
} else {
current = null;
break;
}
}
return current;
}
Navigator.of(context).pop();
},
);
},
),
return m[path];
}
final List<String> mappingKeys =
widget.fillMappings.keys.toList();
print('mappingKeys: $mappingKeys');
final String titleKey =
mappingKeys.isNotEmpty ? mappingKeys[0] : 'name';
final String subtitleKey =
mappingKeys.length > 1 ? mappingKeys[1] : '';
String title = (_readByPath(o, titleKey) ??
o['name'] ??
o['title'] ??
'')
.toString();
String subtitle = subtitleKey.isNotEmpty
? (_readByPath(o, subtitleKey) ?? '').toString()
: (o['description'] ??
o['email'] ??
o['phone'] ??
'')
.toString();
final String leadingText =
(title.isNotEmpty ? title[0] : 'N').toUpperCase();
return ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
cs.primary,
cs.primary.withOpacity(0.7)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: cs.primary.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4)),
],
),
alignment: Alignment.center,
child: Text(leadingText,
style: TextStyle(
color: cs.onPrimary,
fontWeight: FontWeight.w700)),
),
title: Text(title,
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600)),
subtitle: subtitle.isEmpty
? null
: Text(subtitle,
style: TextStyle(color: cs.onSurfaceVariant)),
trailing: Icon(Icons.chevron_right_rounded,
color: cs.onSurfaceVariant),
onTap: () {
Navigator.of(context).pop<Map<String, dynamic>>(o);
},
);
},
),
);
},
),
const SizedBox(height: 16),
],
),
);
},
);
if (!mounted) return;
setState(() => _loading = false);
// After sheet closes, if a selection was returned, fill controllers safely
if (selected != null) {
final scope = EntityFormScope.of(context);
if (scope != null) {
dynamic _readByPath(Map<String, dynamic> m, String path) {
if (m.isEmpty || path.isEmpty) return null;
if (path.contains('.')) {
dynamic current = m;
for (final seg in path.split('.')) {
if (current is Map && current.containsKey(seg)) {
current = current[seg];
} else {
current = null;
break;
}
}
return current;
}
return m[path];
}
widget.fillMappings.forEach((sourceKey, targetFieldKey) {
dynamic v = _readByPath(selected, sourceKey);
if (v == null) {
v = _readByPath(selected, '${sourceKey}1') ??
_readByPath(selected, '${sourceKey}2') ??
_readByPath(selected, sourceKey.toLowerCase()) ??
_readByPath(selected, sourceKey.toUpperCase());
}
final c = scope.controllers[targetFieldKey];
if (c != null) {
try {
c.text = v?.toString() ?? '';
} catch (_) {
// Controller may be disposed; ignore safely
}
}
});
scope.notifyParent();
}
}
}
@override

View File

@ -1,8 +1,7 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../theme/dynamic_color_scheme.dart';
import 'package:flutter/foundation.dart' show compute;
class DynamicThemeProvider extends ChangeNotifier {
ColorScheme? _dynamicColorScheme;