diff --git a/mobile/.gitignore b/mobile/.gitignore index 205aaff..dcde787 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -46,3 +46,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# unnecessary builds +/linux +/macos diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7d8c399..ab0fb74 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -7,7 +7,7 @@ import 'core/constants.dart'; import 'controllers/auth.dart'; import 'controllers/chat.dart'; import 'services/auth.dart'; -import 'pages/login.dart'; +import 'pages/login_page.dart'; import 'pages/home_page.dart'; void main() async { diff --git a/mobile/lib/pages/login.dart b/mobile/lib/pages/login.dart deleted file mode 100644 index 2679b22..0000000 --- a/mobile/lib/pages/login.dart +++ /dev/null @@ -1,638 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../core/constants.dart'; -import '../controllers/auth.dart'; - -class LoginPage extends StatefulWidget { - const LoginPage({super.key}); - - @override - State createState() => _LoginPageState(); -} - -class _LoginPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - final _formKey = GlobalKey(); - - final _usernameController = TextEditingController(); - final _displayNameController = TextEditingController(); - final _passwordController = TextEditingController(); - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - _usernameController.dispose(); - _displayNameController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - void _openServerConfigDialog() { - final hostController = TextEditingController(text: Env.host); - final portController = TextEditingController(text: Env.port); - - showDialog( - context: context, - builder: (dialogContext) => BackdropFilter( - filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: AlertDialog( - backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), - title: Row( - children: [ - Icon(Icons.dns, color: Theme.of(context).colorScheme.primary), - SizedBox(width: 8), - Text( - "Server Settings", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Enter your decentralized peer node coordinates:", - style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - Text( - "Host", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 6), - TextField( - controller: hostController, - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), - decoration: InputDecoration( - hintText: Env.defaultHost, - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - ), - ), - const SizedBox(height: 14), - Text( - "Port", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 6), - TextField( - controller: portController, - keyboardType: TextInputType.number, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), - decoration: InputDecoration( - hintText: Env.defaultPort, - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () async { - final navigator = Navigator.of(dialogContext); - final scaffoldMessenger = ScaffoldMessenger.of(context); - - await Env.updateConfig(Env.defaultHost, Env.defaultPort); - - navigator.pop(); - setState(() {}); - scaffoldMessenger.showSnackBar( - const SnackBar( - content: Text( - "Reset connection to elephant.commandlinecoding.in", - ), - ), - ); - }, - child: Text( - "Reset Default", - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - ), - onPressed: () async { - final newHost = hostController.text.trim(); - final newPort = portController.text.trim(); - - final navigator = Navigator.of(dialogContext); - final scaffoldMessenger = ScaffoldMessenger.of(context); - - await Env.updateConfig(newHost, newPort); - - navigator.pop(); - setState(() {}); - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text("Target base set to: ${Env.httpBaseUrl}"), - ), - ); - }, - child: const Text("Save", style: TextStyle(color: Colors.white)), - ), - ], - ), - ), - ); - } - - void _submit() { - if (_formKey.currentState?.validate() ?? false) { - final auth = context.read(); - if (_tabController.index == 0) { - auth.handleLogin( - _usernameController.text.trim(), - _passwordController.text.trim(), - ); - } else { - auth.handleRegister( - _usernameController.text.trim(), - _displayNameController.text.trim(), - _passwordController.text.trim(), - ); - } - } - } - - Widget _buildSocialButton({required String label, required IconData icon}) { - return Expanded( - child: OutlinedButton.icon( - onPressed: () {}, - icon: Icon(icon, color: Theme.of(context).colorScheme.onSurface, size: 18), - label: Text( - label, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 13, - fontWeight: FontWeight.w600, - ), - ), - style: OutlinedButton.styleFrom( - side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final authState = context.watch(); - - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), - child: Container( - constraints: const BoxConstraints(maxWidth: 420), - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.03), - blurRadius: 16, - offset: const Offset(0, 8), - ), - ], - ), - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.chat_bubble_rounded, - color: Color(0xFF0052CC), - size: 28, - ), - const SizedBox(width: 8), - Text( - 'Elephant', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - letterSpacing: 0.5, - ), - ), - ], - ), - const SizedBox(height: 24), - - Text( - _tabController.index == 0 - ? 'Welcome Back' - : 'Create Account', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 6), - Text( - _tabController.index == 0 - ? 'Access your secure workspace' - : 'Join the peer mesh platform', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 28), - - Container( - height: 46, - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - ), - child: TabBar( - controller: _tabController, - onTap: (index) => setState(() {}), - indicatorSize: TabBarIndicatorSize.tab, - dividerColor: Colors.transparent, - labelColor: Theme.of(context).colorScheme.primary, - unselectedLabelColor: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - indicator: BoxDecoration(color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(6), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - tabs: const [ - Tab( - child: Text( - 'Login', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - Tab( - child: Text( - 'Register', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 24), - - if (authState.errorMessage != null) ...[ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.errorContainer, - borderRadius: BorderRadius.circular(6), - border: Border.all( - color: Theme.of(context).colorScheme.error, - ), - ), - child: Text( - authState.errorMessage!, - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - fontSize: 13, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 20), - ], - - const Text( - "Username", - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF172B4D), - ), - ), - const SizedBox(height: 6), - TextFormField( - controller: _usernameController, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), - decoration: InputDecoration( - hintText: "username", - hintStyle: const TextStyle(color: Colors.black38), - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - ), - validator: (val) => (val == null || val.trim().isEmpty) - ? 'Required field' - : null, - ), - const SizedBox(height: 16), - - if (_tabController.index == 1) ...[ - const Text( - "Display Name", - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF172B4D), - ), - ), - const SizedBox(height: 6), - TextFormField( - controller: _displayNameController, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 14, - ), - decoration: InputDecoration( - hintText: "John Doe", - hintStyle: const TextStyle(color: Colors.black38), - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - ), - validator: (val) => (val == null || val.trim().isEmpty) - ? 'Required field' - : null, - ), - const SizedBox(height: 16), - ], - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Password", - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF172B4D), - ), - ), - if (_tabController.index == 0) - TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - ), - child: const Text( - "Forgot Password?", - style: TextStyle( - color: Color(0xFF0052CC), - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - const SizedBox(height: 6), - TextFormField( - controller: _passwordController, - obscureText: true, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), - decoration: InputDecoration( - hintText: "••••••••", - hintStyle: const TextStyle(color: Colors.black38), - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - ), - validator: (val) => (val == null || val.trim().length < 6) - ? 'Must be at least 6 characters' - : null, - ), - const SizedBox(height: 24), - - ElevatedButton( - onPressed: authState.isLoading ? null : _submit, - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - elevation: 0, - ), - child: authState.isLoading - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.onPrimary, - strokeWidth: 2.5, - ), - ) - : Text( - _tabController.index == 0 ? 'Sign In' : 'Sign Up', - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 24), - - Row( - children: [ - Expanded(child: Divider( - color: Theme.of(context).colorScheme.outlineVariant, - )), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - "or sign in with", - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontSize: 12, - ), - ), - ), - Expanded(child: Divider( - color: Theme.of(context).colorScheme.outlineVariant, - )), - ], - ), - const SizedBox(height: 20), - - Row( - children: [ - _buildSocialButton( - label: "Google", - icon: Icons.g_mobiledata, - ), - const SizedBox(width: 12), - _buildSocialButton(label: "GitHub", icon: Icons.code), - ], - ), - const SizedBox(height: 28), - - Center( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Theme.of(context).colorScheme.tertiary, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.check_circle, - color: Theme.of(context).colorScheme.onTertiaryContainer, - size: 14, - ), - const SizedBox(width: 6), - Text( - "End-to-end encrypted session", - style: TextStyle( - color: Theme.of(context).colorScheme.onTertiaryContainer, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - const SizedBox(height: 20), - - Center( - child: TextButton.icon( - onPressed: _openServerConfigDialog, - icon: Icon( - Icons.dns_outlined, - size: 14, - color: Theme.of(context).colorScheme.primary, - ), - label: Text( - "Connected to: ${Env.host}:${Env.port} ⚙️", - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/login_page.dart b/mobile/lib/pages/login_page.dart new file mode 100644 index 0000000..f9620bb --- /dev/null +++ b/mobile/lib/pages/login_page.dart @@ -0,0 +1,1094 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:mobile/pages/settings%20pages/appearance.dart'; +import 'package:provider/provider.dart'; +import '../core/constants.dart'; +import '../controllers/auth.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final _formKey = GlobalKey(); + + final _usernameController = TextEditingController(); + final _displayNameController = TextEditingController(); + final _passwordController = TextEditingController(); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + _usernameController.dispose(); + _displayNameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + void _openServerConfigDialog() { + final hostController = TextEditingController(text: Env.host); + final portController = TextEditingController(text: Env.port); + final colorScheme = Theme.of(context).colorScheme; + + showDialog( + context: context, + builder: (dialogContext) => BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: AlertDialog( + scrollable: true, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.8), + title: Row( + children: [ + Icon(Icons.dns, color: colorScheme.primary), + SizedBox(width: 8), + Text( + "Server Settings", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter your decentralized peer node coordinates:", + style: TextStyle( + fontSize: 13, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + Text( + "Host", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 6), + TextField( + controller: hostController, + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: 14, + ), + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: colorScheme.primary, + width: 1.5, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + hintText: Env.defaultHost, + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 14), + Text( + "Port", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 6), + TextField( + controller: portController, + keyboardType: TextInputType.number, + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: 14, + ), + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: colorScheme.primary, + width: 1.5, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + hintText: Env.defaultPort, + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () async { + final navigator = Navigator.of(dialogContext); + final scaffoldMessenger = ScaffoldMessenger.of(context); + + await Env.updateConfig(Env.defaultHost, Env.defaultPort); + + navigator.pop(); + setState(() {}); + scaffoldMessenger.showSnackBar( + const SnackBar( + content: Text( + "Reset connection to elephant.commandlinecoding.in", + ), + ), + ); + }, + child: Text( + "Reset Default", + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + ), + onPressed: () async { + final newHost = hostController.text.trim(); + final newPort = portController.text.trim(); + + final navigator = Navigator.of(dialogContext); + final scaffoldMessenger = ScaffoldMessenger.of(context); + + await Env.updateConfig(newHost, newPort); + + navigator.pop(); + setState(() {}); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Target base set to: ${Env.httpBaseUrl}"), + ), + ); + }, + child: const Text("Save", style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + ); + } + + void _submit() { + if (_formKey.currentState?.validate() ?? false) { + final auth = context.read(); + if (_tabController.index == 0) { + auth.handleLogin( + _usernameController.text.trim(), + _passwordController.text.trim(), + ); + } else { + auth.handleRegister( + _usernameController.text.trim(), + _displayNameController.text.trim(), + _passwordController.text.trim(), + ); + } + } + } + + Widget _buildSocialButton({required String label, required IconData icon}) { + final colorScheme = Theme.of(context).colorScheme; + return Expanded( + child: OutlinedButton.icon( + onPressed: () {}, + icon: Icon(icon, color: colorScheme.onSurface, size: 18), + label: Text( + label, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + style: OutlinedButton.styleFrom( + side: BorderSide(color: colorScheme.outlineVariant), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final authState = context.watch(); + final colorScheme = Theme.of(context).colorScheme; + final theme = Theme.of(context); + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + body: Stack( + children: [ + Positioned.fill( + child: Stack( + children: [ + Positioned( + top: -150, + left: -150, + child: + Container( + width: 500, + height: 500, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + colorScheme.primary.withValues(alpha: 0.45), + colorScheme.primary.withValues(alpha: 0.0), + ], + ), + ), + ) + .animate( + onPlay: (controller) => + controller.repeat(reverse: true), + ) + .move( + duration: 6.seconds, + curve: Curves.easeInOutSine, + end: const Offset(300, 250), + ) + .scale( + duration: 4.seconds, + curve: Curves.easeInOutSine, + end: const Offset(1.4, 1.4), + ), + ), + + Positioned( + bottom: -200, + right: -100, + child: + Container( + width: 600, + height: 600, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + colorScheme.tertiary.withValues(alpha: 0.35), + colorScheme.tertiary.withValues(alpha: 0.0), + ], + ), + ), + ) + .animate( + onPlay: (controller) => + controller.repeat(reverse: true), + ) + .move( + duration: 8.seconds, + curve: Curves.easeInOutSine, + end: const Offset(-400, -300), + ) + .scale( + duration: 6.seconds, + curve: Curves.easeInOutSine, + end: const Offset(1.2, 0.8), + ), + ), + + Positioned( + top: MediaQuery.of(context).size.height * 0.2, + left: MediaQuery.of(context).size.width * 0.1, + child: + Container( + width: 400, + height: 400, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + colorScheme.secondary.withValues(alpha: 0.30), + colorScheme.secondary.withValues(alpha: 0.0), + ], + ), + ), + ) + .animate( + onPlay: (controller) => + controller.repeat(reverse: true), + ) + .moveX( + duration: 7.seconds, + curve: Curves.easeInOutSine, + end: MediaQuery.of(context).size.width * 0.5, + ) + .moveY( + duration: 9.seconds, + curve: Curves.easeInOutSine, + end: 300, + ) + .scale( + duration: 5.seconds, + curve: Curves.easeInOutSine, + end: const Offset(1.6, 1.6), + ), + ), + + ], + ), + ), + + Positioned.fill( + child: RepaintBoundary( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 40, + ), + child: ClipRRect( + borderRadius: BorderRadiusGeometry.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + theme.colorScheme.surface.withValues(alpha: 0.25), + theme.colorScheme.surface.withValues(alpha: 0.05), + ], + ), + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: theme.colorScheme.onSurface.withValues( + alpha: 0.20, + ), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.shadow.withValues( + alpha: 0.15, + ), + blurRadius: 40, + spreadRadius: -5, + ), + ], + ), + constraints: const BoxConstraints(maxWidth: 420), + padding: const EdgeInsets.all(32), + + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.chat_bubble_rounded, + color: Color(0xFF0052CC), + size: 28, + ), + const SizedBox(width: 8), + Text( + 'Elephant', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of( + context, + ).colorScheme.primary, + letterSpacing: 0.5, + ), + ), + ], + ) + .animate( + onPlay: (controller) => + controller.repeat(period: 4.seconds), + ) + .shimmer( + duration: 1.5.seconds, + color: theme.colorScheme.onSurface + .withValues(alpha: 0.5), + ), + const SizedBox(height: 24), + + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + switchInCurve: Curves.easeOutCirc, + switchOutCurve: Curves.easeInCirc, + transitionBuilder: + ( + Widget child, + Animation animation, + ) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.0, 0.2), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ); + }, + child: Column( + key: ValueKey(_tabController.index), + children: [ + Text( + _tabController.index == 0 + ? 'Welcome Back' + : 'Create Account', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Theme.of( + context, + ).colorScheme.onSurface, + ), + ), + const SizedBox(height: 6), + Text( + _tabController.index == 0 + ? 'Access your secure workspace' + : 'Join the peer mesh platform', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(height: 28), + + Container( + height: 52, + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(26), + border: Border.all( + color: theme.colorScheme.outlineVariant + .withValues(alpha: 0.3), + ), + ), + child: Stack( + children: [ + AnimatedAlign( + duration: const Duration( + milliseconds: 350, + ), + curve: Curves.fastOutSlowIn, + alignment: _tabController.index == 0 + ? Alignment.centerLeft + : Alignment.centerRight, + child: FractionallySizedBox( + widthFactor: 0.5, + child: Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 22, + ), + color: theme.colorScheme.primary + .withValues(alpha: 0.2), + border: Border.all( + color: theme.colorScheme.primary + .withValues(alpha: 0.5), + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.primary + .withValues(alpha: 0.4), + blurRadius: 12, + ), + ], + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState( + () => _tabController.index = 0, + ), + child: Center( + child: Text( + 'Sign In', + style: TextStyle( + fontWeight: FontWeight.bold, + color: + _tabController.index == 0 + ? Theme.of( + context, + ).colorScheme.primary + : theme + .colorScheme + .onSurfaceVariant, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState( + () => _tabController.index = 1, + ), + child: Center( + child: Text( + 'Register', + style: TextStyle( + fontWeight: FontWeight.bold, + color: + _tabController.index == 1 + ? Theme.of( + context, + ).colorScheme.primary + : theme + .colorScheme + .onSurfaceVariant, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + + if (authState.errorMessage != null) ...[ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.error, + ), + ), + child: Text( + authState.errorMessage!, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onErrorContainer, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 20), + ], + Text( + "Username", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + const SizedBox(height: 6), + TextFormField( + controller: _usernameController, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: colorScheme.primary, + width: 1.5, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + hintText: "username", + hintStyle: TextStyle(color: theme.hintColor), + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + validator: (val) => + (val == null || val.trim().isEmpty) + ? 'Required field' + : null, + ), + + const SizedBox(height: 16), + + AnimatedSize( + duration: const Duration(milliseconds: 500), + curve: Curves.fastLinearToSlowEaseIn, + child: _tabController.index == 1 + ? Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Display Name", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Theme.of( + context, + ).colorScheme.primary, + ), + ), + const SizedBox(height: 6), + TextFormField( + controller: + _displayNameController, + style: TextStyle( + color: theme + .colorScheme + .onSurfaceVariant, + fontSize: 14, + ), + decoration: InputDecoration( + focusedBorder: + OutlineInputBorder( + borderRadius: + BorderRadius.circular( + 8, + ), + borderSide: BorderSide( + color: colorScheme + .primary, + width: 1.5, + ), + ), + contentPadding: + const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + hintText: "John Doe", + hintStyle: TextStyle( + color: Theme.of( + context, + ).hintColor, + ), + filled: true, + fillColor: theme + .colorScheme + .surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular( + 8, + ), + borderSide: BorderSide.none, + ), + ), + validator: (val) => + (val == null || + val.trim().isEmpty) + ? 'Required field' + : null, + ), + const SizedBox(height: 16), + ], + ) + .animate() + .fade(duration: 400.ms, delay: 150.ms) + .slideX(begin: -0.05) + : const SizedBox.shrink(), + ), + + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Password", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Theme.of( + context, + ).colorScheme.primary, + ), + ), + if (_tabController.index == 0) + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + ), + child: const Text( + "Forgot Password?", + style: TextStyle( + color: Color(0xFF0052CC), + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 6), + TextFormField( + controller: _passwordController, + obscureText: true, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: colorScheme.primary, + width: 1.5, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + hintText: "••••••••", + hintStyle: TextStyle(color: theme.hintColor), + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + validator: (val) => + (val == null || val.trim().length < 6) + ? 'Must be at least 6 characters' + : null, + ), + const SizedBox(height: 24), + + ElevatedButton( + onPressed: authState.isLoading + ? null + : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.primary, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: authState.isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Theme.of( + context, + ).colorScheme.onPrimary, + strokeWidth: 2.5, + ), + ) + : AnimatedSwitcher( + duration: const Duration( + milliseconds: 300, + ), + transitionBuilder: + (child, animation) => + FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ), + child: Text( + _tabController.index == 0 + ? 'Sign In' + : 'Sign Up', + key: ValueKey( + _tabController.index, + ), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + ), + ) + .animate( + onPlay: (controller) => + controller.repeat(reverse: true), + ) + .scale( + begin: const Offset(1, 1), + end: const Offset(1.02, 1.02), + duration: 2.seconds, + curve: Curves.easeInOutSine, + ), + const SizedBox(height: 24), + + Row( + children: [ + Expanded( + child: Divider( + color: Theme.of( + context, + ).colorScheme.outlineVariant, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: Text( + "or sign in with", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ), + ), + Expanded( + child: Divider( + color: Theme.of( + context, + ).colorScheme.outlineVariant, + ), + ), + ], + ), + const SizedBox(height: 20), + + Row( + children: [ + _buildSocialButton( + label: "Google", + icon: Icons.g_mobiledata, + ), + const SizedBox(width: 12), + _buildSocialButton( + label: "GitHub", + icon: Icons.code, + ), + ], + ), + const SizedBox(height: 28), + + Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.tertiaryContainer, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.tertiary, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle, + color: Theme.of( + context, + ).colorScheme.onTertiaryContainer, + size: 14, + ), + const SizedBox(width: 6), + Text( + "End-to-end encrypted session", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onTertiaryContainer, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + + Center( + child: TextButton.icon( + onPressed: _openServerConfigDialog, + icon: Icon( + Icons.dns_outlined, + size: 14, + color: Theme.of( + context, + ).colorScheme.primary, + ), + label: Text( + "Connected to: ${Env.host}:${Env.port} ⚙️", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ].animate(interval: 100.ms).fade(duration: 400.ms).slideY(begin: 0.1, curve: Curves.easeOutQuad), + ), + ), + ), + ), + ).animate().fade(duration: 600.ms).slideY(begin: 0.1, curve: Curves.easeOutExpo), + ), + ), + ), + ), + + Positioned( + top: MediaQuery.of(context).padding.top + 16, + right: 24, + child: RepaintBoundary( + child: + ClipRRect( + borderRadius: BorderRadius.circular(30), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface.withValues( + alpha: 0.15, + ), + shape: BoxShape.circle, + border: Border.all( + color: theme.colorScheme.onSurface.withValues( + alpha: 0.20, + ), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.shadow.withValues( + alpha: 0.1, + ), + blurRadius: 20, + ), + ], + ), + child: IconButton( + padding: const EdgeInsets.all(12), + icon: Icon( + Icons.color_lens_outlined, + color: theme.colorScheme.primary, + size: 22, + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AppearanceSettings(), + ), + ); + }, + tooltip: 'Appearance Settings', + ), + ), + ), + ) + .animate() + .fade(delay: 400.ms, duration: 500.ms) + .scale(curve: Curves.easeOutBack), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/themes/theme_provider.dart b/mobile/lib/themes/theme_provider.dart index a98380d..29b6808 100644 --- a/mobile/lib/themes/theme_provider.dart +++ b/mobile/lib/themes/theme_provider.dart @@ -3,7 +3,7 @@ import 'package:mobile/themes/app_themes.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ThemeProvider extends ChangeNotifier { - ThemeType _currentTheme = ThemeType.onyx; + ThemeType _currentTheme = ThemeType.frost; bool _isInitialized = false; ThemeType get currentTheme => _currentTheme; @@ -23,7 +23,7 @@ class ThemeProvider extends ChangeNotifier { savedThemeIndex < ThemeType.values.length) { _currentTheme = ThemeType.values[savedThemeIndex]; } else { - _currentTheme = ThemeType.onyx; + _currentTheme = ThemeType.frost; } _isInitialized = true; diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 939010b..2937f71 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -214,6 +214,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" flutter_chat_core: dependency: transitive description: @@ -318,6 +326,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_test: dependency: "direct dev" description: flutter diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 3590274..e168487 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: simple_rich_text: ^2.0.49 qr_flutter: ^4.1.0 scrollable_positioned_list: ^0.3.8 + flutter_animate: ^4.5.2 launcher_name: default: "Elephant"