Regit
This commit is contained in:
1 parent
f8b5a54e3b
commit
e39fc5593d
5 files changed
+203
-134
No files matched your search
@@ -67,93 +67,9 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
|
||||
}
|
||||
}
|
||||
|
||||
void _onNavTapped(int index, List<Map<String, dynamic>> allowedPrimary, List<Map<String, dynamic>> allowedSecondary) {
|
||||
if (index < allowedPrimary.length) {
|
||||
final label = allowedPrimary[index]['label'] as String;
|
||||
|
||||
// ✅ NEW: Update global state instead of local setState
|
||||
void _onNavTapped(String label) {
|
||||
ref.read(activeTabProvider.notifier).setTab(label);
|
||||
_reloadTabContent(label);
|
||||
} else {
|
||||
_showMoreBottomSheet(allowedPrimary, allowedSecondary);
|
||||
}
|
||||
}
|
||||
|
||||
void _showMoreBottomSheet(List<Map<String, dynamic>> allowedPrimary, List<Map<String, dynamic>> allowedSecondary) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surfaceWhite,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppSizes.radiusLarge)),
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppSizes.p12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Drag Handle
|
||||
Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
margin: const EdgeInsets.only(bottom: AppSizes.p16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(AppSizes.radiusSmall),
|
||||
),
|
||||
),
|
||||
// Build Secondary Tabs
|
||||
...allowedSecondary.asMap().entries.map((entry) {
|
||||
int secondaryIndex = entry.key;
|
||||
var tab = entry.value;
|
||||
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(AppSizes.p8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppSizes.radiusMedium),
|
||||
),
|
||||
child: Icon(tab['icon'] as IconData, color: AppColors.primaryGreen),
|
||||
),
|
||||
title: Text(
|
||||
tab['label'] as String,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.textPrimary),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right, color: AppColors.textMuted),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
final label = tab['label'] as String;
|
||||
// ✅ NEW: Update global state when a "More" option is clicked
|
||||
ref.read(activeTabProvider.notifier).setTab(label);
|
||||
_reloadTabContent(label);
|
||||
},
|
||||
);
|
||||
}),
|
||||
const Divider(),
|
||||
// Logout Option in the More Menu
|
||||
ListTile(
|
||||
leading: const Padding(
|
||||
padding: EdgeInsets.all(AppSizes.p8),
|
||||
child: Icon(Icons.exit_to_app, color: AppColors.error),
|
||||
),
|
||||
title: const Text(
|
||||
'Logout',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.error),
|
||||
),
|
||||
onTap: () {
|
||||
// DO NOT pop the bottom sheet here.
|
||||
// Just show the dialog directly!
|
||||
showLogoutConfirmationDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -180,28 +96,6 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
|
||||
|
||||
_visitedIndices.add(currentIndex);
|
||||
|
||||
int selectedBottomNavIndex = allowedPrimary.indexWhere((t) => t['label'] == activeTab);
|
||||
if (selectedBottomNavIndex == -1) {
|
||||
// If the active tab is NOT in the primary list (e.g., Settings), highlight the "More" tab
|
||||
selectedBottomNavIndex = allowedPrimary.length;
|
||||
}
|
||||
|
||||
List<BottomNavigationBarItem> bottomNavItems = allowedPrimary.map((tab) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: Icon(tab['icon'] as IconData),
|
||||
label: tab['label'] as String,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
if (allowedSecondary.isNotEmpty) {
|
||||
bottomNavItems.add(
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.more_horiz),
|
||||
label: 'More',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
@@ -238,6 +132,69 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.pageBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
title: Text(
|
||||
tabs[currentIndex]['label'] as String,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
UserAccountsDrawerHeader(
|
||||
decoration: const BoxDecoration(color: AppColors.primaryGreen),
|
||||
accountName: Text(
|
||||
'TIS RMS',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.white),
|
||||
),
|
||||
accountEmail: Text(
|
||||
'Role: ${widget.userRole.toUpperCase()}',
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
currentAccountPicture: const CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
backgroundImage: AssetImage('assets/images/logo.png'),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
if (allowedPrimary.isNotEmpty) ...[
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text('MAIN', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
),
|
||||
...allowedPrimary.map((tab) => _buildDrawerItem(tab, currentIndex, tabs)),
|
||||
],
|
||||
if (allowedSecondary.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text('OTHER', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
),
|
||||
...allowedSecondary.map((tab) => _buildDrawerItem(tab, currentIndex, tabs)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.exit_to_app, color: AppColors.error),
|
||||
title: const Text('Logout', style: TextStyle(fontWeight: FontWeight.bold, color: AppColors.error)),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Close drawer first
|
||||
showLogoutConfirmationDialog(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: IndexedStack(
|
||||
index: currentIndex,
|
||||
children: tabs.asMap().entries.map((entry) {
|
||||
@@ -246,30 +203,31 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
|
||||
: const SizedBox.shrink();
|
||||
}).toList(),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
currentIndex: selectedBottomNavIndex,
|
||||
onTap: (i) => _onNavTapped(i, allowedPrimary, allowedSecondary),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
selectedItemColor: Colors.white,
|
||||
unselectedItemColor: Colors.white70,
|
||||
showUnselectedLabels: true,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedLabelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 11),
|
||||
items: bottomNavItems,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDrawerItem(Map<String, dynamic> tab, int currentIndex, List<Map<String, dynamic>> allTabs) {
|
||||
final label = tab['label'] as String;
|
||||
final icon = tab['icon'] as IconData;
|
||||
final index = allTabs.indexWhere((t) => t['label'] == label);
|
||||
final isSelected = index == currentIndex;
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: isSelected ? AppColors.primaryGreen : AppColors.textSecondary),
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
selectedTileColor: AppColors.primaryGreen.withOpacity(0.1),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Close drawer
|
||||
_onNavTapped(label);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
import '../../../core/constants/app_sizes.dart';
|
||||
@@ -876,6 +877,7 @@ class _GradeLevelFormModalState extends ConsumerState<GradeLevelFormModal> {
|
||||
controller: _levelController,
|
||||
prefixIcon: Icons.numbers,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return 'Level is required';
|
||||
if (int.tryParse(v) == null) return 'Must be a valid integer';
|
||||
|
||||
@@ -17,6 +17,29 @@ import '../../../shared/dialogs/success_dialog.dart';
|
||||
import '../../../shared/dialogs/info_dialog.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Auto-capitalises the first letter of every word (works on paste)
|
||||
// ---------------------------------------------------------------
|
||||
class _UpperCaseWordsFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
final text = newValue.text;
|
||||
if (text.isEmpty) return newValue;
|
||||
final capitalized = text.replaceAllMapped(
|
||||
RegExp(r'(^|\s)(\S)'),
|
||||
(m) => '${m[1]}${m[2]!.toUpperCase()}',
|
||||
);
|
||||
return newValue.copyWith(
|
||||
text: capitalized,
|
||||
selection: newValue.selection,
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddEditStudentModal extends ConsumerStatefulWidget {
|
||||
final StudentModel? student;
|
||||
|
||||
@@ -89,6 +112,63 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// EXIT CONFIRMATION
|
||||
// ----------------------------------------------------------------
|
||||
bool get _hasAnyData =>
|
||||
_lrnController.text.isNotEmpty ||
|
||||
_firstNameController.text.isNotEmpty ||
|
||||
_middleNameController.text.isNotEmpty ||
|
||||
_lastNameController.text.isNotEmpty ||
|
||||
_selectedDob != null;
|
||||
|
||||
Future<void> _confirmClose() async {
|
||||
// If editing, or nothing entered yet — close immediately
|
||||
if (widget.student != null || !_hasAnyData) {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
final shouldClose = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text('Discard Changes?'),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'You have unsaved data. Are you sure you want to close without saving?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text(
|
||||
'KEEP EDITING',
|
||||
style: TextStyle(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text(
|
||||
'DISCARD',
|
||||
style: TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (shouldClose == true && mounted) Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// OCR PROCESSING
|
||||
// ----------------------------------------------------------------
|
||||
@@ -379,6 +459,7 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
gradeLevelsAsync,
|
||||
sectionsAsync,
|
||||
isMobile,
|
||||
viewInsets.bottom,
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 32),
|
||||
@@ -394,6 +475,7 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
gradeLevelsAsync,
|
||||
sectionsAsync,
|
||||
isMobile,
|
||||
viewInsets.bottom,
|
||||
)),
|
||||
),
|
||||
),
|
||||
@@ -606,6 +688,7 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
AsyncValue<List<GradeLevelModel>> gradeLevelsAsync,
|
||||
AsyncValue<List<SectionModel>> sectionsAsync,
|
||||
bool isMobile,
|
||||
double keyboardInset,
|
||||
) {
|
||||
final compactTheme = Theme.of(context).copyWith(
|
||||
inputDecorationTheme: Theme.of(context).inputDecorationTheme.copyWith(
|
||||
@@ -646,7 +729,7 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: AppColors.textSecondary),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: _confirmClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -717,6 +800,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _firstNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
validator: (v) => _validateRequired(
|
||||
v,
|
||||
'First name',
|
||||
@@ -733,6 +818,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _middleNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Middle Name (optional)',
|
||||
prefixIcon: Icon(
|
||||
@@ -750,6 +837,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
controller: _lastNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
validator: (v) =>
|
||||
_validateRequired(v, 'Last name'),
|
||||
decoration: const InputDecoration(
|
||||
@@ -764,6 +853,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _extController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ext.',
|
||||
hintText: 'Jr / III',
|
||||
@@ -780,6 +871,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _firstNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
validator: (v) =>
|
||||
_validateRequired(v, 'First name'),
|
||||
decoration: const InputDecoration(
|
||||
@@ -789,6 +882,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
const SizedBox(height: AppSizes.p12),
|
||||
TextFormField(
|
||||
controller: _middleNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Middle Name',
|
||||
),
|
||||
@@ -796,6 +891,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
const SizedBox(height: AppSizes.p12),
|
||||
TextFormField(
|
||||
controller: _lastNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
validator: (v) =>
|
||||
_validateRequired(v, 'Last name'),
|
||||
decoration: const InputDecoration(
|
||||
@@ -805,6 +902,8 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
const SizedBox(height: AppSizes.p12),
|
||||
TextFormField(
|
||||
controller: _extController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
inputFormatters: [_UpperCaseWordsFormatter()],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Extension (Jr / III)',
|
||||
),
|
||||
@@ -1046,6 +1145,7 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
|
||||
// ---- Actions ----
|
||||
if (isMobile)
|
||||
if (keyboardInset == 0) // Hide when keyboard is open
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
@@ -1092,7 +1192,7 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
TextButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => Navigator.of(context).pop(),
|
||||
: _confirmClose,
|
||||
child: const Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
@@ -1131,7 +1231,7 @@ class _AddEditStudentModalState extends ConsumerState<AddEditStudentModal>
|
||||
TextButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => Navigator.of(context).pop(),
|
||||
: _confirmClose,
|
||||
child: const Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
|
||||
@@ -33,8 +33,10 @@ Future<void> showLogoutConfirmationDialog(BuildContext context) async {
|
||||
// 1. Close the dialog immediately for responsiveness
|
||||
navigator.pop();
|
||||
|
||||
// 2. Remove the saved password for security (keep username intact)
|
||||
// 2. Remove all saved credentials for security
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('rememberMe');
|
||||
await prefs.remove('saved_username');
|
||||
await prefs.remove('saved_password');
|
||||
|
||||
// 3. Navigate FIRST to LoginScreen and destroy routing history.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CustomTextField extends StatelessWidget {
|
||||
final String hintText;
|
||||
@@ -14,6 +15,8 @@ class CustomTextField extends StatelessWidget {
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final void Function(String)? onSubmitted;
|
||||
final int? maxLength;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
const CustomTextField({
|
||||
super.key,
|
||||
@@ -30,6 +33,8 @@ class CustomTextField extends StatelessWidget {
|
||||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
this.onSubmitted,
|
||||
this.maxLength,
|
||||
this.inputFormatters,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -41,7 +46,9 @@ class CustomTextField extends StatelessWidget {
|
||||
onChanged: onChanged,
|
||||
validator: validator,
|
||||
maxLines: maxLines,
|
||||
maxLength: maxLength,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
textInputAction: textInputAction,
|
||||
onFieldSubmitted: onSubmitted,
|
||||
decoration: InputDecoration(
|
||||
|
||||
Reference in new issue
Block a user