diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 29a2361..7d8c399 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/providers/basic_providers.dart'; import 'package:mobile/providers/group_controller_provider.dart'; +import 'package:mobile/themes/theme_provider.dart'; import 'package:provider/provider.dart'; import 'core/constants.dart'; import 'controllers/auth.dart'; @@ -14,9 +15,16 @@ void main() async { await Env.init(); await AuthService().initTokens(); + final themeProvider = ThemeProvider(); + + while (!themeProvider.isInitialized) { + await Future.delayed(const Duration(milliseconds: 10)); + } + runApp( MultiProvider( providers: [ + ChangeNotifierProvider.value(value: themeProvider), ChangeNotifierProvider(create: (_) => BasicProviders()), ChangeNotifierProvider(create: (_) => AuthState()), ChangeNotifierProvider(create: (_) => ChatController()), @@ -35,11 +43,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Elephant', debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - colorSchemeSeed: const Color(0xFF0052CC), - scaffoldBackgroundColor: const Color(0xFFFAFBFC), - ), + theme: context.watch().themeData, home: const SessionGateway(), ); } @@ -81,10 +85,12 @@ class _SessionGatewayState extends State { @override Widget build(BuildContext context) { if (!_hasCheckedAutoLogin) { - return const Scaffold( - backgroundColor: Colors.white, + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Center( - child: CircularProgressIndicator(color: Color(0xFF0052CC)), + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.primary, + ), ), ); } diff --git a/mobile/lib/pages/chat_details_page.dart b/mobile/lib/pages/chat_details_page.dart index 336a61f..16bbb21 100644 --- a/mobile/lib/pages/chat_details_page.dart +++ b/mobile/lib/pages/chat_details_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:mobile/controllers/auth.dart'; @@ -61,115 +62,12 @@ class _ChatDetailsPageState extends State { } } - void _showAddParticipantSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) { - return DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.5, - maxChildSize: 0.9, - expand: false, - builder: (context, scrollController) { - return Column( - children: [ - const Padding( - padding: EdgeInsets.all(16.0), - child: Text( - 'Add Participants', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TextField( - decoration: InputDecoration( - hintText: 'Search users...', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onChanged: (query) { - // TODO: Implement your user search logic here - }, - ), - ), - const SizedBox(height: 10), - Expanded( - child: Consumer( - builder: (context, groupController, child) { - if (groupController.isLoading) { - return const Center(child: CircularProgressIndicator()); - } - - // TODO: Replace with your actual searched users list - final searchedUsers = - []; // e.g., authController.searchedUsers - - if (searchedUsers.isEmpty) { - return const Center( - child: Text('Search for users to add.'), - ); - } - - return ListView.builder( - controller: scrollController, - itemCount: searchedUsers.length, - itemBuilder: (context, index) { - final user = searchedUsers[index]; - return ListTile( - leading: const CircleAvatar( - child: Icon(Icons.person), - ), - title: Text(user.displayName), - trailing: IconButton( - icon: const Icon( - Icons.add_circle, - color: Colors.teal, - ), - onPressed: () async { - final success = await context - .read() - .addMemberToGroup(widget.chatId, user.id); - - if (success && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${user.displayName} added!', - ), - ), - ); - // Refresh the group members list behind the sheet - context - .read() - .fetchGroupMembers(widget.chatId); - Navigator.pop(context); // Close sheet - } - }, - ), - ); - }, - ); - }, - ), - ), - ], - ); - }, - ); - }, - ); - } - @override Widget build(BuildContext context) { final chatController = context.watch(); return Scaffold( - backgroundColor: Colors.grey[200], + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: CustomScrollView( slivers: [ _buildSliverAppBar(), @@ -224,9 +122,9 @@ class _ChatDetailsPageState extends State { _ActionIcon( icon: Icons.search, label: 'Search', - onTap: (){ + onTap: () { Navigator.pop(context, 'start_search'); - } + }, ), ], ), @@ -239,10 +137,7 @@ class _ChatDetailsPageState extends State { title: const Text('Media, links, and docs'), trailing: const Row( mainAxisSize: MainAxisSize.min, - children: [ - Text('0'), // Replace with actual count later - Icon(Icons.chevron_right), - ], + children: [Text('0'), Icon(Icons.chevron_right)], ), onTap: () { // TODO: Navigate to Media Page (API calls later) @@ -314,7 +209,7 @@ class _ChatDetailsPageState extends State { final isCurrentUserAdmin = currentUserMember?.role == 'admin'; return Material( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -322,8 +217,8 @@ class _ChatDetailsPageState extends State { padding: const EdgeInsets.all(16.0), child: Text( '${members.length} participants', - style: const TextStyle( - color: Colors.grey, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold, ), ), @@ -331,27 +226,33 @@ class _ChatDetailsPageState extends State { if (isCurrentUserAdmin) ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.teal, - child: Icon(Icons.person_add, color: Colors.white), + leading: CircleAvatar( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + child: Icon( + Icons.person_add, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), ), title: const Text('Add participants'), onTap: () { showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.white, + backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + borderRadius: BorderRadius.vertical( + top: Radius.circular(16), + ), ), - builder: (context) => _AddParticipantSheet(chatId: widget.chatId), + builder: (context) => + _AddParticipantSheet(chatId: widget.chatId), ); }, ), if (isLoading && members.isEmpty) const Padding( - padding: EdgeInsets.all(32.0), + padding: EdgeInsets.only(bottom: 32.0), child: Center(child: CircularProgressIndicator()), ) else @@ -374,9 +275,12 @@ class _ChatDetailsPageState extends State { ), title: Text(member.displayName), subtitle: isThisUserAdmin - ? const Text( + ? Text( 'Admin', - style: TextStyle(color: Colors.teal, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 12, + ), ) : null, onTap: () => @@ -389,7 +293,6 @@ class _ChatDetailsPageState extends State { ); } - // Action when clicking a group member void _showMemberDetailsDialog(ChatMember member, bool isCurrentUserAdmin) { showModalBottomSheet( context: context, @@ -432,7 +335,9 @@ class _ChatDetailsPageState extends State { child: const Text('Cancel'), ), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () { + Navigator.pop(context, true); + }, child: const Text( 'Remove', style: TextStyle(color: Colors.red), @@ -489,7 +394,7 @@ class _SectionContainer extends StatelessWidget { @override Widget build(BuildContext context) { - return Material(color: Colors.white, child: child); + return Material(color: Theme.of(context).colorScheme.surface, child: child); } } @@ -508,12 +413,9 @@ class _ActionIcon extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 16.0), child: Column( children: [ - Icon(icon, color: Theme.of(context).primaryColor, size: 28), + Icon(icon, size: 28), const SizedBox(height: 8), - Text( - label, - style: TextStyle(color: Theme.of(context).primaryColor), - ), + Text(label), ], ), ), @@ -549,13 +451,10 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { chatState.queryUsers(value); } }); - setState(() { - - }); + setState(() {}); } Future _addMember(String userId, String displayName) async { - // Hide keyboard FocusScope.of(context).unfocus(); final groupCtrl = context.read(); @@ -567,9 +466,8 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$displayName added to the group!')), ); - // Refresh the group members list chatCtrl.fetchGroupMembers(widget.chatId); - Navigator.pop(context); // Close the sheet + Navigator.pop(context); } else if (mounted) { ScaffoldMessenger.of( context, @@ -586,73 +484,76 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { final bool isSearching = searchInput.isNotEmpty; final bool hasValidQueryLength = searchInput.length >= 3; - // Get current member IDs to filter them out of search results & recents final currentMemberIds = chatState.currentGroupMembers .map((m) => m.userId) .toSet(); - return Padding( - // Ensure the sheet rises with the keyboard - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: Container( - height: - MediaQuery.of(context).size.height * 0.7, // Take up 70% of screen - padding: const EdgeInsets.symmetric(vertical: 16), - child: Column( - children: [ - const Text( - 'Add Participants', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TextField( - controller: _searchController, - onChanged: (val) => _onSearchChanged(val, chatState), - decoration: InputDecoration( - hintText: "Search name or username", - prefixIcon: const Icon(Icons.search), - suffixIcon: isSearching - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchController.clear(); - chatState.queryUsers(""); - setState((){}); - }, - ) - : null, - filled: true, - fillColor: Colors.grey[100], - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Container( + height: MediaQuery.of(context).size.height * 0.7, + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + const Text( + 'Add Participants', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + controller: _searchController, + onChanged: (val) => _onSearchChanged(val, chatState), + decoration: InputDecoration( + hintText: "Search name or username", + prefixIcon: const Icon(Icons.search), + suffixIcon: isSearching + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + chatState.queryUsers(""); + setState(() {}); + }, + ) + : null, + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), ), ), - ), - ), - const SizedBox(height: 10), + const SizedBox(height: 10), - // Show Loading Indicator for Add API call - if (groupState.isLoading) - const Padding( - padding: EdgeInsets.all(8.0), - child: LinearProgressIndicator(), - ), + if (groupState.isLoading) + const Padding( + padding: EdgeInsets.all(8.0), + child: LinearProgressIndicator(), + ), - Expanded( - child: isSearching - ? _buildSearchResults( - chatState, - currentMemberIds, - hasValidQueryLength, - ) - : _buildRecentContacts(chatState, currentMemberIds), + Expanded( + child: isSearching + ? _buildSearchResults( + chatState, + currentMemberIds, + hasValidQueryLength, + ) + : _buildRecentContacts(chatState, currentMemberIds), + ), + ], ), - ], + ), ), ), ); @@ -672,7 +573,6 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { return const Center(child: CircularProgressIndicator()); } - // Filter out users who are already in the group final results = chatState.contactSearchResults .where((user) => !currentMemberIds.contains(user['id'])) .toList(); @@ -694,7 +594,10 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { title: Text(displayName), subtitle: Text("@$username"), trailing: IconButton( - icon: const Icon(Icons.add_circle, color: Colors.teal), + icon: Icon( + Icons.add_circle, + color: Theme.of(context).colorScheme.primary, + ), onPressed: () => _addMember(uid, displayName), ), ); @@ -706,7 +609,6 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { ChatController chatState, Set currentMemberIds, ) { - // Filter to only 1-on-1 chats of users NOT currently in the group final recents = chatState.inbox .where( (thread) => !thread.isGroup && !currentMemberIds.contains(thread.id), @@ -725,12 +627,12 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.only(left: 16, top: 8, bottom: 8), + Padding( + padding: const EdgeInsets.only(left: 16, top: 8, bottom: 8), child: Text( "Recent Contacts", style: TextStyle( - color: Colors.black38, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold, fontSize: 13, ), @@ -751,7 +653,10 @@ class _AddParticipantSheetState extends State<_AddParticipantSheet> { title: Text(thread.title), subtitle: Text("@${thread.username}"), trailing: IconButton( - icon: const Icon(Icons.add_circle, color: Colors.teal), + icon: Icon( + Icons.add_circle, + color: Theme.of(context).colorScheme.primary, + ), onPressed: () => _addMember(thread.id, thread.title), ), ); diff --git a/mobile/lib/pages/chat_page.dart b/mobile/lib/pages/chat_page.dart index 6c58442..c97dc5a 100644 --- a/mobile/lib/pages/chat_page.dart +++ b/mobile/lib/pages/chat_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,12 +15,14 @@ class ChatPage extends StatefulWidget { final String chatUserId; final String displayName; final bool isGroup; + final bool isNew; const ChatPage({ super.key, required this.chatUserId, required this.displayName, this.isGroup = false, + this.isNew = false, }); @override @@ -61,6 +64,9 @@ class _ChatPageState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _chatController.openChat(widget.chatUserId, isGroup: widget.isGroup); + if (widget.isGroup && widget.isNew) { + _sendMessage('Hey Everyone!!'); + } } }); } @@ -93,21 +99,27 @@ class _ChatPageState extends State with WidgetsBindingObserver { } void _scrollToAndHighlight(String messageId) async { + setState(() { + _isSearchMode = false; + _chatSearchController.clear(); + _searchResults.clear(); + }); + final chatState = context.read(); final activeChat = chatState.activeChat; + final bool isTyping = chatState.isPeerTyping; final targetIndex = activeChat.indexWhere((msg) => msg.id == messageId); if (targetIndex != -1 && _itemScrollController.isAttached) { final int realVisualIndex = - (activeChat.length - 1 - targetIndex) + - (chatState.isPeerTyping ? 2 : 1); + (activeChat.length - 1 - targetIndex) + (isTyping ? 3 : 2); await _itemScrollController.scrollTo( index: realVisualIndex, - duration: const Duration(milliseconds: 300), + duration: const Duration(milliseconds: 1000), curve: Curves.easeInOutCubic, - alignment: 0.5, + alignment: 0.4, ); setState(() { @@ -250,7 +262,13 @@ class _ChatPageState extends State with WidgetsBindingObserver { Widget _buildSearchAppBar(ThemeData theme) { return AppBar( - backgroundColor: theme.colorScheme.surface, + backgroundColor: Colors.transparent, + flexibleSpace: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container(color: theme.colorScheme.surface), + ), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { @@ -683,30 +701,33 @@ class _ChatPageState extends State with WidgetsBindingObserver { sender = widget.displayName; } - return ListTile( - title: Text( - msg.content, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - '$sender • ${DateFormat.yMd().add_jm().format(msg.createdAt)}', - style: TextStyle( - color: theme.colorScheme.primary, + return Material( + color: Colors.transparent, + child: ListTile( + title: Text( + msg.content, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - ), - onTap: () { - // Close search mode - setState(() { - _isSearchMode = false; - _chatSearchController.clear(); - _searchResults.clear(); - }); + subtitle: Text( + '$sender • ${DateFormat.yMd().add_jm().format(msg.createdAt)}', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + onTap: () { + // Close search mode + setState(() { + _isSearchMode = false; + _chatSearchController.clear(); + _searchResults.clear(); + }); - FocusScope.of(context).unfocus(); + FocusScope.of(context).unfocus(); - _scrollToAndHighlight(msg.id); - }, + _scrollToAndHighlight(msg.id); + }, + ), ); }, ), @@ -728,8 +749,8 @@ class _ChatPageState extends State with WidgetsBindingObserver { duration: const Duration(milliseconds: 200), child: FloatingActionButton( mini: true, - backgroundColor: theme.colorScheme.surface, - foregroundColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.primaryContainer, + foregroundColor: theme.colorScheme.onPrimaryContainer, elevation: 4, onPressed: () => _scrollToBottom(animated: true), child: const Icon(Icons.keyboard_arrow_down), @@ -751,69 +772,80 @@ class _ChatPageState extends State with WidgetsBindingObserver { duration: const Duration(milliseconds: 250), opacity: _replyingToMessage != null ? 1.0 : 0.0, child: _replyingToMessage != null - ? Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: theme.colorScheme.surface, - border: Border( - left: BorderSide( - color: theme.colorScheme.primary, - width: 4, - ), - top: BorderSide( - color: theme.dividerColor, - width: 1, - ), + ? ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 15, + sigmaY: 15, ), - boxShadow: [ - BoxShadow( - color: theme.shadowColor.withValues( - alpha: 0.1, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + left: BorderSide( + color: theme.colorScheme.primary, + width: 4, + ), + top: BorderSide( + color: theme.colorScheme.surface + .withValues(alpha: 0.8), + width: 1, + ), ), - blurRadius: 4, - offset: const Offset(0, -2), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues( + alpha: 0.04, + ), + blurRadius: 12, + offset: const Offset(0, -4), + ), + ], ), - ], - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Replying to message", - style: TextStyle( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - fontSize: 12, - ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Replying to message", + style: TextStyle( + fontWeight: FontWeight.bold, + color: + theme.colorScheme.primary, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + _replyingToMessage.content, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: theme + .colorScheme + .onSurface, + ), + ), + ], ), - const SizedBox(height: 4), - Text( - _replyingToMessage.content, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: - theme.colorScheme.onSurface, - ), + ), + IconButton( + icon: Icon( + Icons.close, + size: 20, + color: theme.iconTheme.color, ), - ], - ), - ), - IconButton( - icon: Icon( - Icons.close, - size: 20, - color: theme.iconTheme.color, - ), - onPressed: () => setState( - () => _replyingToMessage = null, - ), + onPressed: () => setState( + () => _replyingToMessage = null, + ), + ), + ], ), - ], + ), ), ) : const SizedBox(width: double.infinity, height: 0), diff --git a/mobile/lib/pages/home_page.dart b/mobile/lib/pages/home_page.dart index 8ee5a74..a127910 100644 --- a/mobile/lib/pages/home_page.dart +++ b/mobile/lib/pages/home_page.dart @@ -138,10 +138,10 @@ class _HomePageState extends State { child: Container( height: 68, decoration: BoxDecoration( - color: theme.colorScheme.surface.withValues(alpha: 0.35), + color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(24), border: Border.all( - color: theme.colorScheme.surface.withValues(alpha: 0.45), + color: theme.colorScheme.surface.withValues(alpha: 0.8), width: 1.5, ), boxShadow: [ @@ -180,16 +180,16 @@ class _HomePageState extends State { child: Container( decoration: BoxDecoration( color: theme.colorScheme.surface.withValues( - alpha: 0.85, + alpha: 0.8, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: theme.colorScheme.primary.withValues( - alpha: 0.08, + alpha: 0.4, ), - blurRadius: 12, - offset: const Offset(0, 4), + blurRadius: 20, + // offset: const Offset(0, 0), ), ], ), @@ -257,9 +257,7 @@ class _HomePageState extends State { child: BackdropFilter( enabled: true, filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: Container( - color: theme.scaffoldBackgroundColor.withValues(alpha: 0.7), - ), + child: Container(color: theme.colorScheme.surface), ), ), leading: _isSelectionMode @@ -281,7 +279,9 @@ class _HomePageState extends State { ), child: CircleAvatar( radius: 18, - backgroundColor: theme.colorScheme.primaryContainer, + backgroundColor: theme.colorScheme.primary.withValues( + alpha: 0.15, + ), child: Icon( Icons.person, color: theme.colorScheme.primary, @@ -374,6 +374,7 @@ class _HomePageState extends State { !_isSelectionMode ? PopupMenuButton( iconColor: theme.colorScheme.onSurface, + color: theme.scaffoldBackgroundColor, onSelected: (String value) {}, borderRadius: BorderRadius.circular(20), itemBuilder: (context) => [ @@ -524,19 +525,50 @@ class _HomePageState extends State { opacity: (_isFabVisible && !_isSelectionMode) ? 1.0 : 0.0, child: Padding( padding: const EdgeInsets.only(bottom: 10), - child: FloatingActionButton( - heroTag: "fab_pen", - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - child: const Icon(Icons.edit), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const NewChatPage(), + child: + SizedBox( + width: 56, + height: 56, + child: ClipRRect( + borderRadius: BorderRadiusGeometry.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.5), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.3), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: IconButton( + icon: Icon( + Icons.edit, + color: Theme.of(context).colorScheme.primary, + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const NewChatPage(), + ), + ); + }, + ), ), - ); - }, + ), + ), ), ), ), diff --git a/mobile/lib/pages/login.dart b/mobile/lib/pages/login.dart index 7e71464..74bc5ff 100644 --- a/mobile/lib/pages/login.dart +++ b/mobile/lib/pages/login.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../core/constants.dart'; @@ -40,130 +42,139 @@ class _LoginPageState extends State showDialog( context: context, - builder: (dialogContext) => AlertDialog( - backgroundColor: Colors.white, - title: const Row( - children: [ - Icon(Icons.dns, color: Color(0xFF0052CC)), - SizedBox(width: 8), - Text( - "Server Settings", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Enter your decentralized peer node coordinates:", - style: TextStyle(fontSize: 13, color: Colors.black54), - ), - const SizedBox(height: 16), - const Text( - "Host", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.black54, + 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), ), - ), - const SizedBox(height: 6), - TextField( - controller: hostController, - style: const TextStyle(color: Colors.black87, fontSize: 14), - decoration: InputDecoration( - hintText: Env.defaultHost, - filled: true, - fillColor: const Color(0xFFF4F5F7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, + ], + ), + 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: 14), - const Text( - "Port", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.black54, + 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: portController, - keyboardType: TextInputType.number, - style: const TextStyle(color: Colors.black87, fontSize: 14), - decoration: InputDecoration( - hintText: Env.defaultPort, - filled: true, - fillColor: const Color(0xFFF4F5F7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + 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, + ), ), - 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, ), ), - ), - ], - ), - 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", + 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, ), ), - ); - }, - child: const Text( - "Reset Default", - style: TextStyle(color: Colors.black54), - ), + ), + ], ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0052CC), + 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), + ), ), - 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)), - ), - ], + 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)), + ), + ], + ), ), ); } @@ -190,17 +201,17 @@ class _LoginPageState extends State return Expanded( child: OutlinedButton.icon( onPressed: () {}, - icon: Icon(icon, color: Colors.black87, size: 18), + icon: Icon(icon, color: Theme.of(context).colorScheme.onSurface, size: 18), label: Text( label, - style: const TextStyle( - color: Colors.black87, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 13, fontWeight: FontWeight.w600, ), ), style: OutlinedButton.styleFrom( - side: const BorderSide(color: Color(0xFFDFE1E6)), + side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), @@ -213,7 +224,7 @@ class _LoginPageState extends State final authState = context.watch(); return Scaffold( - backgroundColor: const Color(0xFFFAFBFC), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), @@ -221,9 +232,12 @@ class _LoginPageState extends State constraints: const BoxConstraints(maxWidth: 420), padding: const EdgeInsets.all(32), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE3E6EB), width: 1), + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + width: 1, + ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.03), @@ -241,9 +255,9 @@ class _LoginPageState extends State Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.security_rounded, - color: Color(0xFF0052CC), + color: Theme.of(context).colorScheme.primary, size: 28, ), const SizedBox(width: 8), @@ -252,9 +266,7 @@ class _LoginPageState extends State style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: const Color( - 0xFF0052CC, - ).withValues(alpha: 0.95), + color: Theme.of(context).colorScheme.primary, letterSpacing: 0.5, ), ), @@ -267,10 +279,10 @@ class _LoginPageState extends State ? 'Welcome Back' : 'Create Account', textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Color(0xFF172B4D), + color: Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(height: 6), @@ -279,9 +291,9 @@ class _LoginPageState extends State ? 'Access your secure workspace' : 'Join the peer mesh platform', textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: Color(0xFF5E6C84), + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 28), @@ -290,7 +302,9 @@ class _LoginPageState extends State height: 46, padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: const Color(0xFFEBECF0), + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: TabBar( @@ -298,10 +312,11 @@ class _LoginPageState extends State onTap: (index) => setState(() {}), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, - labelColor: const Color(0xFF0052CC), - unselectedLabelColor: const Color(0xFF5E6C84), - indicator: BoxDecoration( - color: Colors.white, + 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( @@ -342,14 +357,16 @@ class _LoginPageState extends State vertical: 10, ), decoration: BoxDecoration( - color: const Color(0xFFFFEBE6), + color: Theme.of(context).colorScheme.errorContainer, borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFFFFBDAD)), + border: Border.all( + color: Theme.of(context).colorScheme.error, + ), ), child: Text( authState.errorMessage!, - style: const TextStyle( - color: Color(0xFFBF2600), + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, fontSize: 13, fontWeight: FontWeight.w500, ), @@ -370,12 +387,15 @@ class _LoginPageState extends State const SizedBox(height: 6), TextFormField( controller: _usernameController, - style: const TextStyle(color: Colors.black87, fontSize: 14), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), decoration: InputDecoration( hintText: "elephant_user", hintStyle: const TextStyle(color: Colors.black38), filled: true, - fillColor: const Color(0xFFF4F5F7), + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, @@ -403,15 +423,17 @@ class _LoginPageState extends State const SizedBox(height: 6), TextFormField( controller: _displayNameController, - style: const TextStyle( - color: Colors.black87, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14, ), decoration: InputDecoration( hintText: "John Doe", hintStyle: const TextStyle(color: Colors.black38), filled: true, - fillColor: const Color(0xFFF4F5F7), + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, @@ -461,12 +483,15 @@ class _LoginPageState extends State TextFormField( controller: _passwordController, obscureText: true, - style: const TextStyle(color: Colors.black87, fontSize: 14), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), decoration: InputDecoration( hintText: "••••••••", hintStyle: const TextStyle(color: Colors.black38), filled: true, - fillColor: const Color(0xFFF4F5F7), + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, @@ -485,7 +510,7 @@ class _LoginPageState extends State ElevatedButton( onPressed: authState.isLoading ? null : _submit, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0052CC), + backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( @@ -494,11 +519,11 @@ class _LoginPageState extends State elevation: 0, ), child: authState.isLoading - ? const SizedBox( + ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( - color: Colors.white, + color: Theme.of(context).colorScheme.onPrimary, strokeWidth: 2.5, ), ) @@ -512,20 +537,26 @@ class _LoginPageState extends State ), const SizedBox(height: 24), - const Row( + Row( children: [ - Expanded(child: Divider(color: Color(0xFFDFE1E6))), + Expanded(child: Divider( + color: Theme.of(context).colorScheme.outlineVariant, + )), Padding( - padding: EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( "or sign in with", style: TextStyle( - color: Color(0xFF5E6C84), + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, fontSize: 12, ), ), ), - Expanded(child: Divider(color: Color(0xFFDFE1E6))), + Expanded(child: Divider( + color: Theme.of(context).colorScheme.outlineVariant, + )), ], ), const SizedBox(height: 20), @@ -549,23 +580,25 @@ class _LoginPageState extends State vertical: 6, ), decoration: BoxDecoration( - color: const Color(0xFFE3FCEF), + color: Theme.of(context).colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(20), - border: Border.all(color: const Color(0xFFABF5D1)), + border: Border.all( + color: Theme.of(context).colorScheme.tertiary, + ), ), - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle, - color: Color(0xFF36B37E), + color: Theme.of(context).colorScheme.onTertiaryContainer, size: 14, ), - SizedBox(width: 6), + const SizedBox(width: 6), Text( "End-to-end encrypted session", style: TextStyle( - color: Color(0xFF006644), + color: Theme.of(context).colorScheme.onTertiaryContainer, fontSize: 12, fontWeight: FontWeight.w600, ), @@ -579,15 +612,15 @@ class _LoginPageState extends State Center( child: TextButton.icon( onPressed: _openServerConfigDialog, - icon: const Icon( + icon: Icon( Icons.dns_outlined, size: 14, - color: Color(0xFF5E6C84), + color: Theme.of(context).colorScheme.primary, ), label: Text( "Connected to: ${Env.host}:${Env.port} ⚙️", - style: const TextStyle( - color: Color(0xFF5E6C84), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w600, ), diff --git a/mobile/lib/pages/new_chat_page.dart b/mobile/lib/pages/new_chat_page.dart index 36d311a..0a5a87b 100644 --- a/mobile/lib/pages/new_chat_page.dart +++ b/mobile/lib/pages/new_chat_page.dart @@ -37,11 +37,11 @@ class _NewChatPageState extends State { showDialog( context: context, builder: (context) => AlertDialog( - backgroundColor: Colors.white, - title: const Text( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( "Find by Username", style: TextStyle( - color: Colors.black87, + color: Theme.of(context).colorScheme.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), @@ -49,29 +49,37 @@ class _NewChatPageState extends State { content: TextField( controller: inputController, autofocus: true, - style: const TextStyle(color: Colors.black87), - decoration: const InputDecoration( + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + decoration: InputDecoration( hintText: "Enter minimum 3 characters...", - hintStyle: TextStyle(color: Colors.black38), + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.black26), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant, + ), ), focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xFF1890FF)), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text( + child: Text( "Cancel", - style: TextStyle(color: Colors.black54), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF1890FF), + backgroundColor: Theme.of(context).colorScheme.primary, ), onPressed: () { final text = inputController.text.trim(); @@ -83,16 +91,24 @@ class _NewChatPageState extends State { Navigator.pop(context); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( + SnackBar( content: Text( "Search query must be at least 3 characters.", + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), ), - backgroundColor: Colors.redAccent, + backgroundColor: Theme.of( + context, + ).colorScheme.errorContainer, ), ); } }, - child: const Text("Search", style: TextStyle(color: Colors.white)), + child: Text( + "Search", + style: TextStyle(color: Theme.of(context).colorScheme.onPrimary), + ), ), ], ), @@ -108,13 +124,17 @@ class _NewChatPageState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), leading: CircleAvatar( radius: 20, - backgroundColor: const Color(0xFFF0F2F5), - child: Icon(icon, color: Colors.black54, size: 20), + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + child: Icon( + icon, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 20, + ), ), title: Text( label, - style: const TextStyle( - color: Colors.black87, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 15, fontWeight: FontWeight.w500, ), @@ -131,16 +151,18 @@ class _NewChatPageState extends State { final bool hasValidQueryLength = searchInput.length >= 3; return Scaffold( - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, elevation: 0, scrolledUnderElevation: 0, - iconTheme: const IconThemeData(color: Colors.black87), - title: const Text( + iconTheme: IconThemeData( + color: Theme.of(context).colorScheme.onSurface, + ), + title: Text( 'New message', style: TextStyle( - color: Colors.black87, + color: Theme.of(context).colorScheme.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), @@ -171,14 +193,23 @@ class _NewChatPageState extends State { child: TextField( controller: _searchController, onChanged: (val) => _onSearchChanged(val, chatState), - style: const TextStyle(color: Colors.black87), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( hintText: "Name, username or number", - hintStyle: const TextStyle(color: Colors.black38, fontSize: 15), - prefixIcon: const Icon(Icons.search, color: Colors.black38), + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 15, + ), + prefixIcon: Icon( + Icons.search, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), suffixIcon: isSearching ? IconButton( - icon: const Icon(Icons.clear, color: Colors.black38), + icon: Icon( + Icons.clear, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), onPressed: () { _searchController.clear(); chatState.queryUsers(""); @@ -186,7 +217,9 @@ class _NewChatPageState extends State { ) : null, filled: true, - fillColor: const Color(0xFFF5F5F5), + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, contentPadding: const EdgeInsets.symmetric(vertical: 10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(28), @@ -198,26 +231,31 @@ class _NewChatPageState extends State { Expanded( child: isSearching ? (!hasValidQueryLength - ? const Center( + ? Center( child: Text( "Type at least 3 characters to search...", style: TextStyle( - color: Colors.black45, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, fontSize: 14, ), ), ) : chatState.isSearchLoading - ? const Center( + ? Center( child: CircularProgressIndicator( - color: Color(0xFF1890FF), + color: Theme.of(context).colorScheme.primary, ), ) : chatState.contactSearchResults.isEmpty - ? const Center( + ? Center( child: Text( "No users found", - style: TextStyle(color: Colors.grey, fontSize: 15), + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, fontSize: 15), ), ) : ListView.builder( @@ -233,23 +271,31 @@ class _NewChatPageState extends State { horizontal: 16, vertical: 4, ), - leading: const CircleAvatar( - backgroundColor: Color(0xFFD6E4FF), + leading: CircleAvatar( + backgroundColor: Theme.of( + context, + ).colorScheme.primaryContainer, child: Icon( Icons.person, - color: Color(0xFF1890FF), + color: Theme.of(context).colorScheme.primary, ), ), title: Text( displayName, - style: const TextStyle( - color: Colors.black87, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), subtitle: Text( "@$username", - style: const TextStyle(color: Colors.black45), + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), ), onTap: () { final String uid = user['id']; @@ -291,12 +337,14 @@ class _NewChatPageState extends State { label: "Find by phone number", onTap: () {}, ), - const Padding( - padding: EdgeInsets.only(left: 16, top: 20, bottom: 8), + Padding( + padding: const EdgeInsets.only(left: 16, top: 20, bottom: 8), child: Text( "Recent Contacts", style: TextStyle( - color: Colors.black38, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold, fontSize: 13, ), @@ -311,23 +359,33 @@ class _NewChatPageState extends State { horizontal: 16, vertical: 4, ), - leading: const CircleAvatar( - backgroundColor: Color(0xFFE8E8E8), + leading: CircleAvatar( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, child: Icon( Icons.person, - color: Colors.black54, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), title: Text( thread.title, - style: const TextStyle( - color: Colors.black87, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), subtitle: Text( "@${thread.username}", - style: const TextStyle(color: Colors.black45), + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), ), onTap: () { chatState.openChat(thread.id); diff --git a/mobile/lib/pages/select_contact_page.dart b/mobile/lib/pages/select_contact_page.dart index e646b0b..e1c66c9 100644 --- a/mobile/lib/pages/select_contact_page.dart +++ b/mobile/lib/pages/select_contact_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:mobile/controllers/chat.dart'; import 'package:mobile/pages/chat_page.dart'; @@ -61,26 +62,31 @@ class _SelectContactPageState extends State { return ListTile( selected: isSelected, - selectedTileColor: Colors.blue.withValues(alpha: 0.1), + selectedTileColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.12), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: Stack( children: [ - const CircleAvatar( - backgroundColor: Color(0xFFD6E4FF), - child: Icon(Icons.person, color: Color(0xFF1890FF)), + CircleAvatar( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + child: Icon( + Icons.person, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), ), if (isSelected) Positioned( right: 0, bottom: 0, child: Container( - decoration: const BoxDecoration( - color: Colors.white, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, shape: BoxShape.circle, ), - child: const Icon( + child: Icon( Icons.check_circle, - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, size: 18, ), ), @@ -89,14 +95,14 @@ class _SelectContactPageState extends State { ), title: Text( displayName, - style: const TextStyle( - color: Colors.black87, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), subtitle: Text( username.startsWith('@') ? username : "@$username", - style: const TextStyle(color: Colors.black45), + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), ), onTap: () => _toggleSelection(id, displayName, username), ); @@ -108,9 +114,9 @@ class _SelectContactPageState extends State { final bool isSearching = _searchController.text.trim().isNotEmpty; return Scaffold( - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, scrolledUnderElevation: 0, title: AnimatedSwitcher( switchInCurve: Curves.decelerate, @@ -131,32 +137,33 @@ class _SelectContactPageState extends State { height: 40, child: TextField( controller: _searchController, - onChanged: - _onSearchChanged, - style: const TextStyle(color: Colors.black87), + onChanged: _onSearchChanged, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), autofocus: true, decoration: InputDecoration( hintText: "Name, username or number", - hintStyle: const TextStyle( - color: Colors.black38, + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 15, ), suffixIcon: isSearching ? IconButton( - icon: const Icon( + icon: Icon( Icons.clear, - color: Colors.black38, + color: Theme.of(context).colorScheme.onSurface, ), onPressed: () { _searchController.clear(); - context.read().queryUsers( - "", - ); + context.read().queryUsers(""); }, ) : null, filled: true, - fillColor: const Color(0xFFF5F5F5), + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, contentPadding: const EdgeInsets.symmetric( vertical: 10, horizontal: 16, @@ -168,15 +175,15 @@ class _SelectContactPageState extends State { ), ), ) - : const Text( + : Text( 'Select Contacts', style: TextStyle( letterSpacing: 0.5, fontSize: 20, fontWeight: FontWeight.bold, - color: Colors.black87, + color: Theme.of(context).colorScheme.onSurface, ), - key: ValueKey('title'), + key: const ValueKey('title'), ), ), actions: [ @@ -186,15 +193,13 @@ class _SelectContactPageState extends State { _isSearchOpen = !_isSearchOpen; if (!_isSearchOpen) { _searchController.clear(); - context.read().queryUsers( - "", - ); + context.read().queryUsers(""); } }); }, icon: Icon( _isSearchOpen ? Icons.close : Icons.search, - color: Colors.black87, + color: Theme.of(context).colorScheme.onSurface, ), ), ], @@ -207,15 +212,17 @@ class _SelectContactPageState extends State { child: _selectedContacts.isNotEmpty ? Container( width: double.infinity, - color: Colors.blue.withValues(alpha: 0.1), + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.12), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), child: Text( '${_selectedContacts.length} selected', - style: const TextStyle( - color: Colors.blue, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold, fontSize: 14, ), @@ -226,16 +233,22 @@ class _SelectContactPageState extends State { Expanded( child: isSearching ? (chatState.isSearchLoading - ? const Center( + ? Center( child: CircularProgressIndicator( - color: Color(0xFF1890FF), + color: Theme.of(context).colorScheme.primary, ), ) : chatState.contactSearchResults.isEmpty - ? const Center( + ? Center( child: Text( - "No users found", - style: TextStyle(color: Colors.grey, fontSize: 15), + "No users found (try entering at least 3 characters to search)", + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 15, + ), ), ) : ListView.builder( @@ -261,8 +274,8 @@ class _SelectContactPageState extends State { : ListView( children: [ if (_selectedContacts.isNotEmpty) ...[ - const Padding( - padding: EdgeInsets.only( + Padding( + padding: const EdgeInsets.only( left: 16, top: 20, bottom: 8, @@ -270,7 +283,9 @@ class _SelectContactPageState extends State { child: Text( "Selected Contacts", style: TextStyle( - color: Colors.black38, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold, fontSize: 13, ), @@ -284,12 +299,18 @@ class _SelectContactPageState extends State { ), ), ], - const Padding( - padding: EdgeInsets.only(left: 16, top: 20, bottom: 8), + Padding( + padding: const EdgeInsets.only( + left: 16, + top: 20, + bottom: 8, + ), child: Text( "Recent Contacts", style: TextStyle( - color: Colors.black38, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold, fontSize: 13, ), @@ -318,13 +339,18 @@ class _SelectContactPageState extends State { ), floatingActionButton: _selectedContacts.isNotEmpty ? FloatingActionButton( - backgroundColor: Colors.blue, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, elevation: 4, - child: const Icon(Icons.arrow_forward, color: Colors.white), + child: Icon( + Icons.arrow_forward, + color: Theme.of(context).colorScheme.onPrimary, + ), onPressed: () { context.read().setContacts(_selectedContacts); showModalBottomSheet( + backgroundColor: Colors.transparent, context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( @@ -335,118 +361,194 @@ class _SelectContactPageState extends State { builder: (BuildContext bottomSheetContext) { return Consumer( builder: (modalContext, groupState, child) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of( - bottomSheetContext, - ).viewInsets.bottom, - left: 16, - right: 16, - top: 24, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'New Group', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.85), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), ), - ), - const SizedBox(height: 16), - TextField( - controller: _groupNameController, - autofocus: true, - decoration: const InputDecoration( - hintText: 'Enter group name', - border: OutlineInputBorder(), + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.surface + .withValues(alpha: 0.5), + width: 1, + ), ), ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilledButton( - onPressed: groupState.isLoading - ? null - : () async { - final groupName = - _groupNameController.text - .trim(); - if (groupName.isEmpty) return; + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of( + bottomSheetContext, + ).viewInsets.bottom, + left: 16, + right: 16, + top: 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'New Group', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of( + context, + ).colorScheme.onSurface, + ), + ), + const SizedBox(height: 16), + TextField( + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of( + context, + ).colorScheme.onSurface, + ), + controller: _groupNameController, + autofocus: true, + decoration: InputDecoration( + hintText: 'Enter group name', + hintStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.outlineVariant, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.primary, + ), + ), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilledButton( + onPressed: groupState.isLoading + ? null + : () async { + final groupName = + _groupNameController.text + .trim(); + if (groupName.isEmpty) return; - final memberIds = _selectedContacts - .keys - .toList(); + final memberIds = + _selectedContacts.keys + .toList(); - final rootNavigator = Navigator.of( - context, - ); + final rootNavigator = + Navigator.of(context); - final newGroup = await modalContext - .read() - .createGroup( - groupName: groupName, - memberIds: memberIds, - ); + final newGroup = + await modalContext + .read< + GroupController + >() + .createGroup( + groupName: + groupName, + memberIds: + memberIds, + ); - if (newGroup != null) { - if (!context.mounted) return; + if (newGroup != null) { + if (!context.mounted) + return; - context - .read() - .loadInbox(); + context + .read() + .loadInbox(); - Navigator.of( - bottomSheetContext, - ).pop(); + Navigator.of( + bottomSheetContext, + ).pop(); - _groupNameController.clear(); + _groupNameController + .clear(); - rootNavigator.pushReplacement( - MaterialPageRoute( - builder: (context) => - ChatPage( - chatUserId: newGroup.id, - displayName: - newGroup.name, - isGroup: true, + rootNavigator + .pushReplacement( + MaterialPageRoute( + builder: + ( + context, + ) => ChatPage( + isNew: true, + chatUserId: + newGroup + .id, + displayName: + newGroup + .name, + isGroup: true, + ), + ), + ); + } else { + if (!context.mounted) + return; + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Failed to create group', + style: TextStyle( + color: + Theme.of( + context, + ) + .colorScheme + .onSurface, + ), + ), ), - ), - ); - - context - .read() - .openChat(newGroup.id, isGroup: true); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar( - const SnackBar( - content: Text( - 'Failed to create group', + ); + } + }, + child: groupState.isLoading + ? const SizedBox( + height: 20, + width: 20, + child: + CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + 'Create', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface, ), ), - ); - } - }, - child: groupState.isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Text('Create'), - ), - ], + ), + ], + ), + const SizedBox(height: 16), + ], + ), ), - const SizedBox(height: 16), - ], + ), ), ); }, diff --git a/mobile/lib/pages/settings pages/accounts.dart b/mobile/lib/pages/settings pages/accounts.dart index 144ed7b..82e7ef8 100644 --- a/mobile/lib/pages/settings pages/accounts.dart +++ b/mobile/lib/pages/settings pages/accounts.dart @@ -12,28 +12,41 @@ class AccountsSettings extends StatelessWidget { @override Widget build(BuildContext context) { final user = context.watch().currentUser; + final theme = Theme.of(context); return Scaffold( - backgroundColor: Colors.white, + backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( - title: const Text('Accounts'), - backgroundColor: Colors.white, + title: Text( + 'Accounts', + style: TextStyle(color: theme.colorScheme.onSurface), + ), + iconTheme: IconThemeData(color: theme.colorScheme.onSurface), + backgroundColor: theme.scaffoldBackgroundColor, elevation: 0, scrolledUnderElevation: 0, ), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), children: [ - const SizedBox( + SizedBox( width: double.infinity, height: 150, child: Padding( // Moved padding OUTSIDE the Hero - padding: EdgeInsets.only(left: 16.0, top: 10.0, bottom: 10.0), + padding: const EdgeInsets.only( + left: 16.0, + top: 10.0, + bottom: 10.0, + ), child: Hero( tag: 'User Profile', child: CircleAvatar( - backgroundColor: Color(0xFFD6E4FF), - child: Icon(Icons.person, color: Color(0xFF1890FF), size: 40), + backgroundColor: theme.colorScheme.primaryContainer, + child: Icon( + Icons.person, + color: theme.colorScheme.onPrimaryContainer, + size: 40, + ), ), ), ), @@ -49,9 +62,10 @@ class AccountsSettings extends StatelessWidget { children: [ Text( user != null ? user.displayName : 'Profile N/A', - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, ), ), GestureDetector( @@ -72,7 +86,10 @@ class AccountsSettings extends StatelessWidget { user != null ? '@${user.username}' : 'Could not load profile', - style: const TextStyle(fontSize: 12), + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurfaceVariant, + ), ), ), ], @@ -81,12 +98,12 @@ class AccountsSettings extends StatelessWidget { ), ), const SizedBox(height: 32), - const Padding( - padding: EdgeInsets.only(left: 16, bottom: 8), + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8), child: Text( "Details", style: TextStyle( - color: Colors.black38, + color: theme.colorScheme.primary, fontWeight: FontWeight.bold, fontSize: 13, ), @@ -95,15 +112,22 @@ class AccountsSettings extends StatelessWidget { // Email Tile ListTile( - leading: const Icon(Icons.email_outlined, color: Colors.black54), + leading: Icon( + Icons.email_outlined, + color: theme.colorScheme.onSurfaceVariant, + ), title: const Text("Email"), subtitle: Text( user?.email != null && user!.email!.isNotEmpty ? user.email! : "Not provided", - style: const TextStyle(color: Colors.black87), + style: TextStyle(color: theme.colorScheme.onSurface), + ), + trailing: Icon( + Icons.edit, + size: 18, + color: theme.colorScheme.onSurfaceVariant, ), - trailing: const Icon(Icons.edit, size: 18, color: Colors.black38), onTap: () { // TODO: Navigate to edit email page }, @@ -111,32 +135,49 @@ class AccountsSettings extends StatelessWidget { // Phone Number Tile ListTile( - leading: const Icon(Icons.phone_outlined, color: Colors.black54), + leading: Icon( + Icons.phone_outlined, + color: theme.colorScheme.onSurfaceVariant, + ), title: const Text("Phone Number"), - subtitle: const Text( + subtitle: Text( "Not provided", - style: TextStyle(color: Colors.black87), + style: TextStyle(color: theme.colorScheme.onSurface), + ), + trailing: Icon( + Icons.edit, + size: 18, + color: theme.colorScheme.onSurfaceVariant, ), - trailing: const Icon(Icons.edit, size: 18, color: Colors.black38), onTap: () {}, ), // Bio Tile ListTile( - leading: const Icon(Icons.info_outline, color: Colors.black54), + leading: Icon( + Icons.info_outline, + color: theme.colorScheme.onSurfaceVariant, + ), title: const Text("About"), - subtitle: const Text( + subtitle: Text( "Hey there! I am using Elephant.", - style: TextStyle(color: Colors.black87), + style: TextStyle(color: theme.colorScheme.onSurface), + ), + trailing: Icon( + Icons.edit, + size: 18, + color: theme.colorScheme.onSurfaceVariant, ), - trailing: const Icon(Icons.edit, size: 18, color: Colors.black38), onTap: () {}, ), const SizedBox(height: 30), FilledButton( - style: const ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Colors.redAccent), + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll(theme.colorScheme.error), + foregroundColor: WidgetStatePropertyAll( + theme.colorScheme.onError, + ), ), onPressed: () async { await context.read().logout(); diff --git a/mobile/lib/pages/settings pages/appearance.dart b/mobile/lib/pages/settings pages/appearance.dart index b887b2b..709d0ee 100644 --- a/mobile/lib/pages/settings pages/appearance.dart +++ b/mobile/lib/pages/settings pages/appearance.dart @@ -1,10 +1,173 @@ import 'package:flutter/material.dart'; +import 'package:mobile/themes/app_themes.dart'; +import 'package:mobile/themes/theme_provider.dart'; +import 'package:provider/provider.dart'; class AppearanceSettings extends StatelessWidget { const AppearanceSettings({super.key}); @override Widget build(BuildContext context) { - return Scaffold(appBar: AppBar(title: Text('Appearance'))); + final themeProvider = context.watch(); + + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + title: const Text('Appearance'), + backgroundColor: Theme.of( + context, + ).colorScheme.surface, // Glass effect color + elevation: 0, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Select a Theme', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 24), + Expanded( + child: GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 0.65, // Makes the cards taller like a phone + children: [ + _ThemeCard( + title: 'Frost', + type: ThemeType.frost, + themeData: AppThemes.frost, + isSelected: themeProvider.currentTheme == ThemeType.frost, + ), + _ThemeCard( + title: 'Aura', + type: ThemeType.aura, + themeData: AppThemes.aura, + isSelected: themeProvider.currentTheme == ThemeType.aura, + ), + _ThemeCard( + title: 'Onyx', + type: ThemeType.onyx, + themeData: AppThemes.onyx, + isSelected: themeProvider.currentTheme == ThemeType.onyx, + ), + _ThemeCard( + title: 'Nebula', + type: ThemeType.nebula, + themeData: AppThemes.nebula, + isSelected: + themeProvider.currentTheme == ThemeType.nebula, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ThemeCard extends StatelessWidget { + final String title; + final ThemeType type; + final ThemeData themeData; + final bool isSelected; + + const _ThemeCard({ + required this.title, + required this.type, + required this.themeData, + required this.isSelected, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + context.read().setTheme(type); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: themeData.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: isSelected + ? themeData.colorScheme.primary + : Colors.transparent, + width: 3, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: themeData.colorScheme.primary.withValues(alpha: 0.4), + blurRadius: 12, + ), + ] + : [const BoxShadow(color: Colors.black12, blurRadius: 8)], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(21), + child: Stack( + children: [ + // Mock Chat bubble + Positioned( + right: 16, + top: 60, + child: Container( + width: 60, + height: 30, + decoration: BoxDecoration( + color: themeData.colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), + ), + ), + // Mock Glass AppBar + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: 40, + color: themeData.colorScheme.surface, + ), + ), + // Mock Glass NavBar + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 40, + color: themeData.colorScheme.surface, + ), + ), + // Theme Title + Positioned( + bottom: 50, + left: 0, + right: 0, + child: Center( + child: Text( + title, + style: TextStyle( + color: themeData.colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ); } } diff --git a/mobile/lib/pages/settings_page.dart b/mobile/lib/pages/settings_page.dart index 9b342c7..2ad31c0 100644 --- a/mobile/lib/pages/settings_page.dart +++ b/mobile/lib/pages/settings_page.dart @@ -19,58 +19,67 @@ void showUserCard(BuildContext context) { final user = context.read().currentUser; final String qrData = user?.username ?? 'unknown'; - return Dialog( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Share Your QR!', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black87, + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Dialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: Theme.of(context).colorScheme.surface, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Share Your QR!', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), ), - ), - const SizedBox(height: 24), + const SizedBox(height: 24), - QrImageView( - data: qrData, - version: QrVersions.auto, - size: 200.0, - backgroundColor: Colors.white, - eyeStyle: const QrEyeStyle( - eyeShape: QrEyeShape.square, - color: Colors.black87, + QrImageView( + data: qrData, + version: QrVersions.auto, + size: 200.0, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Theme.of(context).colorScheme.onSurface, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).colorScheme.onSurface, + ), ), - dataModuleStyle: const QrDataModuleStyle( - dataModuleShape: QrDataModuleShape.square, - color: Colors.black87, - ), - ), - const SizedBox(height: 16), - Text( - '@$qrData', - style: const TextStyle( - fontSize: 16, - color: Colors.black54, - fontWeight: FontWeight.w500, + const SizedBox(height: 16), + Text( + '@$qrData', + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), ), - ), - const SizedBox(height: 24), - FilledButton( - onPressed: () => Navigator.pop(context), - style: FilledButton.styleFrom( - backgroundColor: const Color(0xFF1890FF), - minimumSize: const Size(double.infinity, 45), + const SizedBox(height: 24), + FilledButton( + onPressed: () => Navigator.pop(context), + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + minimumSize: const Size(double.infinity, 45), + ), + child: const Text('Close'), ), - child: const Text('Close'), - ), - ], + ], + ), ), ), ); @@ -78,7 +87,6 @@ void showUserCard(BuildContext context) { ); } - class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -87,6 +95,7 @@ class SettingsPage extends StatelessWidget { final user = context.watch().currentUser; return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: CustomScrollView( slivers: [ SliverAppBar( @@ -102,16 +111,18 @@ class SettingsPage extends StatelessWidget { child: BackdropFilter( enabled: true, filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: Container( - color: const Color.fromARGB(78, 255, 255, 255), - ), + child: Container(color: Theme.of(context).colorScheme.surface), ), ), actionsPadding: const EdgeInsets.symmetric(horizontal: 15), - title: const Text( + title: Text( 'Settings', - style: TextStyle(letterSpacing: 1, fontWeight: FontWeight.w400), - key: ValueKey('title'), + style: TextStyle( + letterSpacing: 1, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + key: const ValueKey('title'), ), actions: [ @@ -125,59 +136,77 @@ class SettingsPage extends StatelessWidget { ), SliverList( delegate: SliverChildListDelegate([ - Container( - margin: EdgeInsets.all(20), - padding: EdgeInsets.all(20), - width: double.infinity, - height: 150, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(width: 2, color: Colors.black26), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Hero( - tag: 'User Profile', - child: CircleAvatar( - radius: 40, - backgroundImage: user?.avatarUrl != null - ? NetworkImage(user!.avatarUrl!) - : null, - child: user?.avatarUrl == null - ? const Icon(Icons.person, size: 50) - : null, - ), + GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => AccountsSettings()), + ); + }, + child: Container( + margin: EdgeInsets.all(20), + padding: EdgeInsets.all(20), + width: double.infinity, + height: 150, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.outlineVariant, ), - SizedBox(width: 20), - Hero( - tag: 'User Data', - child: Material( - type: MaterialType.transparency, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Spacer(), - Text( - user != null ? user.displayName : 'Profile N/A', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: 'User Profile', + child: CircleAvatar( + radius: 40, + backgroundImage: user?.avatarUrl != null + ? NetworkImage(user!.avatarUrl!) + : null, + child: user?.avatarUrl == null + ? const Icon(Icons.person, size: 50) + : null, + ), + ), + SizedBox(width: 20), + Hero( + tag: 'User Data', + child: Material( + type: MaterialType.transparency, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Spacer(), + Text( + user != null ? user.displayName : 'Profile N/A', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Theme.of( + context, + ).colorScheme.onSurface, + ), + ), + Text( + user != null + ? '@${user.username}' + : 'Could not load profile', + style: TextStyle( + fontSize: 12, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), ), - ), - Text( - user != null - ? '@${user.username}' - : 'Could not load profile', - style: TextStyle(fontSize: 12), - ), - Spacer(), - ], + Spacer(), + ], + ), ), ), - ), - ], + ], + ), ), ), @@ -186,9 +215,12 @@ class SettingsPage extends StatelessWidget { margin: EdgeInsets.all(20), width: double.infinity, decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), - border: Border.all(width: 2, color: Colors.black26), + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.outlineVariant, + ), ), child: Column( children: [ @@ -259,7 +291,7 @@ Widget _settingsOption({ required BuildContext context, required IconData icon, required String settingName, - Widget trailing = const Icon(Icons.chevron_right), + Widget trailing = const SizedBox.shrink(), Widget? whereTo, }) { return InkWell( @@ -276,11 +308,32 @@ Widget _settingsOption({ height: 60, child: Row( children: [ - CircleAvatar(child: Icon(icon)), - SizedBox(width: 20), - Text(settingName), - Spacer(), - trailing, + CircleAvatar( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + child: Icon( + icon, + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + ), + const SizedBox(width: 20), + Text( + settingName, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500, + fontSize: 15, + ), + ), + const Spacer(), + trailing == const SizedBox.shrink() + ? Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + : trailing, ], ), ), diff --git a/mobile/lib/pages/phone_number_page.dart b/mobile/lib/pages/temp/phone_number_page.dart similarity index 99% rename from mobile/lib/pages/phone_number_page.dart rename to mobile/lib/pages/temp/phone_number_page.dart index 81304ac..3eecdc5 100644 --- a/mobile/lib/pages/phone_number_page.dart +++ b/mobile/lib/pages/temp/phone_number_page.dart @@ -1,4 +1,4 @@ -import 'package:mobile/pages/profile_setup_page.dart'; +import 'package:mobile/pages/temp/profile_setup_page.dart'; import 'package:mobile/providers/timer_provider.dart'; import 'package:flutter/material.dart'; import 'package:intl_phone_field/intl_phone_field.dart'; diff --git a/mobile/lib/pages/profile_setup_page.dart b/mobile/lib/pages/temp/profile_setup_page.dart similarity index 100% rename from mobile/lib/pages/profile_setup_page.dart rename to mobile/lib/pages/temp/profile_setup_page.dart diff --git a/mobile/lib/pages/welcome_page.dart b/mobile/lib/pages/temp/welcome_page.dart similarity index 96% rename from mobile/lib/pages/welcome_page.dart rename to mobile/lib/pages/temp/welcome_page.dart index 6009ee7..0064340 100644 --- a/mobile/lib/pages/welcome_page.dart +++ b/mobile/lib/pages/temp/welcome_page.dart @@ -1,4 +1,4 @@ -import 'package:mobile/pages/phone_number_page.dart'; +import 'package:mobile/pages/temp/phone_number_page.dart'; import 'package:mobile/players/intro_animation.dart'; import 'package:flutter/material.dart'; diff --git a/mobile/lib/themes/app_themes.dart b/mobile/lib/themes/app_themes.dart new file mode 100644 index 0000000..95e4515 --- /dev/null +++ b/mobile/lib/themes/app_themes.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +enum ThemeType { frost, aura, onyx, nebula } + +class AppThemes { + static final ThemeData frost = ThemeData( + brightness: Brightness.light, + scaffoldBackgroundColor: const Color(0xFFF5F7FA), + colorScheme: const ColorScheme.light( + primary: Color(0xFF007AFF), + surface: Color(0x99FFFFFF), + onSurface: Colors.black87, + ), + useMaterial3: true, + ); + + static final ThemeData aura = ThemeData( + brightness: Brightness.light, + scaffoldBackgroundColor: const Color(0xFFFFFBF7), + colorScheme: const ColorScheme.light( + primary: Color(0xFFFF8A65), + surface: Color(0x99FFF0E5), + onSurface: Color(0xFF3E2723), + ), + useMaterial3: true, + ); + + static final ThemeData onyx = ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF121212), + colorScheme: const ColorScheme.dark( + primary: Color(0xFFE0E0E0), + surface: Color(0x991E1E1E), + onSurface: Colors.white, + ), + useMaterial3: true, + ); + + static final ThemeData nebula = ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0B0914), + colorScheme: const ColorScheme.dark( + primary: Color(0xFF8A2BE2), + surface: Color(0x99191330), + onSurface: Color(0xFFE6E6FA), + ), + useMaterial3: true, + ); + + static ThemeData getTheme(ThemeType type) { + switch (type) { + case ThemeType.frost: + return frost; + case ThemeType.aura: + return aura; + case ThemeType.onyx: + return onyx; + case ThemeType.nebula: + return nebula; + } + } +} diff --git a/mobile/lib/themes/theme_provider.dart b/mobile/lib/themes/theme_provider.dart new file mode 100644 index 0000000..a98380d --- /dev/null +++ b/mobile/lib/themes/theme_provider.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:mobile/themes/app_themes.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeProvider extends ChangeNotifier { + ThemeType _currentTheme = ThemeType.onyx; + bool _isInitialized = false; + + ThemeType get currentTheme => _currentTheme; + ThemeData get themeData => AppThemes.getTheme(_currentTheme); + bool get isInitialized => _isInitialized; + + ThemeProvider() { + _loadTheme(); + } + + Future _loadTheme() async { + final prefs = await SharedPreferences.getInstance(); + final savedThemeIndex = prefs.getInt('app_theme'); + + if (savedThemeIndex != null && + savedThemeIndex >= 0 && + savedThemeIndex < ThemeType.values.length) { + _currentTheme = ThemeType.values[savedThemeIndex]; + } else { + _currentTheme = ThemeType.onyx; + } + + _isInitialized = true; + notifyListeners(); + } + + Future setTheme(ThemeType themeType) async { + if (_currentTheme != themeType) { + _currentTheme = themeType; + notifyListeners(); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('app_theme', themeType.index); + } + } +} diff --git a/mobile/lib/widgets/chat_screen_modular_widgets.dart b/mobile/lib/widgets/chat_screen_modular_widgets.dart index 74af9dc..5f4fba1 100644 --- a/mobile/lib/widgets/chat_screen_modular_widgets.dart +++ b/mobile/lib/widgets/chat_screen_modular_widgets.dart @@ -30,7 +30,7 @@ class GlassAppBar extends StatelessWidget implements PreferredSizeWidget { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: AppBar( - backgroundColor: theme.scaffoldBackgroundColor.withValues(alpha: 0.4), + backgroundColor: theme.colorScheme.surface, elevation: 0, scrolledUnderElevation: 0, leading: IconButton( @@ -45,7 +45,10 @@ class GlassAppBar extends StatelessWidget implements PreferredSizeWidget { tag: 'profile', child: CircleAvatar( backgroundColor: theme.colorScheme.primaryContainer, - child: Icon(Icons.person, color: theme.colorScheme.primary), + child: Icon( + Icons.person, + color: theme.colorScheme.onPrimary, + ), ), ), const SizedBox(width: 12), @@ -124,6 +127,7 @@ class ChatBubble extends StatelessWidget { Color primaryColor, Color onPrimary, Color onSurface, + BuildContext context, ) { return MarkdownStyleSheet( p: TextStyle(color: isMe ? onPrimary : onSurface, fontSize: 15), @@ -145,7 +149,7 @@ class ChatBubble extends StatelessWidget { backgroundColor: isMe ? primaryColor.withValues(alpha: 0.8) : Colors.grey.withValues(alpha: 0.3), - color: isMe ? onPrimary : Colors.red[800], + color: isMe ? onPrimary : Theme.of(context).colorScheme.error, fontFamily: 'monospace', ), a: TextStyle( @@ -248,6 +252,7 @@ class ChatBubble extends StatelessWidget { theme.colorScheme.primary, theme.colorScheme.onPrimary, theme.colorScheme.onSurface, + context, ), ), const SizedBox(height: 4), @@ -271,7 +276,9 @@ class ChatBubble extends StatelessWidget { isRead ? Icons.done_all : Icons.done, size: 14, color: isRead - ? Colors.lightBlueAccent + ? (isMe + ? Colors.lightBlueAccent + : theme.colorScheme.primary) : theme.colorScheme.onPrimary.withValues( alpha: 0.7, ), @@ -379,7 +386,15 @@ class _ChatInputAreaState extends State { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: Container( - color: Colors.transparent, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: theme.colorScheme.surface.withValues(alpha: 0.8), + width: 1, + ), + ), + ), padding: EdgeInsets.only( left: 12.0, right: 12.0, @@ -469,7 +484,10 @@ class _ChatInputAreaState extends State { padding: const EdgeInsets.only(bottom: 4), child: IconButton( icon: _isOverLimit - ? const Icon(Icons.not_interested) + ? Icon( + Icons.not_interested, + color: theme.colorScheme.error, + ) : Icon(Icons.send, color: theme.colorScheme.primary), onPressed: _isOverLimit ? null : _submit, ), @@ -480,4 +498,4 @@ class _ChatInputAreaState extends State { ), ); } -} \ No newline at end of file +} diff --git a/mobile/lib/widgets/custom_cards.dart b/mobile/lib/widgets/custom_cards.dart index f8dcc53..4a8e71a 100644 --- a/mobile/lib/widgets/custom_cards.dart +++ b/mobile/lib/widgets/custom_cards.dart @@ -57,7 +57,7 @@ class CustomChatCard extends StatelessWidget { return Material( color: isSelected - ? Colors.blue.withValues(alpha: 0.1) + ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.12) : Colors.transparent, child: InkWell( onLongPress: onLongPress, @@ -91,10 +91,12 @@ class CustomChatCard extends StatelessWidget { children: [ CircleAvatar( radius: 26, - backgroundColor: const Color(0xFFD6E4FF), + backgroundColor: Theme.of( + context, + ).colorScheme.primaryContainer, child: Icon( conversation.isGroup ? Icons.group : Icons.person, - color: const Color(0xFF1890FF), + color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), Positioned( @@ -105,13 +107,13 @@ class CustomChatCard extends StatelessWidget { duration: const Duration(milliseconds: 250), curve: Curves.easeOutBack, child: Container( - decoration: const BoxDecoration( - color: Colors.white, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, shape: BoxShape.circle, ), - child: const Icon( + child: Icon( Icons.check_circle, - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, size: 22, ), ), @@ -127,10 +129,10 @@ class CustomChatCard extends StatelessWidget { children: [ Text( conversation.title, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: Colors.black87, + color: Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(height: 4), @@ -141,7 +143,9 @@ class CustomChatCard extends StatelessWidget { maxLines: 1, textOverflow: TextOverflow.ellipsis, style: TextStyle( - color: hasUnread ? Colors.black87 : Colors.black54, + color: hasUnread + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 15, ), ), @@ -156,8 +160,8 @@ class CustomChatCard extends StatelessWidget { timeLabel, style: TextStyle( color: hasUnread - ? const Color(0xFF1890FF) - : Colors.black38, + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: hasUnread ? FontWeight.w600 @@ -176,7 +180,7 @@ class CustomChatCard extends StatelessWidget { vertical: 2, ), decoration: BoxDecoration( - color: const Color(0xFF1890FF), + color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(10), ), alignment: Alignment.center, @@ -184,8 +188,8 @@ class CustomChatCard extends StatelessWidget { conversation.unreadCount > 99 ? "99+" : "${conversation.unreadCount}", - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, fontSize: 11, fontWeight: FontWeight.bold, ),