diff --git a/lib/core/utils/display_size_detector.dart b/lib/core/utils/display_size_detector.dart new file mode 100644 index 0000000..abf1087 --- /dev/null +++ b/lib/core/utils/display_size_detector.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +enum DisplaySize { + extraSmall, //mobile + small, //tablet + medium, //laptop + large, //desktop +} + +extension DisplaySizeDetector on Size { + DisplaySize get displaySize { + if (width < 600) { + return DisplaySize.extraSmall; + } else if (width < 1024) { + return DisplaySize.small; + } else if (width < 1440) { + return DisplaySize.medium; + } else { + return DisplaySize.large; + } + } + + String get displaySizeName { + switch (displaySize) { + case DisplaySize.extraSmall: + return 'Mobile'; + case DisplaySize.small: + return 'Tablet'; + case DisplaySize.medium: + return 'Laptop'; + case DisplaySize.large: + return 'Desktop'; + } + } +} diff --git a/lib/view_models/login_view_model.dart b/lib/view_models/login_view_model.dart new file mode 100644 index 0000000..340539d --- /dev/null +++ b/lib/view_models/login_view_model.dart @@ -0,0 +1,42 @@ +// lib/view_models/login_view_model.dart +import 'package:flutter/material.dart'; + +class LoginViewModel with ChangeNotifier { + final TextEditingController usernameController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + + final FocusNode usernameFocus = FocusNode(); + final FocusNode passwordFocus = FocusNode(); + + bool _obscurePassword = true; + + LoginViewModel() { + usernameController.addListener(_onTextChanged); + passwordController.addListener(_onTextChanged); + usernameFocus.addListener(_onTextChanged); + passwordFocus.addListener(_onTextChanged); + } + + bool get obscurePassword => _obscurePassword; + bool get isLoginEnabled => + usernameController.text.isNotEmpty && + passwordController.text.isNotEmpty; + + void togglePasswordVisibility() { + _obscurePassword = !_obscurePassword; + notifyListeners(); + } + + void _onTextChanged() { + notifyListeners(); + } + + @override + void dispose() { + usernameController.dispose(); + passwordController.dispose(); + usernameFocus.dispose(); + passwordFocus.dispose(); + super.dispose(); + } +} diff --git a/lib/views/login/login_desktop_screen.dart b/lib/views/login/login_desktop_screen.dart new file mode 100644 index 0000000..83c1167 --- /dev/null +++ b/lib/views/login/login_desktop_screen.dart @@ -0,0 +1,94 @@ +// lib/views/login_desktop_screen.dart +import 'package:flutter/material.dart'; +import 'package:tm_app/core/routes/app_routes.dart'; + +import '../../core/constants/assets.dart'; +import '../../core/constants/strings.dart'; +import '../../core/utils/size_config.dart'; +import 'widgets/widgets.dart'; +import '../../view_models/login_view_model.dart'; + +class LoginDesktopScreen extends StatefulWidget { + const LoginDesktopScreen({super.key}); + + @override + State createState() => _LoginDesktopScreenState(); +} + +class _LoginDesktopScreenState extends State { + late final LoginViewModel _viewModel; + + @override + void initState() { + super.initState(); + _viewModel = LoginViewModel(); + } + + @override + void dispose() { + _viewModel.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + SizeConfig.init(context); + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.0.w()), + child: SizedBox( + width: 120.0.w(), + child: ListenableBuilder( + listenable: _viewModel, + builder: (context, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + LoginLogo(width: 190.0.w(), height: 88.0.h()), + SizedBox(height: 32.0.h()), + const LoginTitle(), + SizedBox(height: 32.0.h()), + + LoginTextField( + controller: _viewModel.usernameController, + focusNode: _viewModel.usernameFocus, + label: AppStrings.usernameLabel, + iconPath: AssetsManager.userIcon, + fieldHeight: 60.0.h(), + borderRedus: 4.0.w(), + prefixIconPadding: 4.0.w(), + ), + SizedBox(height: 16.0.h()), + + LoginTextField( + controller: _viewModel.passwordController, + focusNode: _viewModel.passwordFocus, + label: AppStrings.passwordLabel, + iconPath: AssetsManager.passwordIcon, + prefixIconPadding: 4.0.w(), + isPassword: true, + obscureText: _viewModel.obscurePassword, + fieldHeight: 60.0.h(), + onToggleVisibility: _viewModel.togglePasswordVisibility, + borderRedus: 4.0.w(), + ), + SizedBox(height: 32.0.h()), + + BaseButton( + isEnabled: _viewModel.isLoginEnabled, + onPressed: _viewModel.isLoginEnabled + ? () => HomeRouteData().go(context) + : null, + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/login/login_mobile_screen.dart b/lib/views/login/login_mobile_screen.dart new file mode 100644 index 0000000..0515193 --- /dev/null +++ b/lib/views/login/login_mobile_screen.dart @@ -0,0 +1,88 @@ +// lib/views/login_screen.dart +import 'package:flutter/material.dart'; +import 'package:tm_app/core/routes/app_routes.dart'; + +import '../../core/constants/assets.dart'; +import '../../core/constants/strings.dart'; +import '../../core/utils/size_config.dart'; +import 'widgets/widgets.dart'; +import '../../view_models/login_view_model.dart'; + +class LoginMobileScreen extends StatefulWidget { + const LoginMobileScreen({super.key}); + + @override + State createState() => _LoginMobileScreenState(); +} + +class _LoginMobileScreenState extends State { + late final LoginViewModel _viewModel; + + @override + void initState() { + super.initState(); + _viewModel = LoginViewModel(); + } + + @override + void dispose() { + _viewModel.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + SizeConfig.init(context); + + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.0.w()), + child: ListenableBuilder( + listenable: _viewModel, + builder: (context, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + LoginLogo(width: 190.0.w(), height: 88.0.h()), + SizedBox(height: 32.0.h()), + const LoginTitle(), + SizedBox(height: 32.0.h()), + + LoginTextField( + controller: _viewModel.usernameController, + focusNode: _viewModel.usernameFocus, + label: AppStrings.usernameLabel, + iconPath: AssetsManager.userIcon, + ), + SizedBox(height: 16.0.h()), + + LoginTextField( + controller: _viewModel.passwordController, + focusNode: _viewModel.passwordFocus, + label: AppStrings.passwordLabel, + iconPath: AssetsManager.passwordIcon, + isPassword: true, + obscureText: _viewModel.obscurePassword, + onToggleVisibility: _viewModel.togglePasswordVisibility, + ), + SizedBox(height: 32.0.h()), + + BaseButton( + isEnabled: _viewModel.isLoginEnabled, + onPressed: _viewModel.isLoginEnabled + ? () { + HomeRouteData().go(context); + } + : null, + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/views/login/login_screen.dart b/lib/views/login/login_screen.dart index ec2f2d9..3846846 100644 --- a/lib/views/login/login_screen.dart +++ b/lib/views/login/login_screen.dart @@ -1,103 +1,24 @@ -// lib/views/login_screen.dart import 'package:flutter/material.dart'; -import 'package:tm_app/core/routes/app_routes.dart'; +import 'package:tm_app/core/utils/display_size_detector.dart'; +import 'package:tm_app/views/login/login_desktop_screen.dart'; +import 'package:tm_app/views/login/login_mobile_screen.dart'; +import 'package:tm_app/views/login/login_tablet_screen.dart'; -import '../../core/constants/assets.dart'; -import '../../core/constants/strings.dart'; -import '../../core/utils/size_config.dart'; -import 'widgets/widgets.dart'; - -class LoginScreen extends StatefulWidget { +class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { - final _usernameController = TextEditingController(); - final _passwordController = TextEditingController(); - - final _usernameFocus = FocusNode(); - final _passwordFocus = FocusNode(); - - bool _obscurePassword = true; - - bool get _isLoginEnabled => - _usernameController.text.isNotEmpty && - _passwordController.text.isNotEmpty; - - @override - void initState() { - super.initState(); - _usernameController.addListener(_updateState); - _passwordController.addListener(_updateState); - _usernameFocus.addListener(_updateState); - _passwordFocus.addListener(_updateState); - } - - @override - void dispose() { - _usernameController.dispose(); - _passwordController.dispose(); - _usernameFocus.dispose(); - _passwordFocus.dispose(); - super.dispose(); - } - - void _updateState() => setState(() {}); - - void _togglePasswordVisibility() => - setState(() => _obscurePassword = !_obscurePassword); - @override Widget build(BuildContext context) { - SizeConfig.init(context); - return Scaffold( - backgroundColor: Colors.white, - body: Center( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 24.0.w()), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const LoginLogo(), - SizedBox(height: 32.0.h()), - const LoginTitle(), - SizedBox(height: 32.0.h()), - LoginTextField( - controller: _usernameController, - focusNode: _usernameFocus, - label: AppStrings.usernameLabel, - iconPath: AssetsManager.userIcon, - ), + final displaySize = MediaQuery.of(context).size.displaySize; - SizedBox(height: 16.0.h()), - - LoginTextField( - controller: _passwordController, - focusNode: _passwordFocus, - label: AppStrings.passwordLabel, - iconPath: AssetsManager.passwordIcon, - isPassword: true, - obscureText: _obscurePassword, - onToggleVisibility: _togglePasswordVisibility, - ), - - SizedBox(height: 32.0.h()), - BaseButton( - isEnabled: _isLoginEnabled, - onPressed: - _isLoginEnabled - ? () { - HomeRouteData().go(context); - } - : null, - ), - ], - ), - ), - ), - ); + switch (displaySize) { + case DisplaySize.large: + case DisplaySize.medium: + return const LoginDesktopScreen(); + case DisplaySize.small: + return const LoginTabletScreen(); + case DisplaySize.extraSmall: + return const LoginMobileScreen(); + } } } diff --git a/lib/views/login/login_tablet_screen.dart b/lib/views/login/login_tablet_screen.dart new file mode 100644 index 0000000..96bf8e9 --- /dev/null +++ b/lib/views/login/login_tablet_screen.dart @@ -0,0 +1,94 @@ +// lib/views/login_tablet_screen.dart +import 'package:flutter/material.dart'; +import 'package:tm_app/core/routes/app_routes.dart'; + +import '../../core/constants/assets.dart'; +import '../../core/constants/strings.dart'; +import '../../core/utils/size_config.dart'; +import 'widgets/widgets.dart'; +import '../../view_models/login_view_model.dart'; + +class LoginTabletScreen extends StatefulWidget { + const LoginTabletScreen({super.key}); + + @override + State createState() => _LoginTabletScreenState(); +} + +class _LoginTabletScreenState extends State { + late final LoginViewModel _viewModel; + + @override + void initState() { + super.initState(); + _viewModel = LoginViewModel(); + } + + @override + void dispose() { + _viewModel.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + SizeConfig.init(context); + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.0.w()), + child: SizedBox( + width: 120.0.w(), + child: ListenableBuilder( + listenable: _viewModel, + builder: (context, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + LoginLogo(width: 190.0.w(), height: 88.0.h()), + SizedBox(height: 32.0.h()), + const LoginTitle(), + SizedBox(height: 32.0.h()), + + LoginTextField( + controller: _viewModel.usernameController, + focusNode: _viewModel.usernameFocus, + label: AppStrings.usernameLabel, + iconPath: AssetsManager.userIcon, + fieldHeight: 60.0.h(), + borderRedus: 4.0.w(), + prefixIconPadding: 4.0.w(), + ), + SizedBox(height: 16.0.h()), + + LoginTextField( + controller: _viewModel.passwordController, + focusNode: _viewModel.passwordFocus, + label: AppStrings.passwordLabel, + iconPath: AssetsManager.passwordIcon, + prefixIconPadding: 4.0.w(), + isPassword: true, + obscureText: _viewModel.obscurePassword, + fieldHeight: 60.0.h(), + onToggleVisibility: _viewModel.togglePasswordVisibility, + borderRedus: 4.0.w(), + ), + SizedBox(height: 32.0.h()), + + BaseButton( + isEnabled: _viewModel.isLoginEnabled, + onPressed: _viewModel.isLoginEnabled + ? () => HomeRouteData().go(context) + : null, + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/login/widgets/login_logo.dart b/lib/views/login/widgets/login_logo.dart index bfeecd5..e6fcfb4 100644 --- a/lib/views/login/widgets/login_logo.dart +++ b/lib/views/login/widgets/login_logo.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import '../../../core/constants/assets.dart'; -import '../../../core/utils/size_config.dart'; - +// ignore: must_be_immutable class LoginLogo extends StatelessWidget { - const LoginLogo({super.key}); + LoginLogo({required this.width,required this.height,super.key}); + + double width; + double height; @override Widget build(BuildContext context) { - return Image.asset(AssetsManager.logo, width: 190.0.w(), height: 88.0.h()); + return Image.asset(AssetsManager.logo, ); } } diff --git a/lib/views/login/widgets/login_textfield.dart b/lib/views/login/widgets/login_textfield.dart index a1ce398..a98561a 100644 --- a/lib/views/login/widgets/login_textfield.dart +++ b/lib/views/login/widgets/login_textfield.dart @@ -12,6 +12,9 @@ class LoginTextField extends StatelessWidget { final String iconPath; final bool isPassword; final bool obscureText; + final double? borderRedus; + final double? fieldHeight; + final double? prefixIconPadding; final VoidCallback? onToggleVisibility; const LoginTextField({ @@ -22,52 +25,62 @@ class LoginTextField extends StatelessWidget { super.key, this.isPassword = false, this.obscureText = false, + this.fieldHeight, this.onToggleVisibility, + this.borderRedus, + this.prefixIconPadding, }); @override Widget build(BuildContext context) { - return TextField( - controller: controller, - focusNode: focusNode, - obscureText: isPassword ? obscureText : false, - decoration: InputDecoration( - labelText: label, - prefixIcon: Padding( - padding: EdgeInsets.all(12.0.w()), - child: SvgPicture.asset( - iconPath, - width: 24.0.w(), - height: 24.0.h(), - colorFilter: ColorFilter.mode( - focusNode.hasFocus ? Colors.black87 : Colors.grey, - BlendMode.srcIn, + return SizedBox( + height: fieldHeight ?? 55.0.h(), + child: TextField( + controller: controller, + focusNode: focusNode, + obscureText: isPassword ? obscureText : false, + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: (fieldHeight != null ? (fieldHeight! / 2.8) : 16.0).h(), + horizontal: 12.0.w(), + ), + labelText: label, + prefixIcon: Padding( + padding: EdgeInsets.all(prefixIconPadding ?? 12.0.w()), + child: SvgPicture.asset( + iconPath, + width: 24.0.w(), + height: 24.0.h(), + colorFilter: ColorFilter.mode( + focusNode.hasFocus ? Colors.black87 : Colors.grey, + BlendMode.srcIn, + ), ), ), - ), - suffixIcon: - isPassword - ? IconButton( - onPressed: onToggleVisibility, - icon: SvgPicture.asset( - obscureText - ? AssetsManager.passwordVisibility - : AssetsManager.passwordVisibilityOff, - width: 24.0.w(), - height: 24.0.h(), - colorFilter: ColorFilter.mode( - focusNode.hasFocus ? Colors.black87 : Colors.grey, - BlendMode.srcIn, + suffixIcon: + isPassword + ? IconButton( + onPressed: onToggleVisibility, + icon: SvgPicture.asset( + obscureText + ? AssetsManager.passwordVisibility + : AssetsManager.passwordVisibilityOff, + width: 24.0.w(), + height: 24.0.h(), + colorFilter: ColorFilter.mode( + focusNode.hasFocus ? Colors.black87 : Colors.grey, + BlendMode.srcIn, + ), ), - ), - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.0.w()), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: AppColors.borderBlue), - borderRadius: BorderRadius.circular(12.0.w()), + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRedus ?? 12.0.w()), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: AppColors.borderBlue), + borderRadius: BorderRadius.circular(borderRedus ?? 12.0.w()), + ), ), ), );