this repo has no description

feat: Enable gallery editing by passing existing gallery data to CreateGalleryPage

+295 -120
+136 -74
lib/api.dart
··· 5 5 import 'models/gallery.dart'; 6 6 import 'models/notification.dart' as grain; 7 7 import './auth.dart'; 8 - import 'package:xrpc/xrpc.dart' as xrpc; 9 8 import 'package:http/http.dart' as http; 10 9 import 'dart:convert'; 11 10 import 'package:grain/dpop_client.dart'; ··· 58 57 59 58 Future<Profile?> fetchProfile({required String did}) async { 60 59 appLogger.i('Fetching profile for did: $did'); 61 - final response = await xrpc.query( 62 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 63 - xrpc.NSID.create('actor.grain.social', 'getProfile'), 64 - parameters: {'actor': did}, 65 - to: Profile.fromJson, 60 + final response = await http.get( 61 + Uri.parse('$_apiUrl/xrpc/social.grain.actor.getProfile?actor=$did'), 62 + headers: {'Content-Type': 'application/json'}, 66 63 ); 67 - return response.data; 64 + if (response.statusCode != 200) { 65 + appLogger.w( 66 + 'Failed to fetch profile: ${response.statusCode} ${response.body}', 67 + ); 68 + return null; 69 + } 70 + return Profile.fromJson(jsonDecode(response.body)); 68 71 } 69 72 70 73 Future<List<Gallery>> fetchActorGalleries({required String did}) async { 71 74 appLogger.i('Fetching galleries for actor did: $did'); 72 - final record = await xrpc.query( 73 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 74 - xrpc.NSID.create('gallery.grain.social', 'getActorGalleries'), 75 - parameters: {'actor': did}, 76 - to: (json) => 77 - (json['items'] as List<dynamic>?) 78 - ?.map((item) => Gallery.fromJson(item)) 79 - .toList() ?? 80 - [], 75 + final response = await http.get( 76 + Uri.parse( 77 + '$_apiUrl/xrpc/social.grain.gallery.getActorGalleries?actor=$did', 78 + ), 79 + headers: {'Content-Type': 'application/json'}, 81 80 ); 82 - galleries = record.data; 81 + if (response.statusCode != 200) { 82 + appLogger.w( 83 + 'Failed to fetch galleries: ${response.statusCode} ${response.body}', 84 + ); 85 + return []; 86 + } 87 + final json = jsonDecode(response.body); 88 + galleries = 89 + (json['items'] as List<dynamic>?) 90 + ?.map((item) => Gallery.fromJson(item)) 91 + .toList() ?? 92 + []; 83 93 return galleries; 84 94 } 85 95 ··· 87 97 if (_accessToken == null) { 88 98 return []; 89 99 } 90 - appLogger.i('Fetching timeline with algorithm: ${algorithm ?? 'default'}'); 91 - final record = await xrpc.query( 92 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 93 - xrpc.NSID.create('feed.grain.social', 'getTimeline'), 94 - parameters: algorithm != null ? {'algorithm': algorithm} : null, 95 - headers: {'Authorization': "Bearer $_accessToken"}, 96 - to: (json) => 97 - (json['feed'] as List<dynamic>?) 98 - ?.map((item) => Gallery.fromJson(item as Map<String, dynamic>)) 99 - .toList() ?? 100 - [], 100 + appLogger.i( 101 + 'Fetching timeline with algorithm: \\${algorithm ?? 'default'}', 102 + ); 103 + final uri = algorithm != null 104 + ? Uri.parse( 105 + '$_apiUrl/xrpc/social.grain.feed.getTimeline?algorithm=$algorithm', 106 + ) 107 + : Uri.parse('$_apiUrl/xrpc/social.grain.feed.getTimeline'); 108 + final response = await http.get( 109 + uri, 110 + headers: { 111 + 'Authorization': "Bearer $_accessToken", 112 + 'Content-Type': 'application/json', 113 + }, 101 114 ); 102 - return record.data; 115 + if (response.statusCode != 200) { 116 + appLogger.w( 117 + 'Failed to fetch timeline: ${response.statusCode} ${response.body}', 118 + ); 119 + return []; 120 + } 121 + final json = jsonDecode(response.body); 122 + return (json['feed'] as List<dynamic>?) 123 + ?.map((item) => Gallery.fromJson(item as Map<String, dynamic>)) 124 + .toList() ?? 125 + []; 103 126 } 104 127 105 128 Future<Gallery?> getGallery({required String uri}) async { 106 129 appLogger.i('Fetching gallery for uri: $uri'); 107 - final record = await xrpc.query( 108 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 109 - xrpc.NSID.create('gallery.grain.social', 'getGallery'), 110 - parameters: {'uri': uri}, 111 - to: Gallery.fromJson, 130 + final response = await http.get( 131 + Uri.parse('$_apiUrl/xrpc/social.grain.gallery.getGallery?uri=$uri'), 132 + headers: {'Content-Type': 'application/json'}, 112 133 ); 113 - return record.data; 134 + if (response.statusCode != 200) { 135 + appLogger.w( 136 + 'Failed to fetch gallery: ${response.statusCode} ${response.body}', 137 + ); 138 + return null; 139 + } 140 + try { 141 + final json = jsonDecode(response.body); 142 + if (json is Map<String, dynamic>) { 143 + return Gallery.fromJson(json); 144 + } else { 145 + appLogger.w( 146 + 'Unexpected response type for getGallery: ${response.body}', 147 + ); 148 + return null; 149 + } 150 + } catch (e, st) { 151 + appLogger.e('Error parsing getGallery response: $e', stackTrace: st); 152 + return null; 153 + } 114 154 } 115 155 116 156 Future<Map<String, dynamic>> getGalleryThread({required String uri}) async { 117 157 appLogger.i('Fetching gallery thread for uri: $uri'); 118 - final record = await xrpc.query( 119 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 120 - xrpc.NSID.create('gallery.grain.social', 'getGalleryThread'), 121 - parameters: {'uri': uri}, 122 - to: (json) => json as Map<String, dynamic>, 158 + final response = await http.get( 159 + Uri.parse('$_apiUrl/xrpc/social.grain.gallery.getGalleryThread?uri=$uri'), 160 + headers: {'Content-Type': 'application/json'}, 123 161 ); 124 - return record.data; 162 + if (response.statusCode != 200) { 163 + appLogger.w( 164 + 'Failed to fetch gallery thread: ${response.statusCode} ${response.body}', 165 + ); 166 + return {}; 167 + } 168 + return jsonDecode(response.body) as Map<String, dynamic>; 125 169 } 126 170 127 171 Future<List<grain.Notification>> getNotifications() async { ··· 130 174 return []; 131 175 } 132 176 appLogger.i('Fetching notifications'); 133 - final record = await xrpc.query( 134 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 135 - xrpc.NSID.create('notification.grain.social', 'getNotifications'), 136 - headers: {'Authorization': "Bearer $_accessToken"}, 137 - to: (json) => 138 - (json['notifications'] as List<dynamic>?) 139 - ?.map( 140 - (item) => 141 - grain.Notification.fromJson(item as Map<String, dynamic>), 142 - ) 143 - .toList() ?? 144 - [], 177 + final response = await http.get( 178 + Uri.parse('$_apiUrl/xrpc/social.grain.notification.getNotifications'), 179 + headers: { 180 + 'Authorization': "Bearer $_accessToken", 181 + 'Content-Type': 'application/json', 182 + }, 145 183 ); 146 - return record.data; 184 + if (response.statusCode != 200) { 185 + appLogger.w( 186 + 'Failed to fetch notifications: ${response.statusCode} ${response.body}', 187 + ); 188 + return []; 189 + } 190 + final json = jsonDecode(response.body); 191 + return (json['notifications'] as List<dynamic>?) 192 + ?.map( 193 + (item) => 194 + grain.Notification.fromJson(item as Map<String, dynamic>), 195 + ) 196 + .toList() ?? 197 + []; 147 198 } 148 199 149 200 Future<List<Profile>> searchActors(String query) async { ··· 152 203 return []; 153 204 } 154 205 appLogger.i('Searching actors with query: $query'); 155 - final record = await xrpc.query( 156 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 157 - xrpc.NSID.create('actor.grain.social', 'searchActors'), 158 - parameters: {'q': query}, 159 - to: (json) => 160 - (json['actors'] as List<dynamic>?) 161 - ?.map((item) => Profile.fromJson(item)) 162 - .toList() ?? 163 - [], 206 + final response = await http.get( 207 + Uri.parse('$_apiUrl/xrpc/social.grain.actor.searchActors?q=$query'), 208 + headers: { 209 + 'Authorization': "Bearer $_accessToken", 210 + 'Content-Type': 'application/json', 211 + }, 164 212 ); 165 - return record.data; 213 + if (response.statusCode != 200) { 214 + appLogger.w( 215 + 'Failed to search actors: ${response.statusCode} ${response.body}', 216 + ); 217 + return []; 218 + } 219 + final json = jsonDecode(response.body); 220 + return (json['actors'] as List<dynamic>?) 221 + ?.map((item) => Profile.fromJson(item)) 222 + .toList() ?? 223 + []; 166 224 } 167 225 168 226 Future<List<Gallery>> getActorFavs({required String did}) async { 169 227 appLogger.i('Fetching actor favs for did: $did'); 170 - final record = await xrpc.query( 171 - service: _apiUrl.replaceFirst(RegExp(r'^https?://'), ''), 172 - xrpc.NSID.create('actor.grain.social', 'getActorFavs'), 173 - parameters: {'actor': did}, 174 - to: (json) => 175 - (json['items'] as List<dynamic>?) 176 - ?.map((item) => Gallery.fromJson(item)) 177 - .toList() ?? 178 - [], 228 + final response = await http.get( 229 + Uri.parse('$_apiUrl/xrpc/social.grain.actor.getActorFavs?actor=$did'), 230 + headers: {'Content-Type': 'application/json'}, 179 231 ); 180 - return record.data; 232 + if (response.statusCode != 200) { 233 + appLogger.w( 234 + 'Failed to fetch actor favs: ${response.statusCode} ${response.body}', 235 + ); 236 + return []; 237 + } 238 + final json = jsonDecode(response.body); 239 + return (json['items'] as List<dynamic>?) 240 + ?.map((item) => Gallery.fromJson(item)) 241 + .toList() ?? 242 + []; 181 243 } 182 244 183 245 Future<String?> createGallery({
+20
lib/screens/gallery_page.dart
··· 9 9 import 'package:share_plus/share_plus.dart'; 10 10 import 'package:grain/widgets/app_image.dart'; 11 11 import 'package:cached_network_image/cached_network_image.dart'; 12 + import 'package:grain/screens/create_gallery_page.dart'; 12 13 13 14 class GalleryPage extends StatefulWidget { 14 15 final String uri; ··· 111 112 fontSize: 18, 112 113 fontWeight: FontWeight.w600, 113 114 ), 115 + actions: [ 116 + if (gallery.creator?.did == widget.currentUserDid) 117 + IconButton( 118 + icon: const Icon(Icons.edit), 119 + tooltip: 'Edit Gallery', 120 + onPressed: () async { 121 + await showModalBottomSheet( 122 + context: context, 123 + isScrollControlled: true, 124 + builder: (context) => CreateGalleryPage( 125 + // Optionally pass initial data for editing 126 + gallery: gallery, 127 + ), 128 + ); 129 + // Optionally refresh after editing 130 + _fetchGallery(); 131 + }, 132 + ), 133 + ], 114 134 ), 115 135 body: ListView( 116 136 children: [