✨ Image attachment
This commit is contained in:
		| @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/theme.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| @@ -31,6 +32,7 @@ class SolianApp extends StatelessWidget { | ||||
|         child: MultiProvider( | ||||
|           providers: [ | ||||
|             Provider(create: (_) => SnNetworkProvider()), | ||||
|             Provider(create: (ctx) => SnAttachmentProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (_) => UserProvider()), | ||||
|             ChangeNotifierProvider(create: (_) => ThemeProvider()), | ||||
|           ], | ||||
|   | ||||
							
								
								
									
										47
									
								
								lib/providers/sn_attachment.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/providers/sn_attachment.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
|  | ||||
| class SnAttachmentProvider { | ||||
|   late final SnNetworkProvider sn; | ||||
|  | ||||
|   final Map<String, SnAttachment> _cache = {}; | ||||
|  | ||||
|   SnAttachmentProvider(BuildContext context) { | ||||
|     sn = context.read<SnNetworkProvider>(); | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment> getOne(String rid, {noCache = false}) async { | ||||
|     if (!noCache && _cache.containsKey(rid)) { | ||||
|       return _cache[rid]!; | ||||
|     } | ||||
|  | ||||
|     final resp = await sn.client.get('/cgi/uc/attachments/$rid/meta'); | ||||
|     final out = SnAttachment.fromJson(resp.data); | ||||
|     _cache[rid] = out; | ||||
|  | ||||
|     return out; | ||||
|   } | ||||
|  | ||||
|   Future<List<SnAttachment>> getMultiple(List<String> rids, | ||||
|       {noCache = false}) async { | ||||
|     final pendingFetch = | ||||
|         noCache ? rids : rids.where((rid) => !_cache.containsKey(rid)).toList(); | ||||
|  | ||||
|     if (pendingFetch.isEmpty) { | ||||
|       return rids.map((rid) => _cache[rid]!).toList(); | ||||
|     } | ||||
|  | ||||
|     final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: { | ||||
|       'take': pendingFetch.length, | ||||
|       'id': pendingFetch.join(','), | ||||
|     }); | ||||
|     final out = resp.data['data'].map((e) => SnAttachment.fromJson(e)).toList(); | ||||
|  | ||||
|     for (var i = 0; i < out.length; i++) { | ||||
|       _cache[pendingFetch[i]] = out[i]; | ||||
|     } | ||||
|     return rids.map((rid) => _cache[rid]!).toList(); | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| @@ -30,10 +31,31 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|       'take': 10, | ||||
|       'offset': _posts.length, | ||||
|     }); | ||||
|     final List<SnPost> out = | ||||
|         List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []); | ||||
|  | ||||
|     Set<String> rids = {}; | ||||
|     for (var i = 0; i < out.length; i++) { | ||||
|       rids.addAll(out[i].body['attachments']?.cast<String>() ?? []); | ||||
|     } | ||||
|  | ||||
|     if (!mounted) return; | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final attachments = await attach.getMultiple(rids.toList()); | ||||
|     for (var i = 0; i < out.length; i++) { | ||||
|       out[i] = out[i].copyWith( | ||||
|         preload: SnPostPreload( | ||||
|           attachments: attachments | ||||
|               .where( | ||||
|                 (ele) => out[i].body['attachments']?.contains(ele.rid) ?? false, | ||||
|               ) | ||||
|               .toList(), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     _postCount = resp.data['count']; | ||||
|     _posts.addAll( | ||||
|         resp.data['data']?.map((e) => SnPost.fromJson(e)).cast<SnPost>() ?? []); | ||||
|     _posts.addAll(out); | ||||
|  | ||||
|     if (mounted) setState(() => _isBusy = false); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										56
									
								
								lib/types/attachment.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								lib/types/attachment.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
|  | ||||
| part 'attachment.freezed.dart'; | ||||
| part 'attachment.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnAttachment with _$SnAttachment { | ||||
|   const factory SnAttachment({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required dynamic deletedAt, | ||||
|     required String rid, | ||||
|     required String uuid, | ||||
|     required int size, | ||||
|     required String name, | ||||
|     required String alt, | ||||
|     required String mimetype, | ||||
|     required String hash, | ||||
|     required int destination, | ||||
|     required int refCount, | ||||
|     required dynamic fileChunks, | ||||
|     required dynamic cleanedAt, | ||||
|     required Map<String, dynamic> metadata, | ||||
|     required bool isMature, | ||||
|     required bool isAnalyzed, | ||||
|     required bool isUploaded, | ||||
|     required bool isSelfRef, | ||||
|     required dynamic ref, | ||||
|     required dynamic refId, | ||||
|     required SnAttachmentPool? pool, | ||||
|     required int poolId, | ||||
|     required int accountId, | ||||
|   }) = _SnAttachment; | ||||
|  | ||||
|   factory SnAttachment.fromJson(Map<String, Object?> json) => | ||||
|       _$SnAttachmentFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAttachmentPool with _$SnAttachmentPool { | ||||
|   const factory SnAttachmentPool({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     required String alias, | ||||
|     required String name, | ||||
|     required String description, | ||||
|     required Map<String, dynamic> config, | ||||
|     required int? accountId, | ||||
|   }) = _SnAttachmentPool; | ||||
|  | ||||
|   factory SnAttachmentPool.fromJson(Map<String, Object?> json) => | ||||
|       _$SnAttachmentPoolFromJson(json); | ||||
| } | ||||
							
								
								
									
										1048
									
								
								lib/types/attachment.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1048
									
								
								lib/types/attachment.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										97
									
								
								lib/types/attachment.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								lib/types/attachment.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'attachment.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAttachmentImpl( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'], | ||||
|       rid: json['rid'] as String, | ||||
|       uuid: json['uuid'] as String, | ||||
|       size: (json['size'] as num).toInt(), | ||||
|       name: json['name'] as String, | ||||
|       alt: json['alt'] as String, | ||||
|       mimetype: json['mimetype'] as String, | ||||
|       hash: json['hash'] as String, | ||||
|       destination: (json['destination'] as num).toInt(), | ||||
|       refCount: (json['ref_count'] as num).toInt(), | ||||
|       fileChunks: json['file_chunks'], | ||||
|       cleanedAt: json['cleaned_at'], | ||||
|       metadata: json['metadata'] as Map<String, dynamic>, | ||||
|       isMature: json['is_mature'] as bool, | ||||
|       isAnalyzed: json['is_analyzed'] as bool, | ||||
|       isUploaded: json['is_uploaded'] as bool, | ||||
|       isSelfRef: json['is_self_ref'] as bool, | ||||
|       ref: json['ref'], | ||||
|       refId: json['ref_id'], | ||||
|       pool: json['pool'] == null | ||||
|           ? null | ||||
|           : SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>), | ||||
|       poolId: (json['pool_id'] as num).toInt(), | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt, | ||||
|       'rid': instance.rid, | ||||
|       'uuid': instance.uuid, | ||||
|       'size': instance.size, | ||||
|       'name': instance.name, | ||||
|       'alt': instance.alt, | ||||
|       'mimetype': instance.mimetype, | ||||
|       'hash': instance.hash, | ||||
|       'destination': instance.destination, | ||||
|       'ref_count': instance.refCount, | ||||
|       'file_chunks': instance.fileChunks, | ||||
|       'cleaned_at': instance.cleanedAt, | ||||
|       'metadata': instance.metadata, | ||||
|       'is_mature': instance.isMature, | ||||
|       'is_analyzed': instance.isAnalyzed, | ||||
|       'is_uploaded': instance.isUploaded, | ||||
|       'is_self_ref': instance.isSelfRef, | ||||
|       'ref': instance.ref, | ||||
|       'ref_id': instance.refId, | ||||
|       'pool': instance.pool?.toJson(), | ||||
|       'pool_id': instance.poolId, | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAttachmentPoolImpl( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       alias: json['alias'] as String, | ||||
|       name: json['name'] as String, | ||||
|       description: json['description'] as String, | ||||
|       config: json['config'] as Map<String, dynamic>, | ||||
|       accountId: (json['account_id'] as num?)?.toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentPoolImplToJson( | ||||
|         _$SnAttachmentPoolImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'alias': instance.alias, | ||||
|       'name': instance.name, | ||||
|       'description': instance.description, | ||||
|       'config': instance.config, | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
|  | ||||
| part 'post.freezed.dart'; | ||||
| part 'post.g.dart'; | ||||
| @@ -11,7 +12,7 @@ class SnPost with _$SnPost { | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     required String type, | ||||
|     required dynamic body, | ||||
|     required Map<String, dynamic> body, | ||||
|     required String language, | ||||
|     required String? alias, | ||||
|     required String? aliasPrefix, | ||||
| @@ -39,11 +40,22 @@ class SnPost with _$SnPost { | ||||
|     required int publisherId, | ||||
|     required SnPublisher publisher, | ||||
|     required SnMetric metric, | ||||
|     SnPostPreload? preload, | ||||
|   }) = _SnPost; | ||||
|  | ||||
|   factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnPostPreload with _$SnPostPreload { | ||||
|   const factory SnPostPreload({ | ||||
|     required List<SnAttachment>? attachments, | ||||
|   }) = _SnPostPreload; | ||||
|  | ||||
|   factory SnPostPreload.fromJson(Map<String, Object?> json) => | ||||
|       _$SnPostPreloadFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnBody with _$SnBody { | ||||
|   const factory SnBody({ | ||||
|   | ||||
| @@ -25,7 +25,7 @@ mixin _$SnPost { | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get type => throw _privateConstructorUsedError; | ||||
|   dynamic get body => throw _privateConstructorUsedError; | ||||
|   Map<String, dynamic> get body => throw _privateConstructorUsedError; | ||||
|   String get language => throw _privateConstructorUsedError; | ||||
|   String? get alias => throw _privateConstructorUsedError; | ||||
|   String? get aliasPrefix => throw _privateConstructorUsedError; | ||||
| @@ -53,6 +53,7 @@ mixin _$SnPost { | ||||
|   int get publisherId => throw _privateConstructorUsedError; | ||||
|   SnPublisher get publisher => throw _privateConstructorUsedError; | ||||
|   SnMetric get metric => throw _privateConstructorUsedError; | ||||
|   SnPostPreload? get preload => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnPost to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
| @@ -74,7 +75,7 @@ abstract class $SnPostCopyWith<$Res> { | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String type, | ||||
|       dynamic body, | ||||
|       Map<String, dynamic> body, | ||||
|       String language, | ||||
|       String? alias, | ||||
|       String? aliasPrefix, | ||||
| @@ -101,10 +102,12 @@ abstract class $SnPostCopyWith<$Res> { | ||||
|       dynamic realm, | ||||
|       int publisherId, | ||||
|       SnPublisher publisher, | ||||
|       SnMetric metric}); | ||||
|       SnMetric metric, | ||||
|       SnPostPreload? preload}); | ||||
|  | ||||
|   $SnPublisherCopyWith<$Res> get publisher; | ||||
|   $SnMetricCopyWith<$Res> get metric; | ||||
|   $SnPostPreloadCopyWith<$Res>? get preload; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @@ -127,7 +130,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? type = null, | ||||
|     Object? body = freezed, | ||||
|     Object? body = null, | ||||
|     Object? language = null, | ||||
|     Object? alias = freezed, | ||||
|     Object? aliasPrefix = freezed, | ||||
| @@ -155,6 +158,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | ||||
|     Object? publisherId = null, | ||||
|     Object? publisher = null, | ||||
|     Object? metric = null, | ||||
|     Object? preload = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
| @@ -177,10 +181,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | ||||
|           ? _value.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       body: freezed == body | ||||
|       body: null == body | ||||
|           ? _value.body | ||||
|           : body // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|               as Map<String, dynamic>, | ||||
|       language: null == language | ||||
|           ? _value.language | ||||
|           : language // ignore: cast_nullable_to_non_nullable | ||||
| @@ -289,6 +293,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | ||||
|           ? _value.metric | ||||
|           : metric // ignore: cast_nullable_to_non_nullable | ||||
|               as SnMetric, | ||||
|       preload: freezed == preload | ||||
|           ? _value.preload | ||||
|           : preload // ignore: cast_nullable_to_non_nullable | ||||
|               as SnPostPreload?, | ||||
|     ) as $Val); | ||||
|   } | ||||
|  | ||||
| @@ -311,6 +319,20 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | ||||
|       return _then(_value.copyWith(metric: value) as $Val); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /// Create a copy of SnPost | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnPostPreloadCopyWith<$Res>? get preload { | ||||
|     if (_value.preload == null) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     return $SnPostPreloadCopyWith<$Res>(_value.preload!, (value) { | ||||
|       return _then(_value.copyWith(preload: value) as $Val); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @@ -326,7 +348,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String type, | ||||
|       dynamic body, | ||||
|       Map<String, dynamic> body, | ||||
|       String language, | ||||
|       String? alias, | ||||
|       String? aliasPrefix, | ||||
| @@ -353,12 +375,15 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { | ||||
|       dynamic realm, | ||||
|       int publisherId, | ||||
|       SnPublisher publisher, | ||||
|       SnMetric metric}); | ||||
|       SnMetric metric, | ||||
|       SnPostPreload? preload}); | ||||
|  | ||||
|   @override | ||||
|   $SnPublisherCopyWith<$Res> get publisher; | ||||
|   @override | ||||
|   $SnMetricCopyWith<$Res> get metric; | ||||
|   @override | ||||
|   $SnPostPreloadCopyWith<$Res>? get preload; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @@ -379,7 +404,7 @@ class __$$SnPostImplCopyWithImpl<$Res> | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? type = null, | ||||
|     Object? body = freezed, | ||||
|     Object? body = null, | ||||
|     Object? language = null, | ||||
|     Object? alias = freezed, | ||||
|     Object? aliasPrefix = freezed, | ||||
| @@ -407,6 +432,7 @@ class __$$SnPostImplCopyWithImpl<$Res> | ||||
|     Object? publisherId = null, | ||||
|     Object? publisher = null, | ||||
|     Object? metric = null, | ||||
|     Object? preload = freezed, | ||||
|   }) { | ||||
|     return _then(_$SnPostImpl( | ||||
|       id: null == id | ||||
| @@ -429,10 +455,10 @@ class __$$SnPostImplCopyWithImpl<$Res> | ||||
|           ? _value.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       body: freezed == body | ||||
|           ? _value.body | ||||
|       body: null == body | ||||
|           ? _value._body | ||||
|           : body // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|               as Map<String, dynamic>, | ||||
|       language: null == language | ||||
|           ? _value.language | ||||
|           : language // ignore: cast_nullable_to_non_nullable | ||||
| @@ -541,6 +567,10 @@ class __$$SnPostImplCopyWithImpl<$Res> | ||||
|           ? _value.metric | ||||
|           : metric // ignore: cast_nullable_to_non_nullable | ||||
|               as SnMetric, | ||||
|       preload: freezed == preload | ||||
|           ? _value.preload | ||||
|           : preload // ignore: cast_nullable_to_non_nullable | ||||
|               as SnPostPreload?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| @@ -554,7 +584,7 @@ class _$SnPostImpl implements _SnPost { | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.type, | ||||
|       required this.body, | ||||
|       required final Map<String, dynamic> body, | ||||
|       required this.language, | ||||
|       required this.alias, | ||||
|       required this.aliasPrefix, | ||||
| @@ -581,8 +611,10 @@ class _$SnPostImpl implements _SnPost { | ||||
|       required this.realm, | ||||
|       required this.publisherId, | ||||
|       required this.publisher, | ||||
|       required this.metric}) | ||||
|       : _tags = tags, | ||||
|       required this.metric, | ||||
|       this.preload}) | ||||
|       : _body = body, | ||||
|         _tags = tags, | ||||
|         _categories = categories; | ||||
|  | ||||
|   factory _$SnPostImpl.fromJson(Map<String, dynamic> json) => | ||||
| @@ -598,8 +630,14 @@ class _$SnPostImpl implements _SnPost { | ||||
|   final DateTime? deletedAt; | ||||
|   @override | ||||
|   final String type; | ||||
|   final Map<String, dynamic> _body; | ||||
|   @override | ||||
|   final dynamic body; | ||||
|   Map<String, dynamic> get body { | ||||
|     if (_body is EqualUnmodifiableMapView) return _body; | ||||
|     // ignore: implicit_dynamic_type | ||||
|     return EqualUnmodifiableMapView(_body); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   final String language; | ||||
|   @override | ||||
| @@ -666,10 +704,12 @@ class _$SnPostImpl implements _SnPost { | ||||
|   final SnPublisher publisher; | ||||
|   @override | ||||
|   final SnMetric metric; | ||||
|   @override | ||||
|   final SnPostPreload? preload; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, reactions: $reactions, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, realm: $realm, publisherId: $publisherId, publisher: $publisher, metric: $metric)'; | ||||
|     return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, reactions: $reactions, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, realm: $realm, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -685,7 +725,7 @@ class _$SnPostImpl implements _SnPost { | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.type, type) || other.type == type) && | ||||
|             const DeepCollectionEquality().equals(other.body, body) && | ||||
|             const DeepCollectionEquality().equals(other._body, _body) && | ||||
|             (identical(other.language, language) || | ||||
|                 other.language == language) && | ||||
|             (identical(other.alias, alias) || other.alias == alias) && | ||||
| @@ -727,7 +767,8 @@ class _$SnPostImpl implements _SnPost { | ||||
|                 other.publisherId == publisherId) && | ||||
|             (identical(other.publisher, publisher) || | ||||
|                 other.publisher == publisher) && | ||||
|             (identical(other.metric, metric) || other.metric == metric)); | ||||
|             (identical(other.metric, metric) || other.metric == metric) && | ||||
|             (identical(other.preload, preload) || other.preload == preload)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -739,7 +780,7 @@ class _$SnPostImpl implements _SnPost { | ||||
|         updatedAt, | ||||
|         deletedAt, | ||||
|         type, | ||||
|         const DeepCollectionEquality().hash(body), | ||||
|         const DeepCollectionEquality().hash(_body), | ||||
|         language, | ||||
|         alias, | ||||
|         aliasPrefix, | ||||
| @@ -766,7 +807,8 @@ class _$SnPostImpl implements _SnPost { | ||||
|         const DeepCollectionEquality().hash(realm), | ||||
|         publisherId, | ||||
|         publisher, | ||||
|         metric | ||||
|         metric, | ||||
|         preload | ||||
|       ]); | ||||
|  | ||||
|   /// Create a copy of SnPost | ||||
| @@ -792,7 +834,7 @@ abstract class _SnPost implements SnPost { | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final String type, | ||||
|       required final dynamic body, | ||||
|       required final Map<String, dynamic> body, | ||||
|       required final String language, | ||||
|       required final String? alias, | ||||
|       required final String? aliasPrefix, | ||||
| @@ -819,7 +861,8 @@ abstract class _SnPost implements SnPost { | ||||
|       required final dynamic realm, | ||||
|       required final int publisherId, | ||||
|       required final SnPublisher publisher, | ||||
|       required final SnMetric metric}) = _$SnPostImpl; | ||||
|       required final SnMetric metric, | ||||
|       final SnPostPreload? preload}) = _$SnPostImpl; | ||||
|  | ||||
|   factory _SnPost.fromJson(Map<String, dynamic> json) = _$SnPostImpl.fromJson; | ||||
|  | ||||
| @@ -834,7 +877,7 @@ abstract class _SnPost implements SnPost { | ||||
|   @override | ||||
|   String get type; | ||||
|   @override | ||||
|   dynamic get body; | ||||
|   Map<String, dynamic> get body; | ||||
|   @override | ||||
|   String get language; | ||||
|   @override | ||||
| @@ -889,6 +932,8 @@ abstract class _SnPost implements SnPost { | ||||
|   SnPublisher get publisher; | ||||
|   @override | ||||
|   SnMetric get metric; | ||||
|   @override | ||||
|   SnPostPreload? get preload; | ||||
|  | ||||
|   /// Create a copy of SnPost | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -898,6 +943,166 @@ abstract class _SnPost implements SnPost { | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) { | ||||
|   return _SnPostPreload.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnPostPreload { | ||||
|   List<SnAttachment>? get attachments => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnPostPreload to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Create a copy of SnPostPreload | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnPostPreloadCopyWith<SnPostPreload> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnPostPreloadCopyWith<$Res> { | ||||
|   factory $SnPostPreloadCopyWith( | ||||
|           SnPostPreload value, $Res Function(SnPostPreload) then) = | ||||
|       _$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>; | ||||
|   @useResult | ||||
|   $Res call({List<SnAttachment>? attachments}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload> | ||||
|     implements $SnPostPreloadCopyWith<$Res> { | ||||
|   _$SnPostPreloadCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnPostPreload | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? attachments = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       attachments: freezed == attachments | ||||
|           ? _value.attachments | ||||
|           : attachments // ignore: cast_nullable_to_non_nullable | ||||
|               as List<SnAttachment>?, | ||||
|     ) as $Val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnPostPreloadImplCopyWith<$Res> | ||||
|     implements $SnPostPreloadCopyWith<$Res> { | ||||
|   factory _$$SnPostPreloadImplCopyWith( | ||||
|           _$SnPostPreloadImpl value, $Res Function(_$SnPostPreloadImpl) then) = | ||||
|       __$$SnPostPreloadImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call({List<SnAttachment>? attachments}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnPostPreloadImplCopyWithImpl<$Res> | ||||
|     extends _$SnPostPreloadCopyWithImpl<$Res, _$SnPostPreloadImpl> | ||||
|     implements _$$SnPostPreloadImplCopyWith<$Res> { | ||||
|   __$$SnPostPreloadImplCopyWithImpl( | ||||
|       _$SnPostPreloadImpl _value, $Res Function(_$SnPostPreloadImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnPostPreload | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? attachments = freezed, | ||||
|   }) { | ||||
|     return _then(_$SnPostPreloadImpl( | ||||
|       attachments: freezed == attachments | ||||
|           ? _value._attachments | ||||
|           : attachments // ignore: cast_nullable_to_non_nullable | ||||
|               as List<SnAttachment>?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnPostPreloadImpl implements _SnPostPreload { | ||||
|   const _$SnPostPreloadImpl({required final List<SnAttachment>? attachments}) | ||||
|       : _attachments = attachments; | ||||
|  | ||||
|   factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnPostPreloadImplFromJson(json); | ||||
|  | ||||
|   final List<SnAttachment>? _attachments; | ||||
|   @override | ||||
|   List<SnAttachment>? get attachments { | ||||
|     final value = _attachments; | ||||
|     if (value == null) return null; | ||||
|     if (_attachments is EqualUnmodifiableListView) return _attachments; | ||||
|     // ignore: implicit_dynamic_type | ||||
|     return EqualUnmodifiableListView(value); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnPostPreload(attachments: $attachments)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnPostPreloadImpl && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other._attachments, _attachments)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, const DeepCollectionEquality().hash(_attachments)); | ||||
|  | ||||
|   /// Create a copy of SnPostPreload | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnPostPreloadImplCopyWith<_$SnPostPreloadImpl> get copyWith => | ||||
|       __$$SnPostPreloadImplCopyWithImpl<_$SnPostPreloadImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnPostPreloadImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnPostPreload implements SnPostPreload { | ||||
|   const factory _SnPostPreload( | ||||
|       {required final List<SnAttachment>? attachments}) = _$SnPostPreloadImpl; | ||||
|  | ||||
|   factory _SnPostPreload.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnPostPreloadImpl.fromJson; | ||||
|  | ||||
|   @override | ||||
|   List<SnAttachment>? get attachments; | ||||
|  | ||||
|   /// Create a copy of SnPostPreload | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnPostPreloadImplCopyWith<_$SnPostPreloadImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| SnBody _$SnBodyFromJson(Map<String, dynamic> json) { | ||||
|   return _SnBody.fromJson(json); | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl( | ||||
|           ? null | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       type: json['type'] as String, | ||||
|       body: json['body'], | ||||
|       body: json['body'] as Map<String, dynamic>, | ||||
|       language: json['language'] as String, | ||||
|       alias: json['alias'] as String?, | ||||
|       aliasPrefix: json['alias_prefix'] as String?, | ||||
| @@ -49,6 +49,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl( | ||||
|       publisher: | ||||
|           SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), | ||||
|       metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>), | ||||
|       preload: json['preload'] == null | ||||
|           ? null | ||||
|           : SnPostPreload.fromJson(json['preload'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) => | ||||
| @@ -86,6 +89,19 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) => | ||||
|       'publisher_id': instance.publisherId, | ||||
|       'publisher': instance.publisher.toJson(), | ||||
|       'metric': instance.metric.toJson(), | ||||
|       'preload': instance.preload?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnPostPreloadImpl( | ||||
|       attachments: (json['attachments'] as List<dynamic>?) | ||||
|           ?.map((e) => SnAttachment.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'attachments': instance.attachments?.map((e) => e.toJson()).toList(), | ||||
|     }; | ||||
|  | ||||
| _$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl( | ||||
|   | ||||
							
								
								
									
										25
									
								
								lib/widgets/attachment/attachment_item.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/widgets/attachment/attachment_item.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class AttachmentItem extends StatelessWidget { | ||||
|   final SnAttachment data; | ||||
|   const AttachmentItem({super.key, required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final tp = data.mimetype.split('/').firstOrNull; | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|     switch (tp) { | ||||
|       case 'image': | ||||
|         return AspectRatio( | ||||
|           aspectRatio: data.metadata['ratio']?.toDouble(), | ||||
|           child: UniversalImage(sn.getAttachmentUrl(data.rid)), | ||||
|         ); | ||||
|       default: | ||||
|         return const Placeholder(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										66
									
								
								lib/widgets/attachment/attachment_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								lib/widgets/attachment/attachment_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_item.dart'; | ||||
|  | ||||
| class AttachmentList extends StatelessWidget { | ||||
|   final List<SnAttachment> data; | ||||
|   final bool? bordered; | ||||
|   final double? maxListHeight; | ||||
|   const AttachmentList( | ||||
|       {super.key, required this.data, this.bordered, this.maxListHeight}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final borderSide = (bordered ?? false) | ||||
|         ? BorderSide(width: 1, color: Theme.of(context).dividerColor) | ||||
|         : BorderSide.none; | ||||
|  | ||||
|     if (data.isEmpty) return const SizedBox.shrink(); | ||||
|     if (data.length == 1) { | ||||
|       return Container( | ||||
|         decoration: BoxDecoration( | ||||
|           border: Border(top: borderSide, bottom: borderSide), | ||||
|         ), | ||||
|         child: AttachmentItem(data: data[0]), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Container( | ||||
|       constraints: BoxConstraints(maxHeight: maxListHeight ?? 320), | ||||
|       child: ScrollConfiguration( | ||||
|         behavior: _AttachmentListScrollBehavior(), | ||||
|         child: ListView.separated( | ||||
|           shrinkWrap: true, | ||||
|           itemCount: data.length, | ||||
|           itemBuilder: (context, idx) { | ||||
|             const radius = BorderRadius.all(Radius.circular(8)); | ||||
|             return Container( | ||||
|               decoration: BoxDecoration( | ||||
|                 border: Border(top: borderSide, bottom: borderSide), | ||||
|                 borderRadius: radius, | ||||
|               ), | ||||
|               child: ClipRRect( | ||||
|                 borderRadius: radius, | ||||
|                 child: AttachmentItem(data: data[idx]), | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|           separatorBuilder: (context, index) => const Gap(8), | ||||
|           padding: const EdgeInsets.symmetric(horizontal: 12), | ||||
|           physics: const BouncingScrollPhysics(), | ||||
|           scrollDirection: Axis.horizontal, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _AttachmentListScrollBehavior extends MaterialScrollBehavior { | ||||
|   @override | ||||
|   Set<PointerDeviceKind> get dragDevices => { | ||||
|         PointerDeviceKind.touch, | ||||
|         PointerDeviceKind.mouse, | ||||
|       }; | ||||
| } | ||||
| @@ -4,6 +4,7 @@ import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_list.dart'; | ||||
| import 'package:surface/widgets/markdown_content.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
|  | ||||
| @@ -18,6 +19,8 @@ class PostItem extends StatelessWidget { | ||||
|       children: [ | ||||
|         _PostContentHeader(data: data), | ||||
|         _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), | ||||
|         if (data.preload?.attachments?.isNotEmpty ?? true) | ||||
|           AttachmentList(data: data.preload!.attachments!, bordered: true), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user