UI Redesigns for Users and Settings screens: frosted glass modal, SegmentedButton, FAB, and live password progress bar

This commit is contained in:
ssbg04 committed 2026-07-02 14:02:24 +08:00
1 parent 91d0ff01b4
commit 7a6aa5108d
2 files changed
+216 -134

No files matched your search

@@ -45,9 +45,51 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
bool _isPasswordLoading = false;
ProviderSubscription<String>? _tabListener;
double _passwordStrength = 0.0;
Color _passwordStrengthColor = Colors.grey;
String _passwordStrengthText = '';
void _evaluatePasswordStrength() {
final password = _newPassCtrl.text;
double strength = 0.0;
if (password.isNotEmpty) {
if (password.length >= 8) strength += 0.25;
if (RegExp(r'[A-Z]').hasMatch(password)) strength += 0.25;
if (RegExp(r'[0-9]').hasMatch(password)) strength += 0.25;
if (RegExp(r'[^A-Za-z0-9]').hasMatch(password)) strength += 0.25;
}
Color color = Colors.grey;
String text = '';
if (password.isEmpty) {
color = Colors.grey;
text = '';
} else if (strength <= 0.25) {
color = Colors.red;
text = 'Weak';
} else if (strength == 0.5) {
color = Colors.orange;
text = 'Fair';
} else if (strength == 0.75) {
color = Colors.yellow.shade700;
text = 'Good';
} else {
color = Colors.green;
text = 'Strong';
}
setState(() {
_passwordStrength = strength;
_passwordStrengthColor = color;
_passwordStrengthText = text;
});
}
@override
void initState() {
super.initState();
_newPassCtrl.addListener(_evaluatePasswordStrength);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_tabListener = ref.listenManual<String>(activeTabProvider, (previous, next) {
@@ -561,6 +603,36 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
obscureText: _obscurePasswords,
validator: AppValidators.validatePasswordComplexity,
),
if (_newPassCtrl.text.isNotEmpty) ...[
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: _passwordStrength,
backgroundColor: Colors.grey.shade200,
color: _passwordStrengthColor,
minHeight: 6,
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 50,
child: Text(
_passwordStrengthText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: _passwordStrengthColor,
),
),
),
],
),
],
const SizedBox(height: AppSizes.p12),
CustomTextField(
hintText: 'Confirm New Password',
+144 -134
View File
@@ -1,3 +1,4 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -144,83 +145,112 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
final currentUser = ref.watch(authProvider).value;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppSizes.radiusLarge)),
title: Row(
children: [
Icon(Icons.person, color: _roleColor(user.role)),
const SizedBox(width: 8),
const Text('User Details'),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_detailRow('Full Name', user.fullName),
const SizedBox(height: 8),
_detailRow('Username', '@${user.username}'),
const SizedBox(height: 8),
_detailRow('Access Role', user.role.toUpperCase().replaceAll('_', ' ')),
const SizedBox(height: 8),
_detailRow('Email', user.email ?? '—'),
const SizedBox(height: 8),
_detailRow('Phone', user.phone ?? '—'),
const SizedBox(height: 8),
_detailRow(
'Added By',
user.addedByName != null
? '${user.addedByName} (@${user.addedByUsername})'
: 'System'
),
const SizedBox(height: 8),
_detailRow('Date Joined', user.createdAt?.split('T').first ?? '—'),
],
),
actionsPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Left side actions: Edit / Reset
Row(
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
child: ClipRRect(
borderRadius: BorderRadius.circular(AppSizes.radiusLarge),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(
constraints: const BoxConstraints(maxWidth: 450),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(AppSizes.radiusLarge),
border: Border.all(color: Colors.white.withValues(alpha: 0.5), width: 1.5),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 4))
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
icon: const Icon(Icons.lock_reset, color: Colors.orange),
tooltip: 'Reset Password to "changeme123"',
onPressed: () {
Navigator.of(ctx).pop();
_confirmResetPassword(user);
},
// Header
Padding(
padding: const EdgeInsets.all(AppSizes.p24),
child: Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: _roleColor(user.role).withValues(alpha: 0.15),
child: Icon(Icons.person, color: _roleColor(user.role), size: 28),
),
const SizedBox(width: AppSizes.p16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user.fullName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary)),
Text('@${user.username}', style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)),
],
),
),
// Action buttons in header
IconButton(
icon: const Icon(Icons.lock_reset, color: Colors.orange, size: 20),
tooltip: 'Reset Password to "changeme123"',
onPressed: () {
Navigator.of(ctx).pop();
_confirmResetPassword(user);
},
),
IconButton(
icon: const Icon(Icons.edit, color: AppColors.primaryGreen, size: 20),
tooltip: 'Edit User',
onPressed: () {
Navigator.of(ctx).pop();
_openModal(user: user);
},
),
if (user.id != currentUser?.id)
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
tooltip: 'Delete User',
onPressed: () {
Navigator.of(ctx).pop();
_confirmDelete(user);
},
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
),
IconButton(
icon: const Icon(Icons.edit, color: AppColors.primaryGreen),
tooltip: 'Edit User',
onPressed: () {
Navigator.of(ctx).pop();
_openModal(user: user);
},
const Divider(height: 1, thickness: 1),
// Content
Padding(
padding: const EdgeInsets.all(AppSizes.p24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: _detailRow('Access Role', user.role.toUpperCase().replaceAll('_', ' '))),
Expanded(child: _detailRow('Date Joined', user.createdAt?.split('T').first ?? '—')),
],
),
const SizedBox(height: AppSizes.p16),
Row(
children: [
Expanded(child: _detailRow('Email', user.email?.isNotEmpty == true ? user.email! : '—')),
Expanded(child: _detailRow('Phone', user.phone?.isNotEmpty == true ? user.phone! : '—')),
],
),
const SizedBox(height: AppSizes.p16),
_detailRow('Added By', user.addedByName != null ? '${user.addedByName} (@${user.addedByUsername})' : 'System'),
],
),
),
],
),
// Right side action: Delete
if (user.id != currentUser?.id)
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
tooltip: 'Delete User',
onPressed: () {
Navigator.of(ctx).pop();
_confirmDelete(user);
},
),
],
),
),
],
),
),
);
}
@@ -242,6 +272,14 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
return Scaffold(
backgroundColor: Colors.transparent,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _openModal(),
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
icon: const Icon(Icons.add),
label: const Text('Add User'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppSizes.p24),
@@ -274,22 +312,40 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(AppSizes.radiusLarge),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildSegmentTab('All Users', 'all', users.length),
_buildSegmentTab('Admin', 'admin', users.where((u) => u.role == 'admin').length),
_buildSegmentTab('Teacher', 'teacher', users.where((u) => u.role == 'teacher').length),
],
SegmentedButton<String>(
segments: [
ButtonSegment<String>(
value: 'all',
label: Text('All (${users.length})'),
),
ButtonSegment<String>(
value: 'admin',
label: Text('Admin (${users.where((u) => u.role == 'admin').length})'),
),
ButtonSegment<String>(
value: 'teacher',
label: Text('Teacher (${users.where((u) => u.role == 'teacher').length})'),
),
],
selected: {_roleFilter},
onSelectionChanged: (Set<String> newSelection) {
setState(() {
_roleFilter = newSelection.first;
});
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryGreen.withValues(alpha: 0.15);
}
return Colors.transparent;
}),
foregroundColor: WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryGreen;
}
return AppColors.textSecondary;
}),
),
),
const SizedBox(height: AppSizes.p16),
@@ -344,25 +400,7 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
maxWidth: double.infinity,
),
),
const SizedBox(width: AppSizes.p16),
SizedBox(
width: isDesktop ? 180 : 50,
child: isDesktop
? PrimaryButton(
label: 'ADD USER',
onPressed: () => _openModal(),
)
: ElevatedButton(
onPressed: () => _openModal(),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppSizes.radiusMedium)),
),
child: const Icon(Icons.add),
),
),
],
),
),
@@ -370,35 +408,7 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
);
}
Widget _buildSegmentTab(String label, String value, int count) {
final isSelected = _roleFilter == value;
return GestureDetector(
onTap: () => setState(() => _roleFilter = value),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(AppSizes.radiusMedium),
boxShadow: isSelected ? [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2))] : [],
),
child: Row(
children: [
Text(label, style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, color: isSelected ? AppColors.textPrimary : AppColors.textSecondary, fontSize: 13)),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryGreen.withValues(alpha: 0.1) : Colors.grey.shade200,
borderRadius: BorderRadius.circular(10),
),
child: Text(count.toString(), style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: isSelected ? AppColors.primaryGreen : Colors.grey.shade600)),
),
],
),
),
);
}
Widget _buildDesktopTable(List<SystemUser> users) {
return Container(