diff --git a/backend/main.py b/backend/main.py index a2c1eb2..15b3cc5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,7 +16,7 @@ edit_task, delete_task, get_user_task, - get_group + get_group, create_group, leave_group, delete_group, @@ -234,8 +234,24 @@ def handle_login() -> Response: make_new_log("login", e) return response_json + try: + data_con = connect("data/data.db") + data_cursor = data_con.cursor() + data_cursor.execute( + "SELECT username FROM user WHERE uuid = ?;", + (user_id,), + ) + username = data_cursor.fetchone()[0] + data_con.close() + except Exception as e: + response_json = jsonify( + [{"error_no": "2", "message": "Trouble with backend! Sorry!"}] + ) + make_new_log("login", e) + return response_json + response_json = jsonify( - [{"error_no": "0", "message": "success", "user_id": user_id}] + [{"error_no": "0", "message": "success", "user_id": user_id, "username": username}] ) return response_json @@ -1061,7 +1077,7 @@ def handle_invite_to_group() -> Response: try: # Verify password before inviting - if not check_password(connect("../data/data.db"), inviter_id, password): + if not check_password(connect("data/data.db"), inviter_id, password): response_json = jsonify( [{"error_no": "4", "message": "Password is incorrect"}] ) @@ -1174,6 +1190,7 @@ def handle_respond_to_invite() -> Response: response_json = jsonify([{"error_no": "0", "message": f"Successfully {status} invitation"}]) return response_json + if __name__ == "__main__": print("Running main.py") make_new_log("main", "Server started") # type: ignore diff --git a/roomiebuddy/lib/NavScreen.dart b/roomiebuddy/lib/NavScreen.dart index 73a9ebc..33da0ee 100644 --- a/roomiebuddy/lib/NavScreen.dart +++ b/roomiebuddy/lib/NavScreen.dart @@ -6,7 +6,7 @@ import 'package:roomiebuddy/providers/theme_provider.dart'; import 'pages/home_page.dart'; import 'pages/calendar_page.dart'; import 'pages/add_taskpage.dart'; -import 'pages/add_roomate_page.dart'; +import 'pages/group_page.dart'; import 'pages/settings_page.dart'; class Navscreen extends StatefulWidget { @@ -21,7 +21,7 @@ class _NavscreenState extends State { HomePage(), CalendarPage(), AddTaskpage(), - AddRoomatePage(), + GroupPage(), SettingsPage(), ]; int selectedIndex = 0; diff --git a/roomiebuddy/lib/main.dart b/roomiebuddy/lib/main.dart index 8fc9fa7..a72173b 100644 --- a/roomiebuddy/lib/main.dart +++ b/roomiebuddy/lib/main.dart @@ -17,13 +17,12 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + return MaterialApp( debugShowCheckedModeBanner: false, title: 'Roomie Buddy', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), + theme: themeProvider.themeData, home: const LoginScreen(clearCredentials: false), // Set to false to enable "remember me" functionality ); } diff --git a/roomiebuddy/lib/pages/add_roomate_page.dart b/roomiebuddy/lib/pages/add_roomate_page.dart deleted file mode 100644 index e7f60c8..0000000 --- a/roomiebuddy/lib/pages/add_roomate_page.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:roomiebuddy/providers/theme_provider.dart'; - -class AddRoomatePage extends StatefulWidget { - const AddRoomatePage({super.key}); - - @override - State createState() => _AddRoomatePageState(); -} - -class _AddRoomatePageState extends State { - - String nickName = "Naruto"; - String userID = "235454"; - String groupName = "House of Buddies"; - String groupID = "103958"; - - bool hasRequests = true; - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - final TextEditingController invitedUser = TextEditingController(); //send to back end at somepoint - - return Scaffold( - appBar: AppBar( - title: Text('Add Roommate'), - ), - body: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - - // -------------------- Profile Name & acc# -------------------- // - const Text( - 'About You', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 30), - //Username - Row( - children: [ - const Text( - 'My Nickname: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - nickName, - style: const TextStyle( - fontSize: 24, - //fontWeight: FontWeight.bold, - ), - ), - ] - ), - //User ID - Row( - children: [ - const Text( - 'User ID: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - userID, - style: const TextStyle( - fontSize: 24, - ), - ), - const SizedBox(width: 10), - - //Copy to clipboard button (user ID) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: userID)); - }, - child: Icon( - Icons.copy, - color: themeProvider.lightTextColor, - ), - ), - ] - ), - const SizedBox(height: 20,), - - // -------------------- Group Specs -------------------- // - const Text( - 'Current Group', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 30), - //Group Name - Row( - children: [ - const Text( - 'Group Name: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - groupName, - style: const TextStyle( - fontSize: 24, - //fontWeight: FontWeight.bold, - ), - ), - ] - ), - Row( - children: [ - const Text( - 'Group ID: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - groupID, - style: const TextStyle( - fontSize: 24, - //fontWeight: FontWeight.bold, - ), - ), - - //Copy to Clipboard (group ID) - const SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: groupID)); - }, - child: Icon( - Icons.copy, - color: themeProvider.lightTextColor, - ), - ), - ] - ), - const SizedBox(height: 20), - - // -------------------- Add Roomate -------------------- // - const Text( - 'Add Roommate', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: TextField( - controller: invitedUser, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Rommie Buddy User ID', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ), - - const SizedBox(width: 10), - - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () {}, - child: Text('Invite', style: TextStyle(color: themeProvider.lightTextColor, fontSize: 20)), - ), - ] - ), - const SizedBox(height: 20), - - // -------------------- Join Group -------------------- // - const Text( - 'Join Group', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: TextField( - controller: invitedUser, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Roomie Buddy Group ID', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ), - - const SizedBox(width: 10), - - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () {}, - child: Text('Request Join', style: TextStyle(color: themeProvider.lightTextColor, fontSize: 20)), - ), - ] - ), - const SizedBox(height: 20), - - // -------------------- Pending Requests -------------------- // - const Text( - 'Pending Requests', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - - const SingleChildScrollView( - - child: Column( - - children: [ - Card( - child: ListTile( - title: Text('William wants to join House of Buddies'), - - ), - ), - - Card( - child: ListTile( - title: Text('William has invited you to Doofenshmirtz Evil Incorporated'), - - ), - ), - ] - ) - - - ), - - //No task display - const SizedBox(height: 50), - Text( - hasRequests? '':'No pending join requests or invites at this time.', - ), - - ] //Children - - ) - ) - ); - } -} \ No newline at end of file diff --git a/roomiebuddy/lib/pages/add_taskpage.dart b/roomiebuddy/lib/pages/add_taskpage.dart index 8ddefbe..016e2f2 100644 --- a/roomiebuddy/lib/pages/add_taskpage.dart +++ b/roomiebuddy/lib/pages/add_taskpage.dart @@ -25,7 +25,13 @@ class _AddTaskpageState extends State { return Scaffold( appBar: AppBar( - title: Text('Add Task', style: TextStyle(color: themeProvider.lightTextColor)), + title: Text( + 'Add Task', + style: TextStyle( + color: themeProvider.currentTextColor, + fontWeight: FontWeight.bold + ), + ), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), diff --git a/roomiebuddy/lib/pages/calendar_page.dart b/roomiebuddy/lib/pages/calendar_page.dart index 5d7a97e..a88182f 100644 --- a/roomiebuddy/lib/pages/calendar_page.dart +++ b/roomiebuddy/lib/pages/calendar_page.dart @@ -31,7 +31,13 @@ class _CalendarPageState extends State { return Scaffold( appBar: AppBar( - title: const Text('Task Calendar'), + title: Text( + 'Task Calendar', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Padding( padding: const EdgeInsets.all(16.0), diff --git a/roomiebuddy/lib/pages/group_page.dart b/roomiebuddy/lib/pages/group_page.dart new file mode 100644 index 0000000..228b7a8 --- /dev/null +++ b/roomiebuddy/lib/pages/group_page.dart @@ -0,0 +1,557 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:roomiebuddy/providers/theme_provider.dart'; +import 'package:roomiebuddy/services/api_service.dart'; +import 'package:roomiebuddy/services/auth_storage.dart'; + +class GroupPage extends StatefulWidget { + const GroupPage({super.key}); + + @override + State createState() => _GroupPageState(); +} + +class _GroupPageState extends State { + final _userIdController = TextEditingController(); + final _createGroupNameController = TextEditingController(); + + final ApiService _apiService = ApiService(); + final AuthStorage _authStorage = AuthStorage(); + + final Map _loadingInvites = {}; + + String _userId = ""; + String _password = ""; + List> _userGroups = []; + String? _selectedGroupId; + List> _pendingInvites = []; + String _errorMessage = ""; + bool _isLoading = true; + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + @override + void dispose() { + _userIdController.dispose(); + _createGroupNameController.dispose(); + super.dispose(); + } + + Future _loadUserData() async { + setState(() { + _isLoading = true; + _errorMessage = ""; + }); + + try { + // Get user credentials + final userId = await _authStorage.getUserId(); + final password = await _authStorage.getPassword(); + + // Ensure necessary credentials exist + if (userId == null || password == null) { + setState(() { + _isLoading = false; + _errorMessage = "User not logged in"; + }); + return; + } + _userId = userId; + _password = password; + + // Get user groups + final groupsResponse = await _apiService.getGroupList(userId, password); + if (!groupsResponse['success']) { + setState(() { + _errorMessage = "Failed to load groups: ${groupsResponse['message']}"; + }); + } else { + final groups = groupsResponse['message'] as Map; + _userGroups = groups.values.map((group) => group as Map).toList(); + _selectedGroupId = null; + } + + // Get pending invites + final invitesResponse = await _apiService.getPendingInvites(userId, password); + if (!invitesResponse['success']) { + setState(() { + _errorMessage = "Failed to load invites: ${invitesResponse['message']}"; + }); + } else { + final invites = invitesResponse['message'] as Map; + _pendingInvites = invites.values.map((invite) => invite as Map).toList(); + } + } catch (e) { + setState(() { + _errorMessage = "Error: $e"; + }); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future _createGroup() async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + + // Ensure group name is entered + if (_createGroupNameController.text.isEmpty) { + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Please enter a group name')), + ); + return; + } + + setState(() => _isSubmitting = true); + + try { + final response = await _apiService.createGroup( + _userId, + _password, + _createGroupNameController.text.trim(), + '', + ); + + if (response['success']) { + + // Clear the form + _createGroupNameController.clear(); + + // Let the user know group was created successfully + if (!mounted) return; + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Group created successfully')), + ); + + // Refresh the group list + try { + final groupsResponse = await _apiService.getGroupList(_userId, _password); + if (groupsResponse['success']) { + final groups = groupsResponse['message'] as Map; + if(mounted){ + setState(() { + _userGroups = groups.values.map((group) => group as Map).toList(); + }); + } + } + } catch (e) { + // Silently handle error + } + } else { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Failed to create group: ${response['message']}')), + ); + } + } catch (e) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + if(mounted){ + setState(() => _isSubmitting = false); + } + } + } + + Future _inviteUser() async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + + // Ensure user ID and group are entered + if (_userIdController.text.isEmpty || _selectedGroupId == null) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Please enter a user ID and select a group')), + ); + return; + } + + setState(() => _isSubmitting = true); + + try { + final response = await _apiService.inviteToGroup( + _userId, + _userIdController.text.trim(), + _selectedGroupId!, + _password, + ); + + if (response['success']) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Invitation sent successfully')), + ); + _userIdController.clear(); + } else { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Failed to send invitation: ${response['message']}')), + ); + } + } catch (e) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + if(mounted){ + setState(() => _isSubmitting = false); + } + } + } + + Future _respondToInvite(String inviteId, String status) async { + setState(() { + _loadingInvites[inviteId] = true; + }); + + try { + final response = await _apiService.respondToInvite( + _userId, + inviteId, + status, + _password, + ); + + if (response['success']) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Successfully $status invitation')), + ); + + // Update local state + setState(() { + // Remove the invite locally + _pendingInvites.removeWhere((invite) => invite['invite_id'] == inviteId); + + // If accepted, refresh the groups list + if (status == 'accepted') { + // Get updated groups + _refreshGroupsList(); + } + }); + } else { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to $status invitation: ${response['message']}')), + ); + } + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + // Clear loading state for this invite + setState(() { + _loadingInvites.remove(inviteId); + }); + } + } + + Future _refreshGroupsList() async { + try { + final groupsResponse = await _apiService.getGroupList(_userId, _password); + if (groupsResponse['success']) { + final groups = groupsResponse['message'] as Map; + setState(() { + _userGroups = groups.values.map((group) => group as Map).toList(); + }); + } + } catch (e) { + // Silently handle error + } + } + + @override + Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + + if (_isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + if (_errorMessage.isNotEmpty) { + return Scaffold( + appBar: AppBar(title: const Text('Group Management')), + body: Center(child: Text(_errorMessage)), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text( + 'Group Management', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // -------------------- Create Group -------------------- // + Text( + 'Create Group', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _createGroupNameController, + decoration: InputDecoration( + hintText: 'Enter group name', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 80, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeProvider.themeColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onPressed: _isSubmitting ? null : _createGroup, + child: _isSubmitting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: themeProvider.currentTextColor, + ), + ) + : Text( + 'Create', + style: TextStyle(color: themeProvider.currentTextColor), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // -------------------- Invite Roommate -------------------- // + Text( + 'Invite Roommate', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + + if (_userGroups.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: DropdownButtonFormField( + isExpanded: true, + decoration: InputDecoration( + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + filled: true, + fillColor: themeProvider.currentInputFill, + ), + dropdownColor: themeProvider.currentInputFill, + borderRadius: BorderRadius.circular(8), + value: _selectedGroupId, + hint: const Text('Select Group'), + items: _userGroups.map((group) => + DropdownMenuItem( + value: group['group_id'], + child: Text(group['name'], overflow: TextOverflow.ellipsis), + ) + ).toList(), + onChanged: (value) { + setState(() { + _selectedGroupId = value; + }); + }, + ), + ), + + Row( + children: [ + Expanded( + child: TextField( + controller: _userIdController, + decoration: InputDecoration( + hintText: 'Enter user ID', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 80, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeProvider.themeColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onPressed: _isSubmitting ? null : _inviteUser, + child: _isSubmitting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: themeProvider.currentTextColor, + ), + ) + : Text( + 'Invite', + style: TextStyle(color: themeProvider.currentTextColor), + ), + ), + ), + ], + ), + ], + ) + else + const Text('You must be in a group to invite a roommate.'), + + const SizedBox(height: 16), + + // -------------------- Pending Invites -------------------- // + Text( + 'Pending Invites', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + + Container( + height: 425, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: themeProvider.currentInputFill, + borderRadius: BorderRadius.circular(8), + ), + child: _pendingInvites.isNotEmpty + ? ListView.builder( + itemCount: _pendingInvites.length, + itemBuilder: (context, index) { + final invite = _pendingInvites[index]; + final isLoadingInvite = _loadingInvites[invite['invite_id']] == true; + + return Card( + color: themeProvider.currentBackground, + margin: const EdgeInsets.only(bottom: 12), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + // Group Name + title: Text( + invite['group_name'], + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: themeProvider.currentTextColor, + ), + overflow: TextOverflow.ellipsis, + ), + // Inviter Name + subtitle: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + 'Invited by ${invite['inviter_name']}', + style: TextStyle( + fontSize: 14, + color: themeProvider.currentSecondaryTextColor, + ), + ), + ), + trailing: isLoadingInvite + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: themeProvider.themeColor, + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.check_circle, + color: themeProvider.successColor, + size: 26, + ), + onPressed: () => _respondToInvite(invite['invite_id'], 'accepted'), + tooltip: 'Accept', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: 4), + IconButton( + icon: Icon(Icons.highlight_off, + color: themeProvider.errorColor, + size: 26, + ), + onPressed: () => _respondToInvite(invite['invite_id'], 'rejected'), + tooltip: 'Decline', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + ); + }, + ) + : Center( + child: Text( + 'No pending invites at this time', + style: TextStyle( + color: themeProvider.currentSecondaryTextColor, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/roomiebuddy/lib/pages/home_page.dart b/roomiebuddy/lib/pages/home_page.dart index 4973869..ccf1c7a 100644 --- a/roomiebuddy/lib/pages/home_page.dart +++ b/roomiebuddy/lib/pages/home_page.dart @@ -165,7 +165,7 @@ class HomePageState extends State { style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: themeProvider.lightTextColor, + color: themeProvider.currentTextColor, ), ), ), @@ -177,11 +177,16 @@ class HomePageState extends State { ], ), ), - const Padding( - padding: EdgeInsets.all(16.0), - child: Text('My Tasks', - style: - TextStyle(fontSize: 25, fontWeight: FontWeight.bold)), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'My Tasks', + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), _isLoading ? const Center(child: CircularProgressIndicator()) diff --git a/roomiebuddy/lib/pages/login_screen.dart b/roomiebuddy/lib/pages/login_screen.dart index 1fdcbf4..d5033d5 100644 --- a/roomiebuddy/lib/pages/login_screen.dart +++ b/roomiebuddy/lib/pages/login_screen.dart @@ -112,12 +112,14 @@ class _LoginScreenState extends State { if (result['success']) { // Extract user ID from the response final String userId = result['data']['user_id']; + final String username = result['data']['username']; // Store credentials for future use await _authStorage.storeUserCredentials( userId, emailController.text.trim(), passwordController.text.trim(), + username, ); // Navigate to main screen @@ -159,7 +161,13 @@ class _LoginScreenState extends State { return Scaffold( appBar: AppBar( - title: const Text('Roomie Buddy'), + title: Text( + 'Roomie Buddy', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/roomiebuddy/lib/pages/settings_page.dart b/roomiebuddy/lib/pages/settings_page.dart index 94adb30..82ba93c 100644 --- a/roomiebuddy/lib/pages/settings_page.dart +++ b/roomiebuddy/lib/pages/settings_page.dart @@ -3,6 +3,8 @@ import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:roomiebuddy/pages/login_screen.dart'; +import 'package:roomiebuddy/services/auth_storage.dart'; +import 'package:flutter/services.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -12,6 +14,54 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { + final AuthStorage _authStorage = AuthStorage(); + String _username = ""; + String _userId = ""; + String _errorMessage = ""; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + Future _loadUserData() async { + setState(() { + _isLoading = true; + _errorMessage = ""; + }); + + try { + final userId = await _authStorage.getUserId(); + final username = await _authStorage.getUsername(); + + if (userId == null || username == null) { + setState(() { + _isLoading = false; + _errorMessage = "User data not found"; + }); + return; + } + + if (mounted) { + setState(() { + _userId = userId; + _username = username; + _isLoading = false; + }); + } + + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = "Error loading user data: $e"; + _isLoading = false; + }); + } + } + } + void _openColorPicker(BuildContext context, ThemeProvider themeProvider) { Color pickerColor = themeProvider.themeColor; @@ -19,33 +69,39 @@ class _SettingsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Pick a theme color'), + backgroundColor: themeProvider.currentBackground, + contentPadding: const EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 0.0), content: SingleChildScrollView( child: ColorPicker( pickerColor: pickerColor, onColorChanged: (Color color) { pickerColor = color; }, - pickerAreaHeightPercent: 0.8, + pickerAreaHeightPercent: 0.5, enableAlpha: false, displayThumbColor: true, - labelTypes: const [ - ColorLabelType.hex, - ColorLabelType.rgb, - ColorLabelType.hsv, - ], + labelTypes: const [], paletteType: PaletteType.hsv, ), ), + actionsPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0), + actionsAlignment: MainAxisAlignment.center, actions: [ TextButton( - child: const Text('Cancel'), + child: Text( + 'Cancel', + style: TextStyle(color: themeProvider.currentTextColor), + ), onPressed: () { Navigator.of(context).pop(); }, ), + const SizedBox(width: 4), TextButton( - child: const Text('Apply'), + child: Text( + 'Apply', + style: TextStyle(color: themeProvider.themeColor), + ), onPressed: () { themeProvider.setThemeColor(pickerColor); Navigator.of(context).pop(); @@ -61,42 +117,158 @@ class _SettingsPageState extends State { Widget build(BuildContext context) { final themeProvider = Provider.of(context); + // Wait for the user data to load + if (_isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + // If there is an error, show it + if (_errorMessage.isNotEmpty) { + return Scaffold( + appBar: AppBar(title: const Text('Settings')), + body: Center(child: Text(_errorMessage)), + ); + } + return Scaffold( appBar: AppBar( - title: const Text('Settings'), + title: Text( + 'Settings', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Appearance Section - const Text( + // -------------------- Profile Section -------------------- // + Text( + 'Profile', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + const SizedBox(height: 4), + Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 16.0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + dense: true, + visualDensity: VisualDensity.compact, + contentPadding: EdgeInsets.zero, + leading: Text( + 'Username: ', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + title: Text( + _username, + style: TextStyle( + fontSize: 15, + overflow: TextOverflow.ellipsis, + color: themeProvider.currentTextColor, + ), + ), + ), + ListTile( + dense: true, + visualDensity: VisualDensity.compact, + contentPadding: EdgeInsets.zero, + leading: Text( + 'User ID: ', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + title: Text( + _userId.length > 12 ? "${_userId.substring(0, 11)}..." : _userId, + style: TextStyle( + fontSize: 15, + overflow: TextOverflow.ellipsis, + color: themeProvider.currentTextColor, + ), + ), + trailing: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon( + Icons.copy, + color: themeProvider.currentTextColor, + size: 20, + ), + tooltip: 'Copy User ID', + onPressed: () { + Clipboard.setData(ClipboardData(text: _userId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('User ID copied to clipboard')), + ); + }, + ), + ), + ], + ), + ), + ), + + // -------------------- Appearance Section -------------------- // + Text( 'Appearance', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, ), ), - const SizedBox(height: 12), + const SizedBox(height: 4), // Dark Mode Toggle Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 8.0), child: ListTile( - title: const Text('Dark Mode'), - trailing: Container( - padding: const EdgeInsets.only(right: 0), - width: 50, - child: Switch( - value: themeProvider.isDarkMode, - onChanged: (value) { - themeProvider.toggleDarkMode(); - }, - activeColor: themeProvider.switchActiveThumb, - activeTrackColor: themeProvider.switchActiveTrack, - inactiveThumbColor: themeProvider.switchInactiveThumb, - inactiveTrackColor: themeProvider.switchInactiveTrack, - ), + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 16.0, right: 6.0), + title: Text( + 'Dark Mode', + style: TextStyle(color: themeProvider.currentTextColor, fontSize: 15), + ), + trailing: Switch( + value: themeProvider.isDarkMode, + onChanged: (value) { + themeProvider.toggleDarkMode(); + }, + activeColor: themeProvider.switchActiveThumb, + activeTrackColor: themeProvider.switchActiveTrack, + inactiveThumbColor: themeProvider.switchInactiveThumb, + inactiveTrackColor: themeProvider.switchInactiveTrack, ), ), ), @@ -105,8 +277,20 @@ class _SettingsPageState extends State { // Theme Color Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 8.0), child: ListTile( - title: const Text('Theme Color'), + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0), + title: Text( + 'Theme Color', + style: TextStyle(color: themeProvider.currentTextColor, fontSize: 15), + ), trailing: Container( width: 50, height: 30, @@ -122,24 +306,35 @@ class _SettingsPageState extends State { ), ), - // Account Section - const SizedBox(height: 32), - const Text( + // -------------------- Account Section -------------------- // + const SizedBox(height: 8), + Text( 'Account', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, ), ), - const SizedBox(height: 12), + const SizedBox(height: 4), // Logout Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 8.0), child: ListTile( + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0), title: Text( 'Logout', style: TextStyle( - color: themeProvider.errorColor, + color: themeProvider.currentTextColor, + fontSize: 15, ), ), trailing: Icon( @@ -147,7 +342,6 @@ class _SettingsPageState extends State { color: themeProvider.errorColor, ), onTap: () { - // Show confirmation dialog showDialog( context: context, builder: (BuildContext context) { @@ -157,19 +351,18 @@ class _SettingsPageState extends State { actions: [ TextButton( onPressed: () { - Navigator.of(context).pop(); // Close dialog + Navigator.of(context).pop(); }, child: const Text('Cancel'), ), TextButton( onPressed: () { - // Close dialog Navigator.of(context).pop(); - + // Navigate to login screen Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (context) => const LoginScreen(clearCredentials: true)), - (route) => false, // This removes all previous routes + (route) => false, ); }, child: Text( diff --git a/roomiebuddy/lib/pages/signup_screen.dart b/roomiebuddy/lib/pages/signup_screen.dart index 17cc106..06ae098 100644 --- a/roomiebuddy/lib/pages/signup_screen.dart +++ b/roomiebuddy/lib/pages/signup_screen.dart @@ -31,7 +31,13 @@ class _SignupScreenState extends State { return Scaffold( appBar: AppBar( - title: const Text('Roomie Buddy'), + title: Text( + 'Roomie Buddy', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/roomiebuddy/lib/providers/theme_provider.dart b/roomiebuddy/lib/providers/theme_provider.dart index f869aa0..b144ebc 100644 --- a/roomiebuddy/lib/providers/theme_provider.dart +++ b/roomiebuddy/lib/providers/theme_provider.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; class ThemeProvider extends ChangeNotifier { - bool _isDarkMode = false; // GLOBAL DARK MODE FLAG - Color _themeColor = Colors.greenAccent; // GLOBAL THEME COLOR + bool _isDarkMode = true; // GLOBAL DARK MODE FLAG + Color _themeColor = Colors.blueGrey; // GLOBAL THEME COLOR // Utility colors (Theme independent) Color get errorColor => Colors.red; @@ -10,12 +10,12 @@ class ThemeProvider extends ChangeNotifier { Color get successColor => Colors.green; // Global colors (Light Mode) - Color get lightBackground => Colors.white; - Color get lightCardBackground => Colors.white; + Color get lightBackground => Colors.grey[300]!; + Color get lightCardBackground => Colors.grey[50]!; Color get lightTextColor => Colors.black; Color get lightTextSecondary => Colors.grey[700]!; Color get lightBorder => Colors.grey[300]!; - Color get lightInputFill => Colors.white; + Color get lightInputFill => Colors.grey[100]!; // Global colors (Dark Mode) Color get darkBackground => Colors.grey[850]!; @@ -46,7 +46,7 @@ class ThemeProvider extends ChangeNotifier { bool get isDarkMode => _isDarkMode; Color get themeColor => _themeColor; Color get currentBackground => _isDarkMode ? darkBackground : lightBackground; - Color get currentCardBackground => _isDarkMode ? darkCardBackground : lightCardBackground; + Color get currentCardBackground => _isDarkMode ? darkCardBackground : lightBackground; Color get currentTextColor => _isDarkMode ? darkTextColor : lightTextColor; Color get currentSecondaryTextColor => _isDarkMode ? darkTextSecondary : lightTextSecondary; Color get currentBorderColor => _isDarkMode ? darkBorder : lightBorder; @@ -73,19 +73,19 @@ class ThemeProvider extends ChangeNotifier { canvasColor: backgroundColor, appBarTheme: AppBarTheme( backgroundColor: appBarBgColor, - titleTextStyle: TextStyle(color: lightTextColor, fontSize: 20), - iconTheme: IconThemeData(color: lightTextColor), + titleTextStyle: TextStyle(color: textColor, fontSize: 20), + iconTheme: IconThemeData(color: textColor), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( selectedItemColor: _themeColor, - unselectedItemColor: lightTextColor, - selectedLabelStyle: TextStyle(color: lightTextColor), - unselectedLabelStyle: TextStyle(color: lightTextColor), + unselectedItemColor: textColor, + selectedLabelStyle: TextStyle(color: textColor), + unselectedLabelStyle: TextStyle(color: textColor), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: _themeColor, - foregroundColor: lightTextColor, + foregroundColor: textColor, ), ), cardTheme: CardTheme( diff --git a/roomiebuddy/lib/services/auth_storage.dart b/roomiebuddy/lib/services/auth_storage.dart index 292369a..56273e5 100644 --- a/roomiebuddy/lib/services/auth_storage.dart +++ b/roomiebuddy/lib/services/auth_storage.dart @@ -5,6 +5,7 @@ class AuthStorage { static const String _userIdKey = 'user_id'; static const String _emailKey = 'email'; static const String _passwordKey = 'password'; + static const String _usernameKey = 'username'; // Singleton pattern implementation static final AuthStorage _instance = AuthStorage._internal(); @@ -16,12 +17,13 @@ class AuthStorage { AuthStorage._internal(); // Store user credentials - Future storeUserCredentials(String userId, String email, String password) async { + Future storeUserCredentials(String userId, String email, String password, String username) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_userIdKey, userId); await prefs.setString(_emailKey, email); await prefs.setString(_passwordKey, password); + await prefs.setString(_usernameKey, username); return true; } catch (e) { print('Error storing user credentials: $e'); @@ -62,6 +64,17 @@ class AuthStorage { } } + // Get username + Future getUsername() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_usernameKey); + } catch (e) { + print('Error getting username: $e'); + return null; + } + } + // Check if user is logged in Future isLoggedIn() async { try { @@ -71,14 +84,16 @@ class AuthStorage { final userId = prefs.getString(_userIdKey); final email = prefs.getString(_emailKey); final password = prefs.getString(_passwordKey); + final username = prefs.getString(_usernameKey); // For development/debugging - remove or set to false in production - print("Auth check: UserId=$userId, Email=$email"); + print("Auth check: UserId=$userId, Email=$email, Username=$username"); // Ensure all values exist and aren't empty return userId != null && userId.isNotEmpty && email != null && email.isNotEmpty && - password != null && password.isNotEmpty; + password != null && password.isNotEmpty && + username != null && username.isNotEmpty; } catch (e) { print('Error checking login status: $e'); return false; @@ -92,6 +107,7 @@ class AuthStorage { await prefs.remove(_userIdKey); await prefs.remove(_emailKey); await prefs.remove(_passwordKey); + await prefs.remove(_usernameKey); return true; } catch (e) { print('Error clearing user credentials: $e');