Image attachment

This commit is contained in:
LittleSheep 2024-11-09 12:04:03 +08:00
parent 07b8ec6e96
commit 5e12a8860c
12 changed files with 1627 additions and 28 deletions

View File

@ -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()),
],

View 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();
}
}

View File

@ -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
View 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);
}

File diff suppressed because it is too large Load Diff

View 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,
};

View File

@ -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({

View File

@ -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);
}

View File

@ -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(

View 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();
}
}
}

View 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,
};
}

View File

@ -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),
],
);
}