From c6ed26075e95f894728717c7597c50b8073759d8 Mon Sep 17 00:00:00 2001 From: Sumato42 Date: Mon, 24 Nov 2025 13:55:38 +0100 Subject: [PATCH] new profile picture feature --- lib/providers/auth_provider.dart | 15 ++ lib/screens/profile/profile_screen.dart | 13 +- .../profile/profile_settings_screen.dart | 187 ++++++++++++++++++ lib/services/mock_service.dart | 14 +- pubspec.lock | 8 +- 5 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 lib/screens/profile/profile_settings_screen.dart diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 599be0c..d6a0bd9 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -41,5 +41,20 @@ class AuthProvider with ChangeNotifier { _isGuest = false; notifyListeners(); } + + Future updateProfilePicture(String avatarUrl) async { + if (_user == null) return; + _isLoading = true; + notifyListeners(); + + try { + _user = await _service.updateProfilePicture(_user!.id, avatarUrl); + } catch (e) { + rethrow; + } finally { + _isLoading = false; + notifyListeners(); + } + } } diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index 4690892..1e864cc 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../providers/auth_provider.dart'; import '../../providers/theme_provider.dart'; +import 'profile_settings_screen.dart'; class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); @@ -51,7 +52,17 @@ class ProfileScreen extends StatelessWidget { leading: const Icon(Icons.settings), title: const Text('Settings'), onTap: () { - // Navigate to settings + if (isGuest) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Login to access settings')), + ); + return; + } + Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => const ProfileSettingsScreen(), + ), + ); }, ), ListTile( diff --git a/lib/screens/profile/profile_settings_screen.dart b/lib/screens/profile/profile_settings_screen.dart new file mode 100644 index 0000000..7aadcff --- /dev/null +++ b/lib/screens/profile/profile_settings_screen.dart @@ -0,0 +1,187 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../providers/auth_provider.dart'; + +class ProfileSettingsScreen extends StatefulWidget { + const ProfileSettingsScreen({super.key}); + + @override + State createState() => _ProfileSettingsScreenState(); +} + +class _ProfileSettingsScreenState extends State { + late TextEditingController _avatarController; + String? _selectedAvatar; + bool _isSaving = false; + + final List _presetAvatars = const [ + 'https://picsum.photos/id/1005/200/200', + 'https://picsum.photos/id/1011/200/200', + 'https://picsum.photos/id/1027/200/200', + 'https://picsum.photos/id/1035/200/200', + 'https://picsum.photos/id/237/200/200', + 'https://picsum.photos/id/64/200/200', + ]; + + @override + void initState() { + super.initState(); + final user = Provider.of(context, listen: false).user; + _avatarController = TextEditingController(text: user?.avatarUrl ?? ''); + _selectedAvatar = user?.avatarUrl; + } + + @override + void dispose() { + _avatarController.dispose(); + super.dispose(); + } + + Future _saveAvatar() async { + final auth = Provider.of(context, listen: false); + final avatarUrl = _avatarController.text.trim(); + + if (avatarUrl.isEmpty || auth.user == null) return; + + setState(() => _isSaving = true); + try { + await auth.updateProfilePicture(avatarUrl); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile picture updated')), + ); + Navigator.of(context).pop(); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update picture: $e')), + ); + } finally { + if (mounted) { + setState(() => _isSaving = false); + } + } + } + + @override + Widget build(BuildContext context) { + final auth = Provider.of(context); + final user = auth.user; + + if (user == null) { + return Scaffold( + appBar: AppBar(title: const Text('Profile Settings')), + body: const Center(child: Text('Login to update your profile.')), + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Profile Settings'), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Picture', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + Center( + child: CircleAvatar( + radius: 50, + backgroundImage: + CachedNetworkImageProvider(_selectedAvatar ?? user.avatarUrl), + ), + ), + const SizedBox(height: 24), + Text( + 'Use a custom image URL', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + TextField( + controller: _avatarController, + decoration: const InputDecoration( + labelText: 'Image URL', + hintText: 'https://example.com/avatar.png', + border: OutlineInputBorder(), + ), + onChanged: (value) { + setState(() { + _selectedAvatar = value.trim().isEmpty ? null : value.trim(); + }); + }, + ), + const SizedBox(height: 24), + Text( + 'Or pick a preset', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Expanded( + child: GridView.builder( + itemCount: _presetAvatars.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + ), + itemBuilder: (context, index) { + final avatar = _presetAvatars[index]; + final isSelected = avatar == _selectedAvatar; + return GestureDetector( + onTap: () { + setState(() { + _selectedAvatar = avatar; + _avatarController.text = avatar; + }); + }, + child: Stack( + alignment: Alignment.center, + children: [ + CircleAvatar( + radius: 40, + backgroundImage: + CachedNetworkImageProvider(avatar), + ), + if (isSelected) + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black38, + ), + child: + const Icon(Icons.check, color: Colors.white), + ), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _isSaving ? null : _saveAvatar, + icon: _isSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.save), + label: Text(_isSaving ? 'Saving...' : 'Save Changes'), + ), + ), + ], + ), + ), + ); + } +} + diff --git a/lib/services/mock_service.dart b/lib/services/mock_service.dart index 812adf6..93572ca 100644 --- a/lib/services/mock_service.dart +++ b/lib/services/mock_service.dart @@ -77,7 +77,7 @@ class MockService { ), ]; - static final User _mockUser = User( + static User _mockUser = User( id: 'u1', name: 'John Doe', email: 'john.doe@example.com', @@ -100,6 +100,18 @@ class MockService { return _mockUser; } + Future updateProfilePicture(String userId, String avatarUrl) async { + await Future.delayed(const Duration(milliseconds: 500)); + if (_mockUser.id != userId) throw Exception('User not found'); + _mockUser = User( + id: _mockUser.id, + name: _mockUser.name, + email: _mockUser.email, + avatarUrl: avatarUrl, + ); + return _mockUser; + } + Future> searchProducts(String query) async { await Future.delayed(const Duration(milliseconds: 600)); return _products.where((p) => diff --git a/pubspec.lock b/pubspec.lock index 30db1f6..5b3bef8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -244,10 +244,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" nested: dependency: transitive description: @@ -505,10 +505,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" typed_data: dependency: transitive description: