base_project

This commit is contained in:
2026-01-28 05:53:02 +00:00
commit 664afae198
1349 changed files with 89007 additions and 0 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,205 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
class AutocompleteDropdownField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<List<Map<String, dynamic>>> Function() optionsLoader;
final String valueKey;
final String displayKey;
AutocompleteDropdownField({
required this.fieldKey,
required this.label,
required this.hint,
required this.optionsLoader,
required this.valueKey,
required this.displayKey,
this.isRequired = false,
}) : assert(valueKey != ''),
assert(displayKey != '');
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
return null;
};
@override
Map<String, dynamic>? get customProperties => {
'isAutocomplete': true,
'valueKey': valueKey,
'displayKey': displayKey,
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
// We'll lazily set the UI label into the Autocomplete's textController once options load
String initialLabel = '';
return FutureBuilder<List<Map<String, dynamic>>>(
future: optionsLoader(),
builder: (context, snapshot) {
final List<Map<String, dynamic>> options = snapshot.data ?? const [];
// Resolve initial display label from stored id
if (snapshot.connectionState == ConnectionState.done &&
controller.text.isNotEmpty) {
final match = options.firstWhere(
(o) => (o[valueKey]?.toString() ?? '') == controller.text,
orElse: () => const {},
);
initialLabel =
match.isNotEmpty ? (match[displayKey]?.toString() ?? '') : '';
}
final Iterable<String> displayOptions = options
.map((e) => e[displayKey])
.where((e) => e != null)
.map((e) => e.toString());
return Autocomplete<String>(
optionsBuilder: (TextEditingValue tev) {
if (tev.text.isEmpty) return const Iterable<String>.empty();
return displayOptions.where(
(opt) => opt.toLowerCase().contains(tev.text.toLowerCase()));
},
onSelected: (String selection) {
// set UI label and hidden id
final match = options.firstWhere(
(o) => (o[displayKey]?.toString() ?? '') == selection,
orElse: () => const {},
);
final idStr =
match.isNotEmpty ? (match[valueKey]?.toString() ?? '') : '';
controller.text = idStr;
onChanged?.call();
},
fieldViewBuilder:
(context, textController, focusNode, onFieldSubmitted) {
// Initialize UI text once options are available
if (initialLabel.isNotEmpty && textController.text.isEmpty) {
textController.text = initialLabel;
}
return ModernTextField(
label: label,
hint: hint,
controller: textController,
focusNode: focusNode,
validator: (v) => validator?.call(controller.text),
onChanged: (_) => onChanged?.call(),
suffixIcon: textController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
textController.clear();
controller.clear();
onChanged?.call();
},
)
: const Icon(Icons.search),
);
},
optionsViewBuilder: (context, onSelected, optionsIt) {
final optionsList = optionsIt.toList();
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 6,
color: colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side:
BorderSide(color: colorScheme.outline.withOpacity(0.15)),
),
child: ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: 280, minWidth: 240),
child: optionsList.isEmpty
? Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(Icons.info_outline,
color: colorScheme.onSurfaceVariant),
const SizedBox(width: 8),
Expanded(
child: Text(
'No results',
style: TextStyle(
color: colorScheme.onSurfaceVariant),
),
),
],
),
)
: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: optionsList.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: colorScheme.outline.withOpacity(0.08)),
itemBuilder: (context, index) {
final opt = optionsList[index];
return InkWell(
onTap: () => onSelected(opt),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 10),
child: _buildHighlightedText(
opt, // current query text
// Pull current query from the Autocomplete field's text
// (not stored here directly, so we simply render opt)
'',
colorScheme),
),
);
},
),
),
),
);
},
);
},
);
}
Widget _buildHighlightedText(
String text, String query, ColorScheme colorScheme) {
if (query.isEmpty)
return Text(text, style: TextStyle(color: colorScheme.onSurface));
final lowerText = text.toLowerCase();
final lowerQuery = query.toLowerCase();
final int start = lowerText.indexOf(lowerQuery);
if (start < 0)
return Text(text, style: TextStyle(color: colorScheme.onSurface));
final int end = start + query.length;
return RichText(
text: TextSpan(
children: [
TextSpan(
text: text.substring(0, start),
style: TextStyle(color: colorScheme.onSurface)),
TextSpan(
text: text.substring(start, end),
style: TextStyle(
color: colorScheme.primary, fontWeight: FontWeight.w600)),
TextSpan(
text: text.substring(end),
style: TextStyle(color: colorScheme.onSurface)),
],
),
);
}
}

View File

@@ -0,0 +1,322 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
class AutocompleteMultiSelectField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<List<Map<String, dynamic>>> Function() optionsLoader;
final String valueKey;
final String displayKey;
AutocompleteMultiSelectField({
required this.fieldKey,
required this.label,
required this.hint,
required this.optionsLoader,
required this.valueKey,
required this.displayKey,
this.isRequired = false,
}) : assert(valueKey != ''),
assert(displayKey != '');
@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 _AutocompleteMultiSelectWidget(
fieldKey: fieldKey,
label: label,
hint: hint,
isRequired: isRequired,
optionsLoader: optionsLoader,
valueKey: valueKey,
displayKey: displayKey,
controller: controller,
colorScheme: colorScheme,
onChanged: onChanged,
validator: validator,
);
}
}
class _AutocompleteMultiSelectWidget extends StatefulWidget {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<List<Map<String, dynamic>>> Function() optionsLoader;
final String valueKey;
final String displayKey;
final TextEditingController controller;
final ColorScheme colorScheme;
final VoidCallback? onChanged;
final String? Function(String?)? validator;
const _AutocompleteMultiSelectWidget({
required this.fieldKey,
required this.label,
required this.hint,
required this.isRequired,
required this.optionsLoader,
required this.valueKey,
required this.displayKey,
required this.controller,
required this.colorScheme,
required this.onChanged,
required this.validator,
});
@override
State<_AutocompleteMultiSelectWidget> createState() =>
_AutocompleteMultiSelectWidgetState();
}
class _AutocompleteMultiSelectWidgetState
extends State<_AutocompleteMultiSelectWidget> {
List<Map<String, dynamic>>? _cachedOptions;
bool _isLoading = false;
bool _hasLoaded = false;
@override
String? Function(String?)? get validator => (value) {
if (widget.isRequired && (value == null || value.isEmpty)) {
return '${widget.label} is required';
}
return null;
};
@override
Map<String, dynamic>? get customProperties => {
'isMultiSelect': true,
'valueKey': widget.valueKey,
'displayKey': widget.displayKey,
};
Future<void> _loadOptions() async {
if (_hasLoaded || _isLoading) return;
setState(() {
_isLoading = true;
});
try {
final options = await widget.optionsLoader();
if (mounted) {
setState(() {
_cachedOptions = options;
_isLoading = false;
_hasLoaded = true;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
// Load options when widget is first built
if (!_hasLoaded && !_isLoading) {
_loadOptions();
}
final List<Map<String, dynamic>> options = _cachedOptions ?? const [];
// Get display options for autocomplete
final Iterable<String> displayOptions = options
.map((e) => e[widget.displayKey])
.where((e) => e != null)
.map((e) => e.toString());
// Parse selected values (stored as comma-separated IDs)
final Set<String> selectedIds = widget.controller.text.isEmpty
? <String>{}
: widget.controller.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toSet();
// Get selected display values for chips
final Set<String> selectedDisplays = selectedIds
.map((id) {
final match = options.firstWhere(
(o) => (o[widget.valueKey]?.toString() ?? '') == id,
orElse: () => const {},
);
return match.isNotEmpty
? (match[widget.displayKey]?.toString() ?? '')
: '';
})
.where((display) => display.isNotEmpty)
.toSet();
void toggleSelection(String displayValue) {
// Find the corresponding ID for this display value
final match = options.firstWhere(
(o) => (o[widget.displayKey]?.toString() ?? '') == displayValue,
orElse: () => const {},
);
if (match.isNotEmpty) {
final id = match[widget.valueKey]?.toString() ?? '';
if (selectedIds.contains(id)) {
selectedIds.remove(id);
} else {
selectedIds.add(id);
}
widget.controller.text = selectedIds.join(',');
widget.onChanged?.call();
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Autocomplete<String>(
optionsBuilder: (TextEditingValue tev) {
if (tev.text.isEmpty) return const Iterable<String>.empty();
return displayOptions.where(
(opt) => opt.toLowerCase().contains(tev.text.toLowerCase()));
},
onSelected: (String selection) => toggleSelection(selection),
fieldViewBuilder:
(context, textController, focusNode, onFieldSubmitted) {
return ModernTextField(
label: widget.label,
hint: widget.hint,
controller: textController,
focusNode: focusNode,
onSubmitted: (_) {
final v = textController.text.trim();
if (v.isNotEmpty) {
// Check if the entered text matches any display option
final match = displayOptions.firstWhere(
(opt) => opt.toLowerCase() == v.toLowerCase(),
orElse: () => '',
);
if (match.isNotEmpty) {
toggleSelection(match);
textController.clear();
}
}
},
suffixIcon: textController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
textController.clear();
widget.onChanged?.call();
},
)
: const Icon(Icons.search),
onChanged: (_) => widget.onChanged?.call(),
);
},
optionsViewBuilder: (context, onSelected, optionsIt) {
final list = optionsIt.toList();
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 6,
color: widget.colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: widget.colorScheme.outline.withOpacity(0.15)),
),
child: ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: 280, minWidth: 240),
child: list.isEmpty
? Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(Icons.info_outline,
color: widget.colorScheme.onSurfaceVariant),
const SizedBox(width: 8),
Expanded(
child: Text(
'No results',
style: TextStyle(
color:
widget.colorScheme.onSurfaceVariant),
),
),
],
),
)
: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: list.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color:
widget.colorScheme.outline.withOpacity(0.08)),
itemBuilder: (context, index) {
final opt = list[index];
return ListTile(
title: Text(opt),
onTap: () {
onSelected(opt);
},
);
},
),
),
),
);
},
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: selectedDisplays
.map((displayValue) => Chip(
label: Text(displayValue),
onDeleted: () => toggleSelection(displayValue),
))
.toList(),
),
if (_isLoading)
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: LinearProgressIndicator(minHeight: 2),
),
if (widget.validator != null)
Builder(
builder: (context) {
final error = widget.validator!(widget.controller.text);
return error != null
? Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(error,
style: TextStyle(
color: widget.colorScheme.error, fontSize: 12)),
)
: const SizedBox.shrink();
},
)
],
);
}
}

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

@@ -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,158 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../ui/entity_form.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Calculated field
/// - Select multiple source fields from current form
/// - Apply operation: add, subtract, multiply, divide, concat
/// - Displays result (read-only) and stores it in controller
class CalculatedField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final List<String> sourceKeys; // keys of other fields in same form
final String operation; // add|sub|mul|div|concat
CalculatedField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
required this.sourceKeys,
required this.operation,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
return null;
};
@override
Map<String, dynamic>? get customProperties => const {
'isCalculated': true,
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _CalculatedWidget(
label: label,
hint: hint,
controller: controller,
colorScheme: colorScheme,
sourceKeys: sourceKeys,
operation: operation,
validate: validator,
);
}
}
class _CalculatedWidget extends StatefulWidget {
final String label;
final String hint;
final TextEditingController controller;
final ColorScheme colorScheme;
final List<String> sourceKeys;
final String operation;
final String? Function(String?)? validate;
const _CalculatedWidget({
required this.label,
required this.hint,
required this.controller,
required this.colorScheme,
required this.sourceKeys,
required this.operation,
required this.validate,
});
@override
State<_CalculatedWidget> createState() => _CalculatedWidgetState();
}
class _CalculatedWidgetState extends State<_CalculatedWidget> {
List<TextEditingController> _sources = const [];
@override
void didChangeDependencies() {
super.didChangeDependencies();
final scope = EntityFormScope.of(context);
if (scope != null) {
_sources = widget.sourceKeys
.map((k) => scope.controllers[k])
.whereType<TextEditingController>()
.toList();
for (final c in _sources) {
c.addListener(_recompute);
}
_recompute();
}
}
@override
void dispose() {
for (final c in _sources) {
c.removeListener(_recompute);
}
super.dispose();
}
void _recompute() {
String result = '';
if (widget.operation == 'Concatination') {
result = _sources
.map((c) => c.text.trim())
.where((s) => s.isNotEmpty)
.join(' ');
} else {
final nums =
_sources.map((c) => double.tryParse(c.text.trim()) ?? 0.0).toList();
if (nums.isEmpty) {
result = '';
} else {
double acc = nums.first;
for (int i = 1; i < nums.length; i++) {
switch (widget.operation) {
case 'Addition':
acc += nums[i];
break;
case 'Subtraction':
acc -= nums[i];
break;
case 'Multiplication':
acc *= nums[i];
break;
case 'Division':
if (nums[i] != 0) acc /= nums[i];
break;
}
}
result = acc.toString();
}
}
if (widget.controller.text != result) {
widget.controller.text = result;
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return ModernTextField(
label: widget.label,
hint: widget.hint,
controller: widget.controller,
readOnly: true,
validator: widget.validate,
suffixIcon: const Icon(Icons.functions),
);
}
}

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,58 @@
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,
}) {
// Set default value to false if controller is empty
if (controller.text.isEmpty) {
controller.text = 'false';
}
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,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,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,132 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
/// Read-only Data Grid field
/// - Fetches tabular data from an async loader
/// - Renders a DataTable
/// - Excluded from form submission
class DataGridField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<List<Map<String, dynamic>>> Function() dataLoader;
DataGridField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
required this.dataLoader,
});
@override
String? Function(String?)? get validator => null;
@override
Map<String, dynamic>? get customProperties => const {
'excludeFromSubmit': true,
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _DataGridFieldWidget(
colorScheme: colorScheme,
dataLoader: dataLoader,
);
}
}
class _DataGridFieldWidget extends StatefulWidget {
final ColorScheme colorScheme;
final Future<List<Map<String, dynamic>>> Function() dataLoader;
const _DataGridFieldWidget({
required this.colorScheme,
required this.dataLoader,
});
@override
State<_DataGridFieldWidget> createState() => _DataGridFieldWidgetState();
}
class _DataGridFieldWidgetState extends State<_DataGridFieldWidget> {
late Future<List<Map<String, dynamic>>> _future;
@override
void initState() {
super.initState();
_future = widget.dataLoader();
}
@override
Widget build(BuildContext context) {
final colorScheme = widget.colorScheme;
return FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const LinearProgressIndicator(minHeight: 2);
}
if (snapshot.hasError) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorScheme.error.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.error_outline, color: colorScheme.error),
const SizedBox(width: 8),
Expanded(
child: Text(
snapshot.error.toString(),
style: TextStyle(color: colorScheme.error),
),
),
],
),
);
}
final data = snapshot.data ?? const <Map<String, dynamic>>[];
if (data.isEmpty) {
return Text('No data available',
style: TextStyle(color: colorScheme.onSurfaceVariant));
}
final columns = data.first.keys.toList();
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 24,
headingRowColor: MaterialStateColor.resolveWith(
(_) => colorScheme.primaryContainer.withOpacity(0.3)),
dataRowColor:
MaterialStateColor.resolveWith((_) => colorScheme.surface),
dividerThickness: 0.5,
columns: columns
.map((k) => DataColumn(
label: Text(k,
style: const TextStyle(fontWeight: FontWeight.w600)),
))
.toList(),
rows: data
.map(
(row) => DataRow(
cells: columns
.map((k) => DataCell(Text(row[k]?.toString() ?? '')))
.toList(),
),
)
.toList(),
),
);
},
);
}
}

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,159 @@
// import 'package:flutter/material.dart';
// import 'base_field.dart';
// import 'dependent_dropdown_field.dart';
// import 'dropdown_field.dart';
// /// Example usage of DependentDropdownField
// /// This demonstrates how to create cascading dropdowns with API calls
// class DependentDropdownExample {
// /// Example field definitions for a location form with country -> state -> district
// static List<BaseField> getLocationFields() {
// return [
// // Parent dropdown - Country selection
// DropdownField(
// fieldKey: 'country',
// label: 'Country',
// hint: 'Select country',
// options: const [
// {'id': 'india', 'name': 'India'},
// {'id': 'usa', 'name': 'United States'},
// {'id': 'canada', 'name': 'Canada'},
// ],
// valueKey: 'id',
// displayKey: 'name',
// ),
// // Dependent dropdown - State selection (depends on country)
// DependentDropdownField(
// fieldKey: 'state',
// label: 'State',
// hint: 'Select state',
// dependentFieldKey: 'country',
// apiEndpoint: '/State_ListFilter1/State_ListFilter11',
// valueKey: 'state_name',
// displayKey: 'state_name',
// dependentValueKey:
// 'name', // This is the field from country that gets passed to API
// ),
// // Dependent dropdown - District selection (depends on state)
// DependentDropdownField(
// fieldKey: 'district',
// label: 'District',
// hint: 'Select district',
// dependentFieldKey: 'state',
// apiEndpoint: '/District_ListFilter1/District_ListFilter11',
// valueKey: 'district_name',
// displayKey: 'district_name',
// dependentValueKey:
// 'state_name', // This is the field from state that gets passed to API
// ),
// ];
// }
// /// Example field definitions for a product form with category -> subcategory -> product
// static List<BaseField> getProductFields() {
// return [
// // Parent dropdown - Category selection
// DropdownField(
// fieldKey: 'category',
// label: 'Category',
// hint: 'Select category',
// options: const [
// {'id': 'electronics', 'name': 'Electronics'},
// {'id': 'clothing', 'name': 'Clothing'},
// {'id': 'books', 'name': 'Books'},
// ],
// valueKey: 'id',
// displayKey: 'name',
// ),
// // Dependent dropdown - Subcategory selection (depends on category)
// DependentDropdownField(
// fieldKey: 'subcategory',
// label: 'Subcategory',
// hint: 'Select subcategory',
// dependentFieldKey: 'category',
// apiEndpoint: '/Subcategory_ListFilter1/Subcategory_ListFilter11',
// valueKey: 'subcategory_id',
// displayKey: 'subcategory_name',
// dependentValueKey: 'name',
// ),
// // Dependent dropdown - Product selection (depends on subcategory)
// DependentDropdownField(
// fieldKey: 'product',
// label: 'Product',
// hint: 'Select product',
// dependentFieldKey: 'subcategory',
// apiEndpoint: '/Product_ListFilter1/Product_ListFilter11',
// valueKey: 'product_id',
// displayKey: 'product_name',
// dependentValueKey: 'subcategory_name',
// ),
// ];
// }
// }
// /// How the DependentDropdownField works:
// ///
// /// 1. **Parent Field Selection**: When user selects a value in the parent field (e.g., country)
// /// 2. **API Call Triggered**: The dependent field automatically detects the change
// /// 3. **API Call Made**: Calls the specified API endpoint with the parent field value
// /// 4. **Options Loaded**: The API response is parsed and options are populated
// /// 5. **UI Updated**: The dropdown shows the new options
// /// 6. **Cascading Effect**: If there are more dependent fields, they get cleared and wait for new selection
// ///
// /// **API Endpoint Format**:
// /// - Base URL: `ApiConstants.baseUrl`
// /// - Endpoint: `/State_ListFilter1/State_ListFilter11/{dependentValue}`
// /// - Example: `https://api.example.com/State_ListFilter1/State_ListFilter11/india`
// ///
// /// **API Response Format**:
// /// ```json
// /// [
// /// {
// /// "state_name": "Maharashtra",
// /// "state_id": "1"
// /// },
// /// {
// /// "state_name": "Karnataka",
// /// "state_id": "2"
// /// }
// /// ]
// /// ```
// ///
// /// **Key Features**:
// /// - ✅ **Automatic API Calls**: No manual API handling needed
// /// - ✅ **Loading States**: Shows loading indicator while fetching data
// /// - ✅ **Error Handling**: Displays error messages if API fails
// /// - ✅ **Cascading Clear**: Dependent fields clear when parent changes
// /// - ✅ **Form Integration**: Works seamlessly with EntityForm
// /// - ✅ **Theme Support**: Uses DynamicThemeProvider for consistent styling
// /// - ✅ **Validation**: Built-in required field validation
// /// - ✅ **Responsive**: Works on all screen sizes
// ///
// /// **Usage in Entity**:
// /// ```dart
// /// // In your entity fields file
// /// class MyEntityFields {
// /// static List<BaseField> getFields() {
// /// return [
// /// DropdownField(
// /// fieldKey: 'country',
// /// label: 'Country',
// /// options: countryOptions,
// /// ),
// /// DependentDropdownField(
// /// fieldKey: 'state',
// /// label: 'State',
// /// dependentFieldKey: 'country',
// /// apiEndpoint: '/State_ListFilter1/State_ListFilter11',
// /// valueKey: 'state_name',
// /// displayKey: 'state_name',
// /// dependentValueKey: 'name',
// /// ),
// /// ];
// /// }
// /// }
// /// ```

View File

@@ -0,0 +1,254 @@
// working code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'base_field.dart';
import 'dynamic_dropdown_field.dart';
import '../ui/entity_form.dart';
/// Dependent dropdown field that loads options based on another field's value
/// This field automatically makes API calls when the dependent field changes
class DependentDropdownField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final String dependentFieldKey; // The field this dropdown depends on
final Future<List<Map<String, dynamic>>> Function(String parentValue)
optionsLoader; // Loader using parent value
final String valueKey; // Field name for value in API response
final String displayKey; // Field name for display text in API response
final Map<String, dynamic>? customProperties;
DependentDropdownField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
required this.dependentFieldKey,
required this.optionsLoader,
this.valueKey = 'id',
this.displayKey = 'name',
this.customProperties,
});
@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 _DependentDropdownWidget(
fieldKey: fieldKey,
label: label,
hint: hint,
isRequired: isRequired,
dependentFieldKey: dependentFieldKey,
optionsLoader: optionsLoader,
valueKey: valueKey,
displayKey: displayKey,
controller: controller,
colorScheme: colorScheme,
onChanged: onChanged,
);
}
}
class _DependentDropdownWidget extends StatefulWidget {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final String dependentFieldKey;
final Future<List<Map<String, dynamic>>> Function(String parentValue)
optionsLoader;
final String valueKey;
final String displayKey;
final TextEditingController controller;
final ColorScheme colorScheme;
final VoidCallback? onChanged;
const _DependentDropdownWidget({
required this.fieldKey,
required this.label,
required this.hint,
required this.isRequired,
required this.dependentFieldKey,
required this.optionsLoader,
required this.valueKey,
required this.displayKey,
required this.controller,
required this.colorScheme,
this.onChanged,
});
@override
State<_DependentDropdownWidget> createState() =>
_DependentDropdownWidgetState();
}
class _DependentDropdownWidgetState extends State<_DependentDropdownWidget> {
String? _lastDependentValue;
Key _reloadKey = const ValueKey('init');
@override
void initState() {
super.initState();
// Defer inherited widget access until after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_checkDependentFieldChange();
}
});
}
void _loadInitialOptions() async {}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_checkDependentFieldChange();
}
void _checkDependentFieldChange() {
final formScope = EntityFormScope.of(context);
if (formScope != null) {
final dependentController =
formScope.controllers[widget.dependentFieldKey];
if (dependentController != null) {
final currentValue = dependentController.text;
if (currentValue != _lastDependentValue) {
_lastDependentValue = currentValue;
// Defer mutations to next frame to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
// Clear current selection when parent changes
widget.controller.clear();
// Force re-init of inner DynamicDropdown by changing key
setState(() {
_reloadKey = ValueKey(
'dep-${widget.fieldKey}-${_lastDependentValue ?? 'empty'}-${DateTime.now().millisecondsSinceEpoch}');
});
// Notify parent about change
widget.onChanged?.call();
});
}
}
}
}
@override
Widget build(BuildContext context) {
Future<List<Map<String, dynamic>>> loadOptions() async {
// If no dependent value selected, return empty
final formScope = EntityFormScope.of(context);
final depVal =
formScope?.controllers[widget.dependentFieldKey]?.text ?? '';
if (depVal.isEmpty) return const [];
try {
final response = await widget.optionsLoader(depVal);
return response
.map((item) => {
widget.valueKey: item[widget.valueKey]?.toString() ?? '',
widget.displayKey: item[widget.displayKey]?.toString() ?? '',
})
.toList();
} catch (e) {
// Return empty list on error to prevent showing old data
return const [];
}
}
final innerField = DynamicDropdownField(
fieldKey: widget.fieldKey,
label: widget.label,
hint: widget.hint,
isRequired: widget.isRequired,
optionsLoader: loadOptions,
valueKey: widget.valueKey,
displayKey: widget.displayKey,
);
final child = innerField.buildField(
controller: widget.controller,
colorScheme: widget.colorScheme,
onChanged: widget.onChanged,
);
// Read current parent value for UI guard/notice
final formScope = EntityFormScope.of(context);
final depVal = formScope?.controllers[widget.dependentFieldKey]?.text ?? '';
final keyedChild = KeyedSubtree(key: _reloadKey, child: child);
final content = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// If parent is empty, block interactions and show subtle disabled state
if (depVal.isEmpty)
Opacity(
opacity: 0.6,
child: AbsorbPointer(child: keyedChild),
)
else
keyedChild,
if (depVal.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 6.0),
child: Text(
'Please select ${widget.dependentFieldKey} first',
style: TextStyle(
color: widget.colorScheme.onSurface.withOpacity(0.7),
fontSize: 12,
),
),
),
],
);
return content;
}
}
/// Helper class to manage dependent dropdown relationships
/// This should be used in EntityForm to handle field dependencies
class DependentDropdownManager {
static final Map<String, List<DependentDropdownField>> _dependencies = {};
/// Register a dependent dropdown field
static void registerDependency(
String dependentFieldKey, DependentDropdownField field) {
if (!_dependencies.containsKey(dependentFieldKey)) {
_dependencies[dependentFieldKey] = [];
}
_dependencies[dependentFieldKey]!.add(field);
}
/// Get all dependent fields for a given field
static List<DependentDropdownField> getDependentFields(String fieldKey) {
return _dependencies[fieldKey] ?? [];
}
/// Clear all dependencies (useful for form reset)
static void clearDependencies() {
_dependencies.clear();
}
/// Notify dependent fields when a field value changes
static void notifyDependentFields(String fieldKey, String value) {
final dependentFields = getDependentFields(fieldKey);
for (final field in dependentFields) {
// This would need to be implemented with proper context
// The actual implementation would be in EntityForm
}
}
}

View File

@@ -0,0 +1,54 @@
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; // id field in option map
final String displayKey; // display field in option map
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(
label: label,
options: options,
valueField: valueKey, // id
uiField: displayKey, // label
value: controller.text.isNotEmpty ? controller.text : null,
onChanged: (val) {
controller.text = val ?? '';
if (onChanged != null) onChanged();
},
onSaved: (val) {
controller.text = val ?? '';
},
);
}
}

View File

@@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Dynamic single-select dropdown (no Autocomplete)
/// - Opens a modal list with search
/// - Stores selected id in controller, displays label
class DynamicDropdownField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<List<Map<String, dynamic>>> Function() optionsLoader;
final String valueKey;
final String displayKey;
DynamicDropdownField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
required this.optionsLoader,
required this.valueKey,
required this.displayKey,
});
@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 _DynamicDropdownWidget(
label: label,
hint: hint,
controller: controller,
colorScheme: colorScheme,
optionsLoader: optionsLoader,
valueKey: valueKey,
displayKey: displayKey,
validate: validator,
onChanged: onChanged,
);
}
}
class _DynamicDropdownWidget extends StatefulWidget {
final String label;
final String hint;
final TextEditingController controller;
final ColorScheme colorScheme;
final Future<List<Map<String, dynamic>>> Function() optionsLoader;
final String valueKey;
final String displayKey;
final String? Function(String?)? validate;
final VoidCallback? onChanged;
const _DynamicDropdownWidget({
required this.label,
required this.hint,
required this.controller,
required this.colorScheme,
required this.optionsLoader,
required this.valueKey,
required this.displayKey,
required this.validate,
required this.onChanged,
});
@override
State<_DynamicDropdownWidget> createState() => _DynamicDropdownWidgetState();
}
class _DynamicDropdownWidgetState extends State<_DynamicDropdownWidget> {
List<Map<String, dynamic>> _options = const [];
String _uiLabel = '';
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final opts = await widget.optionsLoader();
setState(() {
_options = opts;
_loading = false;
});
if (widget.controller.text.isNotEmpty) {
final match = _options.firstWhere(
(o) =>
(o[widget.valueKey]?.toString() ?? '') == widget.controller.text,
orElse: () => const {},
);
setState(() {
_uiLabel = match.isNotEmpty
? (match[widget.displayKey]?.toString() ?? '')
: '';
});
}
} catch (_) {
setState(() => _loading = false);
}
}
Future<void> _openPicker() async {
final ColorScheme cs = widget.colorScheme;
String query = '';
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: cs.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
List<Map<String, dynamic>> filtered = _options;
return StatefulBuilder(
builder: (context, setSheetState) {
void applyFilter(String q) {
query = q;
setSheetState(() {
filtered = _options
.where((o) => (o[widget.displayKey]?.toString() ?? '')
.toLowerCase()
.contains(q.toLowerCase()))
.toList();
});
}
return SafeArea(
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 16,
right: 16,
top: 12,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(
hintText: 'Search ${widget.label.toLowerCase()}',
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
),
onChanged: applyFilter,
),
const SizedBox(height: 12),
Flexible(
child: filtered.isEmpty
? Center(
child: Text('No results',
style: TextStyle(color: cs.onSurfaceVariant)),
)
: ListView.separated(
shrinkWrap: true,
itemCount: filtered.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: cs.outline.withOpacity(0.08)),
itemBuilder: (context, index) {
final o = filtered[index];
final id = o[widget.valueKey]?.toString() ?? '';
final name =
o[widget.displayKey]?.toString() ?? '';
final isSelected = widget.controller.text == id;
return ListTile(
title: Text(name),
trailing: isSelected
? Icon(Icons.check, color: cs.primary)
: null,
onTap: () {
setState(() {
widget.controller.text = id;
_uiLabel = name;
});
widget.onChanged?.call();
Navigator.of(context).pop();
},
);
},
),
),
const SizedBox(height: 8),
],
),
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const LinearProgressIndicator(minHeight: 2);
}
return ModernTextField(
label: widget.label,
hint: widget.hint,
readOnly: true,
controller: TextEditingController(text: _uiLabel),
validator: (v) => widget.validate?.call(widget.controller.text),
onTap: _openPicker,
suffixIcon: const Icon(Icons.arrow_drop_down),
);
}
}

View File

@@ -0,0 +1,253 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Dynamic multi-select dropdown (no Autocomplete)
/// - Modal list with search and checkboxes
/// - Stores comma-separated selected labels in controller
class DynamicMultiSelectDropdownField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<List<String>> Function() optionsLoader;
DynamicMultiSelectDropdownField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
required this.optionsLoader,
});
@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 _DynamicMultiSelectWidget(
label: label,
hint: hint,
controller: controller,
colorScheme: colorScheme,
optionsLoader: optionsLoader,
validate: validator,
onChanged: onChanged,
);
}
}
class _DynamicMultiSelectWidget extends StatefulWidget {
final String label;
final String hint;
final TextEditingController controller;
final ColorScheme colorScheme;
final Future<List<String>> Function() optionsLoader;
final String? Function(String?)? validate;
final VoidCallback? onChanged;
const _DynamicMultiSelectWidget({
required this.label,
required this.hint,
required this.controller,
required this.colorScheme,
required this.optionsLoader,
required this.validate,
required this.onChanged,
});
@override
State<_DynamicMultiSelectWidget> createState() =>
_DynamicMultiSelectWidgetState();
}
class _DynamicMultiSelectWidgetState extends State<_DynamicMultiSelectWidget> {
List<String> _options = const [];
late final Set<String> _selected;
bool _loading = true;
@override
void initState() {
super.initState();
_selected = widget.controller.text.isEmpty
? <String>{}
: widget.controller.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toSet();
_load();
}
Future<void> _load() async {
try {
final opts = await widget.optionsLoader();
setState(() {
_options = opts;
_loading = false;
});
} catch (_) {
setState(() => _loading = false);
}
}
Future<void> _openPicker() async {
final ColorScheme cs = widget.colorScheme;
String query = '';
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: cs.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
List<String> filtered = _options;
return StatefulBuilder(
builder: (context, setSheetState) {
void applyFilter(String q) {
query = q;
setSheetState(() {
filtered = _options
.where((o) => o.toLowerCase().contains(q.toLowerCase()))
.toList();
});
}
void toggle(String value) {
setSheetState(() {
if (_selected.contains(value)) {
_selected.remove(value);
} else {
_selected.add(value);
}
});
}
return SafeArea(
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 16,
right: 16,
top: 12,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(
hintText: 'Search ${widget.label.toLowerCase()}',
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
),
onChanged: applyFilter,
),
const SizedBox(height: 12),
Flexible(
child: filtered.isEmpty
? Center(
child: Text('No results',
style: TextStyle(color: cs.onSurfaceVariant)),
)
: ListView.separated(
shrinkWrap: true,
itemCount: filtered.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: cs.outline.withOpacity(0.08)),
itemBuilder: (context, index) {
final name = filtered[index];
final isSelected = _selected.contains(name);
return CheckboxListTile(
title: Text(name),
value: isSelected,
onChanged: (_) => toggle(name),
);
},
),
),
const SizedBox(height: 8),
Row(
children: [
TextButton(
onPressed: () {
setState(() {
widget.controller.text = '';
});
_selected.clear();
widget.onChanged?.call();
Navigator.of(context).pop();
},
child: const Text('Clear'),
),
const Spacer(),
ElevatedButton(
onPressed: () {
setState(() {
widget.controller.text = _selected.join(',');
});
widget.onChanged?.call();
Navigator.of(context).pop();
},
child: const Text('Done'),
),
],
),
const SizedBox(height: 8),
],
),
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const LinearProgressIndicator(minHeight: 2);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ModernTextField(
label: widget.label,
hint: widget.hint,
readOnly: true,
controller: TextEditingController(text: _selected.join(', ')),
validator: widget.validate,
onTap: _openPicker,
suffixIcon: const Icon(Icons.arrow_drop_down),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _selected
.map((v) => Chip(
label: Text(v),
onDeleted: () {
setState(() {
_selected.remove(v);
widget.controller.text = _selected.join(',');
});
widget.onChanged?.call();
},
))
.toList(),
),
],
);
}
}

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,57 @@
import 'package:flutter/material.dart';
import '../ui/entity_screens.dart';
import '../utils/entity_field_store.dart';
typedef UploadHandler = Future<dynamic> Function(
String entityId, String entityName, String fileName, dynamic bytes);
typedef CreateAndReturnId = Future<int> Function(Map<String, dynamic> data);
/// Universal wrapper: Create entity first, then upload files collected by shared upload fields
class EntityCreateWithUploads extends StatelessWidget {
final String title;
final List fields; // List<BaseField>
final CreateAndReturnId createAndReturnId;
final Map<String, UploadHandler>
uploadHandlersByFieldKey; // fieldKey -> uploader
final String entityName; // e.g., 'Adv1'
final bool isLoading;
final String? errorMessage;
const EntityCreateWithUploads({
super.key,
required this.title,
required this.fields,
required this.createAndReturnId,
required this.uploadHandlersByFieldKey,
required this.entityName,
this.isLoading = false,
this.errorMessage,
});
Future<void> _uploadAll(int id) async {
final store = EntityFieldStore.instance;
for (final entry in uploadHandlersByFieldKey.entries) {
final String key = entry.key;
final handler = entry.value;
final items = store.get<List<UploadItem>>(key) ?? const [];
for (final item in items) {
await handler(id.toString(), entityName, item.fileName, item.bytes);
}
}
}
@override
Widget build(BuildContext context) {
return EntityCreateScreen(
title: title,
fields: fields.cast(),
isLoading: isLoading,
errorMessage: errorMessage,
onSubmit: (data) async {
final id = await createAndReturnId(data);
await _uploadAll(id);
EntityFieldStore.instance.clear();
},
);
}
}

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,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,583 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../ui/entity_form.dart';
import 'dart:convert';
import 'custom_text_field.dart' as FText;
import 'number_field.dart' as FNumber;
import 'email_field.dart' as FEmail;
import 'phone_field.dart' as FPhone;
import 'password_field.dart' as FPassword;
import 'dropdown_field.dart' as FDropdown;
import 'date_field.dart' as FDate;
import 'datetime_field.dart' as FDateTime;
import 'switch_field.dart' as FSwitch;
import 'checkbox_field.dart' as FCheckbox;
import 'captcha_field.dart' as FCaptcha;
import 'url_field.dart' as FUrl;
import 'autocomplete_dropdown_field.dart' as FAutoDropdown;
import 'autocomplete_multiselect_field.dart' as FAutoMultiSelect;
/// One-to-many subform builder
/// - Renders repeatable group of sub-fields
/// - Stores JSON array (as string) in controller
class OneToManyField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final List<BaseField>? subFields;
final List<Map<String, dynamic>>? fieldSchema;
OneToManyField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
this.subFields,
this.fieldSchema,
}) : assert(subFields != null || fieldSchema != null,
'Either subFields or fieldSchema must be provided');
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
return null;
};
@override
Map<String, dynamic>? get customProperties => {
'isOneToMany': true,
'excludeFromSubmit': false,
'parseAsJson': true, // Parse JSON string to object before submission
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _OneToManyWidget(
label: label,
controller: controller,
colorScheme: colorScheme,
subFields: subFields,
fieldSchema: fieldSchema,
validate: validator,
onChanged: onChanged,
);
}
}
class _OneToManyWidget extends StatefulWidget {
final String label;
final TextEditingController controller;
final ColorScheme colorScheme;
final List<BaseField>? subFields;
final List<Map<String, dynamic>>? fieldSchema;
final String? Function(String?)? validate;
final VoidCallback? onChanged;
const _OneToManyWidget({
required this.label,
required this.controller,
required this.colorScheme,
this.subFields,
this.fieldSchema,
required this.validate,
required this.onChanged,
});
@override
State<_OneToManyWidget> createState() => _OneToManyWidgetState();
}
class _OneToManyWidgetState extends State<_OneToManyWidget> {
final List<Map<String, TextEditingController>> _rows = [];
@override
void initState() {
super.initState();
// Use WidgetsBinding to defer the initialization after build
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeRows();
});
}
void _initializeRows() {
if (widget.controller.text.isNotEmpty) {
// Try to parse as JSON first
try {
final decoded = const JsonDecoder().convert(widget.controller.text);
if (decoded is List) {
for (final item in decoded) {
if (item is Map) {
final prefillData = <String, String>{};
item.forEach((key, value) {
prefillData[key.toString()] = value?.toString() ?? '';
});
_addRow(prefill: prefillData);
}
}
}
} catch (e) {
// If JSON parsing fails, the data might already be in object format
// This happens when data comes from backend API response
print('JSON parsing failed, trying to handle as object: $e');
// Try to handle the data as if it's already parsed
try {
// Check if controller text looks like a list representation
final text = widget.controller.text.trim();
if (text.startsWith('[') && text.endsWith(']')) {
// This might be a string representation of a list
// Try to extract the data specifically
_handleBackendDataFormat(text);
}
} catch (e2) {
print('Error handling backend data format: $e2');
}
}
}
// Always add at least one empty row if no data exists
if (_rows.isEmpty) _addRow();
}
void _handleBackendDataFormat(String data) {
// Handle backend data format: [{id: 21, name: g1, description: gdesc}, {id: 22, name: g2, description: g2desc}]
try {
// Remove outer brackets and split by }, {
String cleanData = data.trim();
if (cleanData.startsWith('[') && cleanData.endsWith(']')) {
cleanData = cleanData.substring(1, cleanData.length - 1);
}
// Split by }, { to get individual objects
List<String> objects = [];
if (cleanData.contains('}, {')) {
objects = cleanData.split('}, {');
// Fix the first and last objects
if (objects.isNotEmpty) {
objects[0] = objects[0].replaceFirst('{', '');
objects[objects.length - 1] =
objects[objects.length - 1].replaceFirst('}', '');
}
} else if (cleanData.startsWith('{') && cleanData.endsWith('}')) {
// Single object
objects = [cleanData.substring(1, cleanData.length - 1)];
}
// Parse each object
for (String obj in objects) {
final prefillData = <String, String>{};
// Split by comma to get key-value pairs
List<String> pairs = obj.split(', ');
for (String pair in pairs) {
if (pair.contains(':')) {
List<String> keyValue = pair.split(': ');
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
// Remove null values
if (value != 'null') {
prefillData[key] = value;
}
}
}
}
// Only add row if we have meaningful data
if (prefillData.isNotEmpty) {
_addRow(prefill: prefillData);
}
}
print('Successfully parsed ${objects.length} objects from backend data');
} catch (e) {
print('Error parsing backend data format: $e');
// Fallback: add empty row
}
}
void _addRow({Map<String, String>? prefill}) {
final row = <String, TextEditingController>{};
if (widget.subFields != null) {
// Legacy mode: using subFields
for (final f in widget.subFields!) {
row[f.fieldKey] =
TextEditingController(text: prefill?[f.fieldKey] ?? '');
}
} else if (widget.fieldSchema != null) {
// Schema mode: using fieldSchema
for (final f in widget.fieldSchema!) {
final String path = f['path']?.toString() ?? '';
if (path.isNotEmpty) {
row[path] = TextEditingController(text: prefill?[path] ?? '');
}
}
}
setState(() => _rows.add(row));
_syncToParent();
}
void _removeRow(int index) {
if (index < 0 || index >= _rows.length) return;
final removed = _rows.removeAt(index);
for (final c in removed.values) {
c.dispose();
}
setState(() {});
_syncToParent();
}
void _syncToParent() {
final list = <Map<String, String>>[];
for (final row in _rows) {
final map = <String, String>{};
row.forEach((k, c) => map[k] = c.text.trim());
list.add(map);
}
widget.controller.text = const JsonEncoder().convert(list);
widget.onChanged?.call();
EntityFormScope.of(context)?.notifyParent();
}
List<Widget> _buildFields(
Map<String, TextEditingController> ctrls, ColorScheme cs) {
if (widget.subFields != null) {
// Legacy mode: using subFields
return widget.subFields!
.map((f) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (f.label != null && f.label!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
f.label!,
style: TextStyle(
fontWeight: FontWeight.w600,
color: cs.onSurface,
fontSize: 14,
),
),
),
f.buildField(
controller: ctrls[f.fieldKey]!,
colorScheme: cs,
onChanged: _syncToParent,
),
],
),
))
.toList();
} else if (widget.fieldSchema != null) {
// Schema mode: using fieldSchema
return widget.fieldSchema!
.map((f) => _buildSchemaField(f, ctrls, cs))
.toList();
}
return [];
}
Widget _buildSchemaField(Map<String, dynamic> f,
Map<String, TextEditingController> ctrls, ColorScheme cs) {
final String type = f['type']?.toString() ?? 'text';
final String flabel = f['label']?.toString() ?? '';
final String fhint = f['hint']?.toString() ?? '';
final String path = f['path']?.toString() ?? '';
final bool requiredField = f['required'] == true;
final List<Map<String, dynamic>>? options =
(f['options'] as List?)?.cast<Map<String, dynamic>>();
final String valueKey = f['valueKey']?.toString() ?? 'id';
final String displayKey = f['displayKey']?.toString() ?? 'name';
final int? maxLength = f['maxLength'] is int ? f['maxLength'] as int : null;
final int? decimalPlaces =
f['decimalPlaces'] is int ? f['decimalPlaces'] as int : null;
final double? min = (f['min'] is num) ? (f['min'] as num).toDouble() : null;
final double? max = (f['max'] is num) ? (f['max'] as num).toDouble() : null;
final TextEditingController subController = ctrls[path]!;
Widget buildShared() {
switch (type) {
case 'text':
return FText.CustomTextField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
maxLength: maxLength,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'number':
return FNumber.NumberField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
min: min,
max: max,
decimalPlaces: decimalPlaces,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'email':
return FEmail.EmailField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'phone':
return FPhone.PhoneField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'password':
return FPassword.PasswordField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'dropdown':
return FDropdown.DropdownField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
options: options ?? const [],
valueKey: valueKey,
displayKey: displayKey,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'date':
return FDate.DateField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'datetime':
return FDateTime.DateTimeField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'switch':
return FSwitch.SwitchField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'checkbox':
return FCheckbox.CheckboxField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'url':
return FUrl.UrlField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'autocomplete_dropdown':
return FAutoDropdown.AutocompleteDropdownField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
optionsLoader: () async => options ?? [],
valueKey: valueKey,
displayKey: displayKey,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
case 'autocomplete_multiselect':
return FAutoMultiSelect.AutocompleteMultiSelectField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
optionsLoader: () async => options ?? [],
valueKey: valueKey,
displayKey: displayKey,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
default:
return FText.CustomTextField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: cs,
onChanged: _syncToParent,
);
}
}
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (flabel.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
flabel,
style: TextStyle(
fontWeight: FontWeight.w600,
color: cs.onSurface,
fontSize: 14,
),
),
),
buildShared(),
],
),
);
}
@override
Widget build(BuildContext context) {
final cs = widget.colorScheme;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.primary.withOpacity(0.15)),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.label,
style: TextStyle(
color: cs.onSurface, fontWeight: FontWeight.w700)),
const SizedBox(height: 8),
..._rows.asMap().entries.map((entry) {
final index = entry.key;
final ctrls = entry.value;
return Card(
elevation: 0,
color: cs.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: cs.outline.withOpacity(0.12)),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
..._buildFields(ctrls, cs),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: Icon(Icons.delete_outline, color: cs.error),
tooltip: 'Remove',
onPressed: () => _removeRow(index),
),
),
],
),
),
);
}),
const SizedBox(height: 12),
// Add button at the bottom
Center(
child: ElevatedButton.icon(
onPressed: () => _addRow(),
icon: Icon(Icons.add, color: cs.onPrimary),
label: Text('Add ${widget.label}',
style: TextStyle(color: cs.onPrimary)),
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
),
if (widget.validate != null)
Builder(
builder: (context) {
final error = widget.validate!(widget.controller.text);
return error != null
? Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(error,
style: TextStyle(color: cs.error, fontSize: 12)),
)
: const SizedBox.shrink();
},
)
],
),
),
);
}
}

View File

@@ -0,0 +1,322 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'base_field.dart';
import 'captcha_field.dart' as FCaptcha;
import 'custom_text_field.dart' as FText;
import 'date_field.dart' as FDate;
import 'datetime_field.dart' as FDateTime;
import 'dropdown_field.dart' as FDropdown;
import 'email_field.dart' as FEmail;
import 'number_field.dart' as FNumber;
import 'password_field.dart' as FPassword;
import 'phone_field.dart' as FPhone;
import 'switch_field.dart' as FSwitch;
import 'url_field.dart' as FUrl;
class OneToOneRelationField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Map<String, dynamic> relationSchema;
OneToOneRelationField({
required this.fieldKey,
required this.label,
required this.hint,
required this.relationSchema,
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (value) {
// If not required, allow empty
if (!isRequired) return null;
// Required: ensure at least one inner field has a non-empty value
if (value == null || value.isEmpty) {
return '$label is required';
}
try {
final decoded = json.decode(value);
if (decoded is! Map<String, dynamic>) {
return '$label is required';
}
final List fields = (relationSchema['fields'] as List?) ?? const [];
for (final f in fields) {
final String? path = f['path']?.toString();
if (path == null) continue;
final dynamic v = decoded[path];
if (v != null && v.toString().trim().isNotEmpty) {
return null; // at least one provided
}
}
return '$label is required';
} catch (_) {
// If invalid JSON and required, treat as empty
return '$label is required';
}
};
@override
Map<String, dynamic>? get customProperties => {
'isRelation': true,
'assignByJsonPaths': true,
'paths': (relationSchema['fields'] as List?)
?.map((f) => f['path'])
.where((p) => p != null)
.toList() ??
[],
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final List fields = (relationSchema['fields'] as List?) ?? const [];
final bool boxed = relationSchema['box'] == true;
final String title = relationSchema['title']?.toString() ?? label;
Widget content = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
fontSize: 16,
),
),
),
...fields.map<Widget>((f) {
final String type = f['type']?.toString() ?? 'text';
final String flabel = f['label']?.toString() ?? '';
final String fhint = f['hint']?.toString() ?? '';
final String path = f['path']?.toString() ?? '';
final bool requiredField = f['required'] == true;
final List<Map<String, dynamic>>? options =
(f['options'] as List?)?.cast<Map<String, dynamic>>();
final String valueKey = f['valueKey']?.toString() ?? 'id';
final String displayKey = f['displayKey']?.toString() ?? 'name';
final int? maxLength =
f['maxLength'] is int ? f['maxLength'] as int : null;
final int? decimalPlaces =
f['decimalPlaces'] is int ? f['decimalPlaces'] as int : null;
final double? min =
(f['min'] is num) ? (f['min'] as num).toDouble() : null;
final double? max =
(f['max'] is num) ? (f['max'] as num).toDouble() : null;
// Initialize sub field with existing value from parent controller JSON.
// Expect flat map { 'support.field': value }. Fallback to nested traversal.
String initialValue = '';
if (controller.text.isNotEmpty) {
try {
final decoded = json.decode(controller.text);
if (decoded is Map<String, dynamic>) {
if (decoded.containsKey(path)) {
initialValue = decoded[path]?.toString() ?? '';
} else {
final parts = path.split('.');
dynamic curr = decoded;
for (final p in parts) {
if (curr is Map && curr.containsKey(p)) {
curr = curr[p];
} else {
curr = null;
break;
}
}
if (curr != null) initialValue = curr.toString();
}
}
} catch (_) {}
}
void sync(String val) {
Map<String, dynamic> current = {};
if (controller.text.isNotEmpty) {
try {
final decoded = json.decode(controller.text);
if (decoded is Map<String, dynamic>) current = decoded;
} catch (_) {}
}
// Store flat path mapping
if (val.isEmpty) {
// remove the key when emptied
current.remove(path);
} else {
current[path] = val;
}
// If map becomes empty, clear controller to avoid failing validation when optional
controller.text = current.isEmpty ? '' : json.encode(current);
onChanged?.call();
}
final TextEditingController subController =
TextEditingController(text: initialValue);
Widget buildShared() {
switch (type) {
case 'number':
return FNumber.NumberField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
min: min,
max: max,
decimalPlaces: decimalPlaces,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'email':
return FEmail.EmailField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'phone':
return FPhone.PhoneField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'password':
return FPassword.PasswordField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'dropdown':
return FDropdown.DropdownField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
options: options ?? const [],
valueKey: valueKey,
displayKey: displayKey,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'date':
return FDate.DateField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'datetime':
return FDateTime.DateTimeField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'switch':
return FSwitch.SwitchField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'captcha':
return FCaptcha.CaptchaField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'url':
return FUrl.UrlField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
case 'text':
default:
return FText.CustomTextField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
maxLength: maxLength,
).buildField(
controller: subController,
colorScheme: colorScheme,
onChanged: () => sync(subController.text),
);
}
}
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: buildShared(),
);
}).toList(),
],
);
final Widget body = boxed
? Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colorScheme.outline.withOpacity(0.4)),
),
child: content,
)
: content;
return Directionality(textDirection: TextDirection.ltr, child: body);
}
}

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,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

@@ -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,651 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../core/providers/dynamic_theme_provider.dart';
import '../ui/entity_form.dart';
import 'autocomplete_dropdown_field.dart' as FAutoDropdown;
import 'autocomplete_multiselect_field.dart' as FAutoMultiSelect;
import 'base_field.dart';
import 'captcha_field.dart' as FCaptcha;
import 'checkbox_field.dart' as FCheckbox;
import 'custom_text_field.dart' as FText;
import 'date_field.dart' as FDate;
import 'datetime_field.dart' as FDateTime;
import 'dropdown_field.dart' as FDropdown;
import 'email_field.dart' as FEmail;
import 'number_field.dart' as FNumber;
import 'password_field.dart' as FPassword;
import 'phone_field.dart' as FPhone;
import 'switch_field.dart' as FSwitch;
import 'url_field.dart' as FUrl;
/// A generic field that shows an Insert button to create a related entity inline.
///
/// Usage:
/// - Provide `fieldKey` for where to store the selected/created value
/// - Provide `label` for display
/// - Provide `relationTitle` for the modal header
/// - Either provide `fieldSchema` (preferred, like OneToOne/OneToMany) or `formFieldsBuilder`
/// - Provide `onCreate` callback which receives form data and returns created entity map
/// - Configure `valueKey` to pick what to write back (e.g., 'id') into the parent controller
/// - Optionally set `displayKey` to show a summary of the selected value
class RelatedEntityInsertField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final String relationTitle;
final Future<Map<String, dynamic>> Function(Map<String, dynamic> formData)
onCreate;
final List<Map<String, dynamic>>? fieldSchema;
final List<BaseField> Function()? formFieldsBuilder;
final String valueKey;
final String? displayKey;
RelatedEntityInsertField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
required this.relationTitle,
required this.onCreate,
this.fieldSchema,
this.formFieldsBuilder,
this.valueKey = 'id',
this.displayKey,
}) : assert(fieldSchema != null || formFieldsBuilder != null,
'Either fieldSchema or formFieldsBuilder must be provided');
@override
String? Function(String?)? get validator => (value) {
if (!isRequired) return null;
if (value == null || value.trim().isEmpty) return '$label is required';
return null;
};
@override
Map<String, dynamic>? get customProperties => {
'isRelationInsert': true,
'relationTitle': relationTitle,
'valueKey': valueKey,
'displayKey': displayKey,
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _RelatedEntityInsertWidget(
label: label,
hint: hint,
controller: controller,
colorScheme: colorScheme,
relationTitle: relationTitle,
onCreate: onCreate,
fieldSchema: fieldSchema,
formFieldsBuilder: formFieldsBuilder,
valueKey: valueKey,
displayKey: displayKey,
validate: validator,
onChanged: onChanged,
);
}
/// Static helper to open the insert dialog anywhere (e.g., in list actions)
/// Returns the written value (e.g., created id as string) or null if cancelled
static Future<String?> showInsertDialog({
required BuildContext context,
required String relationTitle,
required Future<Map<String, dynamic>> Function(Map<String, dynamic>)
onCreate,
List<Map<String, dynamic>>? fieldSchema,
List<BaseField> Function()? formFieldsBuilder,
String valueKey = 'id',
String? displayKey,
}) async {
final TextEditingController ctrl = TextEditingController();
bool isSubmitting = false;
final List<BaseField> fields = (formFieldsBuilder != null)
? formFieldsBuilder()
: _fieldsFromSchemaStatic(fieldSchema ?? const []);
final Map<String, dynamic>? created =
await showDialog<Map<String, dynamic>>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return Consumer<DynamicThemeProvider>(
builder: (context, theme, child) {
final cs = theme.getCurrentColorScheme(
Theme.of(dialogContext).brightness == Brightness.dark);
return StatefulBuilder(
builder: (context, setLocal) {
return AlertDialog(
backgroundColor: cs.surface,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(UIConstants.radius16)),
title: Text(
relationTitle,
style: TextStyle(
color: cs.onSurface, fontWeight: FontWeight.w700),
),
content: SingleChildScrollView(
child: EntityForm(
fields: fields,
onSubmit: (formData) async {
if (isSubmitting) return;
setLocal(() => isSubmitting = true);
try {
final res = await onCreate(formData);
if (!Navigator.of(dialogContext).mounted) return;
Navigator.of(dialogContext).pop(res);
} catch (_) {
setLocal(() => isSubmitting = false);
}
},
submitButtonText: 'Create',
isLoading: isSubmitting,
),
),
actions: [
TextButton(
onPressed: isSubmitting
? null
: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
],
);
},
);
},
);
},
);
if (created != null) {
ctrl.text = (created[valueKey])?.toString() ?? '';
return ctrl.text.isEmpty ? null : ctrl.text;
}
return null;
}
// Static schema → fields mapper for the dialog helper
static List<BaseField> _fieldsFromSchemaStatic(
List<Map<String, dynamic>> schema) {
return schema.map<BaseField>((f) {
final String type = f['type']?.toString() ?? 'text';
final String flabel = f['label']?.toString() ?? '';
final String fhint = f['hint']?.toString() ?? '';
final String path = f['path']?.toString() ?? '';
final bool requiredField = f['required'] == true;
final List<Map<String, dynamic>>? options =
(f['options'] as List?)?.cast<Map<String, dynamic>>();
final String valueKey = f['valueKey']?.toString() ?? 'id';
final String displayKey = f['displayKey']?.toString() ?? 'name';
final int? maxLength =
f['maxLength'] is int ? f['maxLength'] as int : null;
final int? decimalPlaces =
f['decimalPlaces'] is int ? f['decimalPlaces'] as int : null;
final double? min =
(f['min'] is num) ? (f['min'] as num).toDouble() : null;
final double? max =
(f['max'] is num) ? (f['max'] as num).toDouble() : null;
switch (type) {
case 'number':
return FNumber.NumberField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
min: min,
max: max,
decimalPlaces: decimalPlaces,
);
case 'email':
return FEmail.EmailField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'phone':
return FPhone.PhoneField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'password':
return FPassword.PasswordField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'dropdown':
return FDropdown.DropdownField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
options: options ?? const [],
valueKey: valueKey,
displayKey: displayKey,
);
case 'date':
return FDate.DateField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'datetime':
return FDateTime.DateTimeField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'switch':
return FSwitch.SwitchField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'checkbox':
return FCheckbox.CheckboxField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'captcha':
return FCaptcha.CaptchaField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'url':
return FUrl.UrlField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'autocomplete_dropdown':
return FAutoDropdown.AutocompleteDropdownField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
optionsLoader: () async => options ?? [],
valueKey: valueKey,
displayKey: displayKey,
);
case 'autocomplete_multiselect':
return FAutoMultiSelect.AutocompleteMultiSelectField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
optionsLoader: () async => options ?? [],
valueKey: valueKey,
displayKey: displayKey,
);
case 'text':
default:
return FText.CustomTextField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
maxLength: maxLength,
);
}
}).toList();
}
}
class _RelatedEntityInsertWidget extends StatefulWidget {
final String label;
final String hint;
final TextEditingController controller;
final ColorScheme colorScheme;
final String relationTitle;
final Future<Map<String, dynamic>> Function(Map<String, dynamic>) onCreate;
final List<Map<String, dynamic>>? fieldSchema;
final List<BaseField> Function()? formFieldsBuilder;
final String valueKey;
final String? displayKey;
final String? Function(String?)? validate;
final VoidCallback? onChanged;
const _RelatedEntityInsertWidget({
required this.label,
required this.hint,
required this.controller,
required this.colorScheme,
required this.relationTitle,
required this.onCreate,
this.fieldSchema,
this.formFieldsBuilder,
required this.valueKey,
required this.displayKey,
required this.validate,
required this.onChanged,
});
@override
State<_RelatedEntityInsertWidget> createState() =>
_RelatedEntityInsertWidgetState();
}
class _RelatedEntityInsertWidgetState
extends State<_RelatedEntityInsertWidget> {
bool _creating = false;
@override
Widget build(BuildContext context) {
final cs = widget.colorScheme;
final String selectedDisplay = _computeDisplay();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.label.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
widget.label,
style: TextStyle(
fontWeight: FontWeight.w700,
color: cs.onSurface,
fontSize: 14),
),
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.primary.withOpacity(0.15)),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
cs.surface,
cs.surface.withOpacity(0.97),
],
),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
Expanded(
child: Text(
selectedDisplay.isEmpty
? (widget.hint.isEmpty ? 'No selection' : widget.hint)
: selectedDisplay,
style: TextStyle(
color: cs.onSurface.withOpacity(0.75),
fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _creating ? null : _openInsertDialog,
icon: Icon(Icons.add_rounded, color: cs.onPrimary, size: 18),
label: Text('Insert',
style: TextStyle(
color: cs.onPrimary, fontWeight: FontWeight.w600)),
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
],
),
),
if (widget.validate != null)
Builder(
builder: (context) {
final error = widget.validate!(widget.controller.text);
return error != null
? Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(error,
style: TextStyle(
color: cs.error,
fontSize: 12,
fontWeight: FontWeight.w600)),
)
: const SizedBox.shrink();
},
)
],
);
}
String _computeDisplay() {
if (widget.displayKey == null || widget.displayKey!.isEmpty) {
return widget.controller.text.trim();
}
return widget.controller.text.trim();
}
Future<void> _openInsertDialog() async {
setState(() => _creating = true);
try {
final List<BaseField> fields = (widget.formFieldsBuilder != null)
? widget.formFieldsBuilder!()
: _fieldsFromSchema(widget.fieldSchema ?? const []);
final result = await showDialog<Map<String, dynamic>>(
context: context,
barrierDismissible: false,
builder: (context) {
bool isSubmitting = false;
return StatefulBuilder(
builder: (context, setLocal) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
title: Text(
widget.relationTitle,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w700,
),
),
content: SingleChildScrollView(
child: EntityForm(
fields: fields,
onSubmit: (formData) async {
if (isSubmitting) return;
setLocal(() => isSubmitting = true);
try {
final created = await widget.onCreate(formData);
if (!mounted) return;
Navigator.of(context).pop(created);
} catch (_) {
setLocal(() => isSubmitting = false);
}
},
submitButtonText: 'Create',
isLoading: isSubmitting,
),
),
actions: [
TextButton(
onPressed:
isSubmitting ? null : () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
],
);
},
);
},
);
if (result != null) {
final dynamic value = result[widget.valueKey];
final String textValue = value?.toString() ?? '';
widget.controller.text = textValue;
widget.onChanged?.call();
EntityFormScope.of(context)?.notifyParent();
}
} finally {
if (mounted) setState(() => _creating = false);
}
}
List<BaseField> _fieldsFromSchema(List<Map<String, dynamic>> schema) {
return schema.map<BaseField>((f) {
final String type = f['type']?.toString() ?? 'text';
final String flabel = f['label']?.toString() ?? '';
final String fhint = f['hint']?.toString() ?? '';
final String path = f['path']?.toString() ?? '';
final bool requiredField = f['required'] == true;
final List<Map<String, dynamic>>? options =
(f['options'] as List?)?.cast<Map<String, dynamic>>();
final String valueKey = f['valueKey']?.toString() ?? 'id';
final String displayKey = f['displayKey']?.toString() ?? 'name';
final int? maxLength =
f['maxLength'] is int ? f['maxLength'] as int : null;
final int? decimalPlaces =
f['decimalPlaces'] is int ? f['decimalPlaces'] as int : null;
final double? min =
(f['min'] is num) ? (f['min'] as num).toDouble() : null;
final double? max =
(f['max'] is num) ? (f['max'] as num).toDouble() : null;
switch (type) {
case 'number':
return FNumber.NumberField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
min: min,
max: max,
decimalPlaces: decimalPlaces,
);
case 'email':
return FEmail.EmailField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'phone':
return FPhone.PhoneField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'password':
return FPassword.PasswordField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'dropdown':
return FDropdown.DropdownField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
options: options ?? const [],
valueKey: valueKey,
displayKey: displayKey,
);
case 'date':
return FDate.DateField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'datetime':
return FDateTime.DateTimeField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'switch':
return FSwitch.SwitchField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'checkbox':
return FCheckbox.CheckboxField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'captcha':
return FCaptcha.CaptchaField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'url':
return FUrl.UrlField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
);
case 'autocomplete_dropdown':
return FAutoDropdown.AutocompleteDropdownField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
optionsLoader: () async => options ?? [],
valueKey: valueKey,
displayKey: displayKey,
);
case 'autocomplete_multiselect':
return FAutoMultiSelect.AutocompleteMultiSelectField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
optionsLoader: () async => options ?? [],
valueKey: valueKey,
displayKey: displayKey,
);
case 'text':
default:
return FText.CustomTextField(
fieldKey: path,
label: flabel,
hint: fhint,
isRequired: requiredField,
maxLength: maxLength,
);
}
}).toList();
}
}

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
/// Static Multi-Select field
/// - Accepts a static list of display strings
/// - Stores selected values as a comma-separated string in controller
class StaticMultiSelectField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final List<String> options;
StaticMultiSelectField({
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';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final Set<String> selected = controller.text.isEmpty
? <String>{}
: controller.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toSet();
void toggle(String value) {
if (selected.contains(value)) {
selected.remove(value);
} else {
selected.add(value);
}
controller.text = selected.join(',');
onChanged?.call();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(
color: colorScheme.onSurface, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: options.map((opt) {
final bool isSel = selected.contains(opt);
return FilterChip(
label: Text(opt),
selected: isSel,
onSelected: (_) => toggle(opt),
);
}).toList(),
),
if (validator != null)
Builder(
builder: (context) {
final error = validator!(controller.text);
return error != null
? Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(error,
style: TextStyle(
color: colorScheme.error, fontSize: 12)),
)
: const SizedBox.shrink();
},
)
],
);
}
}

View File

@@ -0,0 +1,58 @@
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,
}) {
// Set default value to true if controller is empty
if (controller.text.isEmpty) {
controller.text = 'true';
}
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,377 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../ui/entity_form.dart';
/// Value List Picker
/// - Shows an icon button
/// - Loads list from API and displays as modal cards
/// - On selecting an item, fills multiple form fields based on mapping
class ValueListPickerField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final Future<List<Map<String, dynamic>>> Function() optionsLoader;
final Map<String, String> fillMappings; // sourceKey -> targetFieldKey in form
final IconData icon;
ValueListPickerField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
required this.optionsLoader,
required this.fillMappings,
this.icon = Icons.playlist_add_check,
});
@override
String? Function(String?)? get validator => null;
@override
Map<String, dynamic>? get customProperties => const {
'excludeFromSubmit': true, // helper-only, no direct submission
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _ValueListPickerWidget(
label: label,
colorScheme: colorScheme,
optionsLoader: optionsLoader,
fillMappings: fillMappings,
icon: icon,
);
}
}
class _ValueListPickerWidget extends StatefulWidget {
final String label;
final ColorScheme colorScheme;
final Future<List<Map<String, dynamic>>> Function() optionsLoader;
final Map<String, String> fillMappings;
final IconData icon;
const _ValueListPickerWidget({
required this.label,
required this.colorScheme,
required this.optionsLoader,
required this.fillMappings,
required this.icon,
});
@override
State<_ValueListPickerWidget> createState() => _ValueListPickerWidgetState();
}
class _ValueListPickerWidgetState extends State<_ValueListPickerWidget> {
bool _loading = false;
String? _loadError;
Future<void> _open() async {
if (!mounted) return;
setState(() => _loading = true);
// 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;
final selected = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: cs.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return SafeArea(
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;
}
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
Widget build(BuildContext context) {
final cs = widget.colorScheme;
return Row(
children: [
Expanded(
child: Text(widget.label,
style:
TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600)),
),
IconButton(
tooltip: 'Open ${widget.label}',
icon: _loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2))
: Icon(widget.icon, color: cs.primary),
onPressed: _loading ? null : _open,
)
],
);
}
}

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

View File

@@ -0,0 +1,492 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../core/providers/dynamic_theme_provider.dart';
/// Reusable card component for displaying entity data
/// Uses dynamic theme and provides consistent styling across all entities
class EntityCard extends StatelessWidget {
final Map<String, dynamic> entity;
final Function(Map<String, dynamic>) onEdit;
final Function(Map<String, dynamic>) onDelete;
final Function(Map<String, dynamic>)? onTap;
final List<Map<String, dynamic>> displayFields;
const EntityCard({
super.key,
required this.entity,
required this.onEdit,
required this.onDelete,
this.onTap,
this.displayFields = const [],
});
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
Theme.of(context).brightness == Brightness.dark,
);
final size = MediaQuery.of(context).size;
final isMobile = size.width < 600;
final isTablet = size.width >= 600 && size.width < 1024;
final double maxCardHeight = isMobile
? size.height * 0.34
: isTablet
? size.height * 0.38
: size.height * 0.42;
final double horizontalPadding = isMobile
? UIConstants.spacing16
: isTablet
? UIConstants.spacing20
: UIConstants.spacing24;
final double verticalPadding = isMobile
? UIConstants.spacing16
: isTablet
? UIConstants.spacing16
: UIConstants.spacing20;
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: 160,
maxHeight: maxCardHeight.clamp(160.0, size.height * 0.8),
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surface,
colorScheme.surface.withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
border: Border.all(
color: colorScheme.primary.withOpacity(0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 2,
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap != null ? () => onTap!(entity) : null,
borderRadius: BorderRadius.circular(UIConstants.radius20),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
vertical: verticalPadding,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with avatar and action buttons
Row(
children: [
_buildAvatar(colorScheme,
isMobile: isMobile, isTablet: isTablet),
const Spacer(),
_buildActionButtons(colorScheme),
],
),
const SizedBox(height: UIConstants.spacing16),
// Dynamic field display (scrollable to avoid overflow)
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildFieldDisplays(colorScheme),
),
),
),
const SizedBox(height: UIConstants.spacing12),
// Status indicator pinned at bottom
_buildStatusIndicator(colorScheme),
],
),
),
),
),
),
);
},
);
}
Widget _buildAvatar(ColorScheme colorScheme,
{required bool isMobile, required bool isTablet}) {
final double side = isMobile
? 44
: isTablet
? 50
: 56;
final double icon = isMobile
? 20
: isTablet
? 22
: 24;
return Container(
width: side,
height: side,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary,
colorScheme.primary.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius12),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Icon(
Icons.person_rounded,
color: colorScheme.onPrimary,
size: icon,
),
);
}
Widget _buildActionButtons(ColorScheme colorScheme) {
return Row(
children: [
// Edit Button
Container(
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(UIConstants.radius8),
border: Border.all(
color: colorScheme.primary.withOpacity(0.2),
width: 1,
),
),
child: IconButton(
onPressed: () => onEdit(entity),
icon: Icon(
Icons.edit_rounded,
color: colorScheme.primary,
size: 18,
),
tooltip: 'Edit',
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
),
const SizedBox(width: UIConstants.spacing8),
// Delete Button
Container(
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(UIConstants.radius8),
border: Border.all(
color: colorScheme.error.withOpacity(0.2),
width: 1,
),
),
child: IconButton(
onPressed: () => onDelete(entity),
icon: Icon(
Icons.delete_rounded,
color: colorScheme.error,
size: 18,
),
tooltip: 'Delete',
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
),
],
);
}
List<Widget> _buildFieldDisplays(ColorScheme colorScheme) {
// Dynamic field display based on entity data
final displayFields = _getDisplayFields();
return displayFields.map((field) {
final value = entity[field['key']]?.toString() ?? '';
if (value.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing8),
child: _buildFieldDisplay(field, value, colorScheme),
);
}).toList();
}
Widget _buildFieldDisplay(
Map<String, dynamic> field, String value, ColorScheme colorScheme) {
switch (field['type']) {
case 'phone':
return _buildPhoneDisplay(value, colorScheme);
case 'email':
return _buildEmailDisplay(value, colorScheme);
case 'number':
return _buildNumberDisplay(field['label'], value, colorScheme);
default:
return _buildTextDisplay(field['label'], value, colorScheme);
}
}
Widget _buildPhoneDisplay(String value, ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing8,
),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius8),
border: Border.all(
color: colorScheme.primary.withOpacity(0.1),
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.phone_rounded,
size: 16,
color: colorScheme.primary,
),
const SizedBox(width: UIConstants.spacing8),
Expanded(
child: Text(
value,
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.8),
fontSize: 13,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildEmailDisplay(String value, ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing8,
),
decoration: BoxDecoration(
color: colorScheme.secondaryContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius8),
border: Border.all(
color: colorScheme.secondary.withOpacity(0.1),
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.email_rounded,
size: 16,
color: colorScheme.secondary,
),
const SizedBox(width: UIConstants.spacing8),
Expanded(
child: Text(
value,
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.8),
fontSize: 13,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildTextDisplay(
String label, String value, ColorScheme colorScheme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (label.isNotEmpty) ...[
const SizedBox(height: UIConstants.spacing4),
Text(
label,
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 12,
fontWeight: FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
);
}
Widget _buildNumberDisplay(
String label, String value, ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing8,
),
decoration: BoxDecoration(
color: colorScheme.tertiaryContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius8),
border: Border.all(
color: colorScheme.tertiary.withOpacity(0.1),
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.numbers_rounded,
size: 16,
color: colorScheme.tertiary,
),
const SizedBox(width: UIConstants.spacing8),
Expanded(
child: Text(
value,
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.8),
fontSize: 13,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildStatusIndicator(ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing8,
vertical: UIConstants.spacing4,
),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
),
),
const SizedBox(width: UIConstants.spacing6),
Text(
'Active',
style: TextStyle(
color: colorScheme.primary,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
List<Map<String, dynamic>> _getDisplayFields() {
// Use provided displayFields or default fields
if (displayFields.isNotEmpty) {
return displayFields;
}
// Default fields - try to get meaningful fields from entity
final defaultFields = <Map<String, dynamic>>[];
// Try to find primary fields
final primaryFields = ['name', 'title', 'id'];
for (final field in primaryFields) {
if (entity.containsKey(field) && entity[field] != null) {
defaultFields.add({'key': field, 'label': '', 'type': 'text'});
break; // Only add first found primary field
}
}
// Add other non-empty fields (max 3 for card display)
int addedCount = 0;
for (final entry in entity.entries) {
if (entry.value != null &&
entry.value.toString().isNotEmpty &&
!primaryFields.contains(entry.key) &&
addedCount < 2) {
defaultFields.add({
'key': entry.key,
'label': _formatFieldLabel(entry.key),
'type': _getFieldType(entry.value)
});
addedCount++;
}
}
return defaultFields;
}
String _formatFieldLabel(String key) {
return key
.split('_')
.map((word) => word[0].toUpperCase() + word.substring(1))
.join(' ');
}
String _getFieldType(dynamic value) {
if (value is num) return 'number';
if (value.toString().contains('@')) return 'email';
if (RegExp(r'^\+?[\d\s\-\(\)]+$').hasMatch(value.toString()))
return 'phone';
return 'text';
}
}

View File

@@ -0,0 +1,584 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/providers/dynamic_theme_provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../shared/widgets/app_bar/modern_app_bar.dart';
import '../../../shared/widgets/buttons/modern_button.dart';
/// Generic details component for displaying entity information
/// This component works with any entity type and provides consistent UI/UX
class EntityDetails extends StatefulWidget {
final Map<String, dynamic> entity;
final Function(Map<String, dynamic>) onEdit;
final Function(Map<String, dynamic>) onDelete;
final String title;
final List<Map<String, dynamic>> displayFields;
final bool isLoading;
const EntityDetails({
super.key,
required this.entity,
required this.onEdit,
required this.onDelete,
required this.title,
this.displayFields = const [],
this.isLoading = false,
});
@override
State<EntityDetails> createState() => _EntityDetailsState();
}
class _EntityDetailsState extends State<EntityDetails>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
}
void _initializeAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic),
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
Theme.of(context).brightness == Brightness.dark,
);
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: ModernAppBar(
title: widget.title,
showBackButton: true,
actions: [
IconButton(
onPressed: () => widget.onEdit(widget.entity),
icon: Icon(
Icons.edit_rounded,
color: colorScheme.primary,
),
tooltip: 'Edit',
),
IconButton(
onPressed: () => _showDeleteDialog(colorScheme),
icon: Icon(
Icons.delete_rounded,
color: colorScheme.error,
),
tooltip: 'Delete',
),
],
),
body: widget.isLoading
? _buildLoadingState(colorScheme)
: _buildContent(colorScheme),
);
},
);
}
Widget _buildLoadingState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
strokeWidth: 3,
),
const SizedBox(height: UIConstants.spacing16),
Text(
'Loading details...',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildContent(ColorScheme colorScheme) {
return SingleChildScrollView(
padding: UIConstants.getResponsivePadding(
context,
mobile: UIConstants.screenPaddingMedium,
tablet: UIConstants.screenPaddingLarge,
desktop: UIConstants.screenPaddingLarge,
),
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Card
_buildHeaderCard(colorScheme),
const SizedBox(height: UIConstants.spacing24),
// Details Section
_buildDetailsSection(colorScheme),
const SizedBox(height: UIConstants.spacing32),
// Action Buttons
_buildActionButtons(colorScheme),
],
),
),
);
},
),
);
}
Widget _buildHeaderCard(ColorScheme colorScheme) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary,
colorScheme.primary.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
spreadRadius: 2,
),
],
),
child: Padding(
padding: UIConstants.cardPaddingLarge,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Avatar and Title
Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: colorScheme.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius16),
),
child: Icon(
Icons.person_rounded,
color: colorScheme.onPrimary,
size: 30,
),
),
const SizedBox(width: UIConstants.spacing16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getPrimaryFieldValue(),
style: TextStyle(
color: colorScheme.onPrimary,
fontSize: 24,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: UIConstants.spacing4),
Text(
widget.title,
style: TextStyle(
color: colorScheme.onPrimary.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
const SizedBox(height: UIConstants.spacing16),
// Status Badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing6,
),
decoration: BoxDecoration(
color: colorScheme.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: colorScheme.onPrimary,
shape: BoxShape.circle,
),
),
const SizedBox(width: UIConstants.spacing8),
Text(
'Active',
style: TextStyle(
color: colorScheme.onPrimary,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
);
}
Widget _buildDetailsSection(ColorScheme colorScheme) {
final displayFields = widget.displayFields.isNotEmpty
? widget.displayFields
: _getDefaultDisplayFields();
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surface,
colorScheme.surface.withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
border: Border.all(
color: colorScheme.primary.withOpacity(0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 2,
),
],
),
child: Padding(
padding: UIConstants.cardPaddingLarge,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Details',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
const SizedBox(height: UIConstants.spacing20),
// Field Details
...displayFields
.map((field) => _buildFieldDetail(field, colorScheme)),
],
),
),
);
}
Widget _buildFieldDetail(
Map<String, dynamic> field, ColorScheme colorScheme) {
final value = widget.entity[field['key']]?.toString() ?? '';
if (value.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
field['label'] ?? field['key'],
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
const SizedBox(height: UIConstants.spacing4),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
decoration: BoxDecoration(
color: _getFieldBackgroundColor(field['type'], colorScheme),
borderRadius: BorderRadius.circular(UIConstants.radius12),
border: Border.all(
color: _getFieldBorderColor(field['type'], colorScheme),
width: 1,
),
),
child: Row(
children: [
Icon(
_getFieldIcon(field['type']),
size: 18,
color: _getFieldIconColor(field['type'], colorScheme),
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Text(
value,
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
),
);
}
Widget _buildActionButtons(ColorScheme colorScheme) {
return Row(
children: [
Expanded(
child: ModernButton(
text: 'Edit',
type: ModernButtonType.secondary,
size: ModernButtonSize.large,
onPressed: () => widget.onEdit(widget.entity),
icon: Icon(Icons.edit_rounded),
),
),
const SizedBox(width: UIConstants.spacing16),
Expanded(
child: ModernButton(
text: 'Delete',
type: ModernButtonType.danger,
size: ModernButtonSize.large,
onPressed: () => _showDeleteDialog(colorScheme),
icon: Icon(Icons.delete_rounded),
),
),
],
);
}
void _showDeleteDialog(ColorScheme colorScheme) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UIConstants.radius20),
),
title: Text(
'Delete ${widget.title}',
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w700,
),
),
content: Text(
'Are you sure you want to delete this ${widget.title.toLowerCase()}? This action cannot be undone.',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.8),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Cancel',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontWeight: FontWeight.w600,
),
),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
widget.onDelete(widget.entity);
},
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.error,
foregroundColor: colorScheme.onError,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
),
child: const Text(
'Delete',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
);
}
String _getPrimaryFieldValue() {
// Try to get the primary field value (usually 'name' or first field)
final primaryFields = ['name', 'title', 'id'];
for (final field in primaryFields) {
final value = widget.entity[field]?.toString();
if (value != null && value.isNotEmpty) {
return value;
}
}
// If no primary field found, return first non-empty field
for (final entry in widget.entity.entries) {
if (entry.value != null && entry.value.toString().isNotEmpty) {
return entry.value.toString();
}
}
return 'Unknown';
}
List<Map<String, dynamic>> _getDefaultDisplayFields() {
return widget.entity.entries
.where(
(entry) => entry.value != null && entry.value.toString().isNotEmpty)
.map((entry) => {
'key': entry.key,
'label': _formatFieldLabel(entry.key),
'type': _getFieldType(entry.value),
})
.toList();
}
String _formatFieldLabel(String key) {
return key
.split('_')
.map((word) => word[0].toUpperCase() + word.substring(1))
.join(' ');
}
String _getFieldType(dynamic value) {
if (value is num) return 'number';
if (value.toString().contains('@')) return 'email';
if (RegExp(r'^\+?[\d\s\-\(\)]+$').hasMatch(value.toString()))
return 'phone';
return 'text';
}
Color _getFieldBackgroundColor(String type, ColorScheme colorScheme) {
switch (type) {
case 'phone':
return colorScheme.primaryContainer.withOpacity(0.2);
case 'email':
return colorScheme.secondaryContainer.withOpacity(0.2);
case 'number':
return colorScheme.tertiaryContainer.withOpacity(0.2);
default:
return colorScheme.surfaceVariant.withOpacity(0.3);
}
}
Color _getFieldBorderColor(String type, ColorScheme colorScheme) {
switch (type) {
case 'phone':
return colorScheme.primary.withOpacity(0.1);
case 'email':
return colorScheme.secondary.withOpacity(0.1);
case 'number':
return colorScheme.tertiary.withOpacity(0.1);
default:
return colorScheme.outline.withOpacity(0.2);
}
}
IconData _getFieldIcon(String type) {
switch (type) {
case 'phone':
return Icons.phone_rounded;
case 'email':
return Icons.email_rounded;
case 'number':
return Icons.numbers_rounded;
default:
return Icons.text_fields_rounded;
}
}
Color _getFieldIconColor(String type, ColorScheme colorScheme) {
switch (type) {
case 'phone':
return colorScheme.primary;
case 'email':
return colorScheme.secondary;
case 'number':
return colorScheme.tertiary;
default:
return colorScheme.onSurface.withOpacity(0.7);
}
}
}

View File

@@ -0,0 +1,693 @@
// import 'package:base_project/core/providers/dynamic_theme_provider.dart';
// import 'package:flutter/material.dart';
// import 'package:provider/provider.dart';
// import '../fields/base_field.dart';
// import '../../../shared/widgets/buttons/modern_button.dart';
// import '../../../core/constants/ui_constants.dart';
// /// Reusable form component that dynamically renders fields based on field definitions
// /// This allows UI to be independent of field types and enables reusability
// class EntityForm extends StatefulWidget {
// final List<BaseField> fields;
// final Map<String, dynamic>? initialData;
// final Function(Map<String, dynamic>) onSubmit;
// final String submitButtonText;
// final bool isLoading;
// const EntityForm({
// super.key,
// required this.fields,
// this.initialData,
// required this.onSubmit,
// this.submitButtonText = 'Submit',
// this.isLoading = false,
// });
// @override
// State<EntityForm> createState() => _EntityFormState();
// }
// class _EntityFormState extends State<EntityForm> {
// final _formKey = GlobalKey<FormState>();
// final Map<String, TextEditingController> _controllers = {};
// final Map<String, BaseField> _fieldByKey = {};
// @override
// void initState() {
// super.initState();
// _initializeControllers();
// }
// void _initializeControllers() {
// for (final field in widget.fields) {
// _controllers[field.fieldKey] = TextEditingController(
// text: widget.initialData?[field.fieldKey]?.toString() ?? '',
// );
// _fieldByKey[field.fieldKey] = field;
// }
// }
// @override
// void dispose() {
// for (final controller in _controllers.values) {
// controller.dispose();
// }
// super.dispose();
// }
// @override
// Widget build(BuildContext context) {
// return Consumer<DynamicThemeProvider>(
// builder: (context, dynamicThemeProvider, child) {
// final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
// Theme.of(context).brightness == Brightness.dark,
// );
// return Form(
// key: _formKey,
// child: Column(
// children: [
// // Dynamic field rendering
// ...widget.fields.map((field) => Padding(
// padding:
// const EdgeInsets.only(bottom: UIConstants.spacing16),
// child: field.buildField(
// controller: _controllers[field.fieldKey]!,
// colorScheme: colorScheme,
// onChanged: () => setState(() {}),
// ),
// )),
// const SizedBox(height: UIConstants.spacing24),
// // Submit button
// ModernButton(
// text: widget.submitButtonText,
// type: ModernButtonType.primary,
// size: ModernButtonSize.large,
// isLoading: widget.isLoading,
// onPressed: widget.isLoading ? null : _handleSubmit,
// ),
// ],
// ),
// );
// },
// );
// }
// void _handleSubmit() {
// if (_formKey.currentState!.validate()) {
// // Dynamic cross-field match for any password-confirm group
// final Map<String, String> passwordByGroup = {};
// final Map<String, String> confirmByGroup = {};
// for (final entry in _controllers.entries) {
// final key = entry.key;
// final field = _fieldByKey[key];
// final props = field?.customProperties ?? const {};
// final isPassword = props['isPassword'] == true;
// if (!isPassword) continue;
// final String? groupId = props['groupId'];
// if (groupId == null) continue;
// final bool isConfirm = props['isConfirm'] == true;
// if (isConfirm) {
// confirmByGroup[groupId] = entry.value.text;
// } else {
// passwordByGroup[groupId] = entry.value.text;
// }
// }
// for (final gid in confirmByGroup.keys) {
// final confirm = confirmByGroup[gid] ?? '';
// final pass = passwordByGroup[gid] ?? '';
// if (confirm.isNotEmpty && confirm != pass) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('Passwords do not match')),
// );
// return;
// }
// }
// final formData = <String, dynamic>{};
// for (final entry in _controllers.entries) {
// final key = entry.key;
// final field = _fieldByKey[key];
// final value = entry.value.text.trim();
// // Skip confirm entries for any password group
// final props = field?.customProperties ?? const {};
// final bool isPassword = props['isPassword'] == true;
// final bool isConfirm = props['isConfirm'] == true;
// if (isPassword && isConfirm) continue;
// formData[key] = value;
// }
// widget.onSubmit(formData);
// }
// }
// }
// SECOND WORKING CODE
// import 'dart:convert';
// import 'package:flutter/material.dart';
// import 'package:provider/provider.dart';
// import '../../../core/constants/ui_constants.dart';
// import '../../../core/providers/dynamic_theme_provider.dart';
// import '../../../shared/widgets/buttons/modern_button.dart';
// import '../fields/base_field.dart';
// /// Reusable form component that dynamically renders fields based on field definitions
// /// This allows UI to be independent of field types and enables reusability
// class EntityForm extends StatefulWidget {
// final List<BaseField> fields;
// final Map<String, dynamic>? initialData;
// final Function(Map<String, dynamic>) onSubmit;
// final String submitButtonText;
// final bool isLoading;
// const EntityForm({
// super.key,
// required this.fields,
// this.initialData,
// required this.onSubmit,
// this.submitButtonText = 'Submit',
// this.isLoading = false,
// });
// @override
// State<EntityForm> createState() => _EntityFormState();
// }
// class _EntityFormState extends State<EntityForm> {
// final _formKey = GlobalKey<FormState>();
// final Map<String, TextEditingController> _controllers = {};
// final Map<String, BaseField> _fieldByKey = {};
// late final Map<String, dynamic> _initialData;
// @override
// void initState() {
// super.initState();
// _initializeControllers();
// }
// void _initializeControllers() {
// _initialData = widget.initialData ?? const {};
// for (final field in widget.fields) {
// final props = field.customProperties ?? const {};
// final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
// final List<dynamic>? paths = props['paths'] as List<dynamic>?;
// String initialText =
// widget.initialData?[field.fieldKey]?.toString() ?? '';
// if (assignByJsonPaths && paths != null) {
// final Map<String, dynamic> values = {};
// for (final p in paths) {
// if (p is String) {
// final v = _readValueByPath(_initialData, p);
// if (v != null) values[p] = v;
// }
// }
// if (values.isNotEmpty) {
// initialText = _encodeMap(values);
// }
// }
// _controllers[field.fieldKey] = TextEditingController(text: initialText);
// _fieldByKey[field.fieldKey] = field;
// }
// }
// @override
// void dispose() {
// for (final controller in _controllers.values) {
// controller.dispose();
// }
// super.dispose();
// }
// @override
// Widget build(BuildContext context) {
// return Consumer<DynamicThemeProvider>(
// builder: (context, dynamicThemeProvider, child) {
// final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
// Theme.of(context).brightness == Brightness.dark,
// );
// return Form(
// key: _formKey,
// child: Column(
// children: [
// // Dynamic field rendering
// ...widget.fields.map((field) => Padding(
// padding:
// const EdgeInsets.only(bottom: UIConstants.spacing16),
// child: field.buildField(
// controller: _controllers[field.fieldKey]!,
// colorScheme: colorScheme,
// onChanged: () => setState(() {}),
// ),
// )),
// const SizedBox(height: UIConstants.spacing24),
// // Submit button
// ModernButton(
// text: widget.submitButtonText,
// type: ModernButtonType.primary,
// size: ModernButtonSize.large,
// isLoading: widget.isLoading,
// onPressed: widget.isLoading ? null : _handleSubmit,
// ),
// ],
// ),
// );
// },
// );
// }
// void _handleSubmit() {
// if (_formKey.currentState!.validate()) {
// // Dynamic cross-field match for any password-confirm group
// final Map<String, String> passwordByGroup = {};
// final Map<String, String> confirmByGroup = {};
// for (final entry in _controllers.entries) {
// final key = entry.key;
// final field = _fieldByKey[key];
// final props = field?.customProperties ?? const {};
// final isPassword = props['isPassword'] == true;
// if (!isPassword) continue;
// final String? groupId = props['groupId'];
// if (groupId == null) continue;
// final bool isConfirm = props['isConfirm'] == true;
// if (isConfirm) {
// confirmByGroup[groupId] = entry.value.text;
// } else {
// passwordByGroup[groupId] = entry.value.text;
// }
// }
// for (final gid in confirmByGroup.keys) {
// final confirm = confirmByGroup[gid] ?? '';
// final pass = passwordByGroup[gid] ?? '';
// if (confirm.isNotEmpty && confirm != pass) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('Passwords do not match')),
// );
// return;
// }
// }
// final formData = <String, dynamic>{};
// for (final entry in _controllers.entries) {
// final key = entry.key;
// final field = _fieldByKey[key];
// final value = entry.value.text.trim();
// // Skip fields that are marked as non-submittable (e.g., DataGrid)
// final props = field?.customProperties ?? const {};
// final bool excludeFromSubmit = props['excludeFromSubmit'] == true;
// if (excludeFromSubmit) {
// continue;
// }
// // Skip confirm entries for any password group
// final bool isPassword = props['isPassword'] == true;
// final bool isConfirm = props['isConfirm'] == true;
// if (isPassword && isConfirm) continue;
// final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
// if (assignByJsonPaths) {
// // If composite and empty -> skip adding base key
// if (value.isEmpty) {
// continue;
// }
// final Map<String, dynamic>? map = _tryDecodeMap(value);
// if (map != null) {
// map.forEach((path, v) {
// if (path is String) {
// _assignValueByPath(formData, path, v);
// }
// });
// continue;
// }
// // If not decodable, also skip to avoid sending invalid base key
// continue;
// }
// formData[key] = value;
// }
// widget.onSubmit(formData);
// }
// }
// dynamic _readValueByPath(Map<String, dynamic>? source, String path) {
// if (source == null) return null;
// final segments = path.split('.');
// dynamic current = source;
// for (final segment in segments) {
// if (current is Map<String, dynamic> && current.containsKey(segment)) {
// current = current[segment];
// } else {
// return null;
// }
// }
// return current;
// }
// void _assignValueByPath(
// Map<String, dynamic> target, String path, dynamic value) {
// final segments = path.split('.');
// Map<String, dynamic> current = target;
// for (int i = 0; i < segments.length; i++) {
// final seg = segments[i];
// final bool isLast = i == segments.length - 1;
// if (isLast) {
// current[seg] = value;
// } else {
// if (current[seg] is! Map<String, dynamic>) {
// current[seg] = <String, dynamic>{};
// }
// current = current[seg] as Map<String, dynamic>;
// }
// }
// }
// String _encodeMap(Map<String, dynamic> map) {
// return const JsonEncoder().convert(map);
// }
// Map<String, dynamic>? _tryDecodeMap(String value) {
// try {
// final decoded = const JsonDecoder().convert(value);
// if (decoded is Map<String, dynamic>) return decoded;
// return null;
// } catch (_) {
// return null;
// }
// }
// }
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/providers/dynamic_theme_provider.dart';
import '../fields/base_field.dart';
import '../fields/dependent_dropdown_field.dart';
import '../../../shared/widgets/buttons/modern_button.dart';
import '../../../core/constants/ui_constants.dart';
import 'dart:convert';
/// Reusable form component that dynamically renders fields based on field definitions
/// This allows UI to be independent of field types and enables reusability
class EntityForm extends StatefulWidget {
final List<BaseField> fields;
final Map<String, dynamic>? initialData;
final Function(Map<String, dynamic>) onSubmit;
final String submitButtonText;
final bool isLoading;
const EntityForm({
super.key,
required this.fields,
this.initialData,
required this.onSubmit,
this.submitButtonText = 'Submit',
this.isLoading = false,
});
@override
State<EntityForm> createState() => _EntityFormState();
}
class _EntityFormState extends State<EntityForm> {
final _formKey = GlobalKey<FormState>();
final Map<String, TextEditingController> _controllers = {};
final Map<String, BaseField> _fieldByKey = {};
final Map<String, List<DependentDropdownField>> _dependentFields = {};
late final Map<String, dynamic> _initialData;
@override
void initState() {
super.initState();
_initializeControllers();
_setupDependentFields();
}
void _initializeControllers() {
_initialData = widget.initialData ?? const {};
for (final field in widget.fields) {
final props = field.customProperties ?? const {};
final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
final List<dynamic>? paths = props['paths'] as List<dynamic>?;
String initialText =
widget.initialData?[field.fieldKey]?.toString() ?? '';
if (assignByJsonPaths && paths != null) {
final Map<String, dynamic> values = {};
for (final p in paths) {
if (p is String) {
final v = _readValueByPath(_initialData, p);
if (v != null) values[p] = v;
}
}
if (values.isNotEmpty) {
initialText = _encodeMap(values);
}
}
_controllers[field.fieldKey] = TextEditingController(text: initialText);
_fieldByKey[field.fieldKey] = field;
}
}
void _setupDependentFields() {
// Group dependent dropdown fields by their dependent field key
for (final field in widget.fields) {
if (field is DependentDropdownField) {
final dependentFieldKey = field.dependentFieldKey;
if (!_dependentFields.containsKey(dependentFieldKey)) {
_dependentFields[dependentFieldKey] = [];
}
_dependentFields[dependentFieldKey]!.add(field);
}
}
}
void _handleFieldChange(String fieldKey) {
setState(() {});
// Check if this field has dependent dropdowns
if (_dependentFields.containsKey(fieldKey)) {
final dependentFields = _dependentFields[fieldKey]!;
final fieldValue = _controllers[fieldKey]?.text ?? '';
// Clear dependent dropdown values when parent field changes
for (final dependentField in dependentFields) {
_controllers[dependentField.fieldKey]?.clear();
}
// Trigger rebuild to update dependent dropdowns
setState(() {});
}
}
@override
void dispose() {
for (final controller in _controllers.values) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
Theme.of(context).brightness == Brightness.dark,
);
return EntityFormScope(
controllers: _controllers,
notifyParent: () => setState(() {}),
child: Form(
key: _formKey,
child: Column(
children: [
// Dynamic field rendering
...widget.fields.map((field) => Padding(
padding:
const EdgeInsets.only(bottom: UIConstants.spacing16),
child: field.buildField(
controller: _controllers[field.fieldKey]!,
colorScheme: colorScheme,
onChanged: () => _handleFieldChange(field.fieldKey),
),
)),
const SizedBox(height: UIConstants.spacing24),
// Submit button
ModernButton(
text: widget.submitButtonText,
type: ModernButtonType.primary,
size: ModernButtonSize.large,
isLoading: widget.isLoading,
onPressed: widget.isLoading ? null : _handleSubmit,
),
],
),
),
);
},
);
}
void _handleSubmit() {
if (_formKey.currentState!.validate()) {
// Dynamic cross-field match for any password-confirm group
final Map<String, String> passwordByGroup = {};
final Map<String, String> confirmByGroup = {};
for (final entry in _controllers.entries) {
final key = entry.key;
final field = _fieldByKey[key];
final props = field?.customProperties ?? const {};
final isPassword = props['isPassword'] == true;
if (!isPassword) continue;
final String? groupId = props['groupId'];
if (groupId == null) continue;
final bool isConfirm = props['isConfirm'] == true;
if (isConfirm) {
confirmByGroup[groupId] = entry.value.text;
} else {
passwordByGroup[groupId] = entry.value.text;
}
}
for (final gid in confirmByGroup.keys) {
final confirm = confirmByGroup[gid] ?? '';
final pass = passwordByGroup[gid] ?? '';
if (confirm.isNotEmpty && confirm != pass) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Passwords do not match')),
);
return;
}
}
final formData = <String, dynamic>{};
for (final entry in _controllers.entries) {
final key = entry.key;
final field = _fieldByKey[key];
final value = entry.value.text.trim();
// Skip fields that are marked as non-submittable (e.g., DataGrid)
final props = field?.customProperties ?? const {};
final bool excludeFromSubmit = props['excludeFromSubmit'] == true;
if (excludeFromSubmit) {
continue;
}
// Skip confirm entries for any password group
final bool isPassword = props['isPassword'] == true;
final bool isConfirm = props['isConfirm'] == true;
if (isPassword && isConfirm) continue;
final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
if (assignByJsonPaths) {
// If composite and empty -> skip adding base key
if (value.isEmpty) {
continue;
}
final Map<String, dynamic>? map = _tryDecodeMap(value);
if (map != null) {
map.forEach((path, v) {
if (path is String) {
_assignValueByPath(formData, path, v);
}
});
continue;
}
// If not decodable, also skip to avoid sending invalid base key
continue;
}
// Handle JSON parsing for OneToMany fields
final bool parseAsJson = props['parseAsJson'] == true;
if (parseAsJson && value.isNotEmpty) {
try {
final decoded = json.decode(value);
formData[key] = decoded;
} catch (e) {
// If JSON parsing fails, send as string
formData[key] = value;
}
} else {
formData[key] = value;
}
}
widget.onSubmit(formData);
}
}
dynamic _readValueByPath(Map<String, dynamic>? source, String path) {
if (source == null) return null;
final segments = path.split('.');
dynamic current = source;
for (final segment in segments) {
if (current is Map<String, dynamic> && current.containsKey(segment)) {
current = current[segment];
} else {
return null;
}
}
return current;
}
void _assignValueByPath(
Map<String, dynamic> target, String path, dynamic value) {
final segments = path.split('.');
Map<String, dynamic> current = target;
for (int i = 0; i < segments.length; i++) {
final seg = segments[i];
final bool isLast = i == segments.length - 1;
if (isLast) {
current[seg] = value;
} else {
if (current[seg] is! Map<String, dynamic>) {
current[seg] = <String, dynamic>{};
}
current = current[seg] as Map<String, dynamic>;
}
}
}
String _encodeMap(Map<String, dynamic> map) {
return const JsonEncoder().convert(map);
}
Map<String, dynamic>? _tryDecodeMap(String value) {
try {
final decoded = const JsonDecoder().convert(value);
if (decoded is Map<String, dynamic>) return decoded;
return null;
} catch (_) {
return null;
}
}
}
/// Inherited scope to provide access to the form's controllers for advanced shared fields
class EntityFormScope extends InheritedWidget {
final Map<String, TextEditingController> controllers;
final VoidCallback notifyParent;
const EntityFormScope({
super.key,
required this.controllers,
required this.notifyParent,
required Widget child,
}) : super(child: child);
static EntityFormScope? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<EntityFormScope>();
}
@override
bool updateShouldNotify(covariant EntityFormScope oldWidget) {
return oldWidget.controllers != controllers;
}
}

View File

@@ -0,0 +1,472 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/providers/dynamic_theme_provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../shared/widgets/app_bar/modern_app_bar.dart';
import 'entity_card.dart';
/// Generic list component for displaying entities with search, pagination, and refresh
/// This component works with any entity type and provides consistent UI/UX
class EntityList extends StatefulWidget {
final List<Map<String, dynamic>> entities;
final bool isLoading;
final String? errorMessage;
final bool hasMoreData;
final String searchQuery;
final Function(String) onSearchChanged;
final Function(Map<String, dynamic>) onEdit;
final Function(Map<String, dynamic>) onDelete;
final Function(Map<String, dynamic>)? onTap;
final Function() onRefresh;
final Function() onLoadMore;
final String title;
final Function()? onAddNew;
final List<Map<String, dynamic>> displayFields;
// Optional: header insert action (e.g., Childform/Insert Support)
final Function()? onInsertAction;
final String? insertActionLabel;
const EntityList({
super.key,
required this.entities,
required this.isLoading,
this.errorMessage,
required this.hasMoreData,
required this.searchQuery,
required this.onSearchChanged,
required this.onEdit,
required this.onDelete,
this.onTap,
required this.onRefresh,
required this.onLoadMore,
required this.title,
this.onAddNew,
this.displayFields = const [],
this.onInsertAction,
this.insertActionLabel,
});
@override
State<EntityList> createState() => _EntityListState();
}
class _EntityListState extends State<EntityList> with TickerProviderStateMixin {
late AnimationController _animationController;
final TextEditingController _searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_initializeAnimations();
_searchController.text = widget.searchQuery;
_scrollController.addListener(_scrollListener);
}
void _initializeAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animationController.forward();
}
void _scrollListener() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
if (widget.hasMoreData && !widget.isLoading) {
widget.onLoadMore();
}
}
}
@override
void dispose() {
_animationController.dispose();
_searchController.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
Theme.of(context).brightness == Brightness.dark,
);
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: ModernAppBar(
title: widget.title,
showBackButton: true,
actions: [
if (widget.onInsertAction != null)
TextButton(
onPressed: widget.onInsertAction,
child: Text(
widget.insertActionLabel ?? 'Unknown',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
if (widget.onAddNew != null)
IconButton(
onPressed: widget.onAddNew,
icon: Icon(Icons.add_rounded, color: colorScheme.primary),
tooltip: 'Add New',
),
],
),
body: Column(
children: [
// Search Bar
_buildSearchBar(colorScheme),
// Content
Expanded(child: _buildContent(colorScheme)),
],
),
);
},
);
}
Widget _buildSearchBar(ColorScheme colorScheme) {
return Container(
margin: UIConstants.getResponsivePadding(
context,
mobile: UIConstants.screenPaddingMedium,
tablet: UIConstants.screenPaddingLarge,
desktop: UIConstants.screenPaddingLarge,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [colorScheme.surface, colorScheme.surface.withOpacity(0.95)],
),
borderRadius: BorderRadius.circular(UIConstants.radius16),
border: Border.all(
color: colorScheme.primary.withOpacity(0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 5),
spreadRadius: 1,
),
],
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
onChanged: widget.onSearchChanged,
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
decoration: InputDecoration(
hintText: 'Search ${widget.title.toLowerCase()}...',
hintStyle: TextStyle(
color: colorScheme.onSurface.withOpacity(0.6),
fontSize: 16,
fontWeight: FontWeight.w400,
),
prefixIcon: Icon(
Icons.search_rounded,
color: colorScheme.primary,
size: 22,
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
widget.onSearchChanged('');
},
icon: Icon(
Icons.clear_rounded,
color: colorScheme.onSurface.withOpacity(0.6),
size: 20,
),
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
),
),
),
IconButton(
onPressed: widget.onRefresh,
icon: Icon(
Icons.refresh_rounded,
color: colorScheme.onSurface.withOpacity(0.8),
size: 22,
),
tooltip: 'Refresh',
),
],
),
);
}
Widget _buildContent(ColorScheme colorScheme) {
if (widget.isLoading && widget.entities.isEmpty) {
return _buildLoadingState(colorScheme);
}
if (widget.errorMessage != null && widget.entities.isEmpty) {
return _buildErrorState(colorScheme);
}
if (widget.entities.isEmpty) {
return _buildEmptyState(colorScheme);
}
return _buildEntityGrid(colorScheme);
}
Widget _buildLoadingState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: colorScheme.primary, strokeWidth: 3),
const SizedBox(height: UIConstants.spacing16),
Text(
'Loading ${widget.title.toLowerCase()}...',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildErrorState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius20),
),
child: Icon(
Icons.error_outline_rounded,
color: colorScheme.error,
size: 40,
),
),
const SizedBox(height: UIConstants.spacing16),
Text(
'Error Loading Data',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing8),
Text(
widget.errorMessage ?? 'Something went wrong',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 14,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
),
const SizedBox(height: UIConstants.spacing24),
ElevatedButton.icon(
onPressed: widget.onRefresh,
icon: Icon(Icons.refresh_rounded, color: colorScheme.onPrimary),
label: Text(
'Retry',
style: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 4,
shadowColor: colorScheme.primary.withOpacity(0.3),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
),
),
],
),
);
}
Widget _buildEmptyState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius20),
),
child: Icon(
Icons.inbox_outlined,
color: colorScheme.primary,
size: 40,
),
),
const SizedBox(height: UIConstants.spacing16),
Text(
'No ${widget.title.toLowerCase()} found',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing8),
Text(
widget.searchQuery.isNotEmpty
? 'Try adjusting your search terms'
: 'Start by adding your first ${widget.title.toLowerCase()}',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 14,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
),
if (widget.onAddNew != null) ...[
const SizedBox(height: UIConstants.spacing24),
ElevatedButton.icon(
onPressed: widget.onAddNew,
icon: Icon(Icons.add_rounded, color: colorScheme.onPrimary),
label: Text(
'Add ${widget.title}',
style: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 4,
shadowColor: colorScheme.primary.withOpacity(0.3),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
),
),
],
],
),
);
}
Widget _buildEntityGrid(ColorScheme colorScheme) {
return RefreshIndicator(
onRefresh: () async => widget.onRefresh(),
color: colorScheme.primary,
child: GridView.builder(
controller: _scrollController,
padding: UIConstants.getResponsivePadding(
context,
mobile: UIConstants.screenPaddingMedium,
tablet: UIConstants.screenPaddingLarge,
desktop: UIConstants.screenPaddingLarge,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: UIConstants.getResponsiveInt(
context,
mobile: 1,
tablet: 2,
desktop: 3,
),
childAspectRatio: 0.85,
crossAxisSpacing: UIConstants.spacing16,
mainAxisSpacing: UIConstants.spacing16,
),
itemCount: widget.entities.length + (widget.isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index < widget.entities.length) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _animationController,
child: Transform.translate(
offset: Offset(0, 20 * (1 - _animationController.value)),
child: EntityCard(
entity: widget.entities[index],
onEdit: widget.onEdit,
onDelete: widget.onDelete,
onTap: widget.onTap,
displayFields: widget.displayFields,
),
),
);
},
);
} else {
return _buildLoadingCard(colorScheme);
}
},
),
);
}
Widget _buildLoadingCard(ColorScheme colorScheme) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [colorScheme.surface, colorScheme.surface.withOpacity(0.95)],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
border: Border.all(
color: colorScheme.primary.withOpacity(0.1),
width: 1.5,
),
),
child: Center(
child: CircularProgressIndicator(
color: colorScheme.primary,
strokeWidth: 2,
),
),
);
}
}

View File

@@ -0,0 +1,608 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/providers/dynamic_theme_provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../shared/widgets/app_bar/modern_app_bar.dart';
import '../../../shared/widgets/buttons/modern_button.dart';
import '../fields/base_field.dart';
import 'entity_form.dart';
/// Generic CRUD screens for entities
/// This provides consistent create and update screens for all entity types
/// Generic Create Entity Screen
class EntityCreateScreen extends StatefulWidget {
final List<BaseField> fields;
final Function(Map<String, dynamic>) onSubmit;
final String title;
final bool isLoading;
final String? errorMessage;
const EntityCreateScreen({
super.key,
required this.fields,
required this.onSubmit,
required this.title,
this.isLoading = false,
this.errorMessage,
});
@override
State<EntityCreateScreen> createState() => _EntityCreateScreenState();
}
class _EntityCreateScreenState extends State<EntityCreateScreen>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
}
void _initializeAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic),
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
Theme.of(context).brightness == Brightness.dark,
);
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: ModernAppBar(
title: 'Create ${widget.title}',
showBackButton: true,
),
body: widget.isLoading
? _buildLoadingState(colorScheme)
: _buildContent(colorScheme),
);
},
);
}
Widget _buildLoadingState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
strokeWidth: 3,
),
const SizedBox(height: UIConstants.spacing16),
Text(
'Creating ${widget.title.toLowerCase()}...',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildContent(ColorScheme colorScheme) {
return SingleChildScrollView(
padding: UIConstants.getResponsivePadding(
context,
mobile: UIConstants.screenPaddingMedium,
tablet: UIConstants.screenPaddingLarge,
desktop: UIConstants.screenPaddingLarge,
),
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
_buildHeader(colorScheme),
const SizedBox(height: UIConstants.spacing24),
// Form
_buildForm(colorScheme),
if (widget.errorMessage != null) ...[
const SizedBox(height: UIConstants.spacing16),
_buildErrorMessage(colorScheme),
],
],
),
),
);
},
),
);
}
Widget _buildHeader(ColorScheme colorScheme) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary,
colorScheme.primary.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
spreadRadius: 2,
),
],
),
child: Padding(
padding: UIConstants.cardPaddingLarge,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: colorScheme.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
child: Icon(
Icons.add_rounded,
color: colorScheme.onPrimary,
size: 24,
),
),
const SizedBox(width: UIConstants.spacing16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create New ${widget.title}',
style: TextStyle(
color: colorScheme.onPrimary,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
const SizedBox(height: UIConstants.spacing4),
Text(
'Fill in the details below to create a new ${widget.title.toLowerCase()}',
style: TextStyle(
color: colorScheme.onPrimary.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
],
),
),
);
}
Widget _buildForm(ColorScheme colorScheme) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surface,
colorScheme.surface.withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
border: Border.all(
color: colorScheme.primary.withOpacity(0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 2,
),
],
),
child: Padding(
padding: UIConstants.cardPaddingLarge,
child: EntityForm(
fields: widget.fields,
onSubmit: widget.onSubmit,
submitButtonText: 'Create ${widget.title}',
isLoading: widget.isLoading,
),
),
);
}
Widget _buildErrorMessage(ColorScheme colorScheme) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(UIConstants.spacing16),
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius12),
border: Border.all(
color: colorScheme.error.withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.error_outline_rounded,
color: colorScheme.error,
size: 20,
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Text(
widget.errorMessage!,
style: TextStyle(
color: colorScheme.error,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}
/// Generic Update Entity Screen
class EntityUpdateScreen extends StatefulWidget {
final List<BaseField> fields;
final Map<String, dynamic> initialData;
final Function(Map<String, dynamic>) onSubmit;
final String title;
final bool isLoading;
final String? errorMessage;
const EntityUpdateScreen({
super.key,
required this.fields,
required this.initialData,
required this.onSubmit,
required this.title,
this.isLoading = false,
this.errorMessage,
});
@override
State<EntityUpdateScreen> createState() => _EntityUpdateScreenState();
}
class _EntityUpdateScreenState extends State<EntityUpdateScreen>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
}
void _initializeAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic),
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
Theme.of(context).brightness == Brightness.dark,
);
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: ModernAppBar(
title: 'Update ${widget.title}',
showBackButton: true,
),
body: widget.isLoading
? _buildLoadingState(colorScheme)
: _buildContent(colorScheme),
);
},
);
}
Widget _buildLoadingState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
strokeWidth: 3,
),
const SizedBox(height: UIConstants.spacing16),
Text(
'Updating ${widget.title.toLowerCase()}...',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildContent(ColorScheme colorScheme) {
return SingleChildScrollView(
padding: UIConstants.getResponsivePadding(
context,
mobile: UIConstants.screenPaddingMedium,
tablet: UIConstants.screenPaddingLarge,
desktop: UIConstants.screenPaddingLarge,
),
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
_buildHeader(colorScheme),
const SizedBox(height: UIConstants.spacing24),
// Form
_buildForm(colorScheme),
if (widget.errorMessage != null) ...[
const SizedBox(height: UIConstants.spacing16),
_buildErrorMessage(colorScheme),
],
],
),
),
);
},
),
);
}
Widget _buildHeader(ColorScheme colorScheme) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.secondary,
colorScheme.secondary.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
boxShadow: [
BoxShadow(
color: colorScheme.secondary.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
spreadRadius: 2,
),
],
),
child: Padding(
padding: UIConstants.cardPaddingLarge,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: colorScheme.onSecondary.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
child: Icon(
Icons.edit_rounded,
color: colorScheme.onSecondary,
size: 24,
),
),
const SizedBox(width: UIConstants.spacing16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Update ${widget.title}',
style: TextStyle(
color: colorScheme.onSecondary,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
const SizedBox(height: UIConstants.spacing4),
Text(
'Modify the details below to update this ${widget.title.toLowerCase()}',
style: TextStyle(
color: colorScheme.onSecondary.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
],
),
),
);
}
Widget _buildForm(ColorScheme colorScheme) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surface,
colorScheme.surface.withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(UIConstants.radius20),
border: Border.all(
color: colorScheme.primary.withOpacity(0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 2,
),
],
),
child: Padding(
padding: UIConstants.cardPaddingLarge,
child: EntityForm(
fields: widget.fields,
initialData: widget.initialData,
onSubmit: widget.onSubmit,
submitButtonText: 'Update ${widget.title}',
isLoading: widget.isLoading,
),
),
);
}
Widget _buildErrorMessage(ColorScheme colorScheme) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(UIConstants.spacing16),
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(UIConstants.radius12),
border: Border.all(
color: colorScheme.error.withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.error_outline_rounded,
color: colorScheme.error,
size: 20,
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Text(
widget.errorMessage!,
style: TextStyle(
color: colorScheme.error,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,37 @@
import 'dart:typed_data';
/// Temporary in-memory store for complex field data (e.g., uploads)
/// Keyed by fieldKey. Values are dynamic but expected shapes are documented.
class EntityFieldStore {
EntityFieldStore._();
static final EntityFieldStore instance = EntityFieldStore._();
final Map<String, dynamic> _data = {};
void set(String fieldKey, dynamic value) {
_data[fieldKey] = value;
}
T? get<T>(String fieldKey) {
final value = _data[fieldKey];
if (value is T) return value as T;
return null;
}
void remove(String fieldKey) {
_data.remove(fieldKey);
}
void clear() {
_data.clear();
}
}
/// Expected value shapes:
/// - For multi-file uploads: List<UploadItem>
class UploadItem {
final String fileName;
final Uint8List bytes;
UploadItem({required this.fileName, required this.bytes});
}

View File

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class ReusableDatePickerField extends StatelessWidget {
final String label;
final String? initialDate;
final TextEditingController controller;
final Function(String?)? onSaved;
const ReusableDatePickerField({
super.key,
required this.label,
required this.controller,
this.initialDate,
required this.onSaved,
});
@override
Widget build(BuildContext context) {
return TextFormField(
initialValue: initialDate,
controller: controller,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
contentPadding:
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 12.0),
suffixIcon: const Icon(Icons.calendar_today),
),
readOnly: true,
onTap: () async {
DateTime? selectedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
controller.text = selectedDate != null
? DateFormat('yyyy-MM-dd').format(selectedDate!)
: '';
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class ReusableDateTimePickerField extends StatelessWidget {
final String label;
final String? initialDateTime;
final TextEditingController controller;
final Function(String?)? onSaved;
const ReusableDateTimePickerField({super.key,
required this.label,
required this.controller,
this.initialDateTime,
required this.onSaved,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
initialValue: initialDateTime,
controller: controller,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
contentPadding:
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 12.0),
suffixIcon: const Icon(Icons.event),
),
readOnly: true,
onTap: () async {
DateTime? selectedDateTime = await showDateTimePicker(
context: context,
initialDateTime: DateTime.now(),
);
if (selectedDateTime != null) {
controller.text =
DateFormat('yyyy-MM-dd HH:mm').format(selectedDateTime);
}
},
onSaved: onSaved,
),
);
}
Future<DateTime?> showDateTimePicker({
required BuildContext context,
required DateTime initialDateTime,
}) async {
// Custom date-time picker implementation or package usage
// This is a placeholder for demonstration purposes.
return await showDatePicker(
context: context,
initialDate: initialDateTime,
firstDate: DateTime(2000),
lastDate: DateTime(2101),
).then((date) {
if (date != null) {
return showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(initialDateTime),
).then((time) {
if (time != null) {
return DateTime(
date.year, date.month, date.day, time.hour, time.minute);
}
return date;
});
}
return null;
});
}
}

View File

@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
class ReusableDropdownField extends StatelessWidget {
final String label;
final List<Map<String, dynamic>> options;
final String? value;
final String valueField; // ID key (dynamic)
final String uiField; // Name key (dynamic)
final void Function(String?)? onChanged;
final void Function(String?)? onSaved;
const ReusableDropdownField({
super.key,
required this.label,
required this.options,
required this.valueField, // Dynamic ID field
required this.uiField, // Dynamic Name field
this.value,
this.onChanged,
this.onSaved,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: Colors.deepPurple),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: const BorderSide(color: Colors.deepPurple, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: const BorderSide(color: Colors.deepPurple, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide:
const BorderSide(color: Colors.deepPurpleAccent, width: 2),
),
),
value: (value != null && value!.isNotEmpty) ? value : null,
items: [
const DropdownMenuItem<String>(
value: '',
child: Text('Select an option'),
),
...options.map<DropdownMenuItem<String>>(
(item) => DropdownMenuItem<String>(
value: item[valueField].toString(),
child: Text(
item[uiField].toString(),
style: const TextStyle(fontSize: 16),
),
),
),
],
onChanged: onChanged,
onSaved: onSaved,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select a $label';
}
return null;
},
),
);
}
}

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class ReusableTextField extends StatelessWidget {
final String label;
final TextEditingController? controller;
final TextInputType keyboardType;
final bool obscureText;
final int maxLines;
final List<TextInputFormatter>? inputFormatters;
final FormFieldValidator<String>? validator;
final Function(DateTime?)? onDateTimeSelected; // Callback for DateTime picker
final Function(String?)? onSaved;
final Function(String?)? onChanged;
final String? initialValue;
const ReusableTextField({
super.key,
required this.label,
this.controller,
this.keyboardType = TextInputType.text,
this.obscureText = false,
this.maxLines = 1,
this.inputFormatters,
this.validator,
this.onDateTimeSelected,
this.onSaved,
this.onChanged,
this.initialValue, // Added callback for DateTime picker
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
initialValue: initialValue,
onSaved: onSaved,
onChanged: onChanged,
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
maxLines: maxLines,
inputFormatters: inputFormatters,
validator: validator,
onTap: () async {
if (keyboardType == TextInputType.datetime &&
onDateTimeSelected != null) {
DateTime? dateTime = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
// Agar user cancel kare, to current date set ho jaye
dateTime ??= DateTime.now();
TimeOfDay? timeOfDay = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (timeOfDay != null) {
dateTime = DateTime(dateTime.year, dateTime.month, dateTime.day,
timeOfDay.hour, timeOfDay.minute);
onDateTimeSelected!(dateTime);
}
}
},
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:base_project/resources/app_colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MyCustomTextFormField extends StatelessWidget {
final String label;
final Widget? prefixIcon;
final Widget? suffixIcon;
final String? initialValue;
final bool? obscureText;
final TextEditingController controller;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
final int? maxLines;
final void Function()? onSuffixIconPressed;
const MyCustomTextFormField({
super.key,
required this.label,
this.prefixIcon,
this.suffixIcon,
this.initialValue,
required this.controller,
this.validator,
this.onChanged,
this.keyboardType,
this.inputFormatters,
this.maxLines = 1, this.obscureText, this.onSuffixIconPressed,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
initialValue: initialValue,
validator: validator,
onChanged: onChanged,
cursorColor: AppColors.primary,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
maxLines: maxLines,
obscureText: obscureText ?? false,
decoration: InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: AppColors.primary),
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
filled: true,
fillColor: Colors.white,
border: InputBorder.none, // No border
contentPadding:
const EdgeInsets.symmetric(vertical: 15.0, horizontal: 12.0),
// For rounded corners
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
),
),
);
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class DrawerItem extends StatelessWidget {
final IconData icon;
final String title;
final VoidCallback onTap;
final Color color;
const DrawerItem({
super.key,
required this.icon,
required this.title,
required this.onTap,
this.color = Colors.blue, // Default color if none is provided
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon, color: color),
title: Text(title),
onTap: onTap,
);
}
}

View File

@@ -0,0 +1,327 @@
import '../../Entity/angulardata/Test_visa/Test_visaView/Test_visa_entity_list_screen.dart';
import '../../Entity/angulardata/Test_visa/Test_visa_viewModel/Test_visa_view_model_screen.dart';
import '../../Entity/angulardata/Basicp/BasicpView/Basicp_entity_list_screen.dart';
import '../../Entity/angulardata/Basicp/Basicp_viewModel/Basicp_view_model_screen.dart';
import '../../Entity/angulardatatype/Ad8/Ad8View/Ad8_entity_list_screen.dart';
import '../../Entity/angulardatatype/Ad8/Ad8_viewModel/Ad8_view_model_screen.dart';
import '../../Entity/angulardatatype/Ad7/Ad7View/Ad7_entity_list_screen.dart';
import '../../Entity/angulardatatype/Ad7/Ad7_viewModel/Ad7_view_model_screen.dart';
import '../../Entity/angulardatatype/Adv5/Adv5View/Adv5_entity_list_screen.dart';
import '../../Entity/angulardatatype/Adv5/Adv5_viewModel/Adv5_view_model_screen.dart';
import '../../Entity/angulardatatype/Adv4/Adv4View/Adv4_entity_list_screen.dart';
import '../../Entity/angulardatatype/Adv4/Adv4_viewModel/Adv4_view_model_screen.dart';
import '../../Entity/angulardatatype/Adv3/Adv3View/Adv3_entity_list_screen.dart';
import '../../Entity/angulardatatype/Adv3/Adv3_viewModel/Adv3_view_model_screen.dart';
import '../../Entity/angulardatatype/Dv2/Dv2View/Dv2_entity_list_screen.dart';
import '../../Entity/angulardatatype/Dv2/Dv2_viewModel/Dv2_view_model_screen.dart';
import '../../Entity/angulardatatype/Adv1/Adv1View/Adv1_entity_list_screen.dart';
import '../../Entity/angulardatatype/Adv1/Adv1_viewModel/Adv1_view_model_screen.dart';
import '../../Entity/angulardatatype/Basicp3/Basicp3View/Basicp3_entity_list_screen.dart';
import '../../Entity/angulardatatype/Basicp3/Basicp3_viewModel/Basicp3_view_model_screen.dart';
import '../../Entity/angulardatatype/Basicp2/Basicp2View/Basicp2_entity_list_screen.dart';
import '../../Entity/angulardatatype/Basicp2/Basicp2_viewModel/Basicp2_view_model_screen.dart';
import '../../Entity/angulardatatype/Basicp1/Basicp1View/Basicp1_entity_list_screen.dart';
import '../../Entity/angulardatatype/Basicp1/Basicp1_viewModel/Basicp1_view_model_screen.dart';
import 'package:base_project/utils/image_constant.dart';
import 'package:base_project/commans/widgets/custome_drawe_item.dart';
import 'package:base_project/resources/app_colors.dart';
import 'package:base_project/routes/route_names.dart';
import 'package:base_project/utils/managers/user_manager.dart';
import 'package:base_project/view_model/profile/profile_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
class MyCustomDrawer extends StatelessWidget {
const MyCustomDrawer({super.key});
@override
Widget build(BuildContext context) {
final email = UserManager().email;
final userName = UserManager().userName;
final provider = Provider.of<ProfileViewModel>(context, listen: false);
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
UserAccountsDrawerHeader(
decoration: const BoxDecoration(
color: AppColors.primary,
),
currentAccountPicture: CircleAvatar(
radius: 60,
backgroundColor: AppColors.primary.withOpacity(0.3),
backgroundImage: provider.profileImageBytes != null
? MemoryImage(provider.profileImageBytes!)
: null,
child: provider.profileImageBytes != null
? null // Use backgroundImage for the actual image, so child should be null
: SvgPicture.asset(
ImageConstant.userProfileImg, // Placeholder SVG asset
// AppImages.userProfileImg, // Placeholder SVG asset
width: 60, // Adjust to fit the CircleAvatar
height: 60,
),
),
accountName: Text("Hello, $userName"),
accountEmail: Text(email.toString()),
),
DrawerItem(
color: AppColors.primary,
icon: Icons.person,
title: 'Profile',
onTap: () {
Navigator.pushNamed(context, RouteNames.profileView);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.system_security_update,
title: 'System Parameters',
onTap: () {
// Add navigation or other logic here
Navigator.pushNamed(context, RouteNames.systemParamsView);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.password,
title: 'change password',
onTap: () {
Navigator.pushNamed(context, RouteNames.changePasswordView);
},
),
// NEW MENU
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Test_visa',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Test_visaViewModelScreen(),
child: test_visa_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Basicp',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => BasicpViewModelScreen(),
child: basicp_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Ad8',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Ad8ViewModelScreen(),
child: ad8_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Ad7',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Ad7ViewModelScreen(),
child: ad7_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Adv5',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Adv5ViewModelScreen(),
child: adv5_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Adv4',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Adv4ViewModelScreen(),
child: adv4_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Adv3',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Adv3ViewModelScreen(),
child: adv3_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Dv2',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Dv2ViewModelScreen(),
child: dv2_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Adv1',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Adv1ViewModelScreen(),
child: adv1_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Basicp3',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Basicp3ViewModelScreen(),
child: basicp3_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Basicp2',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Basicp2ViewModelScreen(),
child: basicp2_entity_list_screen(),
),
),
);
},
),
DrawerItem(
color: AppColors.primary,
icon: Icons.chat_bubble,
title: 'Basicp1',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => Basicp1ViewModelScreen(),
child: basicp_entity_list_screen(),
),
),
);
},
),
DrawerItem(
icon: Icons.logout,
color: Colors.red,
title: 'Logout',
onTap: () async {
await UserManager().clearUser();
Navigator.pushReplacementNamed(context, RouteNames.splashView);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:base_project/resources/app_colors.dart';
import 'package:flutter/material.dart';
class MyCustomElevatedButton extends StatelessWidget {
final Color? backgroundColor;
final Widget child;
final bool isLoading;
final VoidCallback onPressed;
const MyCustomElevatedButton({
super.key,
this.backgroundColor = AppColors.primary,
required this.child,
required this.onPressed,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
minimumSize: const Size(double.infinity, 50), // Full width, height of 50
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // Rounded corners
),
),
child: isLoading
? const Center(
child: CircularProgressIndicator(
strokeWidth: 2.0, // Adjust thickness if needed
),
)
: child,
);
}
}

View File

@@ -0,0 +1,6 @@
export '/core/utils/size_utils.dart';
export '/routes/app_routes.dart';
export '/theme/app_decoration.dart';
export '/theme/custom_text_style.dart';
export '/theme/theme_helper.dart';
export '/widgets/custom_image_view.dart';

View File

@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
class UIConstants {
// Spacing
static const double spacing4 = 4.0;
static const double spacing6 = 6.0;
static const double spacing8 = 8.0;
static const double spacing12 = 12.0;
static const double spacing14 = 14.0;
static const double spacing16 = 16.0;
static const double spacing18 = 18.0;
static const double spacing20 = 20.0;
static const double spacing24 = 24.0;
static const double spacing32 = 32.0;
static const double spacing40 = 40.0;
static const double spacing48 = 48.0;
static const double spacing56 = 56.0;
static const double spacing64 = 64.0;
static const double spacing72 = 72.0;
static const double spacing80 = 80.0;
static const double spacing96 = 96.0;
// Border Radius
static const double radius4 = 4.0;
static const double radius8 = 8.0;
static const double radius12 = 12.0;
static const double radius16 = 16.0;
static const double radius20 = 20.0;
static const double radius24 = 24.0;
static const double radius32 = 32.0;
static const double radius40 = 40.0;
static const double radiusFull = 999.0;
// Elevation
static const double elevation0 = 0.0;
static const double elevation1 = 1.0;
static const double elevation2 = 2.0;
static const double elevation4 = 4.0;
static const double elevation8 = 8.0;
static const double elevation16 = 16.0;
static const double elevation24 = 24.0;
// Animation Durations
static const Duration durationFast = Duration(milliseconds: 150);
static const Duration durationNormal = Duration(milliseconds: 300);
static const Duration durationSlow = Duration(milliseconds: 500);
static const Duration durationVerySlow = Duration(milliseconds: 800);
// Animation Curves
static const Curve curveFast = Curves.easeInOut;
static const Curve curveNormal = Curves.easeInOutCubic;
static const Curve curveSlow = Curves.easeInOutQuart;
static const Curve curveBounce = Curves.bounceOut;
static const Curve curveElastic = Curves.elasticOut;
// Screen Breakpoints
static const double mobileBreakpoint = 600.0;
static const double tabletBreakpoint = 900.0;
static const double desktopBreakpoint = 1200.0;
// Input Field Heights
static const double inputHeightSmall = 40.0;
static const double inputHeightMedium = 48.0;
static const double inputHeightLarge = 56.0;
// Button Heights
static const double buttonHeightSmall = 36.0;
static const double buttonHeightMedium = 48.0;
static const double buttonHeightLarge = 56.0;
// Icon Sizes
static const double iconSizeSmall = 16.0;
static const double iconSizeMedium = 24.0;
static const double iconSizeLarge = 32.0;
static const double iconSizeXLarge = 48.0;
// Logo Sizes
static const double logoSizeSmall = 32.0;
static const double logoSizeMedium = 48.0;
static const double logoSizeLarge = 64.0;
static const double logoSizeXLarge = 96.0;
// Card Padding
static const EdgeInsets cardPaddingSmall = EdgeInsets.all(spacing12);
static const EdgeInsets cardPaddingMedium = EdgeInsets.all(spacing16);
static const EdgeInsets cardPaddingLarge = EdgeInsets.all(spacing24);
// Screen Padding
static const EdgeInsets screenPaddingSmall = EdgeInsets.all(spacing16);
static const EdgeInsets screenPaddingMedium = EdgeInsets.all(spacing24);
static const EdgeInsets screenPaddingLarge = EdgeInsets.all(spacing32);
// Horizontal Padding
static const EdgeInsets horizontalPaddingSmall =
EdgeInsets.symmetric(horizontal: spacing16);
static const EdgeInsets horizontalPaddingMedium =
EdgeInsets.symmetric(horizontal: spacing24);
static const EdgeInsets horizontalPaddingLarge =
EdgeInsets.symmetric(horizontal: spacing32);
// Vertical Padding
static const EdgeInsets verticalPaddingSmall =
EdgeInsets.symmetric(vertical: spacing16);
static const EdgeInsets verticalPaddingMedium =
EdgeInsets.symmetric(vertical: spacing24);
static const EdgeInsets verticalPaddingLarge =
EdgeInsets.symmetric(vertical: spacing32);
// Responsive Helpers
static bool isMobile(BuildContext context) {
return MediaQuery.of(context).size.width < mobileBreakpoint;
}
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= mobileBreakpoint && width < tabletBreakpoint;
}
static bool isDesktop(BuildContext context) {
return MediaQuery.of(context).size.width >= desktopBreakpoint;
}
static double getResponsiveValue(
BuildContext context, {
required double mobile,
required double tablet,
required double desktop,
}) {
if (isMobile(context)) return mobile;
if (isTablet(context)) return tablet;
return desktop;
}
static int getResponsiveInt(
BuildContext context, {
required int mobile,
required int tablet,
required int desktop,
}) {
if (isMobile(context)) return mobile;
if (isTablet(context)) return tablet;
return desktop;
}
static EdgeInsets getResponsivePadding(
BuildContext context, {
required EdgeInsets mobile,
required EdgeInsets tablet,
required EdgeInsets desktop,
}) {
if (isMobile(context)) return mobile;
if (isTablet(context)) return tablet;
return desktop;
}
static double getResponsiveSpacing(
BuildContext context, {
required double mobile,
required double tablet,
required double desktop,
}) {
if (isMobile(context)) return mobile;
if (isTablet(context)) return tablet;
return desktop;
}
// Animation Helpers
static Widget fadeIn({
required Widget child,
Duration duration = durationNormal,
Curve curve = curveNormal,
}) {
return TweenAnimationBuilder<double>(
duration: duration,
curve: curve,
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: child,
);
},
child: child,
);
}
static Widget slideInUp({
required Widget child,
Duration duration = durationNormal,
Curve curve = curveNormal,
double offset = 50.0,
}) {
return TweenAnimationBuilder<Offset>(
duration: duration,
curve: curve,
tween: Tween(begin: Offset(0, offset), end: Offset.zero),
builder: (context, value, child) {
return Transform.translate(
offset: value,
child: child,
);
},
child: child,
);
}
static Widget scaleIn({
required Widget child,
Duration duration = durationNormal,
Curve curve = curveNormal,
double beginScale = 0.8,
}) {
return TweenAnimationBuilder<double>(
duration: duration,
curve: curve,
tween: Tween(begin: beginScale, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: child,
);
},
child: child,
);
}
}

View File

@@ -0,0 +1,81 @@
// import 'package:flutter/material.dart';
// import 'package:connectivity_plus/connectivity_plus.dart';
// // For checking internet connectivity
// abstract class NetworkInfoI {
// Future<bool> isConnected();
// Future<ConnectivityResult> get connectivityResult;
// Stream<ConnectivityResult> get onConnectivityChanged;
// }
// class NetworkInfo implements NetworkInfoI {
// Connectivity connectivity;
// static final NetworkInfo _networkInfo = NetworkInfo._internal(Connectivity());
// factory NetworkInfo() {
// return _networkInfo;
// }
// NetworkInfo._internal(this.connectivity) {
// connectivity = this.connectivity;
// }
// ///checks internet is connected or not
// ///returns [true] if internet is connected
// ///else it will return [false]
// @override
// Future<bool> isConnected() async {
// final result = await connectivity.checkConnectivity();
// if (result != ConnectivityResult.none) {
// return true;
// }
// return false;
// }
// // to check type of internet connectivity
// @override
// Future<ConnectivityResult> get connectivityResult async {
// return connectivity.checkConnectivity();
// }
// //check the type on internet connection on changed of internet connection
// @override
// Stream<ConnectivityResult> get onConnectivityChanged =>
// connectivity.onConnectivityChanged;
// }
// abstract class Failure {}
// // General failures
// class ServerFailure extends Failure {}
// class CacheFailure extends Failure {}
// class NetworkFailure extends Failure {}
// class ServerException implements Exception {}
// class CacheException implements Exception {}
// class NetworkException implements Exception {}
// ///can be used for throwing [NoInternetException]
// class NoInternetException implements Exception {
// late String _message;
// NoInternetException([String message = 'NoInternetException Occurred']) {
// if (globalMessengerKey.currentState != null) {
// globalMessengerKey.currentState!
// .showSnackBar(SnackBar(content: Text(message)));
// }
// this._message = message;
// }
// @override
// String toString() {
// return _message;
// }
// }

View File

@@ -0,0 +1,265 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../theme/dynamic_color_scheme.dart';
class DynamicThemeProvider extends ChangeNotifier {
ColorScheme? _dynamicColorScheme;
List<Color> _logoColors = [];
bool _isUsingDynamicTheme = false;
bool _isLoading = false;
// Manual override
bool _useManualOverride = false;
Color? _manualPrimary;
Color? _manualSecondary;
Color? _manualTertiary;
// Getters
ColorScheme? get dynamicColorScheme => _dynamicColorScheme;
List<Color> get logoColors => _logoColors;
bool get isUsingDynamicTheme => _isUsingDynamicTheme;
bool get isLoading => _isLoading;
bool get useManualOverride => _useManualOverride;
Color? get manualPrimary => _manualPrimary;
Color? get manualSecondary => _manualSecondary;
Color? get manualTertiary => _manualTertiary;
// Set loading state
set isLoading(bool value) {
_isLoading = value;
notifyListeners();
}
// Generate dynamic theme from logo
Future<void> generateThemeFromLogo(Uint8List logoBytes) async {
try {
print("DynamicThemeProvider.generateThemeFromLogo called");
print("Logo bytes length: ${logoBytes.length}");
isLoading = true;
// Extract colors with lightweight, downscaled palette to avoid jank
print("Extracting colors from logo (lite in main isolate)...");
final colors =
await DynamicColorScheme.extractColorsFromBytesLite(logoBytes);
print("Colors extracted: ${colors.length}");
_logoColors = colors;
// Generate light theme
print("Generating light theme...");
final lightTheme =
DynamicColorScheme.generateDynamicColorScheme(colors, isDark: false);
// Generate dark theme
print("Generating dark theme...");
final darkTheme =
DynamicColorScheme.generateDynamicColorScheme(colors, isDark: true);
// Store the dynamic color scheme
_dynamicColorScheme = lightTheme;
_isUsingDynamicTheme = true;
print('Dynamic theme generated with ${colors.length} colors');
print('Primary: ${colors.isNotEmpty ? colors[0] : 'N/A'}');
print('Secondary: ${colors.length > 1 ? colors[1] : 'N/A'}');
print('Tertiary: ${colors.length > 2 ? colors[2] : 'N/A'}');
print('_isUsingDynamicTheme set to: $_isUsingDynamicTheme');
isLoading = false;
print("Notifying listeners...");
notifyListeners();
print("Listeners notified!");
} catch (e) {
print('Error generating dynamic theme: $e');
isLoading = false;
// Fallback to default theme
_isUsingDynamicTheme = false;
_dynamicColorScheme = null;
notifyListeners();
}
}
// Get current color scheme (dynamic or default)
ColorScheme getCurrentColorScheme(bool isDark) {
// Manual overrides take highest precedence
if (_useManualOverride && (_manualPrimary != null)) {
final base = [
_manualPrimary!,
_manualSecondary ?? _manualPrimary!,
_manualTertiary ?? (_manualSecondary ?? _manualPrimary!)
];
return DynamicColorScheme.generateDynamicColorScheme(base,
isDark: isDark);
}
if (_isUsingDynamicTheme && _dynamicColorScheme != null) {
// Return dynamic theme with appropriate brightness
if (isDark) {
return DynamicColorScheme.generateDynamicColorScheme(_logoColors,
isDark: true);
} else {
return _dynamicColorScheme!;
}
} else {
// Return default theme
return DynamicColorScheme.generateDynamicColorScheme([], isDark: isDark);
}
}
// Get gradient colors for UI elements
List<Color> getGradientColors() {
if (_useManualOverride && _manualPrimary != null) {
return DynamicColorScheme.generateGradientColors([
_manualPrimary!,
_manualSecondary ?? _manualPrimary!,
_manualTertiary ?? (_manualSecondary ?? _manualPrimary!)
]);
}
if (_isUsingDynamicTheme && _logoColors.isNotEmpty) {
return DynamicColorScheme.generateGradientColors(_logoColors);
} else {
return DynamicColorScheme.generateGradientColors([]);
}
}
// Get accent colors for semantic elements
Map<String, Color> getAccentColors() {
if (_useManualOverride && _manualPrimary != null) {
return DynamicColorScheme.generateAccentColors([
_manualPrimary!,
_manualSecondary ?? _manualPrimary!,
_manualTertiary ?? (_manualSecondary ?? _manualPrimary!)
]);
}
if (_isUsingDynamicTheme && _logoColors.isNotEmpty) {
return DynamicColorScheme.generateAccentColors(_logoColors);
} else {
return DynamicColorScheme.generateAccentColors([]);
}
}
// Reset to default theme
void resetToDefaultTheme() {
_isUsingDynamicTheme = false;
_dynamicColorScheme = null;
_logoColors = [];
_useManualOverride = false;
_manualPrimary = null;
_manualSecondary = null;
_manualTertiary = null;
notifyListeners();
}
// Enable manual override with provided colors (hex or Color)
void enableManualOverride(
{Color? primary, Color? secondary, Color? tertiary}) {
_useManualOverride = true;
if (primary != null) _manualPrimary = primary;
if (secondary != null) _manualSecondary = secondary;
if (tertiary != null) _manualTertiary = tertiary;
notifyListeners();
}
// Disable manual override
void disableManualOverride() {
_useManualOverride = false;
notifyListeners();
}
// Update manual colors
void setManualColors({Color? primary, Color? secondary, Color? tertiary}) {
if (primary != null) _manualPrimary = primary;
if (secondary != null) _manualSecondary = secondary;
if (tertiary != null) _manualTertiary = tertiary;
// Ensure override is on when user sets colors
_useManualOverride = true;
notifyListeners();
}
// Check if logo has specific color characteristics
bool hasWarmColors() {
if (_logoColors.isEmpty) return false;
for (final color in _logoColors) {
final hsl = HSLColor.fromColor(color);
// Warm colors: red, orange, yellow (0-60 degrees)
if (hsl.hue >= 0 && hsl.hue <= 60) {
return true;
}
}
return false;
}
bool hasCoolColors() {
if (_logoColors.isEmpty) return false;
for (final color in _logoColors) {
final hsl = HSLColor.fromColor(color);
// Cool colors: blue, green, purple (120-300 degrees)
if (hsl.hue >= 120 && hsl.hue <= 300) {
return true;
}
}
return false;
}
bool hasNeutralColors() {
if (_logoColors.isEmpty) return false;
for (final color in _logoColors) {
final hsl = HSLColor.fromColor(color);
// Neutral colors: low saturation
if (hsl.saturation < 0.3) {
return true;
}
}
return false;
}
// Get theme description
String getThemeDescription() {
if (!_isUsingDynamicTheme) {
return 'Default Theme';
}
List<String> characteristics = [];
if (hasWarmColors()) characteristics.add('Warm');
if (hasCoolColors()) characteristics.add('Cool');
if (hasNeutralColors()) characteristics.add('Neutral');
if (characteristics.isEmpty) {
characteristics.add('Dynamic');
}
return '${characteristics.join(', ')} Theme';
}
// Get color palette info
Map<String, dynamic> getColorPaletteInfo() {
if (_logoColors.isEmpty) {
return {
'primary': null,
'secondary': null,
'tertiary': null,
'totalColors': 0,
'description': 'No logo colors available',
};
}
return {
'primary': _logoColors.isNotEmpty ? _logoColors[0] : null,
'secondary': _logoColors.length > 1 ? _logoColors[1] : null,
'tertiary': _logoColors.length > 2 ? _logoColors[2] : null,
'totalColors': _logoColors.length,
'description': 'Generated from uploaded logo',
'characteristics': {
'warm': hasWarmColors(),
'cool': hasCoolColors(),
'neutral': hasNeutralColors(),
},
};
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
class ThemeProvider extends ChangeNotifier {
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
void toggleTheme() {
_isDarkMode = !_isDarkMode;
notifyListeners();
}
void setTheme(bool isDark) {
if (_isDarkMode != isDark) {
_isDarkMode = isDark;
notifyListeners();
}
}
}

View File

@@ -0,0 +1,624 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/dynamic_theme_provider.dart';
import '../providers/theme_provider.dart';
class AppTheme {
static ThemeData getLightTheme(BuildContext context) {
final dynamicThemeProvider =
Provider.of<DynamicThemeProvider>(context, listen: false);
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);
// Get color scheme (dynamic or default)
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(false);
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: colorScheme,
textTheme: _getTextTheme(colorScheme),
appBarTheme: _getAppBarTheme(colorScheme),
cardTheme: _getCardTheme(colorScheme),
elevatedButtonTheme: _getElevatedButtonTheme(colorScheme),
outlinedButtonTheme: _getOutlinedButtonTheme(colorScheme),
textButtonTheme: _getTextButtonTheme(colorScheme),
inputDecorationTheme: _getInputDecorationTheme(colorScheme),
bottomNavigationBarTheme: _getBottomNavigationBarTheme(colorScheme),
floatingActionButtonTheme: _getFloatingActionButtonTheme(colorScheme),
dividerTheme: _getDividerTheme(colorScheme),
iconTheme: _getIconTheme(colorScheme),
chipTheme: _getChipTheme(colorScheme),
switchTheme: _getSwitchTheme(colorScheme),
checkboxTheme: _getCheckboxTheme(colorScheme),
radioTheme: _getRadioTheme(colorScheme),
sliderTheme: _getSliderTheme(colorScheme),
progressIndicatorTheme: _getProgressIndicatorTheme(colorScheme),
snackBarTheme: _getSnackBarTheme(colorScheme),
dialogTheme: _getDialogTheme(colorScheme),
bottomSheetTheme: _getBottomSheetTheme(colorScheme),
tooltipTheme: _getTooltipTheme(colorScheme),
popupMenuTheme: _getPopupMenuTheme(colorScheme),
drawerTheme: _getDrawerTheme(colorScheme),
listTileTheme: _getListTileTheme(colorScheme),
tabBarTheme: _getTabBarTheme(colorScheme),
dataTableTheme: _getDataTableTheme(colorScheme),
expansionTileTheme: _getExpansionTileTheme(colorScheme),
timePickerTheme: _getTimePickerTheme(colorScheme),
datePickerTheme: _getDatePickerTheme(colorScheme),
pageTransitionsTheme: _getPageTransitionsTheme(),
);
}
static ThemeData getDarkTheme(BuildContext context) {
final dynamicThemeProvider =
Provider.of<DynamicThemeProvider>(context, listen: false);
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);
// Get color scheme (dynamic or default)
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(true);
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: colorScheme,
textTheme: _getTextTheme(colorScheme),
appBarTheme: _getAppBarTheme(colorScheme),
cardTheme: _getCardTheme(colorScheme),
elevatedButtonTheme: _getElevatedButtonTheme(colorScheme),
outlinedButtonTheme: _getOutlinedButtonTheme(colorScheme),
textButtonTheme: _getTextButtonTheme(colorScheme),
inputDecorationTheme: _getInputDecorationTheme(colorScheme),
bottomNavigationBarTheme: _getBottomNavigationBarTheme(colorScheme),
floatingActionButtonTheme: _getFloatingActionButtonTheme(colorScheme),
dividerTheme: _getDividerTheme(colorScheme),
iconTheme: _getIconTheme(colorScheme),
chipTheme: _getChipTheme(colorScheme),
switchTheme: _getSwitchTheme(colorScheme),
checkboxTheme: _getCheckboxTheme(colorScheme),
radioTheme: _getRadioTheme(colorScheme),
sliderTheme: _getSliderTheme(colorScheme),
progressIndicatorTheme: _getProgressIndicatorTheme(colorScheme),
snackBarTheme: _getSnackBarTheme(colorScheme),
dialogTheme: _getDialogTheme(colorScheme),
bottomSheetTheme: _getBottomSheetTheme(colorScheme),
tooltipTheme: _getTooltipTheme(colorScheme),
popupMenuTheme: _getPopupMenuTheme(colorScheme),
drawerTheme: _getDrawerTheme(colorScheme),
listTileTheme: _getListTileTheme(colorScheme),
tabBarTheme: _getTabBarTheme(colorScheme),
dataTableTheme: _getDataTableTheme(colorScheme),
expansionTileTheme: _getExpansionTileTheme(colorScheme),
timePickerTheme: _getTimePickerTheme(colorScheme),
datePickerTheme: _getDatePickerTheme(colorScheme),
pageTransitionsTheme: _getPageTransitionsTheme(),
);
}
// Get theme based on current mode
// Get theme based on current mode
static ThemeData getTheme(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);
final dynamicThemeProvider =
Provider.of<DynamicThemeProvider>(context, listen: false);
// Force rebuild when dynamic theme changes
dynamicThemeProvider.isUsingDynamicTheme;
return themeProvider.isDarkMode
? getDarkTheme(context)
: getLightTheme(context);
}
// Text Theme
static TextTheme _getTextTheme(ColorScheme colorScheme) {
return TextTheme(
displayLarge: TextStyle(
fontSize: 57,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: -0.25,
),
displayMedium: TextStyle(
fontSize: 45,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0,
),
displaySmall: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0,
),
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0,
),
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0,
),
headlineSmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0,
),
titleLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
letterSpacing: 0,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
letterSpacing: 0.15,
),
titleSmall: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
letterSpacing: 0.1,
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0.5,
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0.25,
),
bodySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface,
letterSpacing: 0.4,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
letterSpacing: 0.1,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
letterSpacing: 0.5,
),
labelSmall: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
letterSpacing: 0.5,
),
);
}
// AppBar Theme
static AppBarTheme _getAppBarTheme(ColorScheme colorScheme) {
return AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
),
iconTheme: IconThemeData(
color: colorScheme.onSurface,
size: 24,
),
actionsIconTheme: IconThemeData(
color: colorScheme.onSurface,
size: 24,
),
);
}
// Card Theme
static CardThemeData _getCardTheme(ColorScheme colorScheme) {
return CardThemeData(
color: colorScheme.surface,
elevation: 2,
shadowColor: colorScheme.shadow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.all(8),
);
}
// Elevated Button Theme
static ElevatedButtonThemeData _getElevatedButtonTheme(
ColorScheme colorScheme) {
return ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 2,
shadowColor: colorScheme.shadow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
);
}
// Outlined Button Theme
static OutlinedButtonThemeData _getOutlinedButtonTheme(
ColorScheme colorScheme) {
return OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: colorScheme.primary,
side: BorderSide(color: colorScheme.outline, width: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
);
}
// Text Button Theme
static TextButtonThemeData _getTextButtonTheme(ColorScheme colorScheme) {
return TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
);
}
// Input Decoration Theme
static InputDecorationTheme _getInputDecorationTheme(
ColorScheme colorScheme) {
return InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.error, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
labelStyle: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 16,
),
hintStyle: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 16,
),
);
}
// Bottom Navigation Bar Theme
static BottomNavigationBarThemeData _getBottomNavigationBarTheme(
ColorScheme colorScheme) {
return BottomNavigationBarThemeData(
backgroundColor: colorScheme.surface,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurfaceVariant,
type: BottomNavigationBarType.fixed,
elevation: 8,
);
}
// Floating Action Button Theme
static FloatingActionButtonThemeData _getFloatingActionButtonTheme(
ColorScheme colorScheme) {
return FloatingActionButtonThemeData(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
);
}
// Divider Theme
static DividerThemeData _getDividerTheme(ColorScheme colorScheme) {
return DividerThemeData(
color: colorScheme.outlineVariant,
thickness: 1,
space: 1,
);
}
// Icon Theme
static IconThemeData _getIconTheme(ColorScheme colorScheme) {
return IconThemeData(
color: colorScheme.onSurface,
size: 24,
);
}
// Chip Theme
static ChipThemeData _getChipTheme(ColorScheme colorScheme) {
return ChipThemeData(
backgroundColor: colorScheme.surfaceVariant,
selectedColor: colorScheme.primaryContainer,
labelStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
);
}
// Switch Theme
// Switch Theme
static SwitchThemeData _getSwitchTheme(ColorScheme colorScheme) {
return SwitchThemeData(
thumbColor: MaterialStateProperty.all<Color?>(colorScheme.primary),
trackColor: MaterialStateProperty.all<Color?>(colorScheme.surfaceVariant),
);
}
// Checkbox Theme
// Checkbox Theme
static CheckboxThemeData _getCheckboxTheme(ColorScheme colorScheme) {
return CheckboxThemeData(
fillColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.selected)) {
return colorScheme.primary;
}
return colorScheme.surfaceVariant;
}),
checkColor: MaterialStateProperty.all<Color?>(colorScheme.onPrimary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
);
}
// Radio Theme
static RadioThemeData _getRadioTheme(ColorScheme colorScheme) {
return RadioThemeData(
fillColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.selected)) {
return colorScheme.primary;
}
return colorScheme.surfaceVariant;
}),
);
}
// Slider Theme
static SliderThemeData _getSliderTheme(ColorScheme colorScheme) {
return SliderThemeData(
activeTrackColor: colorScheme.primary,
inactiveTrackColor: colorScheme.surfaceVariant,
thumbColor: colorScheme.primary,
overlayColor: colorScheme.primary.withOpacity(0.2),
);
}
// Progress Indicator Theme
static ProgressIndicatorThemeData _getProgressIndicatorTheme(
ColorScheme colorScheme) {
return ProgressIndicatorThemeData(
color: colorScheme.primary,
linearTrackColor: colorScheme.surfaceVariant,
);
}
// SnackBar Theme
static SnackBarThemeData _getSnackBarTheme(ColorScheme colorScheme) {
return SnackBarThemeData(
backgroundColor: colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
);
}
// Dialog Theme
static DialogThemeData _getDialogTheme(ColorScheme colorScheme) {
return DialogThemeData(
backgroundColor: colorScheme.surface,
elevation: 24,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
);
}
// Bottom Sheet Theme
static BottomSheetThemeData _getBottomSheetTheme(ColorScheme colorScheme) {
return BottomSheetThemeData(
backgroundColor: colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
);
}
// Tooltip Theme
static TooltipThemeData _getTooltipTheme(ColorScheme colorScheme) {
return TooltipThemeData(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(8),
),
textStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 14,
),
);
}
// Popup Menu Theme
static PopupMenuThemeData _getPopupMenuTheme(ColorScheme colorScheme) {
return PopupMenuThemeData(
color: colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
);
}
// Drawer Theme
static DrawerThemeData _getDrawerTheme(ColorScheme colorScheme) {
return DrawerThemeData(
backgroundColor: colorScheme.surface,
elevation: 16,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(right: Radius.circular(16)),
),
);
}
// List Tile Theme
static ListTileThemeData _getListTileTheme(ColorScheme colorScheme) {
return ListTileThemeData(
tileColor: colorScheme.surface,
textColor: colorScheme.onSurface,
iconColor: colorScheme.onSurfaceVariant,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12),
// ),
);
}
// Tab Bar Theme
// Tab Bar Theme
static TabBarThemeData _getTabBarTheme(ColorScheme colorScheme) {
return TabBarThemeData(
labelColor: colorScheme.primary,
unselectedLabelColor: colorScheme.onSurfaceVariant,
indicatorColor: colorScheme.primary,
);
}
// Data Table Theme
static DataTableThemeData _getDataTableTheme(ColorScheme colorScheme) {
return DataTableThemeData(
dataTextStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 14,
),
headingTextStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 14,
fontWeight: FontWeight.w600,
),
dividerThickness: 1,
dataRowColor: MaterialStateProperty.all<Color?>(colorScheme.surface),
headingRowColor:
MaterialStateProperty.all<Color?>(colorScheme.surfaceVariant),
);
}
// Expansion Tile Theme
static ExpansionTileThemeData _getExpansionTileTheme(
ColorScheme colorScheme) {
return ExpansionTileThemeData(
backgroundColor: colorScheme.surface,
collapsedBackgroundColor: colorScheme.surfaceVariant,
textColor: colorScheme.onSurface,
collapsedTextColor: colorScheme.onSurfaceVariant,
iconColor: colorScheme.primary,
collapsedIconColor: colorScheme.onSurfaceVariant,
);
}
// Time Picker Theme
static TimePickerThemeData _getTimePickerTheme(ColorScheme colorScheme) {
return TimePickerThemeData(
backgroundColor: colorScheme.surface,
hourMinuteTextColor: colorScheme.onSurface,
hourMinuteColor: colorScheme.surfaceVariant,
dayPeriodTextColor: colorScheme.onSurface,
dayPeriodColor: colorScheme.surfaceVariant,
dialHandColor: colorScheme.primary,
dialBackgroundColor: colorScheme.surfaceVariant,
dialTextColor: colorScheme.onSurface,
entryModeIconColor: colorScheme.onSurfaceVariant,
);
}
// Date Picker Theme
static DatePickerThemeData _getDatePickerTheme(ColorScheme colorScheme) {
return DatePickerThemeData(
backgroundColor: colorScheme.surface,
headerBackgroundColor: colorScheme.primary,
headerForegroundColor: colorScheme.onPrimary,
dayForegroundColor:
MaterialStateProperty.all<Color?>(colorScheme.onSurface),
dayBackgroundColor:
MaterialStateProperty.all<Color?>(colorScheme.surface),
todayForegroundColor:
MaterialStateProperty.all<Color?>(colorScheme.primary),
todayBackgroundColor:
MaterialStateProperty.all<Color?>(colorScheme.primaryContainer),
yearForegroundColor:
MaterialStateProperty.all<Color?>(colorScheme.onSurface),
yearBackgroundColor:
MaterialStateProperty.all<Color?>(colorScheme.surface),
);
}
// Page Transitions Theme
static PageTransitionsTheme _getPageTransitionsTheme() {
return const PageTransitionsTheme(
builders: {
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
},
);
}
}

View File

@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
class AppColorScheme {
// Primary Colors - Modern Blue
static const Color _primaryLight = Color(0xFF2563EB);
static const Color _primaryDark = Color(0xFF3B82F6);
// Secondary Colors - Purple
static const Color _secondaryLight = Color(0xFF7C3AED);
static const Color _secondaryDark = Color(0xFF8B5CF6);
// Tertiary Colors - Teal
static const Color _tertiaryLight = Color(0xFF0D9488);
static const Color _tertiaryDark = Color(0xFF14B8A6);
// Light Color Scheme
static const ColorScheme lightColorScheme = ColorScheme(
brightness: Brightness.light,
primary: _primaryLight,
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFFDBEAFE),
onPrimaryContainer: Color(0xFF001E3C),
secondary: _secondaryLight,
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFEDE9FE),
onSecondaryContainer: Color(0xFF21005D),
tertiary: _tertiaryLight,
onTertiary: Color(0xFFFFFFFF),
tertiaryContainer: Color(0xFFCCFBF1),
onTertiaryContainer: Color(0xFF00201C),
error: Color(0xFFDC2626),
onError: Color(0xFFFFFFFF),
errorContainer: Color(0xFFFEE2E2),
onErrorContainer: Color(0xFF7F1D1D),
background: Color(0xFFFAFAFA),
onBackground: Color(0xFF1A1A1A),
surface: Color(0xFFFFFFFF),
onSurface: Color(0xFF1A1A1A),
surfaceVariant: Color(0xFFF3F4F6),
onSurfaceVariant: Color(0xFF6B7280),
outline: Color(0xFFD1D5DB),
outlineVariant: Color(0xFFE5E7EB),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
inverseSurface: Color(0xFF1F2937),
onInverseSurface: Color(0xFFF9FAFB),
inversePrimary: Color(0xFF93C5FD),
surfaceTint: _primaryLight,
);
// Dark Color Scheme
static const ColorScheme darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: _primaryDark,
onPrimary: Color(0xFF000000),
primaryContainer: Color(0xFF1E3A8A),
onPrimaryContainer: Color(0xFFDBEAFE),
secondary: _secondaryDark,
onSecondary: Color(0xFF000000),
secondaryContainer: Color(0xFF4C1D95),
onSecondaryContainer: Color(0xFFEDE9FE),
tertiary: _tertiaryDark,
onTertiary: Color(0xFF000000),
tertiaryContainer: Color(0xFF0F766E),
onTertiaryContainer: Color(0xFFCCFBF1),
error: Color(0xFFEF4444),
onError: Color(0xFF000000),
errorContainer: Color(0xFF7F1D1D),
onErrorContainer: Color(0xFFFEE2E2),
background: Color(0xFF0F172A),
onBackground: Color(0xFFF8FAFC),
surface: Color(0xFF1E293B),
onSurface: Color(0xFFF8FAFC),
surfaceVariant: Color(0xFF334155),
onSurfaceVariant: Color(0xFFCBD5E1),
outline: Color(0xFF64748B),
outlineVariant: Color(0xFF475569),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
inverseSurface: Color(0xFFF8FAFC),
onInverseSurface: Color(0xFF1E293B),
inversePrimary: Color(0xFF1E40AF),
surfaceTint: _primaryDark,
);
// Additional Custom Colors
static const Color success = Color(0xFF10B981);
static const Color warning = Color(0xFFF59E0B);
static const Color info = Color(0xFF3B82F6);
// Convenience getters for commonly used colors
static Color get primary => _primaryLight;
static Color get secondary => _secondaryLight;
static Color get tertiary => _tertiaryLight;
static Color get error => Color(0xFFDC2626);
// Gradient Colors
static const List<Color> primaryGradient = [
Color(0xFF2563EB),
Color(0xFF1D4ED8),
Color(0xFF1E40AF),
];
static const List<Color> secondaryGradient = [
Color(0xFF7C3AED),
Color(0xFF6D28D9),
Color(0xFF5B21B6),
];
static const List<Color> successGradient = [
Color(0xFF10B981),
Color(0xFF059669),
Color(0xFF047857),
];
static const List<Color> errorGradient = [
Color(0xFFEF4444),
Color(0xFFDC2626),
Color(0xFFB91C1C),
];
// Get color scheme based on brightness
static ColorScheme getColorScheme(Brightness brightness) {
return brightness == Brightness.dark ? darkColorScheme : lightColorScheme;
}
}

View File

@@ -0,0 +1,355 @@
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:palette_generator/palette_generator.dart';
class DynamicColorScheme {
static const Color _defaultPrimary = Color(0xFF3C82FF); // lighter blue
static const Color _defaultSecondary = Color(0xFF7C3AED);
static const Color _defaultTertiary = Color(0xFF0D9488);
static const Color _defaultSurface = Color(0xFFFFFFFF);
static const Color _defaultBackground = Color(0xFFFAFAFA);
// Extract dominant colors from logo image
static Future<List<Color>> extractColorsFromImage(
Uint8List imageBytes) async {
try {
// Convert bytes to image
final codec = await ui.instantiateImageCodec(imageBytes);
final frame = await codec.getNextFrame();
final image = frame.image;
// Generate palette from image
final paletteGenerator = await PaletteGenerator.fromImage(image);
List<Color> colors = [];
// Add dominant colors
if (paletteGenerator.dominantColor != null) {
colors.add(paletteGenerator.dominantColor!.color);
}
// Add vibrant colors
if (paletteGenerator.vibrantColor != null) {
colors.add(paletteGenerator.vibrantColor!.color);
}
// Add muted colors
if (paletteGenerator.mutedColor != null) {
colors.add(paletteGenerator.mutedColor!.color);
}
// Add light vibrant colors
if (paletteGenerator.lightVibrantColor != null) {
colors.add(paletteGenerator.lightVibrantColor!.color);
}
// Add dark vibrant colors
if (paletteGenerator.darkVibrantColor != null) {
colors.add(paletteGenerator.darkVibrantColor!.color);
}
// Ensure we have at least 3 colors
while (colors.length < 3) {
colors.add(_generateComplementaryColor(
colors.isNotEmpty ? colors.first : _defaultPrimary));
}
return colors.take(5).toList(); // Return top 5 colors
} catch (e) {
print('Error extracting colors: $e');
return [_defaultPrimary, _defaultSecondary, _defaultTertiary];
}
}
// Lightweight extraction using ImageProvider with downscaling to reduce jank
static Future<List<Color>> extractColorsFromBytesLite(
Uint8List imageBytes) async {
try {
final provider = MemoryImage(imageBytes);
// Downscale to limit processing cost
final resized = ResizeImage(provider, width: 256, height: 256);
final paletteGenerator =
await PaletteGenerator.fromImageProvider(resized);
final List<Color> colors = [];
if (paletteGenerator.dominantColor != null) {
colors.add(paletteGenerator.dominantColor!.color);
}
if (paletteGenerator.vibrantColor != null) {
colors.add(paletteGenerator.vibrantColor!.color);
}
if (paletteGenerator.mutedColor != null) {
colors.add(paletteGenerator.mutedColor!.color);
}
if (paletteGenerator.lightVibrantColor != null) {
colors.add(paletteGenerator.lightVibrantColor!.color);
}
if (paletteGenerator.darkVibrantColor != null) {
colors.add(paletteGenerator.darkVibrantColor!.color);
}
while (colors.length < 3) {
colors.add(_generateComplementaryColor(
colors.isNotEmpty ? colors.first : _defaultPrimary));
}
return colors.take(5).toList();
} catch (e) {
print('Error extracting colors (lite): $e');
return [_defaultPrimary, _defaultSecondary, _defaultTertiary];
}
}
// Generate complementary color
static Color _generateComplementaryColor(Color baseColor) {
final hsl = HSLColor.fromColor(baseColor);
return hsl.withHue((hsl.hue + 180) % 360).toColor();
}
// Generate dynamic color scheme from logo colors
static ColorScheme generateDynamicColorScheme(List<Color> logoColors,
{bool isDark = false}) {
if (logoColors.isEmpty) {
return _getDefaultColorScheme(isDark);
}
try {
// Extract primary, secondary, and tertiary colors
final primary = logoColors[0];
final secondary = logoColors.length > 1
? logoColors[1]
: _generateComplementaryColor(primary);
final tertiary = logoColors.length > 2
? logoColors[2]
: _generateComplementaryColor(secondary);
// Generate surface and background colors
final surface =
isDark ? _darkenColor(primary, 0.95) : _lightenColor(primary, 0.95);
final background =
isDark ? _darkenColor(primary, 0.98) : _lightenColor(primary, 0.98);
// Generate on-surface and on-background colors
final onSurface = isDark ? Colors.white : Colors.black;
final onBackground = isDark ? Colors.white : Colors.black;
// Generate outline and outline variant
final outline =
isDark ? _lightenColor(primary, 0.3) : _darkenColor(primary, 0.3);
final outlineVariant =
isDark ? _lightenColor(primary, 0.2) : _darkenColor(primary, 0.2);
// Generate surface variant
final surfaceVariant =
isDark ? _darkenColor(primary, 0.9) : _lightenColor(primary, 0.9);
// Generate error, warning, success, info colors
final error = isDark ? Color(0xFFF87171) : Color(0xFFEF4444);
final warning = isDark ? Color(0xFFFBBF24) : Color(0xFFF59E0B);
final success = isDark ? Color(0xFF34D399) : Color(0xFF10B981);
final info = isDark ? Color(0xFF60A5FA) : Color(0xFF3B82F6);
return ColorScheme(
brightness: isDark ? Brightness.dark : Brightness.light,
primary: primary,
onPrimary: _getContrastColor(primary),
primaryContainer: _generateContainerColor(primary, isDark),
onPrimaryContainer:
_getContrastColor(_generateContainerColor(primary, isDark)),
secondary: secondary,
onSecondary: _getContrastColor(secondary),
secondaryContainer: _generateContainerColor(secondary, isDark),
onSecondaryContainer:
_getContrastColor(_generateContainerColor(secondary, isDark)),
tertiary: tertiary,
onTertiary: _getContrastColor(tertiary),
tertiaryContainer: _generateContainerColor(tertiary, isDark),
onTertiaryContainer:
_getContrastColor(_generateContainerColor(tertiary, isDark)),
surface: surface,
onSurface: onSurface,
surfaceVariant: surfaceVariant,
onSurfaceVariant: _getContrastColor(surfaceVariant),
background: background,
onBackground: onBackground,
outline: outline,
outlineVariant: outlineVariant,
error: error,
onError: _getContrastColor(error),
errorContainer: _generateContainerColor(error, isDark),
onErrorContainer:
_getContrastColor(_generateContainerColor(error, isDark)),
shadow: isDark ? Colors.black : Colors.black12,
scrim: isDark ? Colors.black54 : Colors.black26,
inverseSurface: isDark ? background : surface,
onInverseSurface: isDark ? onBackground : onSurface,
inversePrimary:
isDark ? _lightenColor(primary, 0.8) : _darkenColor(primary, 0.8),
);
} catch (e) {
print('Error generating dynamic color scheme: $e');
return _getDefaultColorScheme(isDark);
}
}
// Get default color scheme
static ColorScheme _getDefaultColorScheme(bool isDark) {
if (isDark) {
return const ColorScheme.dark(
primary: Color(0xFF3B82F6),
secondary: Color(0xFF8B5CF6),
tertiary: Color(0xFF14B8A6),
);
} else {
return const ColorScheme.light(
primary: Color(0xFF2563EB),
secondary: Color(0xFF7C3AED),
tertiary: Color(0xFF0D9488),
);
}
}
// Generate container color
static Color _generateContainerColor(Color baseColor, bool isDark) {
if (isDark) {
return _lightenColor(baseColor, 0.2);
} else {
return _darkenColor(baseColor, 0.9);
}
}
// Lighten color
static Color _lightenColor(Color color, double amount) {
final hsl = HSLColor.fromColor(color);
return hsl
.withLightness((hsl.lightness + amount).clamp(0.0, 1.0))
.toColor();
}
// Darken color
static Color _darkenColor(Color color, double amount) {
final hsl = HSLColor.fromColor(color);
return hsl
.withLightness((hsl.lightness - amount).clamp(0.0, 1.0))
.toColor();
}
// Get contrast color (black or white)
static Color _getContrastColor(Color backgroundColor) {
final luminance = backgroundColor.computeLuminance();
return luminance > 0.5 ? Colors.black : Colors.white;
}
// Generate gradient colors from logo
static List<Color> generateGradientColors(List<Color> logoColors) {
if (logoColors.isEmpty) {
return [_defaultPrimary, _defaultSecondary, _defaultTertiary];
}
List<Color> gradientColors = [];
// Add primary colors
gradientColors.addAll(logoColors.take(3));
// Generate complementary colors if needed
while (gradientColors.length < 3) {
final lastColor = gradientColors.last;
gradientColors.add(_generateComplementaryColor(lastColor));
}
return gradientColors;
}
// Generate accent colors for specific UI elements
static Map<String, Color> generateAccentColors(List<Color> logoColors) {
if (logoColors.isEmpty) {
return {
'success': const Color(0xFF10B981),
'warning': const Color(0xFFF59E0B),
'error': const Color(0xFFEF4444),
'info': const Color(0xFF3B82F6),
};
}
final primary = logoColors[0];
return {
'success': _adjustColorForSuccess(primary),
'warning': _adjustColorForWarning(primary),
'error': _adjustColorForError(primary),
'info': _adjustColorForInfo(primary),
};
}
// Adjust colors for semantic meanings
static Color _adjustColorForSuccess(Color baseColor) {
final hsl = HSLColor.fromColor(baseColor);
return hsl.withHue(120).withSaturation(0.8).withLightness(0.5).toColor();
}
static Color _adjustColorForWarning(Color baseColor) {
final hsl = HSLColor.fromColor(baseColor);
return hsl.withHue(45).withSaturation(0.9).withLightness(0.6).toColor();
}
static Color _adjustColorForError(Color baseColor) {
final hsl = HSLColor.fromColor(baseColor);
return hsl.withHue(0).withSaturation(0.8).withLightness(0.5).toColor();
}
static Color _adjustColorForInfo(Color baseColor) {
final hsl = HSLColor.fromColor(baseColor);
return hsl.withHue(210).withSaturation(0.8).withLightness(0.5).toColor();
}
}
// Top-level helper to run in an isolate: returns list of color values (ints)
Future<List<int>> extractColorValuesFromImageBytes(Uint8List imageBytes) async {
try {
// Convert bytes to image
final codec = await ui.instantiateImageCodec(imageBytes);
final frame = await codec.getNextFrame();
final image = frame.image;
// Generate palette from image
final paletteGenerator = await PaletteGenerator.fromImage(image);
final List<int> values = [];
if (paletteGenerator.dominantColor != null) {
values.add(paletteGenerator.dominantColor!.color.value);
}
if (paletteGenerator.vibrantColor != null) {
values.add(paletteGenerator.vibrantColor!.color.value);
}
if (paletteGenerator.mutedColor != null) {
values.add(paletteGenerator.mutedColor!.color.value);
}
if (paletteGenerator.lightVibrantColor != null) {
values.add(paletteGenerator.lightVibrantColor!.color.value);
}
if (paletteGenerator.darkVibrantColor != null) {
values.add(paletteGenerator.darkVibrantColor!.color.value);
}
while (values.length < 3) {
// Fallback complementary color generation using default primary
values.add(DynamicColorScheme._generateComplementaryColor(
values.isNotEmpty
? Color(values.first)
: DynamicColorScheme._defaultPrimary,
).value);
}
return values.take(5).toList();
} catch (e) {
// Fallback to defaults
return [
DynamicColorScheme._defaultPrimary.value,
DynamicColorScheme._defaultSecondary.value,
DynamicColorScheme._defaultTertiary.value,
];
}
}

View File

@@ -0,0 +1,277 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTextTheme {
// Font Family
static const String _fontFamily = 'Inter';
// Light Text Theme
static TextTheme get lightTextTheme {
return GoogleFonts.interTextTheme().copyWith(
displayLarge: GoogleFonts.inter(
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
height: 1.12,
color: const Color(0xFF1A1A1A),
),
displayMedium: GoogleFonts.inter(
fontSize: 45,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.16,
color: const Color(0xFF1A1A1A),
),
displaySmall: GoogleFonts.inter(
fontSize: 36,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.22,
color: const Color(0xFF1A1A1A),
),
headlineLarge: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.25,
color: const Color(0xFF1A1A1A),
),
headlineMedium: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.29,
color: const Color(0xFF1A1A1A),
),
headlineSmall: GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.33,
color: const Color(0xFF1A1A1A),
),
titleLarge: GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.27,
color: const Color(0xFF1A1A1A),
),
titleMedium: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.15,
height: 1.5,
color: const Color(0xFF1A1A1A),
),
titleSmall: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: const Color(0xFF1A1A1A),
),
labelLarge: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: const Color(0xFF1A1A1A),
),
labelMedium: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
height: 1.33,
color: const Color(0xFF1A1A1A),
),
labelSmall: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
height: 1.45,
color: const Color(0xFF1A1A1A),
),
bodyLarge: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.5,
color: const Color(0xFF1A1A1A),
),
bodyMedium: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
color: const Color(0xFF1A1A1A),
),
bodySmall: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
color: const Color(0xFF6B7280),
),
);
}
// Dark Text Theme
static TextTheme get darkTextTheme {
return GoogleFonts.interTextTheme().copyWith(
displayLarge: GoogleFonts.inter(
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
height: 1.12,
color: const Color(0xFFF8FAFC),
),
displayMedium: GoogleFonts.inter(
fontSize: 45,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.16,
color: const Color(0xFFF8FAFC),
),
displaySmall: GoogleFonts.inter(
fontSize: 36,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.22,
color: const Color(0xFFF8FAFC),
),
headlineLarge: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.25,
color: const Color(0xFFF8FAFC),
),
headlineMedium: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.29,
color: const Color(0xFFF8FAFC),
),
headlineSmall: GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.33,
color: const Color(0xFFF8FAFC),
),
titleLarge: GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.27,
color: const Color(0xFFF8FAFC),
),
titleMedium: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.15,
height: 1.5,
color: const Color(0xFFF8FAFC),
),
titleSmall: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: const Color(0xFFF8FAFC),
),
labelLarge: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: const Color(0xFFF8FAFC),
),
labelMedium: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
height: 1.33,
color: const Color(0xFFF8FAFC),
),
labelSmall: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
height: 1.45,
color: const Color(0xFFF8FAFC),
),
bodyLarge: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.5,
color: const Color(0xFFF8FAFC),
),
bodyMedium: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
color: const Color(0xFFF8FAFC),
),
bodySmall: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
color: const Color(0xFFCBD5E1),
),
);
}
// Custom Text Styles for specific use cases
static TextStyle get heroTitle => GoogleFonts.inter(
fontSize: 48,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
height: 1.1,
);
static TextStyle get sectionTitle => GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.4,
);
static TextStyle get cardTitle => GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.33,
);
static TextStyle get buttonText => GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.5,
);
static TextStyle get caption => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
);
static TextStyle get overline => GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w600,
letterSpacing: 1.5,
height: 1.6,
);
// Get text theme based on brightness
static TextTheme getTextTheme(Brightness brightness) {
return brightness == Brightness.dark ? darkTextTheme : lightTextTheme;
}
}

View File

@@ -0,0 +1,16 @@
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
const String dateTimeFormatPattern = 'dd/MM/yyyy';
extension DateTimeExtension on DateTime {
String format({
String pattern = dateTimeFormatPattern,
String? locale,
}) {
if (locale != null && locale.isNotEmpty) {
initializeDateFormatting(locale);
}
return DateFormat(pattern, locale).format(this);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
// These are the Viewport values of your Figma Design.
// These are used in the code as a reference to create your UI Responsively.
const num FIGMA_DESIGN_WIDTH = 428;
const num FIGMA_DESIGN_HEIGHT = 926;
const num FIGMA_DESIGN_STATUS_BAR = 0;
extension ResponsiveExtension on num {
double get _width => SizeUtils.width;
double get _height => SizeUtils.height;
double get h => ((this * _width) / FIGMA_DESIGN_WIDTH);
double get v =>
(this * _height) / (FIGMA_DESIGN_HEIGHT - FIGMA_DESIGN_STATUS_BAR);
double get adaptSize {
var height = v;
var width = h;
return height < width ? height.toDoubleValue() : width.toDoubleValue();
}
double get fSize => adaptSize;
}
extension FormatExtension on double {
double toDoubleValue({int fractionDigits = 2}) {
return double.parse(toStringAsFixed(fractionDigits));
}
double isNonZero({num defaultValue = 0.0}) {
return this > 0 ? this : defaultValue.toDouble();
}
}
enum DeviceType { mobile, tablet, desktop }
typedef ResponsiveBuild = Widget Function(
BuildContext context, Orientation orientation, DeviceType deviceType);
class Sizer extends StatelessWidget {
const Sizer({super.key, required this.builder});
/// Builds the widget whenever the orientation changes.
final ResponsiveBuild builder;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return OrientationBuilder(builder: (context, orientation) {
SizeUtils.setScreenSize(constraints, orientation);
return builder(context, orientation, SizeUtils.deviceType);
});
});
}
}
// ignore_for_file: must_be_immutable
// ignore_for_file: must_be_immutable
class SizeUtils {
/// Device's BoxConstraints
static late BoxConstraints boxConstraints;
/// Device's Orientation
static late Orientation orientation;
/// Type of Device
///
/// This can either be mobile or tablet
static late DeviceType deviceType;
/// Device's Height
static late double height;
/// Device's Width
static late double width;
static void setScreenSize(
BoxConstraints constraints,
Orientation currentOrientation,
) {
boxConstraints = constraints;
orientation = currentOrientation;
if (orientation == Orientation.portrait) {
width =
boxConstraints.maxWidth.isNonZero(defaultValue: FIGMA_DESIGN_WIDTH);
height = boxConstraints.maxHeight.isNonZero();
} else {
width =
boxConstraints.maxHeight.isNonZero(defaultValue: FIGMA_DESIGN_WIDTH);
height = boxConstraints.maxWidth.isNonZero();
}
deviceType = DeviceType.mobile;
}
}

View File

@@ -0,0 +1,13 @@
bool isValidEmail(String? inputString, {bool isRequired = false}) {
bool isInputStringValid = false;
if (!isRequired && (inputString == null ? true : inputString.isEmpty)) {
isInputStringValid = true;
}
if (inputString != null && inputString.isNotEmpty) {
const pattern =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
final regExp = RegExp(pattern);
isInputStringValid = regExp.hasMatch(inputString);
}
return isInputStringValid;
}

View File

@@ -0,0 +1,81 @@
class AppExceptions implements Exception {
final String? _message;
final String? _prefix;
AppExceptions([this._message, this._prefix]);
@override
String toString() {
return "${_prefix ?? ''}${_message ?? 'An unknown error occurred'}";
}
}
// Network error when data fetch fails
class FetchDataException extends AppExceptions {
FetchDataException([String? message])
: super(message ?? "Network Error: Failed to communicate with the server. Please check your internet connection and try again.",
"Error During Communication: ");
}
// Error for invalid or malformed requests
class BadRequestException extends AppExceptions {
BadRequestException([String? message])
: super(message ?? "Client Error: The request sent to the server was malformed or contained invalid parameters.",
"Invalid Request: ");
}
// Error for unauthorized access
class UnauthorizedException extends AppExceptions {
UnauthorizedException([String? message])
: super(message ?? "Authorization Error: You are not authorized to perform this action. Please log in with appropriate credentials.",
"Unauthorized: ");
}
// Error when a resource is not found
class NotFoundException extends AppExceptions {
NotFoundException([String? message])
: super(message ?? "Resource Not Found: The requested resource could not be found on the server. It may have been moved or deleted.",
"Not Found: ");
}
// Error for server-side issues
class InternalServerErrorException extends AppExceptions {
InternalServerErrorException([String? message])
: super(message ?? "Server Error: An unexpected error occurred on the server. Please try again later or contact support.",
"Internal Server Error: ");
}
// Error when user input is invalid
class InvalidInputException extends AppExceptions {
InvalidInputException([String? message])
: super(message ?? "Validation Error: The provided input does not match the required format. Please correct the errors and try again.",
"Invalid Input: ");
}
// Error when a request times out
class TimeoutException extends AppExceptions {
TimeoutException([String? message])
: super(message ?? "Request Timeout: The server took too long to respond. Please check your connection and try again.",
"Timeout: ");
}
// Error when a request conflicts with the current state
class ConflictException extends AppExceptions {
ConflictException([String? message])
: super(message ?? "Conflict Error: The request could not be processed because of a conflict with the current state of the resource.",
"Conflict: ");
}
// Error when the service is unavailable
class ServiceUnavailableException extends AppExceptions {
ServiceUnavailableException([String? message])
: super(message ?? "Service Unavailable: The server is currently unable to handle the request. Please try again later.",
"Service Unavailable: ");
}
// Error when access to a resource is forbidden
class ForbiddenException extends AppExceptions {
ForbiddenException([String? message])
: super(message ?? "Forbidden: You do not have the necessary permissions to access this resource.",
"Forbidden: ");
}

View File

@@ -0,0 +1,7 @@
abstract class BaseNetworkService {
Future<dynamic> getGetApiResponse(String? url);
Future<dynamic> getPostApiResponse(String? url, dynamic body);
Future<dynamic> getPutApiResponse(String? url, dynamic body);
Future<dynamic> getDeleteApiResponse(String? url);
}

View File

@@ -0,0 +1,157 @@
import 'dart:io';
import 'package:base_project/utils/managers/user_manager.dart';
import 'package:dio/dio.dart';
import '../exceptions/app_exceptions.dart';
import 'base_network_service.dart';
class NetworkApiService extends BaseNetworkService {
final Dio _dio = Dio();
NetworkApiService() {
// Optionally configure Dio, e.g. add interceptors
_dio.options.connectTimeout = const Duration(seconds: 30); // 30 seconds
_dio.options.receiveTimeout = const Duration(seconds: 30);
}
@override
Future<dynamic> getGetApiResponse(String? url) async {
try {
final token = UserManager().token;
// print("token..$token");
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.get(
url!,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
@override
Future<dynamic> getPostApiResponse(String? url, dynamic body) async {
try {
final token = UserManager().token;
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.post(
url!,
data: body,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
@override
Future<dynamic> getPutApiResponse(String? url, dynamic body) async {
try {
final token = UserManager().token;
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.put(
url!,
data: body,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
@override
Future<dynamic> getDeleteApiResponse(String? url) async {
try {
final token = UserManager().token;
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.delete(
url!,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
dynamic _handleResponse(Response response) {
switch (response.statusCode) {
case 200:
case 201:
try {
// Handle empty body
if (response.data == null || response.data.toString().isEmpty) {
return null;
}
return response.data;
} catch (e) {
throw FetchDataException('Error parsing response: $e');
}
case 400:
throw BadRequestException('Bad request: ${response.data}');
case 401:
throw UnauthorizedException('Unauthorized request: ${response.data}');
case 403:
throw ForbiddenException('Forbidden request: ${response.data}');
case 404:
throw NotFoundException('Not found: ${response.data}');
case 500:
throw InternalServerErrorException('Server error: ${response.data}');
default:
throw FetchDataException(
'Error while communicating with server: ${response.statusCode}');
}
}
dynamic _handleDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
throw FetchDataException("Request timed out.");
case DioExceptionType.badResponse:
return _handleResponse(error.response!);
case DioExceptionType.cancel:
throw FetchDataException("Request was cancelled.");
case DioExceptionType.connectionError:
throw FetchDataException("Connection failed due to internet issue.");
default:
throw FetchDataException("Unexpected error occurred.");
}
}
}

View File

@@ -0,0 +1,142 @@
import 'dart:io';
import 'package:dio/dio.dart';
import '../exceptions/app_exceptions.dart';
import 'no_token_base_network_service.dart';
class NoTokenNetworkApiService extends NoTokenBaseNetworkService {
final Dio _dio = Dio();
NetworkApiService() {
// Optionally configure Dio, e.g. add interceptors
_dio.options.connectTimeout = const Duration(seconds: 30); // 30 seconds
_dio.options.receiveTimeout = const Duration(seconds: 30);
}
@override
Future<dynamic> getGetApiResponse(String? url) async {
try {
final headers = {
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.get(
url!,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
@override
Future<dynamic> getPostApiResponse(String? url, dynamic body) async {
try {
final headers = {
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.post(
url!,
data: body,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
@override
Future<dynamic> getPutApiResponse(String? url, dynamic body) async {
try {
final headers = {
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.put(
url!,
data: body,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
@override
Future<dynamic> getDeleteApiResponse(String? url) async {
try {
final headers = {
'Content-Type': 'application/json', // Add other headers if needed
};
final response = await _dio.delete(
url!,
options: Options(headers: headers),
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleDioError(e);
} on SocketException {
throw FetchDataException('No Internet Connection');
} catch (e) {
throw FetchDataException('An unexpected error occurred: $e');
}
}
dynamic _handleResponse(Response response) {
switch (response.statusCode) {
case 200:
case 201:
try {
// Handle empty body
if (response.data == null || response.data.toString().isEmpty) {
return null;
}
return response.data;
} catch (e) {
throw FetchDataException('Error parsing response: $e');
}
case 400:
throw BadRequestException('Bad request: ${response.data}');
case 401:
throw UnauthorizedException('Unauthorized request: ${response.data}');
case 403:
throw ForbiddenException('Forbidden request: ${response.data}');
case 404:
throw NotFoundException('Not found: ${response.data}');
case 500:
throw InternalServerErrorException('Server error: ${response.data}');
default:
throw FetchDataException(
'Error while communicating with server: ${response.statusCode}');
}
}
dynamic _handleDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
throw FetchDataException("Request timed out.");
case DioExceptionType.badResponse:
return _handleResponse(error.response!);
case DioExceptionType.cancel:
throw FetchDataException("Request was cancelled.");
case DioExceptionType.connectionError:
throw FetchDataException("Connection failed due to internet issue.");
default:
throw FetchDataException("Unexpected error occurred.");
}
}
}

View File

@@ -0,0 +1,7 @@
abstract class NoTokenBaseNetworkService {
Future<dynamic> getGetApiResponse(String? url);
Future<dynamic> getPostApiResponse(String? url, dynamic body);
Future<dynamic> getPutApiResponse(String? url, dynamic body);
Future<dynamic> getDeleteApiResponse(String? url);
}

View File

@@ -0,0 +1,36 @@
// import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// class NotificationService {
// final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// Future<void> initialize() async {
// const AndroidInitializationSettings initializationSettingsAndroid =
// AndroidInitializationSettings('@mipmap/ic_launcher');
// const InitializationSettings initializationSettings =
// InitializationSettings(android: initializationSettingsAndroid);
// await flutterLocalNotificationsPlugin.initialize(initializationSettings);
// }
// Future<void> showNotification(int id, String title, String body) async {
// const AndroidNotificationDetails androidPlatformChannelSpecifics =
// AndroidNotificationDetails(
// 'channel_id',
// 'channel_name',
// channelDescription: 'channel_description',
// importance: Importance.max,
// priority: Priority.high,
// );
// const NotificationDetails platformChannelSpecifics =
// NotificationDetails(android: androidPlatformChannelSpecifics);
// await flutterLocalNotificationsPlugin.show(
// id,
// title,
// body,
// platformChannelSpecifics,
// );
// }
// }

View File

@@ -0,0 +1,23 @@
import 'package:base_project/data/response/status.dart';
class ApiResponse<T> {
Status? status;
T? data;
String? message;
ApiResponse(this.status, this.data, this.message);
// Named constructor for loading state
ApiResponse.loading() : status = Status.LOADING;
// Named constructor for completed state
ApiResponse.success(this.data) : status = Status.SUCCESS;
// Named constructor for error state
ApiResponse.error(this.message) : status = Status.ERROR;
@override
String toString() {
return "Status: $status \n Message: $message \n Data: $data";
}
}

View File

@@ -0,0 +1 @@
enum Status {LOADING,SUCCESS,ERROR}

View File

@@ -0,0 +1,104 @@
import 'package:base_project/resources/app_colors.dart';
import 'package:base_project/routes/route_names.dart';
import 'package:base_project/view_model/auth/auth_view_model.dart';
import 'package:base_project/view_model/profile/profile_view_model.dart';
import 'package:base_project/view_model/system_params/system_params_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'core/providers/dynamic_theme_provider.dart';
import 'core/providers/theme_provider.dart';
import 'core/theme/app_theme.dart';
import 'routes/app_routes.dart';
import 'utils/managers/user_manager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await UserManager().initialize();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AuthViewModel()),
ChangeNotifierProvider(create: (context) => ProfileViewModel()),
ChangeNotifierProvider(create: (context) => SystemParamsViewModel()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => DynamicThemeProvider()),
],
child: const MyApp(),
));
}
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget {
const MyApp({super.key});
// @override
// Widget build(BuildContext context) {
// // Set the navigation bar color when the app starts
// SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
// systemNavigationBarColor:
// AppColors.primary, // Set your desired color here
// systemNavigationBarIconBrightness: Brightness.light, // Icons color
// ));
// return MaterialApp(
// theme: ThemeData(
// primaryColor: AppColors.primary,
// visualDensity: VisualDensity.adaptivePlatformDensity,
// useMaterial3: false,
// scaffoldBackgroundColor: Colors.grey[200],
// drawerTheme: DrawerThemeData(backgroundColor: Colors.grey[200]),
// iconTheme: const IconThemeData(color: AppColors.primary),
// appBarTheme: const AppBarTheme(
// scrolledUnderElevation: 0, backgroundColor: AppColors.primary)),
// title: 'Base Project',
// debugShowCheckedModeBanner: false,
// // home: const SysParameter(),
// initialRoute: RouteNames.splashView,
// onGenerateRoute: AppRoutes.generateRoutes,
// );
// }
// }
@override
Widget build(BuildContext context) {
return Consumer2<ThemeProvider, DynamicThemeProvider>(
builder: (context, themeProvider, dynamicThemeProvider, child) {
final theme = AppTheme.getTheme(context);
// Set system UI overlay style
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
themeProvider.isDarkMode ? Brightness.light : Brightness.dark,
systemNavigationBarColor: theme.colorScheme.surface,
systemNavigationBarIconBrightness:
themeProvider.isDarkMode ? Brightness.light : Brightness.dark,
));
return MaterialApp(
title: 'AuthSec Flutter',
debugShowCheckedModeBanner: false,
theme: AppTheme.getLightTheme(context),
darkTheme: AppTheme.getDarkTheme(context),
themeMode:
themeProvider.isDarkMode ? ThemeMode.dark : ThemeMode.light,
initialRoute: RouteNames.splashView,
onGenerateRoute: AppRoutes.generateRoutes,
navigatorKey: navigatorKey,
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: MediaQuery.of(context).textScaler.clamp(
minScaleFactor: 0.8,
maxScaleFactor: 1.4,
),
),
child: child!,
);
},
);
},
);
}
}

View File

@@ -0,0 +1,210 @@
class SystemParamsModel {
final int? id;
final int? schedulerTime;
final String? leaseTaxCode;
final int? vesselConfProcessLimit;
final int? rowToDisplay;
final int? linkToDisplay;
final int? rowToAdd;
final int? lovRowToDisplay;
final int? lovLinkToDisplay;
final String? oidserverName;
final String? oidBase;
final String? oidAdminUser;
final int? oidServerPort;
final int? userDefaultGroup;
final String? defaultDepartment;
final String? defaultPosition;
final String? singleCharge;
final String? firstDayOftheWeek;
final int? hourPerShift;
final int? cnBillingFrequency;
final String? billingDepartmentCode;
final String? basePriceList;
final String? nonContainerServiceOrder;
final int? ediMaeSchedulerONOFF;
final String? ediSchedulerONOFF;
final String? uploadLogo;
final String? uploadLogoName;
final String? uploadLogoPath;
final String? companyDisplayName;
final bool? isRegitrationAllowed;
final List<dynamic>? sysParamUploads;
SystemParamsModel({
this.id,
this.schedulerTime,
this.leaseTaxCode,
this.vesselConfProcessLimit,
this.rowToDisplay,
this.linkToDisplay,
this.rowToAdd,
this.lovRowToDisplay,
this.lovLinkToDisplay,
this.oidserverName,
this.oidBase,
this.oidAdminUser,
this.oidServerPort,
this.userDefaultGroup,
this.defaultDepartment,
this.defaultPosition,
this.singleCharge,
this.firstDayOftheWeek,
this.hourPerShift,
this.cnBillingFrequency,
this.billingDepartmentCode,
this.basePriceList,
this.nonContainerServiceOrder,
this.ediMaeSchedulerONOFF,
this.ediSchedulerONOFF,
this.uploadLogo,
this.uploadLogoName,
this.uploadLogoPath,
this.companyDisplayName,
this.isRegitrationAllowed,
this.sysParamUploads,
});
factory SystemParamsModel.fromJson(Map<String, dynamic> json) {
return SystemParamsModel(
id: json['id'],
schedulerTime: json['schedulerTime'],
leaseTaxCode: json['leaseTaxCode'],
vesselConfProcessLimit: json['vesselConfProcessLimit'],
rowToDisplay: json['rowToDisplay'],
linkToDisplay: json['linkToDisplay'],
rowToAdd: json['rowToAdd'],
lovRowToDisplay: json['lovRowToDisplay'],
lovLinkToDisplay: json['lovLinkToDisplay'],
oidserverName: json['oidserverName'],
oidBase: json['oidBase'],
oidAdminUser: json['oidAdminUser'],
oidServerPort: json['oidServerPort'],
userDefaultGroup: json['userDefaultGroup'],
defaultDepartment: json['defaultDepartment'],
defaultPosition: json['defaultPosition'],
singleCharge: json['singleCharge'],
firstDayOftheWeek: json['firstDayOftheWeek'],
hourPerShift: json['hourPerShift'],
cnBillingFrequency: json['cnBillingFrequency'],
billingDepartmentCode: json['billingDepartmentCode'],
basePriceList: json['basePriceList'],
nonContainerServiceOrder: json['nonContainerServiceOrder'],
ediMaeSchedulerONOFF: json['ediMaeSchedulerONOFF'],
ediSchedulerONOFF: json['ediSchedulerONOFF'],
uploadLogo: json['upload_Logo'],
uploadLogoName: json['upload_Logo_name'],
uploadLogoPath: json['upload_Logo_path'],
companyDisplayName: json['Company_Display_Name'],
isRegitrationAllowed: json['isRegitrationAllowed'],
sysParamUploads: json['sysParamUploads'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'schedulerTime': schedulerTime,
'leaseTaxCode': leaseTaxCode,
'vesselConfProcessLimit': vesselConfProcessLimit,
'rowToDisplay': rowToDisplay,
'linkToDisplay': linkToDisplay,
'rowToAdd': rowToAdd,
'lovRowToDisplay': lovRowToDisplay,
'lovLinkToDisplay': lovLinkToDisplay,
'oidserverName': oidserverName,
'oidBase': oidBase,
'oidAdminUser': oidAdminUser,
'oidServerPort': oidServerPort,
'userDefaultGroup': userDefaultGroup,
'defaultDepartment': defaultDepartment,
'defaultPosition': defaultPosition,
'singleCharge': singleCharge,
'firstDayOftheWeek': firstDayOftheWeek,
'hourPerShift': hourPerShift,
'cnBillingFrequency': cnBillingFrequency,
'billingDepartmentCode': billingDepartmentCode,
'basePriceList': basePriceList,
'nonContainerServiceOrder': nonContainerServiceOrder,
'ediMaeSchedulerONOFF': ediMaeSchedulerONOFF,
'ediSchedulerONOFF': ediSchedulerONOFF,
'upload_Logo': uploadLogo,
'upload_Logo_name': uploadLogoName,
'upload_Logo_path': uploadLogoPath,
'Company_Display_Name': companyDisplayName,
'isRegitrationAllowed': isRegitrationAllowed,
'sysParamUploads': sysParamUploads,
};
}
SystemParamsModel copyWith({
int? id,
int? schedulerTime,
String? leaseTaxCode,
int? vesselConfProcessLimit,
int? rowToDisplay,
int? linkToDisplay,
int? rowToAdd,
int? lovRowToDisplay,
int? lovLinkToDisplay,
String? oidserverName,
String? oidBase,
String? oidAdminUser,
int? oidServerPort,
int? userDefaultGroup,
String? defaultDepartment,
String? defaultPosition,
String? singleCharge,
String? firstDayOftheWeek,
int? hourPerShift,
int? cnBillingFrequency,
String? billingDepartmentCode,
String? basePriceList,
String? nonContainerServiceOrder,
int? ediMaeSchedulerONOFF,
String? ediSchedulerONOFF,
String? uploadLogo,
String? uploadLogoName,
String? uploadLogoPath,
String? companyDisplayName,
bool? isRegitrationAllowed,
List<dynamic>? sysParamUploads,
}) {
return SystemParamsModel(
id: id ?? this.id,
schedulerTime: schedulerTime ?? this.schedulerTime,
leaseTaxCode: leaseTaxCode ?? this.leaseTaxCode,
vesselConfProcessLimit:
vesselConfProcessLimit ?? this.vesselConfProcessLimit,
rowToDisplay: rowToDisplay ?? this.rowToDisplay,
linkToDisplay: linkToDisplay ?? this.linkToDisplay,
rowToAdd: rowToAdd ?? this.rowToAdd,
lovRowToDisplay: lovRowToDisplay ?? this.lovRowToDisplay,
lovLinkToDisplay: lovLinkToDisplay ?? this.lovLinkToDisplay,
oidserverName: oidserverName ?? this.oidserverName,
oidBase: oidBase ?? this.oidBase,
oidAdminUser: oidAdminUser ?? this.oidAdminUser,
oidServerPort: oidServerPort ?? this.oidServerPort,
userDefaultGroup: userDefaultGroup ?? this.userDefaultGroup,
defaultDepartment: defaultDepartment ?? this.defaultDepartment,
defaultPosition: defaultPosition ?? this.defaultPosition,
singleCharge: singleCharge ?? this.singleCharge,
firstDayOftheWeek: firstDayOftheWeek ?? this.firstDayOftheWeek,
hourPerShift: hourPerShift ?? this.hourPerShift,
cnBillingFrequency: cnBillingFrequency ?? this.cnBillingFrequency,
billingDepartmentCode:
billingDepartmentCode ?? this.billingDepartmentCode,
basePriceList: basePriceList ?? this.basePriceList,
nonContainerServiceOrder:
nonContainerServiceOrder ?? this.nonContainerServiceOrder,
ediMaeSchedulerONOFF: ediMaeSchedulerONOFF ?? this.ediMaeSchedulerONOFF,
ediSchedulerONOFF: ediSchedulerONOFF ?? this.ediSchedulerONOFF,
uploadLogo: uploadLogo ?? this.uploadLogo,
uploadLogoName: uploadLogoName ?? this.uploadLogoName,
uploadLogoPath: uploadLogoPath ?? this.uploadLogoPath,
companyDisplayName: companyDisplayName ?? this.companyDisplayName,
isRegitrationAllowed: isRegitrationAllowed ?? this.isRegitrationAllowed,
sysParamUploads: sysParamUploads ?? this.sysParamUploads,
);
}
}

View File

@@ -0,0 +1,57 @@
import 'dart:convert';
class LoggedUserModel {
final String token;
final String userId;
final String fullname;
final String? username;
final String email;
final String firstName;
final List<String> roles;
LoggedUserModel({
required this.token,
required this.userId,
required this.fullname,
this.username,
required this.email,
required this.firstName,
required this.roles,
});
// Factory constructor to create a UserModel from a JSON map
factory LoggedUserModel.fromJson(Map<String, dynamic> json) {
return LoggedUserModel(
token: json['token'] as String,
userId: json['userId'] as String,
fullname: json['fullname'] as String,
username: json['username'] as String?,
email: json['email'] as String,
firstName: json['firstName'] as String,
roles: List<String>.from(json['roles'] as List<dynamic>),
);
}
// Method to convert UserModel to JSON map
Map<String, dynamic> toJson() {
return {
'token': token,
'userId': userId,
'fullname': fullname,
'username': username,
'email': email,
'firstName': firstName,
'roles': roles,
};
}
// Method to convert UserModel to a JSON string
String toJsonString() {
return json.encode(toJson());
}
// Factory constructor to create a UserModel from a JSON string
factory LoggedUserModel.fromJsonString(String jsonString) {
return LoggedUserModel.fromJson(json.decode(jsonString));
}
}

View File

@@ -0,0 +1,49 @@
class UserModelAdmin {
final String id;
final String userId;
final String userName;
final String fullName;
final String email;
final bool isPunchIn;
final bool isPunchOut;
final bool onBreak;
UserModelAdmin({
required this.id,
required this.userId,
required this.userName,
required this.fullName,
required this.email,
required this.isPunchIn,
required this.isPunchOut,
required this.onBreak,
});
// Factory method to create a User from JSON
factory UserModelAdmin.fromJson(Map<String, dynamic> json) {
return UserModelAdmin(
id: json['id'].toString(),
userId: json['userId'].toString(),
userName: json['userName'] ?? '',
fullName: json['fullName'] ?? '',
email: json['email'] ?? '',
isPunchIn: json['isPunchIn'] ?? false,
isPunchOut: json['isPunchOut'] ?? false,
onBreak: json['onBreak'] ?? false,
);
}
// Convert User to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'userName': userName,
'fullName': fullName,
'email': email,
'isPunchIn': isPunchIn,
'isPunchOut': isPunchOut,
'onBreak': onBreak,
};
}
}

View File

@@ -0,0 +1,26 @@
class UserProfile {
final dynamic userId;
final String username;
final String email;
final dynamic mobNo;
final String fullName;
UserProfile({
required this.userId,
required this.username,
required this.email,
required this.mobNo,
required this.fullName,
});
factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
userId: json['userId'],
username: json['username'],
email: json['email'],
mobNo: json['mob_no'],
fullName: json['fullName'],
);
}
}

View File

@@ -0,0 +1,71 @@
import 'package:base_project/data/network/no_token_base_network_service.dart';
import 'package:base_project/resources/api_constants.dart';
import '../data/network/no-token_network_api_service.dart';
class AuthRepo {
final NoTokenBaseNetworkService _service = NoTokenNetworkApiService();
Future<dynamic> loginApi(dynamic body) async {
try {
final res =
await _service.getPostApiResponse(ApiConstants.loginEndpoint, body);
return res;
} catch (e) {
rethrow;
}
}
Future<dynamic> getOtpApi(dynamic body) async {
try {
final res =
await _service.getPostApiResponse(ApiConstants.getOtpEndpoint, body);
return res;
} catch (e) {
rethrow;
}
}
Future<dynamic> verifyOtpApi(dynamic body) async {
print(' boody is $body');
try {
final email = Uri.encodeComponent((body['email'] ?? '').toString());
final otp = Uri.encodeComponent((body['otp'] ?? '').toString());
final url = "${ApiConstants.verifyEndpoint}?email=$email&otp=$otp";
final res = await _service.getPostApiResponse(url, null);
return res;
} catch (e) {
rethrow;
}
}
Future<dynamic> resendOtpApi(dynamic body) async {
try {
final res = await _service.getPostApiResponse(
ApiConstants.createAcEndpoint, body);
return res;
} catch (e) {
rethrow;
}
}
Future<dynamic> createUserApi(dynamic body) async {
try {
final res = await _service.getPostApiResponse(
ApiConstants.createUserEndpoint, body);
return res;
} catch (e) {
rethrow;
}
}
Future<dynamic> createAcApi(dynamic body) async {
try {
final res = await _service.getPostApiResponse(
ApiConstants.createAcEndpoint, body);
return res;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,82 @@
import 'package:base_project/data/network/base_network_service.dart';
import 'package:base_project/data/network/network_api_service.dart';
import 'package:base_project/resources/api_constants.dart';
class ProfileRepo {
final BaseNetworkService _networkService = NetworkApiService();
Future<dynamic> getProfileImgApi() async {
try {
final response = _networkService
.getGetApiResponse(ApiConstants.getUserProfileImgEndpoint);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> getProfileApi() async {
try {
final response = _networkService
.getGetApiResponse(ApiConstants.getUserProfileEndpoint);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> updateProfileApi(dynamic data, dynamic uId) async {
final uri = Uri.parse("${ApiConstants.updateUserProfileEndpoint}/$uId");
try {
final response =
await _networkService.getPutApiResponse(uri.toString(), data);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> updateProfileImg(dynamic data) async {
try {
final response = await _networkService.getPostApiResponse(
ApiConstants.updateUserProfileImgEndpoint, data);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> changPassApi(dynamic data) async {
try {
final response = await _networkService.getPostApiResponse(
ApiConstants.changePasswordEndpoint, data);
print('chan res $response');
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> forgotPassApi(dynamic data) async {
try {
final response = await _networkService.getPostApiResponse(
ApiConstants.forgotPasswordEndpoint, data);
return response;
} catch (e) {
rethrow;
}
}
// Update System Account
Future<dynamic> updateAccountApi(dynamic accId, dynamic data) async {
final uri = Uri.parse("${ApiConstants.updateAcEndpoint}/$accId");
try {
final response =
await _networkService.getPutApiResponse(uri.toString(), data);
return response;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,80 @@
import 'package:base_project/data/network/base_network_service.dart';
import 'package:base_project/data/network/network_api_service.dart';
import 'package:base_project/resources/api_constants.dart';
class SystemParamsRepo {
final BaseNetworkService _networkService = NetworkApiService();
Future<dynamic> getProfileImgApi() async {
try {
final response = _networkService
.getGetApiResponse(ApiConstants.getUserProfileImgEndpoint);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> getProfileApi() async {
try {
final response = _networkService
.getGetApiResponse(ApiConstants.getUserProfileEndpoint);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> updateProfileApi(dynamic data, dynamic uId) async {
final uri = Uri.parse("${ApiConstants.updateUserProfileEndpoint}/$uId");
try {
final response =
await _networkService.getPutApiResponse(uri.toString(), data);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> updateProfileImg(dynamic data) async {
try {
final response = await _networkService.getPostApiResponse(
ApiConstants.updateUserProfileImgEndpoint, data);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> getSystemParameters() async {
final uri = Uri.parse("${ApiConstants.getSystemParameters}");
try {
final response = _networkService.getGetApiResponse(uri.toString());
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> updateSystemParameters(dynamic body) async {
try {
final uri = Uri.parse("${ApiConstants.updateSystemParams}");
final response = _networkService.getPutApiResponse(uri.toString(), body);
return response;
} catch (e) {
rethrow;
}
}
Future<dynamic> uploadSystemParamLogo(dynamic data) async {
try {
final uri = Uri.parse("${ApiConstants.uploadSystemParamImg}");
final response =
await _networkService.getPostApiResponse(uri.toString(), data);
return response;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,27 @@
class ApiConstants {
// USER AUTH API'S //
static const loginEndpoint = "$baseUrl/token/session";
static const getOtpEndpoint = "$baseUrl/token/user/send_email";
static const resendOtpEndpoint = "$baseUrl/token/user/resend_otp";
static const verifyEndpoint = "$baseUrl/token/user/otp_verification";
static const createUserEndpoint = "$baseUrl/token/addOneAppUser";
static const createAcEndpoint =
"$baseUrl/token/users/sysaccount/savesysaccount";
static const updateAcEndpoint =
"$baseUrl/token/users/sysaccount/savesysaccount"; // PUT {accId}
// PROFILE API'S //
static const getUserProfileEndpoint = '$baseUrl/api/user-profile';
static const getUserProfileImgEndpoint = '$baseUrl/api/retrieve-image';
static const updateUserProfileEndpoint = '$baseUrl/api/updateAppUserDto';
static const updateUserProfileImgEndpoint = '$baseUrl/api/upload';
static const changePasswordEndpoint = '$baseUrl/api/reset_password';
static const forgotPasswordEndpoint = '$baseUrl/api/resources/forgotpassword';
// SYSTEM PARAMS API'S //
static const uploadSystemParamImg = '$baseUrl/api/logos/upload?ref=test';
static const getSystemParameters = '$baseUrl/sysparam/getSysParams';
static const updateSystemParams = '$baseUrl/sysparam/updateSysParams';
static const baseUrl = 'http://localhost:9292';
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
class AppColors{
// App Main Colors
static const Color primary = Color.fromARGB(255, 8, 50, 112); // Vibrant blue
static const Color primary2 = Color(0xff7647EB); // Purple
static const Color accent = Color(0xffb88cec); // Soft Lavender
static const Color secondary = Color(0xffD2D8EC); // Soft Blue-Gray
// Additional Colors
static const Color complementary = Color(0xffFFA726); // Vibrant Orange
static const Color neutral = Color(0xffF5F5F5); // Light Gray or White
static const Color darkContrast = Color(0xff212121); // Charcoal Gray
// Text colors
static const Color textPrimary = Color(0xff282D40);
static const Color textSecondary = Color (0xffCACCD1);
static const Color textWhite = Colors.white;
// Background colors
static const Color background = Color(0xfff6f6f6);
static const Color btmNavBackground = Color(0xffededed);
static const Color tint = Color(0xffFFFFFF);
static Color? appbarBg = Colors.grey[900];
// Button colors punch in and out
static const Color clockInButtonColor1 = Color(0xfff84756);
static const Color clockInButtonColor2 = Color(0xfff42f40);
static const Color clockOutButtonColor1 = Color(0xff4CAF50);
static const Color clockOutButtonColor2 = Color(0xff2E7D32);
// Button colors break in and out
static const Color startBreakBtnColor1 = Color(0xfff88b47);
static const Color startBreakBtnColor2 = Color(0xfff45d2f);
static const Color endBreakBtnColor1 = Color(0xff2d49d6);
static const Color endBreakBtnColor2 = Color(0xFF1976D2);
// Validation colors
static const Color error = Color(0xffFF4C4C);
static const Color errorAccent = Color(0xffFFDBDB);
static const Color successAccent = Color(0xffDBFFEC);
static const Color success = Color(0xff00D261);
static const Color warning = Color (0xFFF57C00);
static const Color info = Color (0xFF1976D2);
// Neutral shades
static const Color grey = Color(0xff9E9E9E);
static const Color lightGrey = Color(0xffBDBDBD);
static const Color darkGrey = Color(0xff757575);
static const Color darkGrey2 = Color(0xff2e2e2e);
// Admin Card color
static const List<Color> breakIndicator = [Color(0xffdd5c0c),Color(0xffff6200)];
static const List<Color> presentIndicator = [Color(0xff2ca63c), Color(0xff1f8f2d)];
static const List<Color> absentIndicator = [Color(0xffc51f1f),Color(0xffea2424)];
}

View File

@@ -0,0 +1,87 @@
import 'package:base_project/routes/route_names.dart';
import 'package:base_project/view/auth/get_otp.dart';
import 'package:base_project/view/auth/login.dart';
import 'package:base_project/view/auth/register_acc.dart';
import 'package:base_project/view/auth/signup.dart';
import 'package:base_project/view/auth/verify_otp.dart';
import 'package:base_project/view/dashboard/home.dart';
import 'package:base_project/view/dashboard/profile/change_password.dart';
import 'package:base_project/view/dashboard/profile/profile.dart';
import 'package:base_project/view/splash_screen.dart';
import 'package:base_project/view/system_parameters/system_parameters.dart';
import 'package:flutter/material.dart';
class AppRoutes {
static Route<dynamic> generateRoutes(RouteSettings routeSettings) {
switch (routeSettings.name) {
case RouteNames.splashView:
return MaterialPageRoute(
builder: (context) => const SplashScreen(),
);
case RouteNames.loginView:
return MaterialPageRoute(
builder: (context) => const LoginView(),
);
case RouteNames.signUpView:
return MaterialPageRoute(
builder: (context) => const SignupView(),
);
case RouteNames.getOtpView:
return MaterialPageRoute(
builder: (context) => GetOtpView(),
);
case RouteNames.verifyOtpView:
final args = routeSettings.arguments;
String? email;
if (args is Map) {
email = args['email']?.toString();
}
return MaterialPageRoute(
builder: (context) => VerifyOtpView(email: email),
);
case RouteNames.registerAccView:
return MaterialPageRoute(
builder: (context) => const RegisterAccView(),
);
case RouteNames.homeView:
return MaterialPageRoute(
builder: (context) => const HomeView(),
);
case RouteNames.profileView:
return MaterialPageRoute(
builder: (context) => const ProfileView(),
);
// case RouteNames.editProfileView:
// return MaterialPageRoute(
// builder: (context) => const EditProfile(),
// );
case RouteNames.changePasswordView:
return MaterialPageRoute(
builder: (context) => ChangePassword(),
);
case RouteNames.systemParamsView:
return MaterialPageRoute(
builder: (context) => const SystemParametersView(),
);
default:
return MaterialPageRoute(
builder: (context) {
print("Checking Default route");
// SplashViewModel().checkNavigation(context);
// return const Scaffold(
// body: Center(
// child: Text("No route defined"),
// ),
// );
// If we're on System Parameters, do not auto-redirect
if (routeSettings.name == RouteNames.systemParamsView) {
return const SystemParametersView();
}
// Fallback to Splash so SplashViewModel can decide (login vs home)
return const SplashScreen();
},
);
}
}
}

View File

@@ -0,0 +1,18 @@
class RouteNames {
// AUTH VIEW //
static const splashView = '/splash';
static const loginView = '/login';
static const signUpView = '/signUp';
static const getOtpView = '/getOtp';
static const verifyOtpView = '/verifyOtp';
static const registerAccView = '/registerAccount';
static const createAccView = '/createAccount';
// MAIN APP VIEW //
static const homeView = '/home';
static const profileView = '/profile';
static const editProfileView = '/editProfile';
static const changePasswordView = '/changePassword';
static const systemParamsView = '/systemParams';
}

View File

@@ -0,0 +1,221 @@
import 'package:base_project/view_model/system_params/system_params_view_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../core/providers/theme_provider.dart';
import '../theme_toggle.dart';
class ModernAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool automaticallyImplyLeading;
final Color? backgroundColor;
final Color? foregroundColor;
final double? elevation;
final bool centerTitle;
final Widget? titleWidget;
final VoidCallback? onMenuPressed;
final VoidCallback? onProfilePressed;
final String? userAvatar;
final ImageProvider<Object>? userAvatarImage;
final String? userName;
final bool showThemeToggle;
final bool showUserProfile;
final bool showLogoInTitle;
final bool showBackButton;
final ImageProvider<Object>? logoImage;
const ModernAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.automaticallyImplyLeading = true,
this.backgroundColor,
this.foregroundColor,
this.elevation,
this.centerTitle = true,
this.titleWidget,
this.onMenuPressed,
this.onProfilePressed,
this.userAvatar,
this.userAvatarImage,
this.userName,
this.showThemeToggle = true,
this.showUserProfile = true,
this.showLogoInTitle = true,
this.showBackButton = false,
this.logoImage,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDarkMode = theme.brightness == Brightness.dark;
final Widget computedTitle = _buildTitle(context, theme, colorScheme);
return AppBar(
title: computedTitle,
centerTitle: titleWidget != null
? centerTitle
: (showLogoInTitle ? false : centerTitle),
backgroundColor: backgroundColor ?? colorScheme.surface,
foregroundColor: foregroundColor ?? colorScheme.onSurface,
elevation: elevation ?? 0,
automaticallyImplyLeading: automaticallyImplyLeading,
leading: leading ??
(automaticallyImplyLeading
? (showBackButton
? IconButton(
icon: Icon(
Icons.arrow_back,
color: foregroundColor ?? colorScheme.onSurface,
),
onPressed: () {
Navigator.of(context).pop();
},
)
: IconButton(
icon: Icon(
Icons.menu,
color: foregroundColor ?? colorScheme.onSurface,
),
onPressed: onMenuPressed ??
() {
Scaffold.of(context).openDrawer();
},
))
: null),
actions: [
// Theme Toggle
if (showThemeToggle) ...[
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return ThemeToggle(
isDarkMode: themeProvider.isDarkMode,
onThemeChanged: (isDark) {
themeProvider.setTheme(isDark);
},
size: UIConstants.iconSizeMedium,
);
},
),
const SizedBox(width: UIConstants.spacing8),
],
// User Profile
if (showUserProfile) ...[
GestureDetector(
onTap: onProfilePressed,
child: Container(
margin: const EdgeInsets.only(right: UIConstants.spacing16),
padding: const EdgeInsets.all(UIConstants.spacing4),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 2,
),
),
child: CircleAvatar(
radius: UIConstants.iconSizeMedium / 2,
backgroundColor: colorScheme.primaryContainer,
backgroundImage: userAvatarImage ??
(userAvatar != null ? NetworkImage(userAvatar!) : null),
child: userAvatarImage == null && userAvatar == null
? Icon(
Icons.person,
color: colorScheme.onPrimaryContainer,
size: UIConstants.iconSizeMedium * 0.6,
)
: null,
),
),
),
],
// Custom Actions
if (actions != null) ...actions!,
],
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(UIConstants.radius16),
),
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
Widget _buildTitle(
BuildContext context, ThemeData theme, ColorScheme colorScheme) {
if (titleWidget != null) return titleWidget!;
final effectiveLogo = logoImage ?? _resolveLogoImage(context);
if (showLogoInTitle) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: (backgroundColor ?? colorScheme.surface),
borderRadius: BorderRadius.circular(UIConstants.radius12),
border: Border.all(
color: colorScheme.outline.withOpacity(0.15), width: 1),
),
clipBehavior: Clip.antiAlias,
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: effectiveLogo != null
? Image(
image: effectiveLogo,
filterQuality: FilterQuality.high,
)
: Icon(
Icons.image_not_supported_outlined,
color: colorScheme.onSurface.withOpacity(0.5),
),
),
),
const SizedBox(width: UIConstants.spacing12),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: foregroundColor ?? colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
);
}
return Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: foregroundColor ?? colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
);
}
ImageProvider<Object>? _resolveLogoImage(BuildContext context) {
try {
final sysVm = Provider.of<SystemParamsViewModel>(context, listen: true);
if (sysVm.profileImageBytes != null &&
sysVm.profileImageBytes!.isNotEmpty) {
return MemoryImage(sysVm.profileImageBytes!);
}
} catch (_) {}
return const AssetImage('assets/images/image_not_found.png');
}
}

View File

@@ -0,0 +1,342 @@
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
enum ModernButtonType {
primary,
secondary,
outline,
text,
danger,
}
enum ModernButtonSize {
small,
medium,
large,
}
class ModernButton extends StatefulWidget {
final String text;
final VoidCallback? onPressed;
final ModernButtonType type;
final ModernButtonSize size;
final bool isLoading;
final bool isDisabled;
final Widget? icon;
final bool isIconOnly;
final double? width;
final double? height;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
final String? tooltip;
const ModernButton({
super.key,
required this.text,
this.onPressed,
this.type = ModernButtonType.primary,
this.size = ModernButtonSize.medium,
this.isLoading = false,
this.isDisabled = false,
this.icon,
this.isIconOnly = false,
this.width,
this.height,
this.padding,
this.borderRadius,
this.tooltip,
});
@override
State<ModernButton> createState() => _ModernButtonState();
}
class _ModernButtonState extends State<ModernButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _elevationAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_elevationAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
if (!widget.isDisabled && !widget.isLoading) {
setState(() {
_isPressed = true;
});
_animationController.forward();
}
}
void _onTapUp(TapUpDetails details) {
if (!widget.isDisabled && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
void _onTapCancel() {
if (!widget.isDisabled && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
bool get _canPress =>
!widget.isDisabled && !widget.isLoading && widget.onPressed != null;
Color _getBackgroundColor(ThemeData theme) {
if (!_canPress) {
return theme.colorScheme.surfaceVariant;
}
switch (widget.type) {
case ModernButtonType.primary:
return theme.colorScheme.primary;
case ModernButtonType.secondary:
return theme.colorScheme.secondary;
case ModernButtonType.outline:
case ModernButtonType.text:
return Colors.transparent;
case ModernButtonType.danger:
return theme.colorScheme.error;
}
}
Color _getTextColor(ThemeData theme) {
if (!_canPress) {
return theme.colorScheme.onSurfaceVariant;
}
switch (widget.type) {
case ModernButtonType.primary:
case ModernButtonType.secondary:
case ModernButtonType.danger:
return theme.colorScheme.onPrimary;
case ModernButtonType.outline:
return theme.colorScheme.primary;
case ModernButtonType.text:
return theme.colorScheme.primary;
}
}
Color _getBorderColor(ThemeData theme) {
if (!_canPress) {
return theme.colorScheme.outline;
}
switch (widget.type) {
case ModernButtonType.outline:
return theme.colorScheme.primary;
case ModernButtonType.danger:
return theme.colorScheme.error;
default:
return Colors.transparent;
}
}
double _getHeight() {
if (widget.height != null) return widget.height!;
switch (widget.size) {
case ModernButtonSize.small:
return UIConstants.buttonHeightSmall +
4; // extra room to avoid text clip
case ModernButtonSize.medium:
return UIConstants.buttonHeightMedium + 4;
case ModernButtonSize.large:
return UIConstants.buttonHeightLarge + 4;
}
}
EdgeInsetsGeometry _getPadding() {
if (widget.padding != null) return widget.padding!;
switch (widget.size) {
case ModernButtonSize.small:
return const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing6,
);
case ModernButtonSize.medium:
return const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing14,
);
case ModernButtonSize.large:
return const EdgeInsets.symmetric(
horizontal: UIConstants.spacing32,
vertical: UIConstants.spacing18,
);
}
}
double _getBorderRadius() {
if (widget.borderRadius != null) {
return widget.borderRadius!.topLeft.x;
}
switch (widget.size) {
case ModernButtonSize.small:
return UIConstants.radius8;
case ModernButtonSize.medium:
return UIConstants.radius16;
case ModernButtonSize.large:
return UIConstants.radius20;
}
}
TextStyle _getTextStyle(ThemeData theme) {
switch (widget.size) {
case ModernButtonSize.small:
return theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
) ??
const TextStyle();
case ModernButtonSize.medium:
return theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
) ??
const TextStyle();
case ModernButtonSize.large:
return theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
) ??
const TextStyle();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: Container(
width: widget.width,
height: _getHeight(),
decoration: BoxDecoration(
color: _getBackgroundColor(theme),
borderRadius: BorderRadius.circular(_getBorderRadius()),
border: Border.all(
color: _getBorderColor(theme),
width: widget.type == ModernButtonType.outline ? 1.5 : 0,
),
boxShadow: _canPress
? [
BoxShadow(
color: _getBackgroundColor(theme).withOpacity(0.3),
blurRadius: 8 * _elevationAnimation.value,
offset: Offset(0, 4 * _elevationAnimation.value),
),
]
: null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _canPress ? widget.onPressed : null,
borderRadius: BorderRadius.circular(_getBorderRadius()),
child: Container(
padding: _getPadding(),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.isLoading) ...[
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
_getTextColor(theme),
),
),
),
if (!widget.isIconOnly) ...[
const SizedBox(width: UIConstants.spacing12),
],
] else ...[
if (widget.icon != null && !widget.isIconOnly) ...[
IconTheme(
data: IconThemeData(
color: _getTextColor(theme),
size: _getHeight() * 0.4,
),
child: widget.icon!,
),
const SizedBox(width: UIConstants.spacing12),
],
],
if (!widget.isIconOnly) ...[
Flexible(
child: Text(
widget.text,
style: _getTextStyle(theme).copyWith(
color: _getTextColor(theme),
height: 1.2,
),
strutStyle: const StrutStyle(
height: 1.2,
forceStrutHeight: true,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
class QuickActionButton extends StatefulWidget {
final String label;
final IconData icon;
final VoidCallback? onTap;
final Color? backgroundColor;
final Color? iconColor;
final Color? labelColor;
final double? size;
final bool isLoading;
final bool isDisabled;
const QuickActionButton({
super.key,
required this.label,
required this.icon,
this.onTap,
this.backgroundColor,
this.iconColor,
this.labelColor,
this.size,
this.isLoading = false,
this.isDisabled = false,
});
@override
State<QuickActionButton> createState() => _QuickActionButtonState();
}
class _QuickActionButtonState extends State<QuickActionButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _elevationAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_elevationAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
if (widget.onTap != null && !widget.isDisabled && !widget.isLoading) {
_animationController.forward();
}
}
void _onTapUp(TapUpDetails details) {
if (widget.onTap != null && !widget.isDisabled && !widget.isLoading) {
_animationController.reverse();
}
}
void _onTapCancel() {
if (widget.onTap != null && !widget.isDisabled && !widget.isLoading) {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final buttonSize = widget.size ?? UIConstants.logoSizeMedium;
final backgroundColor =
widget.backgroundColor ?? colorScheme.primaryContainer;
final iconColor = widget.iconColor ?? colorScheme.onPrimaryContainer;
final labelColor = widget.labelColor ?? colorScheme.onSurface;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(UIConstants.radius16),
boxShadow: [
BoxShadow(
color: backgroundColor.withOpacity(0.3),
blurRadius: 8 * _elevationAnimation.value,
offset: Offset(0, 4 * _elevationAnimation.value),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: (widget.onTap != null &&
!widget.isDisabled &&
!widget.isLoading)
? widget.onTap
: null,
borderRadius: BorderRadius.circular(UIConstants.radius16),
child: Padding(
padding: const EdgeInsets.all(UIConstants.spacing12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icon
if (widget.isLoading)
SizedBox(
width: buttonSize * 0.3,
height: buttonSize * 0.3,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(iconColor),
),
)
else
Icon(
widget.icon,
color: iconColor,
size: buttonSize * 0.4,
),
const SizedBox(height: UIConstants.spacing8),
// Label
Text(
widget.label,
style: theme.textTheme.labelMedium?.copyWith(
color: labelColor,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,355 @@
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
enum DashboardCardType {
primary,
secondary,
success,
warning,
info,
danger,
}
class DashboardCard extends StatefulWidget {
final String title;
final String? subtitle;
final String? value;
final num? numericValue;
final String? valueSuffix;
final Duration? animationDuration;
final IconData? icon;
final DashboardCardType type;
final VoidCallback? onTap;
final Widget? trailing;
final bool isLoading;
final Color? customColor;
final double? elevation;
final BorderRadius? borderRadius;
const DashboardCard({
super.key,
required this.title,
this.subtitle,
this.value,
this.numericValue,
this.valueSuffix,
this.animationDuration,
this.icon,
this.type = DashboardCardType.primary,
this.onTap,
this.trailing,
this.isLoading = false,
this.customColor,
this.elevation,
this.borderRadius,
});
@override
State<DashboardCard> createState() => _DashboardCardState();
}
class _DashboardCardState extends State<DashboardCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _elevationAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_elevationAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
if (widget.onTap != null && !widget.isLoading) {
setState(() {
_isPressed = true;
});
_animationController.forward();
}
}
void _onTapUp(TapUpDetails details) {
if (widget.onTap != null && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
void _onTapCancel() {
if (widget.onTap != null && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
Color _getCardColor(ThemeData theme) {
if (widget.customColor != null) return widget.customColor!;
switch (widget.type) {
case DashboardCardType.primary:
return theme.colorScheme.primary;
case DashboardCardType.secondary:
return theme.colorScheme.secondary;
case DashboardCardType.success:
return theme.colorScheme.tertiary;
case DashboardCardType.warning:
return const Color(0xFFF59E0B);
case DashboardCardType.info:
return theme.colorScheme.primary;
case DashboardCardType.danger:
return theme.colorScheme.error;
}
}
Color _getIconColor(ThemeData theme) {
if (widget.customColor != null) return Colors.white;
switch (widget.type) {
case DashboardCardType.primary:
case DashboardCardType.secondary:
case DashboardCardType.success:
case DashboardCardType.warning:
case DashboardCardType.info:
case DashboardCardType.danger:
return Colors.white;
}
}
double _getElevation() {
if (widget.elevation != null) return widget.elevation!;
return UIConstants.elevation4;
}
BorderRadius _getBorderRadius() {
if (widget.borderRadius != null) return widget.borderRadius!;
return BorderRadius.circular(UIConstants.radius16);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final cardColor = _getCardColor(theme);
final iconColor = _getIconColor(theme);
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: Container(
decoration: BoxDecoration(
borderRadius: _getBorderRadius(),
color: cardColor,
boxShadow: [
BoxShadow(
color: cardColor.withOpacity(0.3),
blurRadius: 8 * _elevationAnimation.value,
offset: Offset(0, 4 * _elevationAnimation.value),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onTap,
borderRadius: _getBorderRadius(),
child: Padding(
padding: const EdgeInsets.all(UIConstants.spacing16),
child: LayoutBuilder(
builder: (context, constraints) {
final isTight = constraints.maxHeight < 150;
final isVeryTight = constraints.maxHeight < 130;
final spacing16 = isTight
? UIConstants.spacing8
: UIConstants.spacing16;
final spacing12 = isTight
? UIConstants.spacing6
: UIConstants.spacing12;
final iconSize = isTight
? UIConstants.iconSizeMedium
: UIConstants.iconSizeLarge;
final titleStyle =
theme.textTheme.titleMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: isVeryTight ? 12 : (isTight ? 14 : null),
);
final subtitleStyle =
theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.8),
fontSize: isVeryTight ? 10 : (isTight ? 12 : null),
);
final valueStyle =
theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: isVeryTight ? 16 : (isTight ? 18 : null),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Icon
if (widget.icon != null)
Container(
padding: EdgeInsets.all(spacing12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(
UIConstants.radius12),
),
child: Icon(
widget.icon,
color: iconColor,
size: iconSize,
),
),
// Trailing Widget
if (widget.trailing != null) widget.trailing!,
],
),
SizedBox(height: spacing16),
// Title
Text(
widget.title,
style: titleStyle,
maxLines: isTight ? 1 : 2,
overflow: TextOverflow.ellipsis,
),
// Subtitle
if (widget.subtitle != null && !isVeryTight) ...[
SizedBox(height: spacing12),
Text(
widget.subtitle!,
style: subtitleStyle,
maxLines: isTight ? 1 : 2,
overflow: TextOverflow.ellipsis,
),
],
// Value (animated if numeric)
SizedBox(height: spacing16),
if (widget.numericValue != null)
TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: widget.numericValue!.toDouble()),
duration: widget.animationDuration ??
UIConstants.durationNormal,
curve: UIConstants.curveNormal,
builder: (context, value, child) {
final text =
_formatNumber(value, widget.valueSuffix);
final valueText = Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: valueStyle,
);
return isTight
? FittedBox(
alignment: Alignment.centerLeft,
child: valueText)
: valueText;
},
)
else if (widget.value != null)
Builder(builder: (context) {
final valueText = Text(
widget.value!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: valueStyle,
);
return isTight
? FittedBox(
alignment: Alignment.centerLeft,
child: valueText)
: valueText;
}),
// Loading Indicator
if (widget.isLoading) ...[
SizedBox(height: spacing16),
const LinearProgressIndicator(
backgroundColor: Colors.white24,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
],
],
);
},
),
),
),
),
),
),
);
},
);
}
String _formatNumber(double value, String? suffix) {
String formatted;
if (value >= 1000000) {
formatted = "${(value / 1000000).toStringAsFixed(1)}M";
} else if (value >= 1000) {
formatted = "${(value / 1000).toStringAsFixed(1)}K";
} else {
if (value == value.roundToDouble()) {
formatted = value.toInt().toString();
} else {
formatted = value.toStringAsFixed(1);
}
}
if (suffix != null && suffix.isNotEmpty) {
return "$formatted$suffix";
}
return formatted;
}
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import '../../../../core/constants/ui_constants.dart';
class SystemParameterSection extends StatelessWidget {
final String title;
final String? subtitle;
final IconData icon;
final List<Widget> children;
final bool isExpanded;
final VoidCallback? onToggle;
final Color? iconColor;
const SystemParameterSection({
super.key,
required this.title,
this.subtitle,
required this.icon,
required this.children,
this.isExpanded = true,
this.onToggle,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: UIConstants.spacing16),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(UIConstants.radius16),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
border: Border.all(
color: colorScheme.outline.withOpacity(0.1),
width: 1,
),
),
child: Column(
children: [
// Section Header
InkWell(
onTap: onToggle,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(UIConstants.radius16),
),
child: Container(
padding: const EdgeInsets.all(UIConstants.spacing20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primaryContainer.withOpacity(0.3),
colorScheme.secondaryContainer.withOpacity(0.1),
],
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(UIConstants.radius16),
),
),
child: Row(
children: [
// Icon
Container(
padding: const EdgeInsets.all(UIConstants.spacing12),
decoration: BoxDecoration(
color: iconColor?.withOpacity(0.1) ??
colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
child: Icon(
icon,
color: iconColor ?? colorScheme.primary,
size: UIConstants.iconSizeMedium,
),
),
const SizedBox(width: UIConstants.spacing16),
// Title and Subtitle
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
if (subtitle != null) ...[
const SizedBox(height: UIConstants.spacing4),
Text(
subtitle!,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
],
),
),
// Expand/Collapse Icon
if (onToggle != null)
AnimatedRotation(
turns: isExpanded ? 0.5 : 0,
duration: UIConstants.durationFast,
child: Icon(
Icons.keyboard_arrow_down,
color: colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
),
],
),
),
),
// Section Content
AnimatedContainer(
duration: UIConstants.durationFast,
curve: UIConstants.curveNormal,
height: isExpanded ? null : 0,
child: isExpanded
? Container(
padding: const EdgeInsets.all(UIConstants.spacing20),
child: Column(
children: children,
),
)
: const SizedBox.shrink(),
),
],
),
);
}
}

View File

@@ -0,0 +1,357 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
import '../buttons/modern_button.dart';
class ModernImagePicker extends StatefulWidget {
final String? label;
final String? hint;
final Uint8List? imageBytes;
final File? imageFile;
final String? imageUrl;
final VoidCallback? onPickImage;
final VoidCallback? onRemoveImage;
final bool isLoading;
final double? height;
final double? width;
final BorderRadius? borderRadius;
final String? errorText;
final bool showRemoveButton;
final IconData? placeholderIcon;
final String? placeholderText;
const ModernImagePicker({
super.key,
this.label,
this.hint,
this.imageBytes,
this.imageFile,
this.imageUrl,
this.onPickImage,
this.onRemoveImage,
this.isLoading = false,
this.height = 200,
this.width,
this.borderRadius,
this.errorText,
this.showRemoveButton = true,
this.placeholderIcon,
this.placeholderText,
});
@override
State<ModernImagePicker> createState() => _ModernImagePickerState();
}
class _ModernImagePickerState extends State<ModernImagePicker>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
_fadeAnimation = Tween<double>(
begin: 1.0,
end: 0.8,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
bool get hasImage =>
widget.imageBytes != null ||
widget.imageFile != null ||
(widget.imageUrl != null && widget.imageUrl!.isNotEmpty);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
if (widget.label != null) ...[
Text(
widget.label!,
style: theme.textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing12),
],
// Image Container
GestureDetector(
onTapDown: (_) => _animationController.forward(),
onTapUp: (_) => _animationController.reverse(),
onTapCancel: () => _animationController.reverse(),
onTap: widget.onPickImage,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
height: widget.height,
width: widget.width ?? double.infinity,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ??
BorderRadius.circular(UIConstants.radius16),
border: Border.all(
color: widget.errorText != null
? colorScheme.error
: colorScheme.outline.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: widget.borderRadius ??
BorderRadius.circular(UIConstants.radius16),
child: Stack(
children: [
// Image or Placeholder
if (hasImage)
_buildImageWidget()
else
_buildPlaceholder(theme, colorScheme),
// Loading Overlay
if (widget.isLoading)
_buildLoadingOverlay(colorScheme),
// Remove Button
if (hasImage &&
widget.showRemoveButton &&
widget.onRemoveImage != null)
_buildRemoveButton(colorScheme),
],
),
),
),
),
);
},
),
),
// Hint Text
if (widget.hint != null) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
widget.hint!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
// Error Text
if (widget.errorText != null) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
widget.errorText!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.error,
),
),
],
// Action Buttons
const SizedBox(height: UIConstants.spacing16),
Row(
children: [
Expanded(
child: ModernButton(
text: hasImage ? 'Change Image' : 'Pick Image',
type: ModernButtonType.outline,
size: ModernButtonSize.medium,
icon: Icon(hasImage ? Icons.edit : Icons.add_photo_alternate),
onPressed: widget.isLoading ? null : widget.onPickImage,
isLoading: widget.isLoading,
),
),
if (hasImage && widget.onRemoveImage != null) ...[
const SizedBox(width: UIConstants.spacing12),
ModernButton(
text: 'Remove',
type: ModernButtonType.danger,
size: ModernButtonSize.medium,
icon: Icon(Icons.delete_outline),
onPressed: widget.isLoading ? null : widget.onRemoveImage,
),
],
],
),
],
);
}
Widget _buildImageWidget() {
if (widget.imageBytes != null) {
return Image.memory(
widget.imageBytes!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
} else if (widget.imageFile != null) {
return Image.file(
widget.imageFile!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
} else if (widget.imageUrl != null && widget.imageUrl!.isNotEmpty) {
return Image.network(
widget.imageUrl!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholder(
Theme.of(context), Theme.of(context).colorScheme);
},
);
}
return const SizedBox.shrink();
}
Widget _buildPlaceholder(ThemeData theme, ColorScheme colorScheme) {
return Container(
width: double.infinity,
height: double.infinity,
color: colorScheme.surfaceVariant.withOpacity(0.3),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.placeholderIcon ?? Icons.add_photo_alternate,
size: UIConstants.iconSizeXLarge,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: UIConstants.spacing12),
Text(
widget.placeholderText ?? 'Tap to select image',
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: UIConstants.spacing8),
Text(
'JPG, PNG, GIF up to 10MB',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildLoadingOverlay(ColorScheme colorScheme) {
return Container(
width: double.infinity,
height: double.infinity,
color: colorScheme.surface.withOpacity(0.8),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
),
const SizedBox(height: UIConstants.spacing12),
Text(
'Uploading...',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface,
),
),
],
),
),
);
}
Widget _buildRemoveButton(ColorScheme colorScheme) {
return Positioned(
top: UIConstants.spacing8,
right: UIConstants.spacing8,
child: Container(
decoration: BoxDecoration(
color: colorScheme.error,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
onPressed: widget.onRemoveImage,
icon: Icon(
Icons.close,
color: colorScheme.onError,
size: UIConstants.iconSizeSmall,
),
padding: const EdgeInsets.all(UIConstants.spacing4),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
),
);
}
}

View File

@@ -0,0 +1,315 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../core/constants/ui_constants.dart';
class ModernTextField extends StatefulWidget {
final String? label;
final String? hint;
final String? helperText;
final String? errorText;
final TextEditingController? controller;
final TextInputType? keyboardType;
final bool obscureText;
final bool enabled;
final bool readOnly;
final int? maxLines;
final int? maxLength;
final Widget? prefixIcon;
final Widget? suffixIcon;
final VoidCallback? onSuffixIconPressed;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final void Function(String)? onSubmitted;
final List<TextInputFormatter>? inputFormatters;
final FocusNode? focusNode;
final VoidCallback? onTap;
final bool autofocus;
final TextCapitalization textCapitalization;
final TextInputAction? textInputAction;
final bool expands;
final double? height;
final EdgeInsetsGeometry? contentPadding;
const ModernTextField({
super.key,
this.label,
this.hint,
this.helperText,
this.errorText,
this.controller,
this.keyboardType,
this.obscureText = false,
this.enabled = true,
this.readOnly = false,
this.maxLines = 1,
this.maxLength,
this.prefixIcon,
this.suffixIcon,
this.onSuffixIconPressed,
this.validator,
this.onChanged,
this.onSubmitted,
this.inputFormatters,
this.focusNode,
this.onTap,
this.autofocus = false,
this.textCapitalization = TextCapitalization.none,
this.textInputAction,
this.expands = false,
this.height,
this.contentPadding,
});
@override
State<ModernTextField> createState() => _ModernTextFieldState();
}
class _ModernTextFieldState extends State<ModernTextField>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
bool _isFocused = false;
bool _hasError = false;
late bool _obscureText;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_obscureText = widget.obscureText;
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_scaleAnimation = Tween<double>(
begin: 0.95,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onFocusChange(bool hasFocus) {
setState(() {
_isFocused = hasFocus;
});
if (hasFocus) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
void _onChanged(String value) {
setState(() {
_hasError = false;
});
if (widget.onChanged != null) {
widget.onChanged!(value);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.label != null) ...[
Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing8),
child: Text(
widget.label!,
style: theme.textTheme.labelLarge?.copyWith(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
),
],
Focus(
onFocusChange: _onFocusChange,
child: Container(
height: widget.height ?? UIConstants.inputHeightMedium,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(UIConstants.radius16),
color: _isFocused
? colorScheme.surfaceVariant.withOpacity(0.5)
: colorScheme.surfaceVariant.withOpacity(0.3),
border: Border.all(
color: _hasError
? colorScheme.error
: _isFocused
? colorScheme.primary
: colorScheme.outline.withOpacity(0.3),
width: _isFocused ? 2.0 : 1.5,
),
boxShadow: _isFocused
? [
BoxShadow(
color: colorScheme.primary.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: TextFormField(
controller: widget.controller,
focusNode: widget.focusNode,
keyboardType: widget.keyboardType,
obscureText: widget.suffixIcon != null
? widget.obscureText
: _obscureText,
enabled: widget.enabled,
readOnly: widget.readOnly,
maxLines: widget.maxLines,
maxLength: widget.maxLength,
autofocus: widget.autofocus,
textCapitalization: widget.textCapitalization,
textInputAction: widget.textInputAction,
expands: widget.expands,
inputFormatters: widget.inputFormatters,
validator: (value) {
if (widget.validator != null) {
final result = widget.validator!(value);
setState(() {
_hasError = result != null;
});
return result;
}
return null;
},
onChanged: _onChanged,
onFieldSubmitted: widget.onSubmitted,
onTap: widget.onTap,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: widget.hint,
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
prefixIcon: widget.prefixIcon != null
? Padding(
padding: const EdgeInsets.only(
left: UIConstants.spacing16),
child: IconTheme(
data: IconThemeData(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
child: widget.prefixIcon!,
),
)
: null,
suffixIcon: (widget.suffixIcon != null)
? Padding(
padding: const EdgeInsets.only(
right: UIConstants.spacing16),
child: IconTheme(
data: IconThemeData(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
child: widget.suffixIcon!,
),
)
: (widget.obscureText
? IconButton(
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
icon: Icon(
_obscureText
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
)
: null),
border: InputBorder.none,
contentPadding: widget.contentPadding ??
const EdgeInsets.symmetric(
horizontal: UIConstants.spacing20,
vertical: UIConstants.spacing16,
),
counterText: '',
),
),
),
),
if (widget.helperText != null && !_hasError) ...[
Padding(
padding: const EdgeInsets.only(top: UIConstants.spacing8),
child: Text(
widget.helperText!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
],
if (widget.errorText != null && _hasError) ...[
Padding(
padding: const EdgeInsets.only(top: UIConstants.spacing8),
child: Row(
children: [
Icon(
Icons.error_outline,
size: UIConstants.iconSizeSmall,
color: colorScheme.error,
),
const SizedBox(width: UIConstants.spacing8),
Expanded(
child: Text(
widget.errorText!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.error,
),
),
),
],
),
),
],
],
),
);
},
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../utils/managers/user_manager.dart';
import '../../../view_model/profile/profile_view_model.dart';
import '../buttons/modern_button.dart';
class ModernDrawer extends StatelessWidget {
final List<DrawerItem> items;
final Widget? header;
final Widget? footer;
final Color? backgroundColor;
final double? elevation;
const ModernDrawer({
super.key,
required this.items,
this.header,
this.footer,
this.backgroundColor,
this.elevation,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final email = UserManager().email;
final userName = UserManager().userName;
return Drawer(
backgroundColor: backgroundColor ?? colorScheme.surface,
elevation: elevation ?? UIConstants.elevation8,
child: Column(
children: [
// Header
header ??
_buildDefaultHeader(context, theme, colorScheme, userName, email),
// Navigation Items
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return _buildDrawerItem(context, theme, colorScheme, item);
},
),
),
// Footer
if (footer != null) footer!,
// Logout Section
_buildLogoutSection(context, theme, colorScheme),
],
),
);
}
Widget _buildDefaultHeader(
BuildContext context,
ThemeData theme,
ColorScheme colorScheme,
String? userName,
String? email,
) {
return Consumer<ProfileViewModel>(
builder: (context, provider, child) {
return Container(
padding: const EdgeInsets.all(UIConstants.spacing24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary,
colorScheme.secondary,
colorScheme.tertiary,
],
),
),
child: SafeArea(
child: Column(
children: [
// Profile Picture
Container(
width: UIConstants.logoSizeLarge,
height: UIConstants.logoSizeLarge,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 3,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: CircleAvatar(
radius: UIConstants.logoSizeLarge / 2,
backgroundColor: Colors.white.withOpacity(0.2),
backgroundImage: provider.profileImageBytes != null
? MemoryImage(provider.profileImageBytes!)
: null,
child: provider.profileImageBytes != null
? null
: Icon(
Icons.person,
size: UIConstants.iconSizeLarge,
color: Colors.white,
),
),
),
const SizedBox(height: UIConstants.spacing16),
// User Name
Text(
"Hello, ${userName ?? 'User'}",
style: theme.textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
// User Email
if (email != null) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
email,
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
],
),
),
);
},
);
}
Widget _buildDrawerItem(
BuildContext context,
ThemeData theme,
ColorScheme colorScheme,
DrawerItem item,
) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing4,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(UIConstants.radius12),
color: item.isSelected
? colorScheme.primaryContainer.withOpacity(0.3)
: Colors.transparent,
),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(UIConstants.spacing8),
decoration: BoxDecoration(
color: item.isSelected
? colorScheme.primaryContainer
: colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(UIConstants.radius8),
),
child: Icon(
item.icon,
color: item.isSelected
? colorScheme.onPrimaryContainer
: item.color ?? colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
),
title: Text(
item.title,
style: theme.textTheme.titleMedium?.copyWith(
color:
item.isSelected ? colorScheme.onSurface : colorScheme.onSurface,
fontWeight: item.isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
subtitle: item.subtitle != null
? Text(
item.subtitle!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
)
: null,
trailing: item.trailing,
onTap: () => item.onTap(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
),
);
}
Widget _buildLogoutSection(
BuildContext context,
ThemeData theme,
ColorScheme colorScheme,
) {
return Container(
padding: const EdgeInsets.all(UIConstants.spacing24),
child: ModernButton(
text: 'Logout',
type: ModernButtonType.danger,
size: ModernButtonSize.medium,
icon: Icon(Icons.logout),
onPressed: () async {
await UserManager().clearUser();
if (context.mounted) {
Navigator.pushReplacementNamed(context, '/splash');
}
},
),
);
}
}
class DrawerItem {
final IconData icon;
final String title;
final String? subtitle;
final void Function(BuildContext) onTap;
final Color? color;
final bool isSelected;
final Widget? trailing;
const DrawerItem({
required this.icon,
required this.title,
this.subtitle,
required this.onTap,
this.color,
this.isSelected = false,
this.trailing,
});
}

View File

@@ -0,0 +1,275 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/constants/ui_constants.dart';
import '../../core/providers/dynamic_theme_provider.dart';
class ThemePreview extends StatelessWidget {
const ThemePreview({super.key});
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
if (!dynamicThemeProvider.isUsingDynamicTheme) {
return const SizedBox.shrink();
}
final colorScheme = dynamicThemeProvider.dynamicColorScheme;
final logoColors = dynamicThemeProvider.logoColors;
final paletteInfo = dynamicThemeProvider.getColorPaletteInfo();
if (colorScheme == null || logoColors.isEmpty) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.all(UIConstants.spacing16),
padding: const EdgeInsets.all(UIConstants.spacing20),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(UIConstants.radius16),
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Icon(
Icons.palette,
color: colorScheme.primary,
size: UIConstants.iconSizeLarge,
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dynamic Theme Generated',
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
Text(
dynamicThemeProvider.getThemeDescription(),
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
IconButton(
onPressed: () => dynamicThemeProvider.resetToDefaultTheme(),
icon: Icon(
Icons.refresh,
color: colorScheme.primary,
),
tooltip: 'Reset to Default Theme',
),
],
),
const SizedBox(height: UIConstants.spacing20),
// Color Palette
Text(
'Color Palette',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing12),
// Color Swatches
Wrap(
spacing: UIConstants.spacing12,
runSpacing: UIConstants.spacing12,
children: logoColors.take(5).map((color) {
final index = logoColors.indexOf(color);
final labels = [
'Primary',
'Secondary',
'Tertiary',
'Accent 1',
'Accent 2'
];
return Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Icon(
Icons.check,
color: _getContrastColor(color),
size: 24,
),
),
),
const SizedBox(height: UIConstants.spacing4),
Text(
index < labels.length
? labels[index]
: 'Color ${index + 1}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
);
}).toList(),
),
const SizedBox(height: UIConstants.spacing20),
// Theme Info
Container(
padding: const EdgeInsets.all(UIConstants.spacing16),
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(UIConstants.radius12),
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Theme Information',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing8),
_buildInfoRow(
'Total Colors', '${paletteInfo['totalColors']}'),
_buildInfoRow('Description', paletteInfo['description']),
if (paletteInfo['characteristics'] != null) ...[
_buildInfoRow(
'Warm Colors',
paletteInfo['characteristics']['warm']
? 'Yes'
: 'No'),
_buildInfoRow(
'Cool Colors',
paletteInfo['characteristics']['cool']
? 'Yes'
: 'No'),
_buildInfoRow(
'Neutral Colors',
paletteInfo['characteristics']['neutral']
? 'Yes'
: 'No'),
],
],
),
),
const SizedBox(height: UIConstants.spacing16),
// Preview Buttons
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(UIConstants.radius12),
),
),
child: const Text('Primary Button'),
),
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: colorScheme.primary,
side: BorderSide(color: colorScheme.outline),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(UIConstants.radius12),
),
),
child: const Text('Secondary Button'),
),
),
],
),
],
),
);
},
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
);
}
Color _getContrastColor(Color backgroundColor) {
final luminance = backgroundColor.computeLuminance();
return luminance > 0.5 ? Colors.black : Colors.white;
}
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import '../../core/constants/ui_constants.dart';
class ThemeToggle extends StatefulWidget {
final bool isDarkMode;
final ValueChanged<bool> onThemeChanged;
final double size;
final Color? backgroundColor;
final Color? iconColor;
const ThemeToggle({
super.key,
required this.isDarkMode,
required this.onThemeChanged,
this.size = 48.0,
this.backgroundColor,
this.iconColor,
});
@override
State<ThemeToggle> createState() => _ThemeToggleState();
}
class _ThemeToggleState extends State<ThemeToggle>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _rotationAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationNormal,
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.5,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.8,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleTheme() {
_animationController.forward().then((_) {
widget.onThemeChanged(!widget.isDarkMode);
_animationController.reverse();
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.backgroundColor ??
(widget.isDarkMode
? colorScheme.primaryContainer
: colorScheme.surfaceVariant),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _toggleTheme,
borderRadius: BorderRadius.circular(widget.size / 2),
child: Center(
child: Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159,
child: Icon(
widget.isDarkMode ? Icons.light_mode : Icons.dark_mode,
size: widget.size * 0.5,
color: widget.iconColor ??
(widget.isDarkMode
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant),
),
),
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,662 @@
import 'package:base_project/core/app_export.dart';
import 'package:flutter/material.dart';
import '../utils/color_constants.dart';
import '../utils/size_utils.dart';
class AppDecoration {
// Gradient decorations
static BoxDecoration get gradientOnErrorContainerToOnErrorContainer =>
BoxDecoration(
gradient: LinearGradient(
begin: const Alignment(0.32, 0.2),
end: const Alignment(0.75, 0.83),
colors: [
theme.colorScheme.onErrorContainer.withOpacity(0.7),
theme.colorScheme.onErrorContainer.withOpacity(0.7),
theme.colorScheme.onErrorContainer.withOpacity(0.7)
],
),
);
static BoxDecoration get fillBlueGray => BoxDecoration(
color: appTheme.blueGray100,
);
static BoxDecoration get fillBlue5001 => BoxDecoration(
color: ColorConstant.blue5001,
);
static BoxDecoration get outlineGray5002 => BoxDecoration(
color: ColorConstant.gray5002,
border: Border.all(
color: ColorConstant.gray5002,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineBlueA70001 => BoxDecoration(
border: Border.all(
color: ColorConstant.blueA70001,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineGray60026 => BoxDecoration(
color: ColorConstant.whiteA700,
boxShadow: [
BoxShadow(
color: ColorConstant.gray60026,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
2.41,
),
),
],
);
static BoxDecoration get txtFillBluegray100 => BoxDecoration(
color: ColorConstant.blueGray100,
);
static BoxDecoration get fillBlueA700 => BoxDecoration(
color: ColorConstant.blueA700,
);
static BoxDecoration get fillBluegray50 => BoxDecoration(
color: ColorConstant.blueGray50,
);
static BoxDecoration get fillBlack => BoxDecoration(
color: appTheme.black900,
);
static BoxDecoration get fillGray => BoxDecoration(
color: appTheme.gray10001,
);
static BoxDecoration get fillGray700 => BoxDecoration(
color: appTheme.gray700,
);
static BoxDecoration get fillLightGreenA => BoxDecoration(
color: appTheme.lightGreenA200,
);
static BoxDecoration get fillOnErrorContainer => BoxDecoration(
color: theme.colorScheme.onErrorContainer.withOpacity(1),
);
static BoxDecoration get fillPrimary => BoxDecoration(
color: theme.colorScheme.primary,
);
static BoxDecoration get outlineBlueA7002 => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border.all(
color: ColorConstant.blueA700,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineBlueA7001 => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border.all(
color: ColorConstant.blueA700,
width: getHorizontalSize(
1,
),
),
boxShadow: [
BoxShadow(
color: ColorConstant.gray60019,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
12,
),
),
],
);
static BoxDecoration get outlineGray70011 => BoxDecoration(
color: ColorConstant.whiteA700,
boxShadow: [
BoxShadow(
color: ColorConstant.gray70011,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
0,
),
),
],
);
static BoxDecoration get outlineGray600191 => const BoxDecoration();
static BoxDecoration get txtOutlineBlueA700 => BoxDecoration(
border: Border.all(
color: ColorConstant.blueA700,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineBlue50 => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border.all(
color: ColorConstant.blue50,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get txtFillBlueA700 => BoxDecoration(
color: ColorConstant.blueA700,
);
static BoxDecoration get outlineBlack90019 => BoxDecoration(
color: ColorConstant.blueA700,
boxShadow: [
BoxShadow(
color: ColorConstant.black90019,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
2,
),
),
],
);
static BoxDecoration get outlineBluegray100 => BoxDecoration(
color: ColorConstant.gray50,
border: Border(
bottom: BorderSide(
color: ColorConstant.blueGray100,
width: getHorizontalSize(
1,
),
),
),
);
static BoxDecoration get fillGray50 => BoxDecoration(
color: ColorConstant.gray50,
);
static BoxDecoration get outlineBluegray10001 => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border.all(
color: ColorConstant.blueGray10001,
width: getHorizontalSize(
1,
),
),
boxShadow: [
BoxShadow(
color: ColorConstant.black90033,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
1,
),
),
],
);
static BoxDecoration get fillBlack900b2 => BoxDecoration(
color: ColorConstant.black900B2,
);
static BoxDecoration get outlineBlack90033 => BoxDecoration(
border: Border.all(
color: ColorConstant.black90033,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineBlack90011 => BoxDecoration(
color: ColorConstant.whiteA700,
boxShadow: [
BoxShadow(
color: ColorConstant.black90011,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
0,
),
),
],
);
static BoxDecoration get outlineGray30001 => BoxDecoration(
border: Border.all(
color: ColorConstant.gray30001,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineBlue200 => BoxDecoration(
border: Border.all(
color: ColorConstant.blue200,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineGray60019 => BoxDecoration(
color: ColorConstant.whiteA700,
boxShadow: [
BoxShadow(
color: ColorConstant.gray60019,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
12,
),
),
],
);
static BoxDecoration get outlineGray700261 => BoxDecoration(
color: ColorConstant.whiteA700,
boxShadow: [
BoxShadow(
color: ColorConstant.gray70026,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
0,
),
),
],
);
static BoxDecoration get fillWhiteA700 => BoxDecoration(
color: ColorConstant.whiteA700,
);
static BoxDecoration get outlineBlueA700 => BoxDecoration(
color: ColorConstant.gray50,
border: Border.all(
color: ColorConstant.blueA700,
width: getHorizontalSize(
2,
),
strokeAlign: strokeAlignOutside,
),
);
static BoxDecoration get fillBlue900 => BoxDecoration(
color: ColorConstant.blue900,
);
static BoxDecoration get outlineBluegray1002 => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border(
top: BorderSide(
color: ColorConstant.blueGray100,
width: getHorizontalSize(
1,
),
),
bottom: BorderSide(
color: ColorConstant.blueGray100,
width: getHorizontalSize(
1,
),
),
),
);
static BoxDecoration get fillRed100 => BoxDecoration(
color: ColorConstant.red100,
);
static BoxDecoration get outlineBluegray1001 => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border.all(
color: ColorConstant.blueGray100,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get outlineYellow9003f => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border.all(
color: ColorConstant.yellow9003f,
width: getHorizontalSize(
1,
),
strokeAlign: strokeAlignOutside,
),
);
static BoxDecoration get fillBlue50 => BoxDecoration(
color: ColorConstant.blue50,
);
static BoxDecoration get outlineGray70026 => BoxDecoration(
color: ColorConstant.whiteA70099,
boxShadow: [
BoxShadow(
color: ColorConstant.gray70026,
spreadRadius: getHorizontalSize(
2,
),
blurRadius: getHorizontalSize(
2,
),
offset: const Offset(
0,
0,
),
),
],
);
static BoxDecoration get txtOutlineBlack9000c => BoxDecoration(
color: ColorConstant.gray100,
border: Border.all(
color: ColorConstant.black9000c,
width: getHorizontalSize(
1,
),
),
);
static BoxDecoration get fillRed700 => BoxDecoration(
color: ColorConstant.red700,
);
static BoxDecoration get fillGray5003 => BoxDecoration(
color: ColorConstant.gray5003,
);
static BoxDecoration get fillGray200 => BoxDecoration(
color: ColorConstant.gray200,
);
static BoxDecoration get outlineBluegray50 => BoxDecoration(
color: ColorConstant.whiteA700,
border: Border.all(
color: ColorConstant.blueGray50,
width: getHorizontalSize(
1,
),
),
);
// Outline decorations
static BoxDecoration get outlineOnPrimaryContainer => BoxDecoration(
color: appTheme.whiteA700,
border: Border.all(
color: theme.colorScheme.onPrimaryContainer,
width: 1.h,
),
);
// Outline decorations
static BoxDecoration get outlineBlack => const BoxDecoration();
// Fill decorations
static BoxDecoration get fillBlueA => BoxDecoration(
color: appTheme.blueA200,
);
static BoxDecoration get fillGray800 => BoxDecoration(
color: appTheme.gray800,
);
static BoxDecoration get fillOnError => BoxDecoration(
color: theme.colorScheme.onError,
);
static BoxDecoration get fillPink => BoxDecoration(
color: appTheme.pink400,
);
static BoxDecoration get fillPrimary1 => BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.05),
);
static BoxDecoration get fillPrimary2 => BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
);
static BoxDecoration get fillWhiteA => BoxDecoration(
color: appTheme.whiteA700,
);
static BoxDecoration get fillYellow => BoxDecoration(
color: appTheme.yellow600,
);
// Outline decorations
static BoxDecoration get outlineGray => BoxDecoration(
color: appTheme.whiteA700.withOpacity(0.6),
border: Border.all(
color: appTheme.gray300,
width: 1.h,
),
);
static BoxDecoration get outlineGray300 => BoxDecoration(
color: appTheme.whiteA700.withOpacity(0.6),
border: Border(
bottom: BorderSide(
color: appTheme.gray300,
width: 1.h,
),
),
);
static BoxDecoration get outlineGray3001 => BoxDecoration(
color: appTheme.whiteA700,
border: Border(
bottom: BorderSide(
color: appTheme.gray300,
width: 1.h,
),
),
);
static BoxDecoration get outlineGray3002 => BoxDecoration(
border: Border(
bottom: BorderSide(
color: appTheme.gray300,
width: 1.h,
),
),
);
static BoxDecoration get outlineGray3003 => BoxDecoration(
border: Border.all(
color: appTheme.gray300,
width: 1.h,
),
);
static BoxDecoration get outlineGray3004 => BoxDecoration(
border: Border(
left: BorderSide(
color: appTheme.gray300,
width: 1.h,
),
right: BorderSide(
color: appTheme.gray300,
width: 1.h,
),
),
);
static BoxDecoration get outlineGray3005 => BoxDecoration(
color: appTheme.whiteA700.withOpacity(0.05),
border: Border.all(
color: appTheme.gray300,
width: 1.h,
),
);
static BoxDecoration get outlineGray3006 => BoxDecoration(
border: Border(
bottom: BorderSide(
color: appTheme.gray300,
width: 1.h,
),
),
);
}
class BorderRadiusStyle {
static BorderRadius customBorderTL50 = BorderRadius.only(
topLeft: Radius.circular(
getHorizontalSize(
50,
),
),
bottomLeft: Radius.circular(
getHorizontalSize(
50,
),
),
);
static BorderRadius customBorderTL10 = BorderRadius.only(
topLeft: Radius.circular(
getHorizontalSize(
10,
),
),
topRight: Radius.circular(
getHorizontalSize(
10,
),
),
);
static BorderRadius circleBorder9 = BorderRadius.circular(
getHorizontalSize(
9,
),
);
static BorderRadius circleBorder22 = BorderRadius.circular(
getHorizontalSize(
22,
),
);
static BorderRadius roundedBorder16 = BorderRadius.circular(
getHorizontalSize(
16,
),
);
static BorderRadius circleBorder12 = BorderRadius.circular(
getHorizontalSize(
12,
),
);
static BorderRadius roundedBorder6 = BorderRadius.circular(
getHorizontalSize(
6,
),
);
static BorderRadius circleBorder25 = BorderRadius.circular(
getHorizontalSize(
25,
),
);
static BorderRadius roundedBorder3 = BorderRadius.circular(
getHorizontalSize(
3,
),
);
static BorderRadius circleBorder30 = BorderRadius.circular(
getHorizontalSize(
30,
),
);
static BorderRadius circleBorder76 = BorderRadius.circular(
getHorizontalSize(
76,
),
);
static BorderRadius txtRoundedBorder6 = BorderRadius.circular(
getHorizontalSize(
6,
),
);
static BorderRadius circleBorder61 = BorderRadius.circular(
getHorizontalSize(
61,
),
);
static BorderRadius get roundedBorder8 => BorderRadius.circular(
8.h,
);
// Circle borders
static BorderRadius get circleBorder24 => BorderRadius.circular(
24.h,
);
static BorderRadius get circleBorder50 => BorderRadius.circular(
50.h,
);
// Rounded borders
static BorderRadius get roundedBorder10 => BorderRadius.circular(
10.h,
);
static BorderRadius get roundedBorder15 => BorderRadius.circular(
15.h,
);
static BorderRadius get roundedBorder19 => BorderRadius.circular(
19.h,
);
static BorderRadius get roundedBorder5 => BorderRadius.circular(
5.h,
);
static BorderRadius get circleBorder20 => BorderRadius.circular(
20.h,
);
// Custom borders
static BorderRadius get customBorderBL12 => BorderRadius.vertical(
bottom: Radius.circular(12.h),
);
static BorderRadius get customBorderTL12 => BorderRadius.vertical(
top: Radius.circular(12.h),
);
static BorderRadius get roundedBorder24 => BorderRadius.circular(
24.h,
);
}
// Comment/Uncomment the below code based on your Flutter SDK version.
// For Flutter SDK Version 3.7.2 or greater.
double get strokeAlignInside => BorderSide.strokeAlignInside;
double get strokeAlignCenter => BorderSide.strokeAlignCenter;
double get strokeAlignOutside => BorderSide.strokeAlignOutside;
// For Flutter SDK Version 3.7.1 or less.
// StrokeAlign get strokeAlignInside => StrokeAlign.inside;
//
// StrokeAlign get strokeAlignCenter => StrokeAlign.center;
//
// StrokeAlign get strokeAlignOutside => StrokeAlign.outside;

View File

@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import '../core/app_export.dart';
/// A class that offers pre-defined button styles for customizing button appearance.
class CustomButtonStyles {
static BoxDecoration get fullyBlack => BoxDecoration(
borderRadius: BorderRadius.circular(24.h),
color: Colors.black,
);
static BoxDecoration
get gradientOnErrorContainerToOnErrorContainerDecoration => BoxDecoration(
borderRadius: BorderRadius.circular(24.h),
boxShadow: [
BoxShadow(
color: appTheme.black900.withOpacity(0.5),
spreadRadius: 2.h,
blurRadius: 2.h,
offset: const Offset(
4,
38,
),
)
],
gradient: LinearGradient(
begin: const Alignment(0.32, 0),
end: const Alignment(0.75, 0),
colors: [
theme.colorScheme.onErrorContainer.withOpacity(0.7),
theme.colorScheme.onErrorContainer.withOpacity(0.7)
],
),
);
static BoxDecoration get gradientGrayBToOnErrorContainerDecoration =>
BoxDecoration(
borderRadius: BorderRadius.circular(24.h),
boxShadow: [
BoxShadow(
color: appTheme.black900.withOpacity(0.5),
spreadRadius: 2.h,
blurRadius: 2.h,
offset: const Offset(
4,
38,
),
)
],
color: Colors.black,
gradient: LinearGradient(
begin: const Alignment(0.32, 0),
end: const Alignment(0.75, 0),
colors: [
appTheme.black900,
theme.colorScheme.onErrorContainer.withOpacity(1)
],
),
);
static BoxDecoration get gradientWhiteAToWhiteADecoration => BoxDecoration(
borderRadius: BorderRadius.circular(17.h),
gradient: LinearGradient(
begin: const Alignment(0.32, 0),
end: const Alignment(0.75, 0),
colors: [
appTheme.whiteA700.withOpacity(0.7),
appTheme.whiteA700.withOpacity(0.7)
],
),
);
// Outline button style
static ButtonStyle get outlineBlack => ElevatedButton.styleFrom(
backgroundColor: appTheme.blueA100,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.h),
),
shadowColor: appTheme.black900.withOpacity(0.4),
elevation: 2,
);
// text button style
static ButtonStyle get none => ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(Colors.transparent),
elevation: WidgetStateProperty.all<double>(0),
);
// Filled button style
static ButtonStyle get fillBlueA => ElevatedButton.styleFrom(
backgroundColor: appTheme.blueA20001,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(21.h),
),
);
static ButtonStyle get fillPrimary => ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.h),
),
);
static ButtonStyle get fillPrimaryTL16 => ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.h),
),
);
static ButtonStyle get fillWhiteA => ElevatedButton.styleFrom(
backgroundColor: appTheme.whiteA700,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.h),
),
);
// Gradient button style
static BoxDecoration get gradientGrayBToWhiteADecoration => BoxDecoration(
borderRadius: BorderRadius.circular(24.h),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.5),
spreadRadius: 2.h,
blurRadius: 2.h,
offset: const Offset(
4,
38,
),
)
],
gradient: LinearGradient(
begin: const Alignment(0.32, 0),
end: const Alignment(0.75, 0),
colors: [appTheme.gray100B2, appTheme.whiteA700.withOpacity(0.7)],
),
);
static BoxDecoration get gradientWhiteAToWhiteATL25Decoration =>
BoxDecoration(
borderRadius: BorderRadius.circular(25.h),
gradient: LinearGradient(
begin: const Alignment(0.32, 0),
end: const Alignment(0.75, 0),
colors: [
appTheme.whiteA700.withOpacity(0.7),
appTheme.whiteA700.withOpacity(0.7)
],
),
);
}

View File

@@ -0,0 +1,492 @@
import 'package:flutter/material.dart';
import '../core/app_export.dart';
extension on TextStyle {
TextStyle get poppins {
return copyWith(
fontFamily: 'Poppins',
);
}
TextStyle get openSans {
return copyWith(
fontFamily: 'Open Sans',
);
}
TextStyle get roboto {
return copyWith(
fontFamily: 'Roboto',
);
}
TextStyle get montserrat {
return copyWith(
fontFamily: 'Montserrat',
);
}
TextStyle get dMSans {
return copyWith(
fontFamily: 'DM Sans',
);
}
TextStyle get sourceSansPro {
return copyWith(
fontFamily: 'Source Sans Pro',
);
}
TextStyle get dMMono {
return copyWith(
fontFamily: 'DM Mono',
);
}
TextStyle get sFProText {
return copyWith(
fontFamily: 'SF Pro Text',
);
}
TextStyle get hammersmithOne {
return copyWith(
fontFamily: 'Hammersmith One',
);
}
TextStyle get urbanist {
return copyWith(
fontFamily: 'Urbanist',
);
}
TextStyle get sFPro {
return copyWith(
fontFamily: 'SF Pro',
);
}
}
/// A collection of pre-defined text styles for customizing text appearance,
/// categorized by different font families and weights.
/// Additionally, this class includes extensions on [TextStyle] to easily apply specific font families to text.
class CustomTextStyles {
static get titleLargePoppinsBlack40 =>
theme.textTheme.titleMedium!.poppins.copyWith(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 40.fSize,
);
static get titleSmallPoppinsWhite =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 22.fSize,
);
static get titleSmallRed700 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.red700,
fontWeight: FontWeight.w700,
);
static get titleLargePoppinsBlack =>
theme.textTheme.titleMedium!.poppins.copyWith(
color: Colors.black,
fontSize: 30.fSize,
);
static get titleMediumMediumWhit => theme.textTheme.titleMedium!.copyWith(
fontSize: 16.fSize, fontWeight: FontWeight.w500, color: Colors.white);
// Headline text style
static get headlineLargePoppinsBlack900 =>
theme.textTheme.headlineLarge!.poppins.copyWith(
color: appTheme.black900,
fontWeight: FontWeight.w700,
);
// Label text style
static get labelLargeCyan900 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.cyan900,
);
static get labelLargeGray500 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.gray500,
fontWeight: FontWeight.w500,
);
// Title text style
static get titleSmallMontserratBlack900 =>
theme.textTheme.titleSmall!.montserrat.copyWith(
color: appTheme.black900,
fontSize: 14.fSize,
fontWeight: FontWeight.w700,
);
static get titleSmallPoppinsWhiteA700 =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: appTheme.whiteA700,
fontSize: 14.fSize,
fontWeight: FontWeight.w700,
);
static get headlineSmallBlack900 => theme.textTheme.headlineSmall!.copyWith(
color: appTheme.black900,
fontWeight: FontWeight.w500,
);
static get headlineSmallBlack900_1 => theme.textTheme.headlineSmall!.copyWith(
color: appTheme.black900,
);
// Headline text style
static get headlineLargeGray900 => theme.textTheme.headlineLarge!.copyWith(
color: appTheme.gray900,
);
// Title text style
static get titleMedium18 => theme.textTheme.titleMedium!.copyWith(
fontSize: 18.fSize,
);
static get titleMediumErrorContainer => theme.textTheme.titleMedium!.copyWith(
color: theme.colorScheme.errorContainer,
fontWeight: FontWeight.w500,
);
static get titleMediumPrimary => theme.textTheme.titleMedium!.copyWith(
color: theme.colorScheme.primary,
fontSize: 18.fSize,
);
static get titleSmallDeeppurpleA400 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.deepPurpleA400,
fontWeight: FontWeight.w700,
);
static get titleSmallErrorContainer => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.errorContainer,
);
static get titleSmallGray600 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.gray600,
fontSize: 14.fSize,
fontWeight: FontWeight.w600,
);
static get titleSmallPoppinsBlack900 =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: appTheme.black900,
);
static get titleSmallPoppinsDeeppurpleA400 =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: appTheme.deepPurpleA400,
fontWeight: FontWeight.w700,
);
static get titleSmallPoppinsGray90001 =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: appTheme.gray90001,
);
static get titleSmallPoppinsPrimaryContainer =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: theme.colorScheme.primaryContainer,
fontWeight: FontWeight.w600,
);
static get titleSmallPoppinsPrimaryContainer_1 =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: theme.colorScheme.primaryContainer,
);
static get titleSmallPrimaryContainer => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.primaryContainer,
);
// Body text style
static get bodyMediumPoppinsBluegray50 =>
theme.textTheme.bodyMedium!.poppins.copyWith(
color: appTheme.blueGray50,
);
static get bodySmallMontserratBluegray400 =>
theme.textTheme.bodySmall!.montserrat.copyWith(
color: appTheme.blueGray400,
fontSize: 10.fSize,
fontWeight: FontWeight.w400,
);
static get bodySmallOpenSansOnPrimaryContainer =>
theme.textTheme.bodySmall!.openSans.copyWith(
color: theme.colorScheme.onPrimaryContainer.withOpacity(1),
fontWeight: FontWeight.w400,
);
static get bodySmallRegular => theme.textTheme.bodySmall!.copyWith(
fontSize: 10.fSize,
fontWeight: FontWeight.w400,
);
static get bodySmallYellow900 => theme.textTheme.bodySmall!.copyWith(
color: appTheme.yellow900,
fontSize: 12.fSize,
fontWeight: FontWeight.w400,
);
// Headline text style
static get headlineLargeBold => theme.textTheme.headlineLarge!.copyWith(
fontSize: 32.fSize,
fontWeight: FontWeight.w700,
);
static get headlineLargeExtraBold => theme.textTheme.headlineLarge!.copyWith(
fontSize: 32.fSize,
fontWeight: FontWeight.w800,
);
static get headlineSmallOnPrimaryContainer =>
theme.textTheme.headlineSmall!.copyWith(
color: theme.colorScheme.onPrimaryContainer.withOpacity(1),
fontSize: 24.fSize,
);
static get headlineSmallSemiBold => theme.textTheme.headlineSmall!.copyWith(
fontSize: 24.fSize,
fontWeight: FontWeight.w600,
);
// Label text style
static get labelLargeBluegray700 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.blueGray700,
);
static get labelLargeMontserratCyan900 =>
theme.textTheme.labelLarge!.montserrat.copyWith(
color: appTheme.cyan900,
fontWeight: FontWeight.w700,
);
static get labelLargeMontserratGray500 =>
theme.textTheme.labelLarge!.montserrat.copyWith(
color: appTheme.gray500,
);
static get labelLargeMontserratTeal100 =>
theme.textTheme.labelLarge!.montserrat.copyWith(
color: appTheme.teal100,
fontWeight: FontWeight.w700,
);
static get labelMediumGray500 => theme.textTheme.labelMedium!.copyWith(
color: appTheme.gray500,
);
static get labelMediumGreen80001 => theme.textTheme.labelMedium!.copyWith(
color: appTheme.green80001,
);
static get labelMediumMontserratCyan900 =>
theme.textTheme.labelMedium!.montserrat.copyWith(
color: appTheme.cyan900,
fontWeight: FontWeight.w700,
);
static get labelMediumOpenSans =>
theme.textTheme.labelMedium!.openSans.copyWith(
fontWeight: FontWeight.w700,
);
static get labelMediumRed700 => theme.textTheme.labelMedium!.copyWith(
color: appTheme.red700,
);
static get labelSmallBold => theme.textTheme.labelSmall!.copyWith(
fontWeight: FontWeight.w700,
);
// Open text style
static get openSansOnPrimaryContainer => TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontSize: 6.fSize,
fontWeight: FontWeight.w400,
).openSans;
// Title text style
static get titleMediumMedium => theme.textTheme.titleMedium!.copyWith(
fontSize: 16.fSize,
fontWeight: FontWeight.w500,
);
static get titleSmallPoppinsOnPrimaryContainer =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: theme.colorScheme.onPrimaryContainer.withOpacity(1),
fontWeight: FontWeight.w500,
);
static get titleSmallPoppinsOnPrimaryContainerMedium =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: theme.colorScheme.onPrimaryContainer.withOpacity(1),
fontWeight: FontWeight.w500,
);
static get titleSmallPoppinsOnPrimaryContainer_1 =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: theme.colorScheme.onPrimaryContainer.withOpacity(1),
);
static get bodySmallOpenSansOnErrorContainer =>
theme.textTheme.bodySmall!.openSans.copyWith(
color: theme.colorScheme.onErrorContainer.withOpacity(1),
fontSize: 8.fSize,
);
// Headline text style
static get headlineLargeSemiBold => theme.textTheme.headlineLarge!.copyWith(
fontSize: 30.fSize,
fontWeight: FontWeight.w600,
);
static get headlineSmallOnErrorContainer =>
theme.textTheme.headlineSmall!.copyWith(
color: theme.colorScheme.onErrorContainer.withOpacity(1),
fontSize: 24.fSize,
);
// Label text style
static get labelLargeDMSansOnPrimaryContainer =>
theme.textTheme.labelLarge!.dMSans.copyWith(
color: theme.colorScheme.onPrimaryContainer,
fontSize: 13.fSize,
);
static get labelLargeDMSansOnPrimaryContainer_1 =>
theme.textTheme.labelLarge!.dMSans.copyWith(
color: theme.colorScheme.onPrimaryContainer,
);
static get labelLargeErrorContainer => theme.textTheme.labelLarge!.copyWith(
color: theme.colorScheme.errorContainer,
);
static get labelLarge_1 => theme.textTheme.labelLarge!;
// Open text style
static get openSansOnErrorContainer => TextStyle(
color: theme.colorScheme.onErrorContainer,
fontSize: 6.fSize,
fontWeight: FontWeight.w400,
).openSans;
// Title text style
static get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.black900,
);
static get titleMediumBlack900Medium => theme.textTheme.titleMedium!.copyWith(
color: appTheme.black900,
fontSize: 16.fSize,
fontWeight: FontWeight.w500,
);
static get titleMediumGray300 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.gray300,
);
static get titleMediumGray50 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.gray50,
fontWeight: FontWeight.w700,
);
static get titleMediumSourceSansPro =>
theme.textTheme.titleMedium!.sourceSansPro.copyWith(
fontSize: 16.fSize,
);
static get titleSmallPoppinsOnErrorContainer =>
theme.textTheme.titleSmall!.poppins.copyWith(
color: theme.colorScheme.onErrorContainer.withOpacity(1),
);
static get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.whiteA700,
);
// Body text style
static get bodyLargeGray50 => theme.textTheme.bodyLarge!.copyWith(
color: appTheme.gray50,
);
// Label text style
static get labelLargeAmber300 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.amber300,
);
static get labelLargePoppinsGray50 =>
theme.textTheme.labelLarge!.poppins.copyWith(
color: appTheme.gray50,
fontWeight: FontWeight.w700,
);
static get labelLargePrimary => theme.textTheme.labelLarge!.copyWith(
color: theme.colorScheme.primary,
);
static get labelLargePrimaryBold => theme.textTheme.labelLarge!.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w700,
);
static get labelLargePrimaryExtraBold => theme.textTheme.labelLarge!.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w800,
);
static get labelLargeSFProTextErrorContainer =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: theme.colorScheme.errorContainer,
fontWeight: FontWeight.w700,
);
static get labelLargeSFProTextErrorContainer_1 =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: theme.colorScheme.errorContainer,
);
static get labelLargeSFProTextPrimary =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w500,
);
static get labelLargeSFProTextPrimaryMedium =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: theme.colorScheme.primary.withOpacity(0.53),
fontWeight: FontWeight.w500,
);
static get labelLargeSFProTextPrimaryMedium_1 =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: theme.colorScheme.primary.withOpacity(0.56),
fontWeight: FontWeight.w500,
);
static get labelLargeSFProTextPrimary_1 =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: theme.colorScheme.primary.withOpacity(0.7),
);
static get labelLargeSFProTextPrimary_2 =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: theme.colorScheme.primary,
);
static get labelLargeSFProTextRed600 =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: appTheme.red600,
);
static get labelLargeSFProTextWhiteA700 =>
theme.textTheme.labelLarge!.sFProText.copyWith(
color: appTheme.whiteA700,
fontWeight: FontWeight.w500,
);
static get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700,
);
static get labelLargeWhiteA700ExtraBold =>
theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700,
fontWeight: FontWeight.w800,
);
static get labelLargeWhiteA700_1 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700.withOpacity(0.9),
);
static get labelLargeWhiteA700_2 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700.withOpacity(0.9),
);
static get labelMediumPrimary => theme.textTheme.labelMedium!.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w800,
);
static get labelMediumSFProTextRed600 =>
theme.textTheme.labelMedium!.sFProText.copyWith(
color: appTheme.red600,
);
static get labelMediumWhiteA700 => theme.textTheme.labelMedium!.copyWith(
color: appTheme.whiteA700,
fontWeight: FontWeight.w600,
);
static get labelMediumWhiteA700ExtraBold =>
theme.textTheme.labelMedium!.copyWith(
color: appTheme.whiteA700.withOpacity(0.7),
fontWeight: FontWeight.w800,
);
static get labelMediumWhiteA700ExtraBold_1 =>
theme.textTheme.labelMedium!.copyWith(
color: appTheme.whiteA700,
fontWeight: FontWeight.w800,
);
// Title text style
static get titleLargeWhiteA700 => theme.textTheme.titleLarge!.copyWith(
color: appTheme.whiteA700,
);
static get titleLargeWhiteA700_1 => theme.textTheme.titleLarge!.copyWith(
color: appTheme.whiteA700,
);
static get titleMediumDeeporange300 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.deepOrange300,
fontSize: 18.fSize,
fontWeight: FontWeight.w800,
);
static get titleMediumPoppins =>
theme.textTheme.titleMedium!.poppins.copyWith(
fontSize: 18.fSize,
fontWeight: FontWeight.w600,
);
static get titleMediumPoppinsGray50 =>
theme.textTheme.titleMedium!.poppins.copyWith(
color: appTheme.gray50,
fontSize: 18.fSize,
);
static get titleMediumRed600 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.red600,
);
static get titleMediumSFProText => theme.textTheme.titleMedium!.sFProText;
}

View File

@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import '../core/app_export.dart';
String _appTheme = "lightCode";
LightCodeColors get appTheme => ThemeHelper().themeColor();
ThemeData get theme => ThemeHelper().themeData();
/// Helper class for managing themes and colors.
// ignore_for_file: must_be_immutable
// ignore_for_file: must_be_immutable
class ThemeHelper {
// A map of custom color themes supported by the app
final Map<String, LightCodeColors> _supportedCustomColor = {
'lightCode': LightCodeColors()
};
// A map of color schemes supported by the app
final Map<String, ColorScheme> _supportedColorScheme = {
'lightCode': ColorSchemes.lightCodeColorScheme
};
/// Changes the app theme to [newTheme].
void changeTheme(String newTheme) {
_appTheme = newTheme;
}
/// Returns the lightCode colors for the current theme.
LightCodeColors _getThemeColors() {
return _supportedCustomColor[_appTheme] ?? LightCodeColors();
}
/// Returns the current theme data.
ThemeData _getThemeData() {
var colorScheme =
_supportedColorScheme[_appTheme] ?? ColorSchemes.lightCodeColorScheme;
return ThemeData(
visualDensity: VisualDensity.standard,
colorScheme: colorScheme,
textTheme: TextThemes.textTheme(colorScheme),
scaffoldBackgroundColor: appTheme.gray100,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
shadowColor: appTheme.black900.withOpacity(0.4),
elevation: 2,
visualDensity: const VisualDensity(
vertical: -4,
horizontal: -4,
),
padding: EdgeInsets.zero,
),
),
dividerTheme: DividerThemeData(
thickness: 1,
space: 1,
color: colorScheme.onPrimaryContainer.withOpacity(0.2),
),
);
}
/// Returns the lightCode colors for the current theme.
LightCodeColors themeColor() => _getThemeColors();
/// Returns the current theme data.
ThemeData themeData() => _getThemeData();
}
/// Class containing the supported text theme styles.
class TextThemes {
static TextTheme textTheme(ColorScheme colorScheme) => TextTheme(
bodyMedium: TextStyle(
color: colorScheme.onPrimaryContainer.withOpacity(1),
fontSize: 13.fSize,
fontFamily: 'Roboto',
fontWeight: FontWeight.w400,
),
bodySmall: TextStyle(
color: appTheme.gray500,
fontSize: 8.fSize,
fontFamily: 'Poppins',
fontWeight: FontWeight.w300,
),
headlineLarge: TextStyle(
color: appTheme.black900,
fontSize: 30.fSize,
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
),
headlineSmall: TextStyle(
color: appTheme.black900,
fontSize: 25.fSize,
fontFamily: 'Poppins',
fontWeight: FontWeight.w400,
),
labelLarge: TextStyle(
color: colorScheme.onPrimaryContainer.withOpacity(1),
fontSize: 12.fSize,
fontFamily: 'Poppins',
fontWeight: FontWeight.w500,
),
labelMedium: TextStyle(
color: colorScheme.onPrimaryContainer.withOpacity(1),
fontSize: 10.fSize,
fontFamily: 'Poppins',
fontWeight: FontWeight.w500,
),
labelSmall: TextStyle(
color: colorScheme.onPrimaryContainer.withOpacity(1),
fontSize: 8.fSize,
fontFamily: 'Open Sans',
fontWeight: FontWeight.w600,
),
titleLarge: TextStyle(
color: appTheme.black900,
fontSize: 20.fSize,
fontFamily: 'Poppins',
fontWeight: FontWeight.w400,
),
titleMedium: TextStyle(
color: appTheme.black900,
fontSize: 18.fSize,
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
),
titleSmall: TextStyle(
color: appTheme.black900,
fontSize: 14.fSize,
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
),
);
}
/// Class containing the supported color schemes.
class ColorSchemes {
static const lightCodeColorScheme = ColorScheme.light(
primary: Color(0XFF8DB1F7),
primaryContainer: Color(0XFF2E353E),
errorContainer: Color(0XFF777777),
onPrimary: Color(0XFF1C2026),
onPrimaryContainer: Color(0X75FFFFFF),
);
}
/// Class containing custom colors for a lightCode theme.
class LightCodeColors {
// Black
Color get black900 => const Color(0XFF000000);
// Blue
Color get blueA200 => const Color(0XFF5285E8);
// BlueGray
Color get blueGray400 => const Color(0XFF888888);
Color get blueGray50 => const Color(0XFFF1F1F1);
Color get blueGray700 => const Color(0XFF535D67);
Color get blueGray800 => const Color(0XFF454B55);
// Cyan
Color get cyan600 => const Color(0XFF1DA1BE);
Color get cyan900 => const Color(0XFF105955);
// Gray
Color get gray100 => const Color(0XFFF0F5F4);
Color get gray500 => const Color(0XFFAAAAAA);
Color get gray700 => const Color(0XFF636363);
Color get gray900 => const Color(0XFF1E232C);
// Green
Color get green50 => const Color(0XFFD9F9DA);
Color get green800 => const Color(0XFF159021);
Color get green80001 => const Color(0XFF0CAC13);
Color get gray50 => const Color(0XFFF7F8F9);
// Blue
Color get blue400 => const Color(0XFF34AADF);
Color get blueA100 => const Color(0XFF8DB1F7);
Color get blueA400 => const Color(0XFF337FFF);
// BlueGray
Color get blueGray200 => const Color(0XFFB8BCCA);
Color get blueGray40001 => const Color(0XFF888888);
// DeepOrange
Color get deepOrangeA400 => const Color(0XFFFF4500);
// Indigo
Color get indigo50 => const Color(0XFFE8ECF4);
// LightGreen
Color get lightGreenA200 => const Color(0XFFC7FC6C);
Color get lightGreenA20001 => const Color(0XFFC0FE53);
// Red
Color get red100 => const Color(0XFFF8C6CC);
Color get red600 => const Color(0XFFE73E3E);
Color get red700 => const Color(0XFFDA2037);
// Teal
Color get teal100 => const Color(0XFFA5E0DD);
// Yellow
Color get yellow900 => const Color(0XFFEE7429);
Color get gray600 => const Color(0XFF6A707C);
// DeepPurple
Color get deepPurpleA400 => const Color(0XFF5030E5);
// Gray
Color get gray10001 => const Color(0XFFF0F5F4);
Color get gray90001 => const Color(0XFF1B1919);
// Red
Color get red500 => const Color(0XFFF14336);
// White
Color get whiteA700 => const Color(0XFFFFFFFF);
// GrayB
Color get gray100B2 => const Color(0XB2F7F7F7);
Color get gray300 => const Color(0XFFE4E4E6);
// Amber
Color get amberA700 => const Color(0XFFFFAE00);
// LightBlue
Color get lightBlue900 => const Color(0XFF006699);
Color get lightBlueA200 => const Color(0XFF33CCFF);
Color get lightBlueA20001 => const Color(0XFF36C5F0);
// BlueGray
Color get blueGray100 => const Color(0XFFD9D9D9);
// LightGreen
Color get lightGreen100 => const Color(0XFFDDF8BB);
Color get lightGreenA20000 => const Color(0X00BBFB4C);
// Lime
Color get limeA200 => const Color(0XFFF8FF4A);
// Yellow
Color get yellow400 => const Color(0XFFE8FF61);
// Amber
Color get amber300 => const Color(0XFFFFDB61);
Color get blueA20001 => const Color(0XFF4C7FE4);
Color get blueA20002 => const Color(0XFF5285E8);
// DeepOrange
Color get deepOrange300 => const Color(0XFFFF9969);
Color get deepOrangeA200 => const Color(0XFFFF6C2C);
Color get gray200 => const Color(0XFFEFEFEF);
Color get gray800 => const Color(0XFF383838);
// Green
Color get green600 => const Color(0XFF239F57);
Color get green900 => const Color(0XFF096A2F);
Color get greenA700 => const Color(0XFF13A445);
// Orange
Color get orange900 => const Color(0XFFD15C0B);
// Pink
Color get pink400 => const Color(0XFFD44164);
Color get red900 => const Color(0XFFAF000D);
// Yellow
Color get yellow600 => const Color(0XFFFFDA2B);
}

View File

@@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
class ColorConstant {
static Color gray5001 = fromHex('#f6f7fb');
static Color purple211 = const Color.fromARGB(255, 93, 63, 211);
static Color gray5002 = fromHex('#f8f9fa');
static Color black900B2 = fromHex('#b2000000');
static Color gray5003 = fromHex('#fafcff');
static Color lightBlue100 = fromHex('#b0e5fc');
static Color gray80049 = fromHex('#493c3c43');
static Color yellow9003f = fromHex('#3feb9612');
static Color iris = fromHex('5D3FD3');
static Color red200 = fromHex('#fa9a9a');
static Color gray4004c = fromHex('#4cc4c4c4');
static Color blueA200 = fromHex('#468ee5');
static Color greenA100 = fromHex('#b5eacd');
static Color black9003f = fromHex('#3f000000');
static Color gray30099 = fromHex('#99e4e4e4');
static Color black90087 = fromHex('#87000000');
static Color whiteA70099 = fromHex('#99ffffff');
static Color black90001 = fromHex('#000000');
static Color blueGray90002 = fromHex('#24363c');
static Color blueGray90001 = fromHex('#2e3637');
static Color blueGray700 = fromHex('#535763');
static Color blueGray900 = fromHex('#262b35');
static Color black90003 = fromHex('#0b0a0a');
static Color black90002 = fromHex('#090b0d');
static Color redA700 = fromHex('#d80027');
static Color black90004 = fromHex('#000000');
static Color gray400 = fromHex('#c4c4c4');
static Color blue900 = fromHex('#003399');
static Color blueGray100 = fromHex('#d6dae2');
static Color blue700 = fromHex('#1976d2');
static Color blueGray300 = fromHex('#9ea8ba');
static Color amber500 = fromHex('#feb909');
static Color redA200 = fromHex('#fe555d');
static Color gray80099 = fromHex('#993c3c43');
static Color black9000c = fromHex('#0c000000');
static Color gray200 = fromHex('#efefef');
static Color gray60026 = fromHex('#266d6d6d');
static Color blue50 = fromHex('#e0ebff');
static Color indigo400 = fromHex('#4168d7');
static Color blueGray1006c = fromHex('#6cd1d3d4');
static Color black90011 = fromHex('#11000000');
static Color gray40001 = fromHex('#b3b3b3');
static Color whiteA70067 = fromHex('#67ffffff');
static Color gray10001 = fromHex('#fbf1f2');
static Color black90019 = fromHex('#19000000');
static Color blueGray40001 = fromHex('#888888');
static Color whiteA700 = fromHex('#ffffff');
static Color blueGray50 = fromHex('#eaecf0');
static Color red700 = fromHex('#d03329');
static Color blueA700 = fromHex('#0061ff');
static Color blueGray10001 = fromHex('#d6d6d6');
static Color gray60019 = fromHex('#197e7e7e');
static Color green600 = fromHex('#349765');
static Color blueA70001 = fromHex('#0068ff');
static Color gray50 = fromHex('#f9fbff');
static Color red100 = fromHex('#f6d6d4');
static Color blueGray20001 = fromHex('#adb5bd');
static Color black900 = fromHex('#000919');
static Color blueGray800 = fromHex('#37334d');
static Color blue5001 = fromHex('#eef4ff');
static Color deepOrange400 = fromHex('#d58c48');
static Color deepOrangeA400 = fromHex('#ff4b00');
static Color gray70011 = fromHex('#11555555');
static Color indigoA20033 = fromHex('#334871e3');
static Color gray90002 = fromHex('#0d062d');
static Color gray700 = fromHex('#666666');
static Color blueGray200 = fromHex('#bac1ce');
static Color blueGray400 = fromHex('#74839d');
static Color blue800 = fromHex('#2953c7');
static Color blueGray600 = fromHex('#5f6c86');
static Color gray900 = fromHex('#2a2a2a');
static Color gray90001 = fromHex('#212529');
static Color gray300 = fromHex('#d2efe0');
static Color gray30001 = fromHex('#e3e4e5');
static Color gray100 = fromHex('#f3f4f5');
static Color black90075 = fromHex('#75000000');
static Color deepOrangeA10033 = fromHex('#33dfa874');
static Color gray70026 = fromHex('#26555555');
static Color black90033 = fromHex('#33000000');
static Color blue200 = fromHex('#a6c8ff');
static Color purple900 = Colors.purple.shade900;
static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
}

View File

@@ -0,0 +1,61 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:base_project/resources/app_colors.dart';
import 'package:flutter/material.dart';
class FlushBarMessageUtil {
static void showFlushBar({
required BuildContext context,
required String message,
FlushBarType flushBarType = FlushBarType.info,
int durationInSeconds = 3,
}) {
Flushbar(
message: message,
duration: Duration(seconds: durationInSeconds),
backgroundColor: _getBackgroundColor(flushBarType),
margin: const EdgeInsets.all(8.0),
borderRadius: BorderRadius.circular(8.0),
flushbarPosition: FlushbarPosition.BOTTOM,
icon: _getIcon(flushBarType),
leftBarIndicatorColor: _getBackgroundColor(flushBarType),
).show(context);
}
static Color _getBackgroundColor(FlushBarType flushBarType) {
switch (flushBarType) {
case FlushBarType.success:
return AppColors.success;
case FlushBarType.error:
return AppColors.error;
case FlushBarType.warning:
return AppColors.warning;
case FlushBarType.info:
return AppColors.info;
case FlushBarType.general:
return AppColors.darkGrey;
default:
return AppColors.info;
}
}
static Icon _getIcon(FlushBarType flushBarType) {
switch (flushBarType) {
case FlushBarType.success:
return const Icon(Icons.check_circle, color: Colors.white);
case FlushBarType.error:
return const Icon(Icons.error, color: Colors.white);
case FlushBarType.warning:
return const Icon(Icons.warning, color: Colors.white);
case FlushBarType.info:
return const Icon(Icons.info, color: Colors.white);
case FlushBarType.general:
return const Icon(Icons.notifications, color: Colors.white);
default:
return const Icon(Icons.info, color: Colors.white);
}
}
}
enum FlushBarType { success, error, warning, info, general }

View File

@@ -0,0 +1,35 @@
class FormValidators {
// Validator for non-empty fields
static String? validateNotEmpty(String? value) {
return value == null || value.isEmpty ? 'This field cannot be empty' : null;
}
// Validator for email format
static String? validateEmail(String? value) {
const emailPattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
final regex = RegExp(emailPattern);
return value == null || !regex.hasMatch(value) ? 'Enter a valid email address' : null;
}
// Validator for phone number format
static String? validatePhoneNumber(String? value) {
const phonePattern = r'^\+?[1-9]\d{1,14}$'; // E.164 format
final regex = RegExp(phonePattern);
return value == null || !regex.hasMatch(value) ? 'Enter a valid phone number' : null;
}
// Validator for min length
static String? validateMinLength(String? value, int minLength) {
return value == null || value.length < minLength ? 'Must be at least $minLength characters long' : null;
}
// Validator for max length
static String? validateMaxLength(String? value, int maxLength) {
return value != null && value.length > maxLength ? 'Must be no more than $maxLength characters long' : null;
}
// Validator for matching passwords
static String? validatePasswordMatch(String? value, String password) {
return value != password ? 'Passwords do not match' : null;
}
}

Some files were not shown because too many files have changed in this diff Show More