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