base_project
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'base_field.dart';
|
||||
import '../utils/entity_field_store.dart';
|
||||
|
||||
class AudioUploadField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
AudioUploadField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Select audio file',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) {
|
||||
final items =
|
||||
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
const [];
|
||||
if (isRequired && items.isEmpty) return '$label is required';
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
<UploadItem>[];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await FilePicker.platform.pickFiles(type: FileType.audio);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
final f = result.files.single;
|
||||
final Uint8List? bytes = f.bytes ??
|
||||
(f.path != null
|
||||
? await File(f.path!).readAsBytes()
|
||||
: null);
|
||||
if (bytes != null) {
|
||||
final updated = List<UploadItem>.from(items)
|
||||
..add(UploadItem(fileName: f.name, bytes: bytes));
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.audiotrack_rounded),
|
||||
label: const Text('Add Audio'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...items.map((u) => ListTile(
|
||||
leading: const Icon(Icons.music_note_rounded),
|
||||
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () {
|
||||
final updated = List<UploadItem>.from(items)..remove(u);
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
/// Generic Captcha field: shows generated code and asks user to type it
|
||||
class CaptchaField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final int length;
|
||||
|
||||
CaptchaField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Enter CAPTCHA',
|
||||
this.isRequired = true,
|
||||
this.length = 6,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return _CaptchaFieldWidget(
|
||||
key: ValueKey(fieldKey),
|
||||
label: label,
|
||||
hint: hint,
|
||||
length: length,
|
||||
colorScheme: colorScheme,
|
||||
controller: controller,
|
||||
baseValidator: validator,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
String _generateCaptcha(int length) {
|
||||
final random = Random();
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
return String.fromCharCodes(Iterable.generate(
|
||||
length,
|
||||
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _CaptchaFieldWidget extends StatefulWidget {
|
||||
final String label;
|
||||
final String hint;
|
||||
final int length;
|
||||
final ColorScheme colorScheme;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?)? baseValidator;
|
||||
final VoidCallback? onChanged;
|
||||
|
||||
const _CaptchaFieldWidget({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
required this.length,
|
||||
required this.colorScheme,
|
||||
required this.controller,
|
||||
required this.baseValidator,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CaptchaFieldWidget> createState() => _CaptchaFieldWidgetState();
|
||||
}
|
||||
|
||||
class _CaptchaFieldWidgetState extends State<_CaptchaFieldWidget> {
|
||||
late String _captcha;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_captcha = _generateCaptcha(widget.length);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border:
|
||||
Border.all(color: widget.colorScheme.primary.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_captcha,
|
||||
style: TextStyle(
|
||||
color: widget.colorScheme.primary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 3,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Refresh',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_captcha = _generateCaptcha(widget.length);
|
||||
});
|
||||
if (widget.onChanged != null) widget.onChanged!();
|
||||
},
|
||||
icon: Icon(Icons.refresh_rounded,
|
||||
color: widget.colorScheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ModernTextField(
|
||||
controller: widget.controller,
|
||||
hint: widget.hint,
|
||||
validator: (value) {
|
||||
final base = widget.baseValidator;
|
||||
final err = base != null ? base(value) : null;
|
||||
if (err != null) return err;
|
||||
if ((value ?? '').trim() != _captcha) {
|
||||
return 'CAPTCHA does not match';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged:
|
||||
widget.onChanged != null ? (_) => widget.onChanged!() : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _generateCaptcha(int length) {
|
||||
final random = Random();
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
return String.fromCharCodes(Iterable.generate(
|
||||
length,
|
||||
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
/// Date input field implementation with native date picker
|
||||
class DateField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final DateTime? initialDate;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
DateField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.initialDate,
|
||||
DateTime? firstDate,
|
||||
DateTime? lastDate,
|
||||
}) : firstDate = firstDate ?? DateTime(2000),
|
||||
lastDate = lastDate ?? DateTime(2101);
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint.isNotEmpty ? hint : label,
|
||||
readOnly: true,
|
||||
validator: validator,
|
||||
suffixIcon: const Icon(Icons.calendar_today_rounded),
|
||||
onTap: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final now = DateTime.now();
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initialDate ?? now,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
helpText: label,
|
||||
);
|
||||
if (picked != null) {
|
||||
final String value = _formatDate(picked);
|
||||
controller.text = value;
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
String two(int n) => n < 10 ? '0$n' : '$n';
|
||||
return '${date.year}-${two(date.month)}-${two(date.day)}';
|
||||
}
|
||||
}
|
||||
|
||||
// No global navigator key required; Builder provides local context
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// DateTime input field implementation with native date & time pickers
|
||||
class DateTimeField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final DateTime? initialDateTime;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
DateTimeField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.initialDateTime,
|
||||
DateTime? firstDate,
|
||||
DateTime? lastDate,
|
||||
}) : firstDate = firstDate ?? DateTime(2000),
|
||||
lastDate = lastDate ?? DateTime(2101);
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint.isNotEmpty ? hint : label,
|
||||
readOnly: true,
|
||||
validator: validator,
|
||||
suffixIcon: const Icon(Icons.access_time_rounded),
|
||||
onTap: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final now = DateTime.now();
|
||||
final datePicked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: (initialDateTime ?? now),
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
helpText: label,
|
||||
);
|
||||
if (datePicked == null) return;
|
||||
final timePicked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(initialDateTime ?? now),
|
||||
helpText: label,
|
||||
);
|
||||
if (timePicked == null) return;
|
||||
final combined = DateTime(
|
||||
datePicked.year,
|
||||
datePicked.month,
|
||||
datePicked.day,
|
||||
timePicked.hour,
|
||||
timePicked.minute,
|
||||
);
|
||||
controller.text = _formatDateTime(combined);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime dt) {
|
||||
String two(int n) => n < 10 ? '0$n' : '$n';
|
||||
return '${dt.year}-${two(dt.month)}-${two(dt.day)} ${two(dt.hour)}:${two(dt.minute)}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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',
|
||||
// /// ),
|
||||
// /// ];
|
||||
// /// }
|
||||
// /// }
|
||||
// /// ```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ?? '';
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// Email input field implementation
|
||||
class EmailField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
EmailField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(value)) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'base_field.dart';
|
||||
import '../utils/entity_field_store.dart';
|
||||
|
||||
/// Multi-file upload field; stores files in EntityFieldStore under fieldKey
|
||||
class FileUploadField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
FileUploadField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Select files',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) {
|
||||
final items =
|
||||
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
const [];
|
||||
if (isRequired && items.isEmpty) {
|
||||
return '$label is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
<UploadItem>[];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await FilePicker.platform.pickFiles(allowMultiple: true);
|
||||
if (result != null) {
|
||||
final List<UploadItem> updated = List<UploadItem>.from(items);
|
||||
for (final f in result.files) {
|
||||
final Uint8List? bytes = f.bytes;
|
||||
if (bytes == null) continue;
|
||||
updated.add(UploadItem(fileName: f.name, bytes: bytes));
|
||||
}
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.attach_file_rounded),
|
||||
label: const Text('Add Files'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...items.map((u) => ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file_rounded),
|
||||
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () {
|
||||
final updated = List<UploadItem>.from(items)..remove(u);
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'base_field.dart';
|
||||
import '../utils/entity_field_store.dart';
|
||||
|
||||
class ImageUploadField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
ImageUploadField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = 'Select images',
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (_) {
|
||||
final items =
|
||||
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
const [];
|
||||
if (isRequired && items.isEmpty) return '$label is required';
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
|
||||
<UploadItem>[];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? picked =
|
||||
await picker.pickImage(source: ImageSource.gallery);
|
||||
if (picked != null) {
|
||||
final Uint8List bytes = await picked.readAsBytes();
|
||||
final updated = List<UploadItem>.from(items)
|
||||
..add(UploadItem(fileName: picked.name, bytes: bytes));
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.image_rounded),
|
||||
label: const Text('Add Image'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: items
|
||||
.map((u) => Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.memory(u.bytes,
|
||||
width: 72, height: 72, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final updated = List<UploadItem>.from(items)
|
||||
..remove(u);
|
||||
EntityFieldStore.instance.set(fieldKey, updated);
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// Number input field implementation
|
||||
class NumberField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final double? min;
|
||||
final double? max;
|
||||
final int? decimalPlaces;
|
||||
|
||||
NumberField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
this.min,
|
||||
this.max,
|
||||
this.decimalPlaces,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final number = double.tryParse(value);
|
||||
if (number == null) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
if (min != null && number < min!) {
|
||||
return '$label must be at least $min';
|
||||
}
|
||||
if (max != null && number > max!) {
|
||||
return '$label must be at most $max';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: decimalPlaces != null && decimalPlaces! > 0,
|
||||
),
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
|
||||
class RadioField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
final List<String> options; // e.g., ['yes','no']
|
||||
|
||||
RadioField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
this.hint = '',
|
||||
this.isRequired = false,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty && !options.contains(value)) {
|
||||
return 'Invalid selection';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
final String current = controller.text;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
children: options.map((opt) {
|
||||
return ChoiceChip(
|
||||
label: Text(opt),
|
||||
selected: current == opt,
|
||||
selectedColor: colorScheme.primary.withOpacity(0.2),
|
||||
onSelected: (_) {
|
||||
controller.text = opt;
|
||||
if (onChanged != null) onChanged();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base_field.dart';
|
||||
import '../../../shared/widgets/inputs/modern_text_field.dart';
|
||||
|
||||
/// URL input field implementation using ModernTextField
|
||||
class UrlField extends BaseField {
|
||||
final String fieldKey;
|
||||
final String label;
|
||||
final String hint;
|
||||
final bool isRequired;
|
||||
|
||||
UrlField({
|
||||
required this.fieldKey,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String? Function(String?)? get validator => (value) {
|
||||
if (isRequired && (value == null || value.isEmpty)) {
|
||||
return '$label is required';
|
||||
}
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || !(uri.hasScheme && uri.hasAuthority)) {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildField({
|
||||
required TextEditingController controller,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onChanged,
|
||||
}) {
|
||||
return ModernTextField(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
keyboardType: TextInputType.url,
|
||||
prefixIcon: const Icon(Icons.link_rounded),
|
||||
validator: validator,
|
||||
onChanged: onChanged != null ? (_) => onChanged() : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
@@ -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(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: ");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
enum Status {LOADING,SUCCESS,ERROR}
|
||||
@@ -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!,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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)];
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
Reference in New Issue
Block a user