Grain flutter app

feat: add cameras field to Profile model and update related serialization; enhance UI to display cameras in profile page

+79 -16
+1 -1
lib/app_theme.dart
··· 24 dividerColor: Colors.grey[300], 25 colorScheme: ColorScheme.light( 26 primary: primaryColor, 27 - surface: Colors.white, 28 onSurface: Colors.black87, 29 onSurfaceVariant: Colors.black54, 30 onPrimary: Colors.white,
··· 24 dividerColor: Colors.grey[300], 25 colorScheme: ColorScheme.light( 26 primary: primaryColor, 27 + surface: Colors.grey[200]!, 28 onSurface: Colors.black87, 29 onSurfaceVariant: Colors.black54, 30 onPrimary: Colors.white,
+1
lib/models/profile.dart
··· 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;
··· 18 int? followsCount, 19 int? galleryCount, 20 ProfileViewer? viewer, 21 + List<String>? cameras, 22 // Added field for description facets used on profile page 23 List<Map<String, dynamic>>? descriptionFacets, 24 }) = _Profile;
+34 -4
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 => 34 throw _privateConstructorUsedError; // Added field for description facets used on profile page 35 List<Map<String, dynamic>>? get descriptionFacets => 36 throw _privateConstructorUsedError; ··· 60 int? followsCount, 61 int? galleryCount, 62 ProfileViewer? viewer, 63 List<Map<String, dynamic>>? descriptionFacets, 64 }); 65 ··· 91 Object? followsCount = freezed, 92 Object? galleryCount = freezed, 93 Object? viewer = freezed, 94 Object? descriptionFacets = freezed, 95 }) { 96 return _then( ··· 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 ··· 178 int? followsCount, 179 int? galleryCount, 180 ProfileViewer? viewer, 181 List<Map<String, dynamic>>? descriptionFacets, 182 }); 183 ··· 209 Object? followsCount = freezed, 210 Object? galleryCount = freezed, 211 Object? viewer = freezed, 212 Object? descriptionFacets = freezed, 213 }) { 214 return _then( ··· 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 ··· 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 ··· 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 ··· 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, ··· 360 followsCount, 361 galleryCount, 362 viewer, 363 const DeepCollectionEquality().hash(_descriptionFacets), 364 ); 365 ··· 389 final int? followsCount, 390 final int? galleryCount, 391 final ProfileViewer? viewer, 392 final List<Map<String, dynamic>>? descriptionFacets, 393 }) = _$ProfileImpl; 394 ··· 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
··· 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 + List<String>? get cameras => 35 throw _privateConstructorUsedError; // Added field for description facets used on profile page 36 List<Map<String, dynamic>>? get descriptionFacets => 37 throw _privateConstructorUsedError; ··· 61 int? followsCount, 62 int? galleryCount, 63 ProfileViewer? viewer, 64 + List<String>? cameras, 65 List<Map<String, dynamic>>? descriptionFacets, 66 }); 67 ··· 93 Object? followsCount = freezed, 94 Object? galleryCount = freezed, 95 Object? viewer = freezed, 96 + Object? cameras = freezed, 97 Object? descriptionFacets = freezed, 98 }) { 99 return _then( ··· 138 ? _value.viewer 139 : viewer // ignore: cast_nullable_to_non_nullable 140 as ProfileViewer?, 141 + cameras: freezed == cameras 142 + ? _value.cameras 143 + : cameras // ignore: cast_nullable_to_non_nullable 144 + as List<String>?, 145 descriptionFacets: freezed == descriptionFacets 146 ? _value.descriptionFacets 147 : descriptionFacets // ignore: cast_nullable_to_non_nullable ··· 185 int? followsCount, 186 int? galleryCount, 187 ProfileViewer? viewer, 188 + List<String>? cameras, 189 List<Map<String, dynamic>>? descriptionFacets, 190 }); 191 ··· 217 Object? followsCount = freezed, 218 Object? galleryCount = freezed, 219 Object? viewer = freezed, 220 + Object? cameras = freezed, 221 Object? descriptionFacets = freezed, 222 }) { 223 return _then( ··· 262 ? _value.viewer 263 : viewer // ignore: cast_nullable_to_non_nullable 264 as ProfileViewer?, 265 + cameras: freezed == cameras 266 + ? _value._cameras 267 + : cameras // ignore: cast_nullable_to_non_nullable 268 + as List<String>?, 269 descriptionFacets: freezed == descriptionFacets 270 ? _value._descriptionFacets 271 : descriptionFacets // ignore: cast_nullable_to_non_nullable ··· 289 this.followsCount, 290 this.galleryCount, 291 this.viewer, 292 + final List<String>? cameras, 293 final List<Map<String, dynamic>>? descriptionFacets, 294 + }) : _cameras = cameras, 295 + _descriptionFacets = descriptionFacets; 296 297 factory _$ProfileImpl.fromJson(Map<String, dynamic> json) => 298 _$$ProfileImplFromJson(json); ··· 317 final int? galleryCount; 318 @override 319 final ProfileViewer? viewer; 320 + final List<String>? _cameras; 321 + @override 322 + List<String>? get cameras { 323 + final value = _cameras; 324 + if (value == null) return null; 325 + if (_cameras is EqualUnmodifiableListView) return _cameras; 326 + // ignore: implicit_dynamic_type 327 + return EqualUnmodifiableListView(value); 328 + } 329 + 330 // Added field for description facets used on profile page 331 final List<Map<String, dynamic>>? _descriptionFacets; 332 // Added field for description facets used on profile page ··· 342 343 @override 344 String toString() { 345 + return 'Profile(cid: $cid, did: $did, handle: $handle, displayName: $displayName, description: $description, avatar: $avatar, followersCount: $followersCount, followsCount: $followsCount, galleryCount: $galleryCount, viewer: $viewer, cameras: $cameras, descriptionFacets: $descriptionFacets)'; 346 } 347 348 @override ··· 365 (identical(other.galleryCount, galleryCount) || 366 other.galleryCount == galleryCount) && 367 (identical(other.viewer, viewer) || other.viewer == viewer) && 368 + const DeepCollectionEquality().equals(other._cameras, _cameras) && 369 const DeepCollectionEquality().equals( 370 other._descriptionFacets, 371 _descriptionFacets, ··· 386 followsCount, 387 galleryCount, 388 viewer, 389 + const DeepCollectionEquality().hash(_cameras), 390 const DeepCollectionEquality().hash(_descriptionFacets), 391 ); 392 ··· 416 final int? followsCount, 417 final int? galleryCount, 418 final ProfileViewer? viewer, 419 + final List<String>? cameras, 420 final List<Map<String, dynamic>>? descriptionFacets, 421 }) = _$ProfileImpl; 422 ··· 441 @override 442 int? get galleryCount; 443 @override 444 + ProfileViewer? get viewer; 445 + @override 446 + List<String>? get cameras; // Added field for description facets used on profile page 447 @override 448 List<Map<String, dynamic>>? get descriptionFacets; 449
+4
lib/models/profile.g.dart
··· 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(), ··· 37 'followsCount': instance.followsCount, 38 'galleryCount': instance.galleryCount, 39 'viewer': instance.viewer, 40 'descriptionFacets': instance.descriptionFacets, 41 };
··· 20 viewer: json['viewer'] == null 21 ? null 22 : ProfileViewer.fromJson(json['viewer'] as Map<String, dynamic>), 23 + cameras: (json['cameras'] as List<dynamic>?) 24 + ?.map((e) => e as String) 25 + .toList(), 26 descriptionFacets: (json['descriptionFacets'] as List<dynamic>?) 27 ?.map((e) => e as Map<String, dynamic>) 28 .toList(), ··· 40 'followsCount': instance.followsCount, 41 'galleryCount': instance.galleryCount, 42 'viewer': instance.viewer, 43 + 'cameras': instance.cameras, 44 'descriptionFacets': instance.descriptionFacets, 45 };
+1 -1
lib/providers/profile_provider.g.dart
··· 6 // RiverpodGenerator 7 // ************************************************************************** 8 9 - String _$profileNotifierHash() => r'a955b4d4a22d864fc88f77dad30b5d3019ba8dfe'; 10 11 /// Copied from Dart SDK 12 class _SystemHash {
··· 6 // RiverpodGenerator 7 // ************************************************************************** 8 9 + String _$profileNotifierHash() => r'6d06fd234febd0ddc0c638fb7cd7c013c8e78652'; 10 11 /// Copied from Dart SDK 12 class _SystemHash {
+5 -2
lib/screens/comments_page.dart
··· 283 Row( 284 mainAxisAlignment: MainAxisAlignment.spaceBetween, 285 children: [ 286 - TextButton( 287 onPressed: _posting ? null : () => Navigator.pop(context), 288 - child: const Text('Cancel'), 289 ), 290 AppButton( 291 borderRadius: 22,
··· 283 Row( 284 mainAxisAlignment: MainAxisAlignment.spaceBetween, 285 children: [ 286 + AppButton( 287 + label: 'Cancel', 288 + variant: AppButtonVariant.text, 289 + size: AppButtonSize.small, 290 + disabled: _posting, 291 onPressed: _posting ? null : () => Navigator.pop(context), 292 ), 293 AppButton( 294 borderRadius: 22,
+29
lib/screens/profile_page.dart
··· 285 0) 286 .toString(), 287 ), 288 if ((profile.description ?? '').isNotEmpty) ...[ 289 const SizedBox(height: 16), 290 FacetedText(
··· 285 0) 286 .toString(), 287 ), 288 + if ((profile.cameras?.isNotEmpty ?? false)) ...[ 289 + const SizedBox(height: 16), 290 + Wrap( 291 + spacing: 8, 292 + runSpacing: 4, 293 + children: profile.cameras! 294 + .map( 295 + (camera) => Container( 296 + padding: const EdgeInsets.symmetric( 297 + horizontal: 12, 298 + vertical: 4, 299 + ), 300 + decoration: BoxDecoration( 301 + color: theme.colorScheme.surface, 302 + borderRadius: BorderRadius.circular(999), 303 + ), 304 + child: Text( 305 + '📷 $camera', 306 + style: TextStyle( 307 + color: theme.colorScheme.onSurface, 308 + fontWeight: FontWeight.w500, 309 + fontSize: 12, 310 + ), 311 + ), 312 + ), 313 + ) 314 + .toList(), 315 + ), 316 + ], 317 if ((profile.description ?? '').isNotEmpty) ...[ 318 const SizedBox(height: 16), 319 FacetedText(
+1 -5
lib/widgets/app_button.dart
··· 37 final theme = Theme.of(context); 38 final Color primaryColor = theme.colorScheme.primary; 39 final Color secondaryColor = theme.colorScheme.surfaceContainerHighest; 40 - final Color secondaryBorder = theme.dividerColor; 41 final Color secondaryText = theme.colorScheme.onSurface; 42 final Color primaryText = theme.colorScheme.onPrimary; 43 final bool isPrimary = variant == AppButtonVariant.primary; ··· 87 : (disabled ? secondaryColor.withOpacity(0.5) : secondaryColor), 88 foregroundColor: isPrimary ? primaryText : secondaryText, 89 elevation: 0, 90 - shape: RoundedRectangleBorder( 91 - borderRadius: BorderRadius.circular(resolvedBorderRadius), 92 - side: isPrimary ? BorderSide.none : BorderSide(color: secondaryBorder, width: 1), 93 - ), 94 padding: resolvedPadding, 95 textStyle: theme.textTheme.labelLarge?.copyWith( 96 fontWeight: FontWeight.w600,
··· 37 final theme = Theme.of(context); 38 final Color primaryColor = theme.colorScheme.primary; 39 final Color secondaryColor = theme.colorScheme.surfaceContainerHighest; 40 final Color secondaryText = theme.colorScheme.onSurface; 41 final Color primaryText = theme.colorScheme.onPrimary; 42 final bool isPrimary = variant == AppButtonVariant.primary; ··· 86 : (disabled ? secondaryColor.withOpacity(0.5) : secondaryColor), 87 foregroundColor: isPrimary ? primaryText : secondaryText, 88 elevation: 0, 89 + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(resolvedBorderRadius)), 90 padding: resolvedPadding, 91 textStyle: theme.textTheme.labelLarge?.copyWith( 92 fontWeight: FontWeight.w600,
+2 -2
lib/widgets/gallery_preview.dart
··· 11 Widget build(BuildContext context) { 12 final theme = Theme.of(context); 13 final Color bgColor = theme.brightness == Brightness.dark 14 - ? Colors.grey[900]! 15 - : Colors.grey[100]!; 16 final photos = gallery.items 17 .where((item) => item.thumb != null && item.thumb!.isNotEmpty) 18 .toList();
··· 11 Widget build(BuildContext context) { 12 final theme = Theme.of(context); 13 final Color bgColor = theme.brightness == Brightness.dark 14 + ? Colors.grey[850]! 15 + : Colors.grey[200]!; 16 final photos = gallery.items 17 .where((item) => item.thumb != null && item.thumb!.isNotEmpty) 18 .toList();
+1 -1
lib/widgets/plain_text_field.dart
··· 36 const SizedBox(height: 6), 37 Container( 38 decoration: BoxDecoration( 39 - color: theme.brightness == Brightness.dark ? Colors.grey[850] : Colors.grey[200], 40 borderRadius: BorderRadius.circular(8), 41 ), 42 child: Focus(
··· 36 const SizedBox(height: 6), 37 Container( 38 decoration: BoxDecoration( 39 + color: theme.brightness == Brightness.dark ? Colors.grey[850] : Colors.grey[300], 40 borderRadius: BorderRadius.circular(8), 41 ), 42 child: Focus(