···923923 appLogger.i('Created photo exif record result: $result');
924924 return result['uri'] as String?;
925925 }
926926+927927+ /// Updates multiple photo records in the social.grain.photo collection using applyWrites.
928928+ /// Each photo in [updates] should have: photoUri, photo, aspectRatio, alt, createdAt
929929+ /// Returns true on success, false on failure.
930930+ Future<bool> updatePhotos(List<Map<String, dynamic>> updates) async {
931931+ final session = await auth.getValidSession();
932932+ if (session == null) {
933933+ appLogger.w('No valid session for updatePhotosBatch');
934934+ return false;
935935+ }
936936+ final dpopClient = DpopHttpClient(dpopKey: session.dpopJwk);
937937+ final issuer = session.issuer;
938938+ final did = session.subject;
939939+ final url = Uri.parse('$issuer/xrpc/com.atproto.repo.applyWrites');
940940+941941+ // Fetch current photo records for all photos
942942+ final photoRecords = await fetchPhotoRecords();
943943+944944+ final writes = <Map<String, dynamic>>[];
945945+ for (final update in updates) {
946946+ String rkey = '';
947947+ try {
948948+ rkey = AtUri.parse(update['photoUri'] as String).rkey;
949949+ } catch (_) {}
950950+ if (rkey.isEmpty) {
951951+ appLogger.w('No rkey found in photoUri: ${update['photoUri']}');
952952+ continue;
953953+ }
954954+955955+ // Get the full photo record for this photoUri
956956+ final record = photoRecords[update['photoUri']];
957957+ if (record == null) {
958958+ appLogger.w('No photo record found for photoUri: ${update['photoUri']}');
959959+ continue;
960960+ }
961961+962962+ // Use provided values or fallback to the record's values
963963+ final photoBlobRef = update['photo'] ?? record['photo'];
964964+ final aspectRatio = update['aspectRatio'] ?? record['aspectRatio'];
965965+ final createdAt = update['createdAt'] ?? record['createdAt'];
966966+967967+ if (photoBlobRef == null) {
968968+ appLogger.w('No blobRef found for photoUri: ${update['photoUri']}');
969969+ continue;
970970+ }
971971+972972+ writes.add({
973973+ '\$type': 'com.atproto.repo.applyWrites#update',
974974+ 'collection': 'social.grain.photo',
975975+ 'rkey': rkey,
976976+ 'value': {
977977+ 'photo': photoBlobRef,
978978+ 'aspectRatio': aspectRatio,
979979+ 'alt': update['alt'] ?? '',
980980+ 'createdAt': createdAt,
981981+ },
982982+ });
983983+ }
984984+ if (writes.isEmpty) {
985985+ appLogger.w('No valid photo updates to apply');
986986+ return false;
987987+ }
988988+ final payload = {'repo': did, 'validate': false, 'writes': writes};
989989+ appLogger.i('Applying batch photo updates: $payload');
990990+ final response = await dpopClient.send(
991991+ method: 'POST',
992992+ url: url,
993993+ accessToken: session.accessToken,
994994+ headers: {'Content-Type': 'application/json'},
995995+ body: jsonEncode(payload),
996996+ );
997997+ if (response.statusCode != 200 && response.statusCode != 201) {
998998+ appLogger.w('Failed to apply batch photo updates: ${response.statusCode} ${response.body}');
999999+ return false;
10001000+ }
10011001+ appLogger.i('Batch photo updates applied successfully');
10021002+ return true;
10031003+ }
10041004+10051005+ /// Fetches the full photo record for each photo in social.grain.photo.
10061006+ /// Returns a map of photoUri -> photo record (Map`<`String, dynamic`>`).
10071007+ Future<Map<String, dynamic>> fetchPhotoRecords() async {
10081008+ final session = await auth.getValidSession();
10091009+ if (session == null) {
10101010+ appLogger.w('No valid session for fetchPhotoRecords');
10111011+ return {};
10121012+ }
10131013+ final dpopClient = DpopHttpClient(dpopKey: session.dpopJwk);
10141014+ final issuer = session.issuer;
10151015+ final did = session.subject;
10161016+ final url = Uri.parse(
10171017+ '$issuer/xrpc/com.atproto.repo.listRecords?repo=$did&collection=social.grain.photo',
10181018+ );
10191019+10201020+ final response = await dpopClient.send(
10211021+ method: 'GET',
10221022+ url: url,
10231023+ accessToken: session.accessToken,
10241024+ headers: {'Content-Type': 'application/json'},
10251025+ );
10261026+10271027+ if (response.statusCode != 200) {
10281028+ appLogger.w('Failed to list photo records: ${response.statusCode} ${response.body}');
10291029+ return {};
10301030+ }
10311031+10321032+ final json = jsonDecode(response.body) as Map<String, dynamic>;
10331033+ final records = json['records'] as List<dynamic>? ?? [];
10341034+ final photoRecords = <String, dynamic>{};
10351035+10361036+ for (final record in records) {
10371037+ final uri = record['uri'] as String?;
10381038+ final value = record['value'] as Map<String, dynamic>?;
10391039+ if (uri != null && value != null) {
10401040+ photoRecords[uri] = value;
10411041+ }
10421042+ }
10431043+ return photoRecords;
10441044+ }
9261045}
92710469281047final apiService = ApiService();
+30
lib/providers/gallery_cache_provider.dart
···327327 setGalleries(galleries);
328328 return galleries;
329329 }
330330+331331+ /// Updates alt text for multiple photos by calling apiService.updatePhotos, then updates the gallery cache state manually.
332332+ /// [galleryUri]: The URI of the gallery containing the photos.
333333+ /// [altUpdates]: List of maps with keys: photoUri, alt (and optionally aspectRatio, createdAt, photo).
334334+ Future<bool> updatePhotoAltTexts({
335335+ required String galleryUri,
336336+ required List<Map<String, dynamic>> altUpdates,
337337+ }) async {
338338+ final success = await apiService.updatePhotos(altUpdates);
339339+ if (!success) return false;
340340+341341+ // Update the gallery photos' alt text in the cache manually
342342+ final gallery = state[galleryUri];
343343+ if (gallery == null) return false;
344344+345345+ // Build a map of photoUri to new alt text
346346+ final altMap = {for (final update in altUpdates) update['photoUri'] as String: update['alt']};
347347+348348+ final updatedPhotos = gallery.items.map((photo) {
349349+ final newAlt = altMap[photo.uri];
350350+ if (newAlt != null) {
351351+ return photo.copyWith(alt: newAlt);
352352+ }
353353+ return photo;
354354+ }).toList();
355355+356356+ final updatedGallery = gallery.copyWith(items: updatedPhotos);
357357+ state = {...state, galleryUri: updatedGallery};
358358+ return true;
359359+ }
330360}