Grain flutter app
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}