diff --git a/lib/screens/manage_collection_screen.dart b/lib/screens/manage_collection_screen.dart index 1614e54..8a20afb 100644 --- a/lib/screens/manage_collection_screen.dart +++ b/lib/screens/manage_collection_screen.dart @@ -321,128 +321,136 @@ class _ManageCollectionScreenState extends State { ), ], ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - // ── Description ── - if (_collection.description != null && - _collection.description!.isNotEmpty) ...[ - Text( - _collection.description!, - style: const TextStyle( - fontSize: 14, color: AppColors.textSecondary), - ), - const SizedBox(height: 16), - ], - - // ── Members section ── - Row( - children: [ + body: RefreshIndicator( + onRefresh: _loadMembers, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + if (_collection.description != null && + _collection.description!.isNotEmpty) ...[ Text( - 'Members', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + _collection.description!, + style: const TextStyle( + fontSize: 14, color: AppColors.textSecondary), ), - const Spacer(), - if (_collection.isOwner) - TextButton.icon( - onPressed: _inviteMember, - icon: const Icon(Icons.person_add, size: 18), - label: const Text('Invite'), - ), + const SizedBox(height: 16), ], - ), - const SizedBox(height: 8), - if (_isLoading) - const Center( - child: Padding( - padding: EdgeInsets.all(24), - child: CircularProgressIndicator(), - ), - ) - else - ...List.generate(_members.length, (i) { - final m = _members[i]; - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: CircleAvatar( - backgroundColor: m.isOwner - ? AppColors.orange - : AppColors.navy, - child: Icon( - m.isOwner ? Icons.star : Icons.person, - color: Colors.white, - size: 20, + Row( + children: [ + Text( + 'Members', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (_collection.isOwner) + TextButton.icon( + onPressed: _inviteMember, + icon: const Icon(Icons.person_add, size: 18), + label: const Text('Invite'), + ), + ], + ), + const SizedBox(height: 8), + + if (_isLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(24), + child: CircularProgressIndicator(), + ), + ) + else + ...List.generate(_members.length, (i) { + final member = _members[i]; + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: + member.isOwner ? AppColors.orange : AppColors.navy, + child: Icon( + member.isOwner ? Icons.star : Icons.person, + color: Colors.white, + size: 20, + ), ), + title: Text( + member.email, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text( + member.isOwner ? 'Owner' : 'Member', + style: const TextStyle(fontSize: 12), + ), + trailing: (!member.isOwner && + _collection.isOwner && + member.userId != currentUserId) + ? IconButton( + icon: const Icon(Icons.remove_circle_outline, + color: AppColors.error), + onPressed: () => _removeMember(member), + ) + : null, ), - title: Text( - m.email, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text( - m.isOwner ? 'Owner' : 'Member', - style: const TextStyle(fontSize: 12), - ), - trailing: (!m.isOwner && - _collection.isOwner && - m.userId != currentUserId) - ? IconButton( - icon: const Icon(Icons.remove_circle_outline, - color: AppColors.error), - onPressed: () => _removeMember(m), - ) - : null, - ), - ); - }), + ); + }), - const SizedBox(height: 32), - const Divider(), - const SizedBox(height: 16), + const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 16), - // ── Danger zone ── - Text( - 'Danger Zone', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: AppColors.error, - ), - ), - const SizedBox(height: 12), - - if (!_collection.isOwner) - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _leaveCollection, - icon: const Icon(Icons.exit_to_app, color: AppColors.error), - label: const Text('Leave Collection', - style: TextStyle(color: AppColors.error)), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: AppColors.error), - padding: const EdgeInsets.symmetric(vertical: 14), - ), + Text( + 'Danger Zone', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: AppColors.error, ), ), + const SizedBox(height: 12), - if (_collection.isOwner) - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _deleteCollection, - icon: const Icon(Icons.delete_forever, color: AppColors.error), - label: const Text('Delete Collection', - style: TextStyle(color: AppColors.error)), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: AppColors.error), - padding: const EdgeInsets.symmetric(vertical: 14), + if (!_collection.isOwner) + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _leaveCollection, + icon: const Icon(Icons.exit_to_app, color: AppColors.error), + label: const Text('Leave Collection', + style: TextStyle(color: AppColors.error)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: AppColors.error), + padding: const EdgeInsets.symmetric(vertical: 14), + ), + ), + ) + else ...[ + const Text( + 'As owner, you cannot leave this collection. You can delete it instead.', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, ), ), - ), - ], + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _deleteCollection, + icon: const Icon(Icons.delete_forever, + color: AppColors.error), + label: const Text('Delete Collection', + style: TextStyle(color: AppColors.error)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: AppColors.error), + padding: const EdgeInsets.symmetric(vertical: 14), + ), + ), + ), + ], + ], + ), ), ); } diff --git a/lib/screens/scan_tab.dart b/lib/screens/scan_tab.dart index d2b69d5..f901ef4 100644 --- a/lib/screens/scan_tab.dart +++ b/lib/screens/scan_tab.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../main.dart'; import '../scanner_screen.dart'; import '../services/collection_service.dart'; @@ -12,6 +13,8 @@ class ScanTab extends StatefulWidget { } class ScanTabState extends State { + static const _activeCollectionPrefKey = 'active_collection_id'; + bool _isBusy = false; List _collections = []; Collection? _selectedCollection; @@ -28,12 +31,29 @@ class ScanTabState extends State { Future _loadCollections() async { try { final list = await CollectionService.getMyCollections(); + final prefs = await SharedPreferences.getInstance(); + final persistedId = prefs.getString(_activeCollectionPrefKey); + + Collection? selected; + if (persistedId != null) { + final matching = list.where((c) => c.id == persistedId); + if (matching.isNotEmpty) { + selected = matching.first; + } + } + + selected ??= list.isNotEmpty ? list.first : null; + if (!mounted) return; setState(() { _collections = list; - _selectedCollection = list.isNotEmpty ? list.first : null; + _selectedCollection = selected; _loadingCollections = false; }); + + if (selected != null) { + await prefs.setString(_activeCollectionPrefKey, selected.id); + } } catch (e) { if (!mounted) return; setState(() => _loadingCollections = false); @@ -133,11 +153,7 @@ class ScanTabState extends State { )) .toList(), onChanged: (id) { - setState(() { - final matching = _collections.where((c) => c.id == id); - _selectedCollection = - matching.isNotEmpty ? matching.first : null; - }); + _setActiveCollection(id); }, ), ), @@ -208,6 +224,17 @@ class ScanTabState extends State { ); } + Future _setActiveCollection(String? id) async { + if (id == null) return; + final matching = _collections.where((c) => c.id == id); + if (matching.isEmpty) return; + + setState(() => _selectedCollection = matching.first); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_activeCollectionPrefKey, id); + } + Future _openScanner() async { await navigatorKey.currentState!.push( MaterialPageRoute( diff --git a/lib/services/collection_service.dart b/lib/services/collection_service.dart index dd2df56..a7e6b79 100644 --- a/lib/services/collection_service.dart +++ b/lib/services/collection_service.dart @@ -259,6 +259,21 @@ class CollectionService { required String collectionId, required String email, }) async { + final currentUserId = supabase.auth.currentUser!.id; + + final collection = await supabase + .from('collections') + .select('owner_id') + .eq('id', collectionId) + .maybeSingle(); + + if (collection == null) { + throw Exception('Collection not found.'); + } + if (collection['owner_id'] != currentUserId) { + throw Exception('Only the collection owner can invite members.'); + } + // Call an RPC to look up the user ID by email. final result = await supabase.rpc('get_user_id_by_email', params: { 'lookup_email': email.trim().toLowerCase(), @@ -271,6 +286,10 @@ class CollectionService { final userId = result is List ? result.first['id'] as String : result as String; + if (userId == currentUserId) { + throw Exception('You are already in this collection.'); + } + // Check if already a member. final existing = await supabase .from('collection_members') @@ -295,6 +314,35 @@ class CollectionService { required String collectionId, required String membershipId, }) async { + final currentUserId = supabase.auth.currentUser!.id; + + final collection = await supabase + .from('collections') + .select('owner_id') + .eq('id', collectionId) + .maybeSingle(); + + if (collection == null) { + throw Exception('Collection not found.'); + } + if (collection['owner_id'] != currentUserId) { + throw Exception('Only the collection owner can remove members.'); + } + + final target = await supabase + .from('collection_members') + .select('role, user_id') + .eq('id', membershipId) + .eq('collection_id', collectionId) + .maybeSingle(); + + if (target == null) { + throw Exception('Member not found.'); + } + if (target['role'] == 'owner') { + throw Exception('Collection owner cannot be removed.'); + } + await supabase .from('collection_members') .delete() @@ -304,6 +352,21 @@ class CollectionService { /// Leave a collection (for non-owners). static Future leave(String collectionId) async { final userId = supabase.auth.currentUser!.id; + + final membership = await supabase + .from('collection_members') + .select('role') + .eq('collection_id', collectionId) + .eq('user_id', userId) + .maybeSingle(); + + if (membership == null) { + throw Exception('You are not a member of this collection.'); + } + if (membership['role'] == 'owner') { + throw Exception('Owner cannot leave. Delete the collection instead.'); + } + await supabase .from('collection_members') .delete() diff --git a/pubspec.lock b/pubspec.lock index 83de094..a64e144 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -841,7 +841,7 @@ packages: source: hosted version: "0.28.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" diff --git a/pubspec.yaml b/pubspec.yaml index e2711b8..228412b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: cached_network_image: ^3.4.1 image: ^4.2.0 package_info_plus: ^8.1.3 + shared_preferences: ^2.5.3 dev_dependencies: flutter_test: