at main 7.0 kB view raw
1import 'dart:io'; 2 3import 'package:bluesky_text/bluesky_text.dart'; 4import 'package:flutter/foundation.dart'; 5import 'package:grain/api.dart'; 6import 'package:grain/models/gallery.dart'; 7import 'package:grain/models/procedures/create_follow_request.dart'; 8import 'package:grain/models/procedures/delete_follow_request.dart'; 9import 'package:grain/models/procedures/update_profile_request.dart'; 10import 'package:grain/models/profile_with_galleries.dart'; 11import 'package:grain/photo_manip.dart'; 12import 'package:grain/providers/gallery_cache_provider.dart'; 13import 'package:image_picker/image_picker.dart'; 14import 'package:riverpod_annotation/riverpod_annotation.dart'; 15 16part 'profile_provider.g.dart'; 17 18@Riverpod(keepAlive: true) 19class ProfileNotifier extends _$ProfileNotifier { 20 @override 21 Future<ProfileWithGalleries?> build(String did) async { 22 return _fetchProfile(did); 23 } 24 25 // Extract facets 26 Future<List<Map<String, dynamic>>?> _extractFacets(String? description) async { 27 final desc = description ?? ''; 28 if (desc.isEmpty) return null; 29 try { 30 final blueskyText = BlueskyText(desc); 31 final entities = blueskyText.entities; 32 return entities.toFacets(); 33 } catch (_) { 34 return null; 35 } 36 } 37 38 Future<ProfileWithGalleries?> _fetchProfile(String did) async { 39 final profile = await apiService.fetchProfile(did: did); 40 final galleries = await apiService.fetchActorGalleries(did: did); 41 final favs = await apiService.getActorFavs(did: did); 42 if (profile != null) { 43 final facets = await _extractFacets(profile.description); 44 return ProfileWithGalleries( 45 profile: profile.copyWith(descriptionFacets: facets), 46 galleries: galleries, 47 favs: favs, 48 ); 49 } 50 return null; 51 } 52 53 Future<void> refresh() async { 54 state = const AsyncValue.loading(); 55 state = await AsyncValue.guard(() => _fetchProfile(did)); 56 } 57 58 Future<bool> updateProfile({ 59 required String displayName, 60 required String description, 61 dynamic avatarFile, 62 }) async { 63 File? file; 64 if (avatarFile is XFile) { 65 file = File(avatarFile.path); 66 } else if (avatarFile is File) { 67 file = avatarFile; 68 } else { 69 file = null; 70 } 71 final profileRes = await apiService.updateProfile( 72 request: UpdateProfileRequest(displayName: displayName, description: description), 73 ); 74 bool avatarSuccess = true; 75 if (file != null) { 76 final resizedResult = await compute<File, ResizeResult>((f) => resizeImage(file: f), file); 77 final avatarRes = await apiService.updateAvatar(avatarFile: resizedResult.file); 78 avatarSuccess = avatarRes.success; 79 } 80 // Refetch updated profile if update succeeded 81 if (profileRes.success || avatarSuccess) { 82 final updated = await apiService.fetchProfile(did: did); 83 if (updated != null) { 84 final galleries = await apiService.fetchActorGalleries(did: did); 85 final facets = await _extractFacets(updated.description); 86 ref.read(galleryCacheProvider.notifier).setGalleriesForActor(did, galleries); 87 state = AsyncValue.data( 88 ProfileWithGalleries( 89 profile: updated.copyWith(descriptionFacets: facets), 90 galleries: galleries, 91 favs: state.value?.favs ?? [], 92 ), 93 ); 94 } else { 95 state = const AsyncValue.data(null); 96 await refresh(); 97 } 98 return true; 99 } else { 100 await refresh(); 101 return false; 102 } 103 } 104 105 Future<void> toggleFollow(String? followerDid) async { 106 final current = state.value; 107 final profile = current?.profile; 108 if (profile == null || followerDid == null) return; 109 final viewer = profile.viewer; 110 final followUri = viewer?.following; 111 if (followUri != null && followUri.isNotEmpty) { 112 // Unfollow 113 final res = await apiService.deleteFollow(request: DeleteFollowRequest(uri: followUri)); 114 if (res.success) { 115 final updatedProfile = profile.copyWith( 116 viewer: viewer?.copyWith(following: null), 117 followersCount: (profile.followersCount ?? 1) - 1, 118 ); 119 state = AsyncValue.data( 120 ProfileWithGalleries( 121 profile: updatedProfile, 122 galleries: current?.galleries ?? [], 123 favs: current?.favs, 124 ), 125 ); 126 } 127 } else { 128 // Follow 129 final res = await apiService.createFollow(request: CreateFollowRequest(subject: followerDid)); 130 if (res.followUri.isNotEmpty) { 131 final updatedProfile = profile.copyWith( 132 viewer: viewer?.copyWith(following: res.followUri), 133 followersCount: (profile.followersCount ?? 0) + 1, 134 ); 135 state = AsyncValue.data( 136 ProfileWithGalleries( 137 profile: updatedProfile, 138 galleries: current?.galleries ?? [], 139 favs: current?.favs, 140 ), 141 ); 142 } 143 } 144 } 145 146 void addGalleryToProfile(Gallery newGallery) { 147 final currentProfile = state.value; 148 if (currentProfile != null) { 149 final updatedGalleries = [newGallery, ...currentProfile.galleries]; 150 final updatedProfile = currentProfile.profile.copyWith( 151 galleryCount: (currentProfile.profile.galleryCount ?? 0) + 1, 152 ); 153 state = AsyncValue.data( 154 ProfileWithGalleries( 155 profile: updatedProfile, 156 galleries: updatedGalleries, 157 favs: currentProfile.favs, 158 ), 159 ); 160 } 161 } 162 163 void removeGalleryFromProfile(String galleryUri) { 164 final currentProfile = state.value; 165 if (currentProfile != null) { 166 final updatedGalleries = currentProfile.galleries.where((g) => g.uri != galleryUri).toList(); 167 final updatedProfile = currentProfile.profile.copyWith( 168 galleryCount: (currentProfile.profile.galleryCount ?? 1) - 1, 169 ); 170 state = AsyncValue.data( 171 ProfileWithGalleries( 172 profile: updatedProfile, 173 galleries: updatedGalleries, 174 favs: currentProfile.favs, 175 ), 176 ); 177 } 178 } 179 180 /// Adds a gallery to the user's favorites in the profile state. 181 void addFavorite(Gallery gallery) { 182 final currentProfile = state.value; 183 if (currentProfile != null) { 184 final updatedFavs = [gallery, ...?currentProfile.favs]; 185 state = AsyncValue.data( 186 ProfileWithGalleries( 187 profile: currentProfile.profile, 188 galleries: currentProfile.galleries, 189 favs: updatedFavs, 190 ), 191 ); 192 } 193 } 194 195 /// Removes a gallery from the user's favorites in the profile state by URI. 196 void removeFavorite(String galleryUri) { 197 final currentProfile = state.value; 198 if (currentProfile != null) { 199 final updatedFavs = (currentProfile.favs ?? []).where((g) => g.uri != galleryUri).toList(); 200 state = AsyncValue.data( 201 ProfileWithGalleries( 202 profile: currentProfile.profile, 203 galleries: currentProfile.galleries, 204 favs: updatedFavs, 205 ), 206 ); 207 } 208 } 209}