From 7aa44b4daa6a16ac3b128ac9cfa0e16e2f64d933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SUGGESTIED=20=E2=9C=A8?= <74950245+suggestied@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:32:02 +0100 Subject: [PATCH] Create settings account page view and controller Add settings account page view and controller to upsert user profile, load initial data, and include avatar uploader. * **Controller**: Add `SettingsAccountController` in `lib/controllers/settings_account_controller.dart` to handle upserting user profile, loading initial data, and image uploading. * **Dependency Injection**: Update `lib/di/service_locator.dart` to register `SettingsAccountController`. * **Routing**: Update `lib/router/app_router.dart` to include a route for the settings account page. * **View**: Add `lib/ui/screens/wardrobe/settings/account_page.dart` to create the settings account page view with avatar uploader and profile data. * **Navigation**: Update `lib/ui/screens/wardrobe/settings/page.dart` to navigate to the new settings account page when "Account" option is selected. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/OpenWardrobe/app/tree/develop?shareId=XXXX-XXXX-XXXX-XXXX). --- .../settings_account_controller.dart | 76 ++++++++++++ lib/di/service_locator.dart | 7 +- lib/router/app_router.dart | 6 + .../wardrobe/settings/account_page.dart | 116 ++++++++++++++++++ lib/ui/screens/wardrobe/settings/page.dart | 9 +- 5 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 lib/controllers/settings_account_controller.dart create mode 100644 lib/ui/screens/wardrobe/settings/account_page.dart diff --git a/lib/controllers/settings_account_controller.dart b/lib/controllers/settings_account_controller.dart new file mode 100644 index 0000000..cb938fa --- /dev/null +++ b/lib/controllers/settings_account_controller.dart @@ -0,0 +1,76 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart' show Uint8List, kIsWeb; +import 'package:get_it/get_it.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:openwardrobe/brick/models/user_profile.model.dart'; +import 'package:openwardrobe/repositories/app_repository.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class SettingsAccountController { + final AppRepository _appRepository = GetIt.instance(); + + Future fetchUserProfile() async { + try { + final profiles = await _appRepository.get(); + return profiles.first; + } catch (e) { + throw Exception('Failed to fetch user profile: $e'); + } + } + + Future upsertUserProfile(UserProfile profile) async { + try { + await _appRepository.upsert(profile); + } catch (e) { + throw Exception('Failed to upsert user profile: $e'); + } + } + + Future uploadAvatar(File imageFile) async { + try { + final response = await Supabase.instance.client.storage + .from('avatars') + .upload(imageFile.path, imageFile); + return response.data['path']; + } catch (e) { + throw Exception('Failed to upload avatar: $e'); + } + } + + Future uploadWebAvatar(Uint8List imageBytes, String fileName) async { + try { + final response = await Supabase.instance.client.storage + .from('avatars') + .uploadBinary(fileName, imageBytes); + return response.data['path']; + } catch (e) { + throw Exception('Failed to upload web avatar: $e'); + } + } + + Future pickImage({bool fromGallery = false}) async { + final picker = ImagePicker(); + final pickedFile = await picker.pickImage( + source: fromGallery ? ImageSource.gallery : ImageSource.camera, + ); + + if (pickedFile != null) { + return File(pickedFile.path); + } else { + return null; + } + } + + Future pickWebImage() async { + final picker = ImagePicker(); + final pickedFile = await picker.pickImage( + source: ImageSource.gallery, + ); + + if (pickedFile != null) { + return await pickedFile.readAsBytes(); + } else { + return null; + } + } +} diff --git a/lib/di/service_locator.dart b/lib/di/service_locator.dart index 5e3af03..1521d1c 100644 --- a/lib/di/service_locator.dart +++ b/lib/di/service_locator.dart @@ -5,8 +5,7 @@ import '../controllers/camera_controller.dart'; import '../controllers/home_controller.dart'; import '../controllers/wardrobe_controller.dart'; import '../controllers/lookbook_controller.dart'; - - +import '../controllers/settings_account_controller.dart'; // Import the new controller final getIt = GetIt.instance; @@ -19,5 +18,5 @@ void setupLocator() { getIt.registerLazySingleton(() => HomeController()); getIt.registerLazySingleton(() => WardrobeController()); getIt.registerLazySingleton(() => LookbookController()); - -} \ No newline at end of file + getIt.registerLazySingleton(() => SettingsAccountController()); // Register the new controller +} diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index b718738..dc80c55 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -11,6 +11,7 @@ import '../ui/screens/home/page.dart'; import '../ui/screens/wardrobe/page.dart'; import '../ui/screens/wardrobe/add/page.dart'; import '../ui/widgets/scaffold_with_navbar.dart'; +import '../ui/screens/wardrobe/settings/account_page.dart'; // Import the new settings account page class AppRouter { static final GlobalKey _rootNavigatorKey = @@ -43,6 +44,11 @@ class AppRouter { name: 'Add Item', builder: (context, state) => const CameraScreen(), ), + GoRoute( + path: '/settings/account', + name: 'SettingsAccount', + builder: (context, state) => const SettingsAccountPage(), + ), StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) { return ScaffoldWithNavBar(navigationShell: navigationShell); diff --git a/lib/ui/screens/wardrobe/settings/account_page.dart b/lib/ui/screens/wardrobe/settings/account_page.dart new file mode 100644 index 0000000..5e5cc09 --- /dev/null +++ b/lib/ui/screens/wardrobe/settings/account_page.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:openwardrobe/controllers/settings_account_controller.dart'; +import 'package:openwardrobe/brick/models/user_profile.model.dart'; + +class SettingsAccountPage extends StatefulWidget { + const SettingsAccountPage({Key? key}) : super(key: key); + + @override + _SettingsAccountPageState createState() => _SettingsAccountPageState(); +} + +class _SettingsAccountPageState extends State { + final SettingsAccountController _controller = GetIt.instance(); + late Future _userProfileFuture; + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _displayNameController = TextEditingController(); + final TextEditingController _bioController = TextEditingController(); + String? _avatarUrl; + + @override + void initState() { + super.initState(); + _userProfileFuture = _controller.fetchUserProfile(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Account Settings'), + ), + body: FutureBuilder( + future: _userProfileFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData) { + return const Center(child: Text('No profile found')); + } + + final userProfile = snapshot.data!; + _usernameController.text = userProfile.username; + _displayNameController.text = userProfile.displayName ?? ''; + _bioController.text = userProfile.bio ?? ''; + _avatarUrl = userProfile.avatarUrl; + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: GestureDetector( + onTap: () async { + final imageFile = await _controller.pickImage(fromGallery: true); + if (imageFile != null) { + final avatarUrl = await _controller.uploadAvatar(imageFile); + setState(() { + _avatarUrl = avatarUrl; + }); + } + }, + child: CircleAvatar( + radius: 50, + backgroundImage: _avatarUrl != null ? NetworkImage(_avatarUrl!) : null, + child: _avatarUrl == null ? const Icon(Icons.person, size: 50) : null, + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _usernameController, + decoration: const InputDecoration(labelText: 'Username'), + ), + const SizedBox(height: 16), + TextField( + controller: _displayNameController, + decoration: const InputDecoration(labelText: 'Display Name'), + ), + const SizedBox(height: 16), + TextField( + controller: _bioController, + decoration: const InputDecoration(labelText: 'Bio'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () async { + final updatedProfile = UserProfile( + id: userProfile.id, + username: _usernameController.text, + displayName: _displayNameController.text, + bio: _bioController.text, + avatarUrl: _avatarUrl, + socialLinks: userProfile.socialLinks, + isPublic: userProfile.isPublic, + createdAt: userProfile.createdAt, + updatedAt: DateTime.now(), + ); + await _controller.upsertUserProfile(updatedProfile); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile updated successfully')), + ); + }, + child: const Text('Save'), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/screens/wardrobe/settings/page.dart b/lib/ui/screens/wardrobe/settings/page.dart index 30b5769..f27e9c6 100644 --- a/lib/ui/screens/wardrobe/settings/page.dart +++ b/lib/ui/screens/wardrobe/settings/page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; - +import 'package:go_router/go_router.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({Key? key}) : super(key: key); @@ -16,31 +16,26 @@ class SettingsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( - title: const Text('Account'), onTap: () { - // Navigate to account settings - Navigator.pushNamed(context, '/settings/account'); + context.push('/settings/account'); }, ), ListTile( title: const Text('Notifications'), onTap: () { - // Navigate to notification settings Navigator.pushNamed(context, '/settings/notifications'); }, ), ListTile( title: const Text('Privacy'), onTap: () { - // Navigate to privacy settings Navigator.pushNamed(context, '/settings/privacy'); }, ), ListTile( title: const Text('About'), onTap: () { - // Navigate to about page Navigator.pushNamed(context, '/settings/about'); }, ),