Add AddFeaturesDialog widget and enhance error handling in login form and account settings dialog

This commit is contained in:
Leon Bösche
2026-02-13 18:05:33 +01:00
parent 111e403ebd
commit 4e133b4cb8
5 changed files with 487 additions and 40 deletions

View File

@@ -21,6 +21,7 @@ import 'login_form.dart' show LoginForm;
import 'file_explorer.dart';
import '../widgets/organization_settings_dialog.dart';
import '../widgets/account_settings_dialog.dart';
import '../widgets/add_features_dialog.dart';
import '../widgets/audio_player_bar.dart';
import '../injection.dart';
@@ -231,6 +232,13 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
);
}
void _showAddFeaturesDialog(BuildContext context) {
showDialog(
context: context,
builder: (dialogContext) => const AddFeaturesDialog(),
);
}
Widget _buildOrgRow(BuildContext context) {
return BlocBuilder<OrganizationBloc, OrganizationState>(
builder: (context, state) {
@@ -384,6 +392,7 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
IconData icon, {
bool isAvatar = false,
VoidCallback? onTap,
bool showSoonBadge = false,
}) {
final isSelected = _selectedTab == label;
final highlightColor = const Color.fromARGB(255, 100, 200, 255);
@@ -391,7 +400,7 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
? AppTheme.primaryText
: AppTheme.secondaryText;
return GestureDetector(
Widget button = GestureDetector(
onTap:
onTap ??
() {
@@ -508,6 +517,36 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
],
),
);
if (showSoonBadge) {
return Stack(
clipBehavior: Clip.none,
children: [
button,
Positioned(
top: -6,
right: -12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.accentColor,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'soon',
style: TextStyle(
color: Colors.black,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
return button;
}
@override
@@ -599,12 +638,17 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
children: [
_buildNavButton('Drive', Icons.cloud),
const SizedBox(width: 16),
_buildNavButton('Mail', Icons.mail),
_buildNavButton(
'Mail',
Icons.mail,
showSoonBadge: true,
),
const SizedBox(width: 16),
_buildNavButton(
'Add',
Icons.add,
onTap: () {},
onTap: () =>
_showAddFeaturesDialog(context),
),
const SizedBox(width: 16),
if (hasSelectedOrg)

View File

@@ -354,20 +354,18 @@ class _LoginFormState extends State<LoginForm> {
isLoading: state is AuthLoading,
onPressed: () {
if (_usernameController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Username is required'),
),
);
setState(() {
_usernameHasError = true;
_usernameErrorText = 'username is required';
});
return;
}
if (_isSignup) {
if (_passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Password is required'),
),
);
setState(() {
_passwordHasError = true;
_passwordErrorText = 'password is required';
});
return;
}
context.read<AuthBloc>().add(
@@ -387,11 +385,11 @@ class _LoginFormState extends State<LoginForm> {
);
} else {
if (_passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Password is required'),
),
);
setState(() {
_passwordHasError = true;
_passwordErrorText =
'password is required';
});
return;
}
context.read<AuthBloc>().add(

View File

@@ -35,6 +35,14 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
late TextEditingController _newPasswordController;
late TextEditingController _confirmPasswordController;
// Password field error state
bool _currentPasswordHasError = false;
bool _newPasswordHasError = false;
bool _confirmPasswordHasError = false;
String? _currentPasswordErrorText;
String? _newPasswordErrorText;
String? _confirmPasswordErrorText;
User? _currentUser;
@override
@@ -236,12 +244,50 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
}
Future<void> _changePassword() async {
if (_newPasswordController.text != _confirmPasswordController.text) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
// Clear previous errors
setState(() {
_currentPasswordHasError = false;
_newPasswordHasError = false;
_confirmPasswordHasError = false;
_currentPasswordErrorText = null;
_newPasswordErrorText = null;
_confirmPasswordErrorText = null;
});
// Validate current password
if (_currentPasswordController.text.isEmpty) {
setState(() {
_currentPasswordHasError = true;
_currentPasswordErrorText = 'current password is required';
});
return;
}
// Validate new password
if (_newPasswordController.text.isEmpty) {
setState(() {
_newPasswordHasError = true;
_newPasswordErrorText = 'new password is required';
});
return;
}
// Validate confirm password
if (_confirmPasswordController.text.isEmpty) {
setState(() {
_confirmPasswordHasError = true;
_confirmPasswordErrorText = 'please confirm your password';
});
return;
}
// Validate passwords match
if (_newPasswordController.text != _confirmPasswordController.text) {
setState(() {
_newPasswordHasError = true;
_confirmPasswordHasError = true;
_confirmPasswordErrorText = 'passwords do not match';
});
return;
}
@@ -258,16 +304,35 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
);
}
// Clear fields
// Clear fields and errors
_currentPasswordController.clear();
_newPasswordController.clear();
_confirmPasswordController.clear();
setState(() {
_currentPasswordHasError = false;
_newPasswordHasError = false;
_confirmPasswordHasError = false;
_currentPasswordErrorText = null;
_newPasswordErrorText = null;
_confirmPasswordErrorText = null;
});
} catch (e) {
if (mounted) {
final errorMsg = e.toString().toLowerCase();
if (errorMsg.contains('incorrect') ||
errorMsg.contains('invalid') ||
errorMsg.contains('wrong')) {
setState(() {
_currentPasswordHasError = true;
_currentPasswordErrorText = 'incorrect password';
_currentPasswordController.clear();
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to change password: $e')),
);
}
}
} finally {
setState(() => _isLoading = false);
}
@@ -784,7 +849,9 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
color: AppTheme.primaryBackground.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.3),
color: _currentPasswordHasError
? Colors.red
: AppTheme.accentColor.withValues(alpha: 0.3),
),
),
child: TextFormField(
@@ -792,6 +859,15 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
obscureText: true,
cursorColor: AppTheme.accentColor,
style: TextStyle(color: AppTheme.primaryText),
onChanged: (_) {
if (_currentPasswordHasError ||
_currentPasswordErrorText != null) {
setState(() {
_currentPasswordHasError = false;
_currentPasswordErrorText = null;
});
}
},
decoration: InputDecoration(
hintText: 'Enter current password',
hintStyle: TextStyle(color: AppTheme.secondaryText),
@@ -800,6 +876,14 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
),
),
),
if (_currentPasswordErrorText != null)
Padding(
padding: const EdgeInsets.only(top: 6, left: 8),
child: Text(
_currentPasswordErrorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
const SizedBox(height: 16),
// New Password
@@ -816,7 +900,9 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
color: AppTheme.primaryBackground.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.3),
color: _newPasswordHasError
? Colors.red
: AppTheme.accentColor.withValues(alpha: 0.3),
),
),
child: TextFormField(
@@ -824,6 +910,14 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
obscureText: true,
cursorColor: AppTheme.accentColor,
style: TextStyle(color: AppTheme.primaryText),
onChanged: (_) {
if (_newPasswordHasError || _newPasswordErrorText != null) {
setState(() {
_newPasswordHasError = false;
_newPasswordErrorText = null;
});
}
},
decoration: InputDecoration(
hintText: 'Enter new password',
hintStyle: TextStyle(color: AppTheme.secondaryText),
@@ -832,6 +926,14 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
),
),
),
if (_newPasswordErrorText != null)
Padding(
padding: const EdgeInsets.only(top: 6, left: 8),
child: Text(
_newPasswordErrorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
const SizedBox(height: 16),
// Confirm Password
@@ -848,7 +950,9 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
color: AppTheme.primaryBackground.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.3),
color: _confirmPasswordHasError
? Colors.red
: AppTheme.accentColor.withValues(alpha: 0.3),
),
),
child: TextFormField(
@@ -856,6 +960,15 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
obscureText: true,
cursorColor: AppTheme.accentColor,
style: TextStyle(color: AppTheme.primaryText),
onChanged: (_) {
if (_confirmPasswordHasError ||
_confirmPasswordErrorText != null) {
setState(() {
_confirmPasswordHasError = false;
_confirmPasswordErrorText = null;
});
}
},
decoration: InputDecoration(
hintText: 'Confirm new password',
hintStyle: TextStyle(color: AppTheme.secondaryText),
@@ -864,6 +977,14 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
),
),
),
if (_confirmPasswordErrorText != null)
Padding(
padding: const EdgeInsets.only(top: 6, left: 8),
child: Text(
_confirmPasswordErrorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
const SizedBox(height: 24),
// Change Password Button

View File

@@ -0,0 +1,284 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class AddFeaturesDialog extends StatelessWidget {
const AddFeaturesDialog({super.key});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: AppTheme.primaryBackground,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
width: 500,
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Icon(
Icons.add_circle_outline,
color: AppTheme.accentColor,
size: 28,
),
const SizedBox(width: 12),
Text(
'Add Features',
style: TextStyle(
color: AppTheme.primaryText,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.close, color: AppTheme.secondaryText),
splashRadius: 20,
),
],
),
const SizedBox(height: 8),
Text(
'Enhance your workspace with additional features',
style: TextStyle(color: AppTheme.secondaryText, fontSize: 14),
),
const SizedBox(height: 24),
// Features list
_FeatureCard(
icon: Icons.calendar_month,
title: 'Calendar',
description:
'Schedule events, set reminders, and manage your time efficiently. Sync with your team and never miss important deadlines.',
onAdd: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Calendar feature coming soon!'),
),
);
},
),
const SizedBox(height: 16),
_FeatureCard(
icon: Icons.dashboard_customize,
title: 'Board',
description:
'Organize tasks with kanban-style boards. Create columns, drag cards, and track progress visually across your projects.',
onAdd: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Board feature coming soon!')),
);
},
),
const SizedBox(height: 24),
// Footer hint
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.accentColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.3),
),
),
child: Row(
children: [
Icon(
Icons.info_outline,
color: AppTheme.accentColor,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'More features will be available soon. Stay tuned!',
style: TextStyle(
color: AppTheme.accentColor,
fontSize: 13,
),
),
),
],
),
),
],
),
),
);
}
}
class _FeatureCard extends StatefulWidget {
final IconData icon;
final String title;
final String description;
final VoidCallback onAdd;
const _FeatureCard({
required this.icon,
required this.title,
required this.description,
required this.onAdd,
});
@override
State<_FeatureCard> createState() => _FeatureCardState();
}
class _FeatureCardState extends State<_FeatureCard> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _isHovered
? Colors.white.withValues(alpha: 0.08)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _isHovered
? AppTheme.accentColor.withValues(alpha: 0.5)
: Colors.white.withValues(alpha: 0.1),
),
),
child: Row(
children: [
// Feature icon with soon badge
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.accentColor.withValues(alpha: 0.3),
AppTheme.accentColor.withValues(alpha: 0.1),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
widget.icon,
color: AppTheme.accentColor,
size: 28,
),
),
Positioned(
top: -6,
right: -8,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 2,
),
decoration: BoxDecoration(
color: AppTheme.accentColor,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'soon',
style: TextStyle(
color: Colors.black,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
const SizedBox(width: 16),
// Feature details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: TextStyle(
color: AppTheme.primaryText,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
widget.description,
style: TextStyle(
color: AppTheme.secondaryText,
fontSize: 13,
height: 1.4,
),
),
],
),
),
const SizedBox(width: 12),
// Add button
_AddButton(onPressed: widget.onAdd),
],
),
),
);
}
}
class _AddButton extends StatefulWidget {
final VoidCallback onPressed;
const _AddButton({required this.onPressed});
@override
State<_AddButton> createState() => _AddButtonState();
}
class _AddButtonState extends State<_AddButton> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 40,
height: 40,
decoration: BoxDecoration(
color: _isHovered
? AppTheme.accentColor
: AppTheme.accentColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.5),
),
),
child: Icon(
Icons.add,
color: _isHovered ? Colors.black : AppTheme.accentColor,
size: 24,
),
),
),
);
}
}

View File

@@ -69,10 +69,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
clock:
dependency: transitive
description:
@@ -205,10 +205,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.2.0"
file:
dependency: transitive
description:
@@ -535,10 +535,10 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@@ -988,10 +988,10 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.3.6"
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
@@ -1092,10 +1092,10 @@ packages:
dependency: transitive
description:
name: video_player_avfoundation
sha256: f46e9e20f1fe429760cf4dc118761336320d1bec0f50d255930c2355f2defb5b
sha256: f93b93a3baa12ca0ff7d00ca8bc60c1ecd96865568a01ff0c18a99853ee201a5
url: "https://pub.dev"
source: hosted
version: "2.9.1"
version: "2.9.3"
video_player_platform_interface:
dependency: transitive
description: