👽 Fix attachment uploading

This commit is contained in:
LittleSheep 2024-12-28 17:19:20 +08:00
parent 91c85e8a58
commit cf0df91d8c
6 changed files with 785 additions and 152 deletions

View File

@ -21,7 +21,7 @@ class SnAttachmentProvider {
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) { void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
for (final item in items) { for (final item in items) {
if ((item.isAnalyzed && item.isUploaded) || noCheck) { if (item.isAnalyzed || noCheck) {
_cache[item.rid] = item; _cache[item.rid] = item;
} }
} }
@ -34,7 +34,7 @@ class SnAttachmentProvider {
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta'); final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
final out = SnAttachment.fromJson(resp.data); final out = SnAttachment.fromJson(resp.data);
if (out.isAnalyzed && out.isUploaded) { if (out.isAnalyzed) {
_cache[rid] = out; _cache[rid] = out;
} }
@ -62,11 +62,12 @@ class SnAttachmentProvider {
'id': pendingFetch.join(','), 'id': pendingFetch.join(','),
}, },
); );
final out = resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).toList(); final List<SnAttachment?> out =
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
for (final item in out) { for (final item in out) {
if (item == null) continue; if (item == null) continue;
if (item.isAnalyzed && item.isUploaded) { if (item.isAnalyzed) {
_cache[item.rid] = item; _cache[item.rid] = item;
} }
result[randomMapping[item.rid]!] = item; result[randomMapping[item.rid]!] = item;
@ -117,7 +118,7 @@ class SnAttachmentProvider {
return SnAttachment.fromJson(resp.data); return SnAttachment.fromJson(resp.data);
} }
Future<(SnAttachment, int)> chunkedUploadInitialize( Future<(SnAttachmentFragment, int)> chunkedUploadInitialize(
int size, int size,
String filename, String filename,
String pool, String pool,
@ -134,7 +135,7 @@ class SnAttachmentProvider {
mimetypeOverride = mimetype; mimetypeOverride = mimetype;
} }
final resp = await _sn.client.post('/cgi/uc/attachments/multipart', data: { final resp = await _sn.client.post('/cgi/uc/fragments', data: {
'alt': fileAlt, 'alt': fileAlt,
'name': filename, 'name': filename,
'pool': pool, 'pool': pool,
@ -143,17 +144,17 @@ class SnAttachmentProvider {
if (mimetypeOverride != null) 'mimetype': mimetypeOverride, if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
}); });
return (SnAttachment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int); return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
} }
Future<SnAttachment> _chunkedUploadOnePart( Future<dynamic> _chunkedUploadOnePart(
Uint8List data, Uint8List data,
String rid, String rid,
String cid, { String cid, {
Function(double progress)? onProgress, Function(double progress)? onProgress,
}) async { }) async {
final resp = await _sn.client.post( final resp = await _sn.client.post(
'/cgi/uc/attachments/multipart/$rid/$cid', '/cgi/uc/fragments/$rid/$cid',
data: data, data: data,
options: Options(headers: {'Content-Type': 'application/octet-stream'}), options: Options(headers: {'Content-Type': 'application/octet-stream'}),
onSendProgress: (count, total) { onSendProgress: (count, total) {
@ -163,21 +164,27 @@ class SnAttachmentProvider {
}, },
); );
return SnAttachment.fromJson(resp.data); if (resp.data['attachment'] != null) {
return SnAttachment.fromJson(resp.data['attachment']);
} else {
return SnAttachmentFragment.fromJson(resp.data['fragment']);
}
} }
Future<SnAttachment> chunkedUploadParts( Future<SnAttachment> chunkedUploadParts(
XFile file, XFile file,
SnAttachment place, SnAttachmentFragment place,
int chunkSize, { int chunkSize, {
Function(double progress)? onProgress, Function(double progress)? onProgress,
}) async { }) async {
final Map<String, dynamic> chunks = place.fileChunks ?? {}; final Map<String, dynamic> chunks = place.fileChunks;
var currentTask = 0; var currentTask = 0;
final queue = Queue<Future<void>>(); final queue = Queue<Future<void>>();
final activeTasks = <Future<void>>[]; final activeTasks = <Future<void>>[];
late SnAttachment out;
for (final entry in chunks.entries) { for (final entry in chunks.entries) {
queue.add(() async { queue.add(() async {
final beginCursor = entry.value * chunkSize; final beginCursor = entry.value * chunkSize;
@ -187,7 +194,7 @@ class SnAttachmentProvider {
); );
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList()); final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
place = await _chunkedUploadOnePart( final result = await _chunkedUploadOnePart(
data, data,
place.rid, place.rid,
entry.key, entry.key,
@ -197,6 +204,12 @@ class SnAttachmentProvider {
onProgress?.call(overallProgress); onProgress?.call(overallProgress);
currentTask++; currentTask++;
if (result is SnAttachmentFragment) {
place = result;
} else {
out = result as SnAttachment;
}
}()); }());
} }
@ -213,7 +226,7 @@ class SnAttachmentProvider {
} }
} }
return place; return out;
} }
Future<SnAttachment> updateOne( Future<SnAttachment> updateOne(

View File

@ -19,7 +19,7 @@ class SnAttachment with _$SnAttachment {
required int id, required int id,
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
required dynamic deletedAt, required DateTime? deletedAt,
required String rid, required String rid,
required String uuid, required String uuid,
required int size, required int size,
@ -31,19 +31,20 @@ class SnAttachment with _$SnAttachment {
required int refCount, required int refCount,
@Default(0) int contentRating, @Default(0) int contentRating,
@Default(0) int qualityRating, @Default(0) int qualityRating,
required dynamic fileChunks, required DateTime? cleanedAt,
required dynamic cleanedAt,
required bool isAnalyzed, required bool isAnalyzed,
required bool isUploaded,
required bool isSelfRef, required bool isSelfRef,
required dynamic ref, required SnAttachment? ref,
required dynamic refId, required int? refId,
required SnAttachmentPool? pool, required SnAttachmentPool? pool,
required int poolId, required int poolId,
required int accountId, required int accountId,
int? thumbnailId,
SnAttachment? thumbnail,
int? compressedId,
SnAttachment? compressed,
@Default({}) Map<String, dynamic> usermeta, @Default({}) Map<String, dynamic> usermeta,
@Default({}) Map<String, dynamic> metadata, @Default({}) Map<String, dynamic> metadata,
String? thumbnail,
}) = _SnAttachment; }) = _SnAttachment;
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json); factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json);
@ -61,6 +62,37 @@ class SnAttachment with _$SnAttachment {
}; };
} }
@freezed
class SnAttachmentFragment with _$SnAttachmentFragment {
const SnAttachmentFragment._();
const factory SnAttachmentFragment({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String rid,
required String uuid,
required int size,
required String name,
required String alt,
required String mimetype,
required String hash,
String? fingerprint,
@Default({}) Map<String, int> fileChunks,
@Default([]) List<String> fileChunksMissing,
}) = _SnAttachmentFragment;
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) => _$SnAttachmentFragmentFromJson(json);
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
'image' => SnMediaType.image,
'video' => SnMediaType.video,
'audio' => SnMediaType.audio,
_ => SnMediaType.file,
};
}
@freezed @freezed
class SnAttachmentPool with _$SnAttachmentPool { class SnAttachmentPool with _$SnAttachmentPool {
const factory SnAttachmentPool({ const factory SnAttachmentPool({

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'], deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
rid: json['rid'] as String, rid: json['rid'] as String,
uuid: json['uuid'] as String, uuid: json['uuid'] as String,
size: (json['size'] as num).toInt(), size: (json['size'] as num).toInt(),
@ -23,21 +25,30 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
refCount: (json['ref_count'] as num).toInt(), refCount: (json['ref_count'] as num).toInt(),
contentRating: (json['content_rating'] as num?)?.toInt() ?? 0, contentRating: (json['content_rating'] as num?)?.toInt() ?? 0,
qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0, qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0,
fileChunks: json['file_chunks'], cleanedAt: json['cleaned_at'] == null
cleanedAt: json['cleaned_at'], ? null
: DateTime.parse(json['cleaned_at'] as String),
isAnalyzed: json['is_analyzed'] as bool, isAnalyzed: json['is_analyzed'] as bool,
isUploaded: json['is_uploaded'] as bool,
isSelfRef: json['is_self_ref'] as bool, isSelfRef: json['is_self_ref'] as bool,
ref: json['ref'], ref: json['ref'] == null
refId: json['ref_id'], ? null
: SnAttachment.fromJson(json['ref'] as Map<String, dynamic>),
refId: (json['ref_id'] as num?)?.toInt(),
pool: json['pool'] == null pool: json['pool'] == null
? null ? null
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>), : SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
poolId: (json['pool_id'] as num).toInt(), poolId: (json['pool_id'] as num).toInt(),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
thumbnail: json['thumbnail'] == null
? null
: SnAttachment.fromJson(json['thumbnail'] as Map<String, dynamic>),
compressedId: (json['compressed_id'] as num?)?.toInt(),
compressed: json['compressed'] == null
? null
: SnAttachment.fromJson(json['compressed'] as Map<String, dynamic>),
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {}, usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
metadata: json['metadata'] as Map<String, dynamic>? ?? const {}, metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
thumbnail: json['thumbnail'] as String?,
); );
Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
@ -45,7 +56,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
'id': instance.id, 'id': instance.id,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt, 'deleted_at': instance.deletedAt?.toIso8601String(),
'rid': instance.rid, 'rid': instance.rid,
'uuid': instance.uuid, 'uuid': instance.uuid,
'size': instance.size, 'size': instance.size,
@ -57,19 +68,66 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
'ref_count': instance.refCount, 'ref_count': instance.refCount,
'content_rating': instance.contentRating, 'content_rating': instance.contentRating,
'quality_rating': instance.qualityRating, 'quality_rating': instance.qualityRating,
'file_chunks': instance.fileChunks, 'cleaned_at': instance.cleanedAt?.toIso8601String(),
'cleaned_at': instance.cleanedAt,
'is_analyzed': instance.isAnalyzed, 'is_analyzed': instance.isAnalyzed,
'is_uploaded': instance.isUploaded,
'is_self_ref': instance.isSelfRef, 'is_self_ref': instance.isSelfRef,
'ref': instance.ref, 'ref': instance.ref?.toJson(),
'ref_id': instance.refId, 'ref_id': instance.refId,
'pool': instance.pool?.toJson(), 'pool': instance.pool?.toJson(),
'pool_id': instance.poolId, 'pool_id': instance.poolId,
'account_id': instance.accountId, 'account_id': instance.accountId,
'thumbnail_id': instance.thumbnailId,
'thumbnail': instance.thumbnail?.toJson(),
'compressed_id': instance.compressedId,
'compressed': instance.compressed?.toJson(),
'usermeta': instance.usermeta, 'usermeta': instance.usermeta,
'metadata': instance.metadata, 'metadata': instance.metadata,
'thumbnail': instance.thumbnail, };
_$SnAttachmentFragmentImpl _$$SnAttachmentFragmentImplFromJson(
Map<String, dynamic> json) =>
_$SnAttachmentFragmentImpl(
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),
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,
fingerprint: json['fingerprint'] as String?,
fileChunks: (json['file_chunks'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
) ??
const {},
fileChunksMissing: (json['file_chunks_missing'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$SnAttachmentFragmentImplToJson(
_$SnAttachmentFragmentImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'rid': instance.rid,
'uuid': instance.uuid,
'size': instance.size,
'name': instance.name,
'alt': instance.alt,
'mimetype': instance.mimetype,
'hash': instance.hash,
'fingerprint': instance.fingerprint,
'file_chunks': instance.fileChunks,
'file_chunks_missing': instance.fileChunksMissing,
}; };
_$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson( _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson(

View File

@ -215,9 +215,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: Stack( child: Stack(
children: [ children: [
if (widget.data.thumbnail?.isNotEmpty ?? false) if (widget.data.thumbnail != null)
AutoResizeUniversalImage( AutoResizeUniversalImage(
sn.getAttachmentUrl(widget.data.thumbnail!), sn.getAttachmentUrl(widget.data.thumbnail!.rid),
fit: BoxFit.cover, fit: BoxFit.cover,
) )
else else

View File

@ -315,7 +315,7 @@ class _PostMediaPendingItem extends StatelessWidget {
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (media.attachment?.thumbnail != null) if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!)), AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
const Icon(Symbols.videocam, color: Colors.white, shadows: [ const Icon(Symbols.videocam, color: Colors.white, shadows: [
Shadow( Shadow(
offset: Offset(1, 1), offset: Offset(1, 1),
@ -332,7 +332,7 @@ class _PostMediaPendingItem extends StatelessWidget {
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (media.attachment?.thumbnail != null) if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!)), AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
const Icon(Symbols.audio_file, color: Colors.white, shadows: [ const Icon(Symbols.audio_file, color: Colors.white, shadows: [
Shadow( Shadow(
offset: Offset(1, 1), offset: Offset(1, 1),