this repo has no description

feat: add description facets to profile model and update profile fetching logic

+114 -48
+1 -3
lib/main.dart
··· 74 } 75 76 void handleSignIn() async { 77 - final container = ProviderScope.containerOf(context, listen: false); 78 - 79 setState(() { 80 isSignedIn = true; 81 }); ··· 87 void handleSignOut(BuildContext context) async { 88 final container = ProviderScope.containerOf(context, listen: false); 89 await auth.clearSession(); // Clear session data 90 - // Invalidate Riverpod providers for profile and gallery state 91 container.invalidate(profileNotifierProvider); 92 // Add any other providers you want to invalidate here 93 setState(() {
··· 74 } 75 76 void handleSignIn() async { 77 setState(() { 78 isSignedIn = true; 79 }); ··· 85 void handleSignOut(BuildContext context) async { 86 final container = ProviderScope.containerOf(context, listen: false); 87 await auth.clearSession(); // Clear session data 88 + // Invalidate Riverpod providers for profile state 89 container.invalidate(profileNotifierProvider); 90 // Add any other providers you want to invalidate here 91 setState(() {
+2
lib/models/profile.dart
··· 18 int? followsCount, 19 int? galleryCount, 20 ProfileViewer? viewer, 21 }) = _Profile; 22 23 factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
··· 18 int? followsCount, 19 int? galleryCount, 20 ProfileViewer? viewer, 21 + // Added field for description facets used on profile page 22 + List<Map<String, dynamic>>? descriptionFacets, 23 }) = _Profile; 24 25 factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
+41 -5
lib/models/profile.freezed.dart
··· 30 int? get followersCount => throw _privateConstructorUsedError; 31 int? get followsCount => throw _privateConstructorUsedError; 32 int? get galleryCount => throw _privateConstructorUsedError; 33 - ProfileViewer? get viewer => throw _privateConstructorUsedError; 34 35 /// Serializes this Profile to a JSON map. 36 Map<String, dynamic> toJson() => throw _privateConstructorUsedError; ··· 57 int? followsCount, 58 int? galleryCount, 59 ProfileViewer? viewer, 60 }); 61 62 $ProfileViewerCopyWith<$Res>? get viewer; ··· 87 Object? followsCount = freezed, 88 Object? galleryCount = freezed, 89 Object? viewer = freezed, 90 }) { 91 return _then( 92 _value.copyWith( ··· 130 ? _value.viewer 131 : viewer // ignore: cast_nullable_to_non_nullable 132 as ProfileViewer?, 133 ) 134 as $Val, 135 ); ··· 169 int? followsCount, 170 int? galleryCount, 171 ProfileViewer? viewer, 172 }); 173 174 @override ··· 199 Object? followsCount = freezed, 200 Object? galleryCount = freezed, 201 Object? viewer = freezed, 202 }) { 203 return _then( 204 _$ProfileImpl( ··· 242 ? _value.viewer 243 : viewer // ignore: cast_nullable_to_non_nullable 244 as ProfileViewer?, 245 ), 246 ); 247 } ··· 261 this.followsCount, 262 this.galleryCount, 263 this.viewer, 264 - }); 265 266 factory _$ProfileImpl.fromJson(Map<String, dynamic> json) => 267 _$$ProfileImplFromJson(json); ··· 286 final int? galleryCount; 287 @override 288 final ProfileViewer? viewer; 289 290 @override 291 String toString() { 292 - return 'Profile(cid: $cid, did: $did, handle: $handle, displayName: $displayName, description: $description, avatar: $avatar, followersCount: $followersCount, followsCount: $followsCount, galleryCount: $galleryCount, viewer: $viewer)'; 293 } 294 295 @override ··· 311 other.followsCount == followsCount) && 312 (identical(other.galleryCount, galleryCount) || 313 other.galleryCount == galleryCount) && 314 - (identical(other.viewer, viewer) || other.viewer == viewer)); 315 } 316 317 @JsonKey(includeFromJson: false, includeToJson: false) ··· 328 followsCount, 329 galleryCount, 330 viewer, 331 ); 332 333 /// Create a copy of Profile ··· 356 final int? followsCount, 357 final int? galleryCount, 358 final ProfileViewer? viewer, 359 }) = _$ProfileImpl; 360 361 factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson; ··· 379 @override 380 int? get galleryCount; 381 @override 382 - ProfileViewer? get viewer; 383 384 /// Create a copy of Profile 385 /// with the given fields replaced by the non-null parameter values.
··· 30 int? get followersCount => throw _privateConstructorUsedError; 31 int? get followsCount => throw _privateConstructorUsedError; 32 int? get galleryCount => throw _privateConstructorUsedError; 33 + ProfileViewer? get viewer => 34 + throw _privateConstructorUsedError; // Added field for description facets used on profile page 35 + List<Map<String, dynamic>>? get descriptionFacets => 36 + throw _privateConstructorUsedError; 37 38 /// Serializes this Profile to a JSON map. 39 Map<String, dynamic> toJson() => throw _privateConstructorUsedError; ··· 60 int? followsCount, 61 int? galleryCount, 62 ProfileViewer? viewer, 63 + List<Map<String, dynamic>>? descriptionFacets, 64 }); 65 66 $ProfileViewerCopyWith<$Res>? get viewer; ··· 91 Object? followsCount = freezed, 92 Object? galleryCount = freezed, 93 Object? viewer = freezed, 94 + Object? descriptionFacets = freezed, 95 }) { 96 return _then( 97 _value.copyWith( ··· 135 ? _value.viewer 136 : viewer // ignore: cast_nullable_to_non_nullable 137 as ProfileViewer?, 138 + descriptionFacets: freezed == descriptionFacets 139 + ? _value.descriptionFacets 140 + : descriptionFacets // ignore: cast_nullable_to_non_nullable 141 + as List<Map<String, dynamic>>?, 142 ) 143 as $Val, 144 ); ··· 178 int? followsCount, 179 int? galleryCount, 180 ProfileViewer? viewer, 181 + List<Map<String, dynamic>>? descriptionFacets, 182 }); 183 184 @override ··· 209 Object? followsCount = freezed, 210 Object? galleryCount = freezed, 211 Object? viewer = freezed, 212 + Object? descriptionFacets = freezed, 213 }) { 214 return _then( 215 _$ProfileImpl( ··· 253 ? _value.viewer 254 : viewer // ignore: cast_nullable_to_non_nullable 255 as ProfileViewer?, 256 + descriptionFacets: freezed == descriptionFacets 257 + ? _value._descriptionFacets 258 + : descriptionFacets // ignore: cast_nullable_to_non_nullable 259 + as List<Map<String, dynamic>>?, 260 ), 261 ); 262 } ··· 276 this.followsCount, 277 this.galleryCount, 278 this.viewer, 279 + final List<Map<String, dynamic>>? descriptionFacets, 280 + }) : _descriptionFacets = descriptionFacets; 281 282 factory _$ProfileImpl.fromJson(Map<String, dynamic> json) => 283 _$$ProfileImplFromJson(json); ··· 302 final int? galleryCount; 303 @override 304 final ProfileViewer? viewer; 305 + // Added field for description facets used on profile page 306 + final List<Map<String, dynamic>>? _descriptionFacets; 307 + // Added field for description facets used on profile page 308 + @override 309 + List<Map<String, dynamic>>? get descriptionFacets { 310 + final value = _descriptionFacets; 311 + if (value == null) return null; 312 + if (_descriptionFacets is EqualUnmodifiableListView) 313 + return _descriptionFacets; 314 + // ignore: implicit_dynamic_type 315 + return EqualUnmodifiableListView(value); 316 + } 317 318 @override 319 String toString() { 320 + return 'Profile(cid: $cid, did: $did, handle: $handle, displayName: $displayName, description: $description, avatar: $avatar, followersCount: $followersCount, followsCount: $followsCount, galleryCount: $galleryCount, viewer: $viewer, descriptionFacets: $descriptionFacets)'; 321 } 322 323 @override ··· 339 other.followsCount == followsCount) && 340 (identical(other.galleryCount, galleryCount) || 341 other.galleryCount == galleryCount) && 342 + (identical(other.viewer, viewer) || other.viewer == viewer) && 343 + const DeepCollectionEquality().equals( 344 + other._descriptionFacets, 345 + _descriptionFacets, 346 + )); 347 } 348 349 @JsonKey(includeFromJson: false, includeToJson: false) ··· 360 followsCount, 361 galleryCount, 362 viewer, 363 + const DeepCollectionEquality().hash(_descriptionFacets), 364 ); 365 366 /// Create a copy of Profile ··· 389 final int? followsCount, 390 final int? galleryCount, 391 final ProfileViewer? viewer, 392 + final List<Map<String, dynamic>>? descriptionFacets, 393 }) = _$ProfileImpl; 394 395 factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson; ··· 413 @override 414 int? get galleryCount; 415 @override 416 + ProfileViewer? get viewer; // Added field for description facets used on profile page 417 + @override 418 + List<Map<String, dynamic>>? get descriptionFacets; 419 420 /// Create a copy of Profile 421 /// with the given fields replaced by the non-null parameter values.
+4
lib/models/profile.g.dart
··· 20 viewer: json['viewer'] == null 21 ? null 22 : ProfileViewer.fromJson(json['viewer'] as Map<String, dynamic>), 23 ); 24 25 Map<String, dynamic> _$$ProfileImplToJson(_$ProfileImpl instance) => ··· 34 'followsCount': instance.followsCount, 35 'galleryCount': instance.galleryCount, 36 'viewer': instance.viewer, 37 };
··· 20 viewer: json['viewer'] == null 21 ? null 22 : ProfileViewer.fromJson(json['viewer'] as Map<String, dynamic>), 23 + descriptionFacets: (json['descriptionFacets'] as List<dynamic>?) 24 + ?.map((e) => e as Map<String, dynamic>) 25 + .toList(), 26 ); 27 28 Map<String, dynamic> _$$ProfileImplToJson(_$ProfileImpl instance) => ··· 37 'followsCount': instance.followsCount, 38 'galleryCount': instance.galleryCount, 39 'viewer': instance.viewer, 40 + 'descriptionFacets': instance.descriptionFacets, 41 };
+42 -1
lib/providers/profile_provider.dart
··· 1 import 'dart:io'; 2 3 import 'package:grain/api.dart'; 4 import 'package:grain/models/profile.dart'; 5 import 'package:grain/models/profile_with_galleries.dart'; ··· 16 return _fetchProfile(did); 17 } 18 19 Future<ProfileWithGalleries?> _fetchProfile(String did) async { 20 final profile = await apiService.fetchProfile(did: did); 21 final galleries = await apiService.fetchActorGalleries(did: did); 22 if (profile != null) { 23 - return ProfileWithGalleries(profile: profile, galleries: galleries); 24 } 25 return null; 26 }
··· 1 import 'dart:io'; 2 3 + import 'package:bluesky_text/bluesky_text.dart'; 4 import 'package:grain/api.dart'; 5 import 'package:grain/models/profile.dart'; 6 import 'package:grain/models/profile_with_galleries.dart'; ··· 17 return _fetchProfile(did); 18 } 19 20 + // @TODO: Facets don't always render correctly. 21 + List<Map<String, dynamic>>? _filterValidFacets( 22 + List<Map<String, dynamic>>? computedFacets, 23 + String desc, 24 + ) { 25 + if (computedFacets == null) return null; 26 + return computedFacets.where((facet) { 27 + final index = facet['index']; 28 + if (index is Map) { 29 + final start = index['byteStart'] ?? 0; 30 + final end = index['byteEnd'] ?? 0; 31 + return start is int && end is int && start >= 0 && end > start && end <= desc.length; 32 + } 33 + final start = facet['index'] ?? facet['offset'] ?? 0; 34 + final end = facet['end']; 35 + final length = facet['length']; 36 + if (end is int && start is int) { 37 + return start >= 0 && end > start && end <= desc.length; 38 + } else if (length is int && start is int) { 39 + return start >= 0 && length > 0 && start + length <= desc.length; 40 + } 41 + return false; 42 + }).toList(); 43 + } 44 + 45 Future<ProfileWithGalleries?> _fetchProfile(String did) async { 46 final profile = await apiService.fetchProfile(did: did); 47 final galleries = await apiService.fetchActorGalleries(did: did); 48 if (profile != null) { 49 + List<Map<String, dynamic>>? facets; 50 + final desc = profile.description ?? ''; 51 + if (desc.isNotEmpty) { 52 + try { 53 + final blueskyText = BlueskyText(desc); 54 + final entities = blueskyText.entities; 55 + final computedFacets = await entities.toFacets(); 56 + facets = _filterValidFacets(computedFacets, desc); 57 + } catch (_) { 58 + facets = null; 59 + } 60 + } 61 + return ProfileWithGalleries( 62 + profile: profile.copyWith(descriptionFacets: facets), 63 + galleries: galleries, 64 + ); 65 } 66 return null; 67 }
+1 -1
lib/providers/profile_provider.g.dart
··· 6 // RiverpodGenerator 7 // ************************************************************************** 8 9 - String _$profileNotifierHash() => r'0a4d40043196309ae74bdb3b88d7d63d25b8b4f6'; 10 11 /// Copied from Dart SDK 12 class _SystemHash {
··· 6 // RiverpodGenerator 7 // ************************************************************************** 8 9 + String _$profileNotifierHash() => r'a955b4d4a22d864fc88f77dad30b5d3019ba8dfe'; 10 11 /// Copied from Dart SDK 12 class _SystemHash {
+2
lib/screens/home_page.dart
··· 1 import 'package:flutter/material.dart'; 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 import 'package:grain/api.dart'; ··· 397 ? FloatingActionButton( 398 shape: const CircleBorder(), 399 onPressed: () { 400 showModalBottomSheet( 401 context: context, 402 isScrollControlled: true,
··· 1 import 'package:flutter/material.dart'; 2 + import 'package:flutter/services.dart'; 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 5 import 'package:grain/api.dart'; ··· 398 ? FloatingActionButton( 399 shape: const CircleBorder(), 400 onPressed: () { 401 + HapticFeedback.mediumImpact(); 402 showModalBottomSheet( 403 context: context, 404 isScrollControlled: true,
+21 -38
lib/screens/profile_page.dart
··· 1 - import 'package:bluesky_text/bluesky_text.dart'; 2 import 'package:flutter/material.dart'; 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 import 'package:grain/api.dart'; ··· 133 } 134 final profile = profileWithGalleries.profile; 135 final galleries = profileWithGalleries.galleries; 136 - Future<List<Map<String, dynamic>>?> getDescriptionFacets() async { 137 - final desc = profile.description ?? ''; 138 - if (desc.isEmpty) return null; 139 - try { 140 - final blueskyText = BlueskyText(desc); 141 - final entities = blueskyText.entities; 142 - return await entities.toFacets(); 143 - } catch (_) { 144 - return null; 145 - } 146 - } 147 148 return Scaffold( 149 backgroundColor: theme.scaffoldBackgroundColor, ··· 298 ), 299 if ((profile.description ?? '').isNotEmpty) ...[ 300 const SizedBox(height: 16), 301 - FutureBuilder<List<Map<String, dynamic>>?>( 302 - future: getDescriptionFacets(), 303 - builder: (context, snapshot) { 304 - return FacetedText( 305 - text: profile.description ?? '', 306 - facets: snapshot.data, 307 - onMentionTap: (didOrHandle) { 308 - Navigator.of(context).push( 309 - MaterialPageRoute( 310 - builder: (context) => 311 - ProfilePage(did: didOrHandle, showAppBar: true), 312 - ), 313 - ); 314 - }, 315 - onLinkTap: (url) { 316 - // TODO: Implement WebViewPage navigation 317 - }, 318 - onTagTap: (tag) => Navigator.push( 319 - context, 320 - MaterialPageRoute( 321 - builder: (_) => HashtagPage(hashtag: tag), 322 - ), 323 - ), 324 - linkStyle: TextStyle( 325 - color: Theme.of(context).colorScheme.primary, 326 - fontWeight: FontWeight.w600, 327 ), 328 ); 329 }, 330 ), 331 ], 332 const SizedBox(height: 24),
··· 1 import 'package:flutter/material.dart'; 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 import 'package:grain/api.dart'; ··· 132 } 133 final profile = profileWithGalleries.profile; 134 final galleries = profileWithGalleries.galleries; 135 136 return Scaffold( 137 backgroundColor: theme.scaffoldBackgroundColor, ··· 286 ), 287 if ((profile.description ?? '').isNotEmpty) ...[ 288 const SizedBox(height: 16), 289 + FacetedText( 290 + text: profile.description ?? '', 291 + facets: profile.descriptionFacets, 292 + onMentionTap: (didOrHandle) { 293 + Navigator.of(context).push( 294 + MaterialPageRoute( 295 + builder: (context) => 296 + ProfilePage(did: didOrHandle, showAppBar: true), 297 ), 298 ); 299 }, 300 + onLinkTap: (url) { 301 + // TODO: Implement WebViewPage navigation 302 + }, 303 + onTagTap: (tag) => Navigator.push( 304 + context, 305 + MaterialPageRoute( 306 + builder: (_) => HashtagPage(hashtag: tag), 307 + ), 308 + ), 309 + linkStyle: TextStyle( 310 + color: Theme.of(context).colorScheme.primary, 311 + fontWeight: FontWeight.w600, 312 + ), 313 ), 314 ], 315 const SizedBox(height: 24),