diff --git a/api/Paperclip/Activate Boost.bru b/api/Paperclip/Activate Boost.bru new file mode 100644 index 0000000..72d4959 --- /dev/null +++ b/api/Paperclip/Activate Boost.bru @@ -0,0 +1,30 @@ +meta { + name: Activate Boost + type: http + seq: 1 +} + +post { + url: {{endpoint}}/cgi/uc/boosts/1/activate + body: none + auth: bearer +} + +auth:bearer { + token: {{atk}} +} + +body:json { + { + "client_id": "{{third_client_id}}", + "client_secret":"{{third_client_tk}}", + "type": "general", + "subject": "Merry Christmas!", + "subtitle": "一条来自 Solar Network 团队的信息", + "content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄", + "metadata": { + "image": "6EqsYQwmFRCkbmhR" + }, + "priority": 10 + } +} diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 4793594..9d18cce 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -309,7 +309,15 @@ "attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality.", "attachmentUploaded": "Uploaded", "attachmentPending": "Pending", - "attachmentCopyCompressed": "Has compressed copy", + "attachmentCopyCompressed": "Copy compressed", + "attachmentGotBoosted": "Boosted", + "attachmentBoost": "Boost", + "attachmentCreateBoost": "Create Boost", + "attachmentBoostHint": "Boost is a feature that allows you to upload attachments to a server closer to your audience or a faster content network. This feature is currently in beta and is subject to change. It's all free for now, you can feel free to try, you will get notified when the pricing plan changed.", + "attachmentDestinationRegion": "Destination Region", + "attachmentDestinationRegionAPAC": "Asia Pacific", + "attachmentDestinationRegionNGB": "Ning Bo, China, Zhejiang", + "attachmentDestinationRegionHKG": "Hong Kong", "notification": "Notification", "notificationUnreadCount": { "zero": "All notifications read", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 1a02e0b..660c130 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -308,6 +308,14 @@ "attachmentUploaded": "已上传", "attachmentPending": "未上传", "attachmentCopyCompressed": "有压缩副本", + "attachmentGotBoosted": "有加速传递", + "attachmentBoost": "加速包", + "attachmentCreateBoost": "加速传递", + "attachmentBoostHint": "加速传递允许您将附件上传到更近的受众或更快的内容网络。该功能目前处于 Beta 阶段。该功能限时免费,当有价格计划更改时,您将会被通知。", + "attachmentDestinationRegion": "目标节点", + "attachmentDestinationRegionAPAC": "亚太地区", + "attachmentDestinationRegionNGB": "中国 · 浙江 · 宁波", + "attachmentDestinationRegionHKG": "香港", "notification": "通知", "notificationUnreadCount": { "zero": "无未读通知", diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 31979a9..e81be62 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -308,6 +308,14 @@ "attachmentUploaded": "已上傳", "attachmentPending": "未上傳", "attachmentCopyCompressed": "有壓縮副本", + "attachmentGotBoosted": "有加速傳遞", + "attachmentBoost": "加速包", + "attachmentCreateBoost": "加速傳遞", + "attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。", + "attachmentDestinationRegion": "目標節點", + "attachmentDestinationRegionAPAC": "亞太地區", + "attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波", + "attachmentDestinationRegionHKG": "香港", "notification": "通知", "notificationUnreadCount": { "zero": "無未讀通知", diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 5f5a99c..ec2f8ce 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -308,6 +308,14 @@ "attachmentUploaded": "已上傳", "attachmentPending": "未上傳", "attachmentCopyCompressed": "有壓縮副本", + "attachmentGotBoosted": "有加速傳遞", + "attachmentBoost": "加速包", + "attachmentCreateBoost": "加速傳遞", + "attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。", + "attachmentDestinationRegion": "目標節點", + "attachmentDestinationRegionAPAC": "亞太地區", + "attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波", + "attachmentDestinationRegionHKG": "香港", "notification": "通知", "notificationUnreadCount": { "zero": "無未讀通知", diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart index bd95ad1..98ceaaf 100644 --- a/lib/controllers/post_write_controller.dart +++ b/lib/controllers/post_write_controller.dart @@ -231,7 +231,8 @@ class PostWriteController extends ChangeNotifier { } } - Future _uploadAttachment(BuildContext context, PostWriteMedia media, {bool isCompressed = false}) async { + Future _uploadAttachment(BuildContext context, PostWriteMedia media, + {bool isCompressed = false}) async { final attach = context.read(); final place = await attach.chunkedUploadInitialize( @@ -242,7 +243,7 @@ class PostWriteController extends ChangeNotifier { mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, ); - final item = await attach.chunkedUploadParts( + var item = await attach.chunkedUploadParts( media.toFile()!, place.$1, place.$2, @@ -253,9 +254,13 @@ class PostWriteController extends ChangeNotifier { ); if (media.type == SnMediaType.video && !isCompressed && context.mounted) { - final compressedAttachment = await _tryCompressVideoCopy(context, media); - if (compressedAttachment != null) { - await attach.updateOne(item, compressedId: compressedAttachment.id); + try { + final compressedAttachment = await _tryCompressVideoCopy(context, media); + if (compressedAttachment != null) { + item = await attach.updateOne(item, compressedId: compressedAttachment.id); + } + } catch (err) { + if (context.mounted) context.showErrorDialog(err); } } @@ -336,7 +341,7 @@ class PostWriteController extends ChangeNotifier { mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, ); - final item = await attach.chunkedUploadParts( + var item = await attach.chunkedUploadParts( media.toFile()!, place.$1, place.$2, @@ -347,11 +352,13 @@ class PostWriteController extends ChangeNotifier { }, ); - if (media.type == SnMediaType.video && context.mounted) { + try { final compressedAttachment = await _tryCompressVideoCopy(context, media); if (compressedAttachment != null) { - await attach.updateOne(item, compressedId: compressedAttachment.id); + item = await attach.updateOne(item, compressedId: compressedAttachment.id); } + } catch (err) { + if (context.mounted) context.showErrorDialog(err); } progress = (i + 1) / attachments.length * kAttachmentProgressWeight; diff --git a/lib/providers/sn_attachment.dart b/lib/providers/sn_attachment.dart index cfc0a5e..ce7da94 100644 --- a/lib/providers/sn_attachment.dart +++ b/lib/providers/sn_attachment.dart @@ -86,6 +86,7 @@ class SnAttachmentProvider { Map? metadata, { String? mimetype, Function(double progress)? onProgress, + bool analyzeNow = false, }) async { final filePayload = MultipartFile.fromBytes(data, filename: filename); final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename; @@ -108,6 +109,7 @@ class SnAttachmentProvider { final resp = await _sn.client.post( '/cgi/uc/attachments', data: formData, + queryParameters: {'analyzeNow': analyzeNow}, onSendProgress: (count, total) { if (onProgress != null) { onProgress(count / total); diff --git a/lib/types/attachment.dart b/lib/types/attachment.dart index 1bf9948..e8a43be 100644 --- a/lib/types/attachment.dart +++ b/lib/types/attachment.dart @@ -38,12 +38,13 @@ class SnAttachment with _$SnAttachment { required SnAttachment? ref, required int? refId, required SnAttachmentPool? pool, - required int poolId, + required int? poolId, required int accountId, int? thumbnailId, SnAttachment? thumbnail, int? compressedId, SnAttachment? compressed, + @Default([]) List boosts, @Default({}) Map usermeta, @Default({}) Map metadata, }) = _SnAttachment; @@ -110,3 +111,33 @@ class SnAttachmentPool with _$SnAttachmentPool { factory SnAttachmentPool.fromJson(Map json) => _$SnAttachmentPoolFromJson(json); } + +@freezed +class SnAttachmentDestination with _$SnAttachmentDestination { + const factory SnAttachmentDestination({ + @Default(0) int id, + required String type, + required String label, + required String region, + required bool isBoost, + }) = _SnAttachmentDestination; + + factory SnAttachmentDestination.fromJson(Map json) => _$SnAttachmentDestinationFromJson(json); +} + +@freezed +class SnAttachmentBoost with _$SnAttachmentBoost { + const factory SnAttachmentBoost({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + required int status, + required int destination, + required int attachmentId, + required SnAttachment attachment, + required int account, + }) = _SnAttachmentBoost; + + factory SnAttachmentBoost.fromJson(Map json) => _$SnAttachmentBoostFromJson(json); +} diff --git a/lib/types/attachment.freezed.dart b/lib/types/attachment.freezed.dart index d50cf22..1894796 100644 --- a/lib/types/attachment.freezed.dart +++ b/lib/types/attachment.freezed.dart @@ -42,12 +42,13 @@ mixin _$SnAttachment { SnAttachment? get ref => throw _privateConstructorUsedError; int? get refId => throw _privateConstructorUsedError; SnAttachmentPool? get pool => throw _privateConstructorUsedError; - int get poolId => throw _privateConstructorUsedError; + int? get poolId => throw _privateConstructorUsedError; int get accountId => throw _privateConstructorUsedError; int? get thumbnailId => throw _privateConstructorUsedError; SnAttachment? get thumbnail => throw _privateConstructorUsedError; int? get compressedId => throw _privateConstructorUsedError; SnAttachment? get compressed => throw _privateConstructorUsedError; + List get boosts => throw _privateConstructorUsedError; Map get usermeta => throw _privateConstructorUsedError; Map get metadata => throw _privateConstructorUsedError; @@ -90,12 +91,13 @@ abstract class $SnAttachmentCopyWith<$Res> { SnAttachment? ref, int? refId, SnAttachmentPool? pool, - int poolId, + int? poolId, int accountId, int? thumbnailId, SnAttachment? thumbnail, int? compressedId, SnAttachment? compressed, + List boosts, Map usermeta, Map metadata}); @@ -142,12 +144,13 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> Object? ref = freezed, Object? refId = freezed, Object? pool = freezed, - Object? poolId = null, + Object? poolId = freezed, Object? accountId = null, Object? thumbnailId = freezed, Object? thumbnail = freezed, Object? compressedId = freezed, Object? compressed = freezed, + Object? boosts = null, Object? usermeta = null, Object? metadata = null, }) { @@ -240,10 +243,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> ? _value.pool : pool // ignore: cast_nullable_to_non_nullable as SnAttachmentPool?, - poolId: null == poolId + poolId: freezed == poolId ? _value.poolId : poolId // ignore: cast_nullable_to_non_nullable - as int, + as int?, accountId: null == accountId ? _value.accountId : accountId // ignore: cast_nullable_to_non_nullable @@ -264,6 +267,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> ? _value.compressed : compressed // ignore: cast_nullable_to_non_nullable as SnAttachment?, + boosts: null == boosts + ? _value.boosts + : boosts // ignore: cast_nullable_to_non_nullable + as List, usermeta: null == usermeta ? _value.usermeta : usermeta // ignore: cast_nullable_to_non_nullable @@ -363,12 +370,13 @@ abstract class _$$SnAttachmentImplCopyWith<$Res> SnAttachment? ref, int? refId, SnAttachmentPool? pool, - int poolId, + int? poolId, int accountId, int? thumbnailId, SnAttachment? thumbnail, int? compressedId, SnAttachment? compressed, + List boosts, Map usermeta, Map metadata}); @@ -417,12 +425,13 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> Object? ref = freezed, Object? refId = freezed, Object? pool = freezed, - Object? poolId = null, + Object? poolId = freezed, Object? accountId = null, Object? thumbnailId = freezed, Object? thumbnail = freezed, Object? compressedId = freezed, Object? compressed = freezed, + Object? boosts = null, Object? usermeta = null, Object? metadata = null, }) { @@ -515,10 +524,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> ? _value.pool : pool // ignore: cast_nullable_to_non_nullable as SnAttachmentPool?, - poolId: null == poolId + poolId: freezed == poolId ? _value.poolId : poolId // ignore: cast_nullable_to_non_nullable - as int, + as int?, accountId: null == accountId ? _value.accountId : accountId // ignore: cast_nullable_to_non_nullable @@ -539,6 +548,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> ? _value.compressed : compressed // ignore: cast_nullable_to_non_nullable as SnAttachment?, + boosts: null == boosts + ? _value._boosts + : boosts // ignore: cast_nullable_to_non_nullable + as List, usermeta: null == usermeta ? _value._usermeta : usermeta // ignore: cast_nullable_to_non_nullable @@ -583,9 +596,11 @@ class _$SnAttachmentImpl extends _SnAttachment { this.thumbnail, this.compressedId, this.compressed, + final List boosts = const [], final Map usermeta = const {}, final Map metadata = const {}}) - : _usermeta = usermeta, + : _boosts = boosts, + _usermeta = usermeta, _metadata = metadata, super._(); @@ -639,7 +654,7 @@ class _$SnAttachmentImpl extends _SnAttachment { @override final SnAttachmentPool? pool; @override - final int poolId; + final int? poolId; @override final int accountId; @override @@ -650,6 +665,15 @@ class _$SnAttachmentImpl extends _SnAttachment { final int? compressedId; @override final SnAttachment? compressed; + final List _boosts; + @override + @JsonKey() + List get boosts { + if (_boosts is EqualUnmodifiableListView) return _boosts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_boosts); + } + final Map _usermeta; @override @JsonKey() @@ -670,7 +694,7 @@ class _$SnAttachmentImpl extends _SnAttachment { @override String toString() { - return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, usermeta: $usermeta, metadata: $metadata)'; + return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; } @override @@ -723,6 +747,7 @@ class _$SnAttachmentImpl extends _SnAttachment { other.compressedId == compressedId) && (identical(other.compressed, compressed) || other.compressed == compressed) && + const DeepCollectionEquality().equals(other._boosts, _boosts) && const DeepCollectionEquality().equals(other._usermeta, _usermeta) && const DeepCollectionEquality().equals(other._metadata, _metadata)); } @@ -759,6 +784,7 @@ class _$SnAttachmentImpl extends _SnAttachment { thumbnail, compressedId, compressed, + const DeepCollectionEquality().hash(_boosts), const DeepCollectionEquality().hash(_usermeta), const DeepCollectionEquality().hash(_metadata) ]); @@ -803,12 +829,13 @@ abstract class _SnAttachment extends SnAttachment { required final SnAttachment? ref, required final int? refId, required final SnAttachmentPool? pool, - required final int poolId, + required final int? poolId, required final int accountId, final int? thumbnailId, final SnAttachment? thumbnail, final int? compressedId, final SnAttachment? compressed, + final List boosts, final Map usermeta, final Map metadata}) = _$SnAttachmentImpl; const _SnAttachment._() : super._(); @@ -861,7 +888,7 @@ abstract class _SnAttachment extends SnAttachment { @override SnAttachmentPool? get pool; @override - int get poolId; + int? get poolId; @override int get accountId; @override @@ -873,6 +900,8 @@ abstract class _SnAttachment extends SnAttachment { @override SnAttachment? get compressed; @override + List get boosts; + @override Map get usermeta; @override Map get metadata; @@ -1676,3 +1705,570 @@ abstract class _SnAttachmentPool implements SnAttachmentPool { _$$SnAttachmentPoolImplCopyWith<_$SnAttachmentPoolImpl> get copyWith => throw _privateConstructorUsedError; } + +SnAttachmentDestination _$SnAttachmentDestinationFromJson( + Map json) { + return _SnAttachmentDestination.fromJson(json); +} + +/// @nodoc +mixin _$SnAttachmentDestination { + int get id => throw _privateConstructorUsedError; + String get type => throw _privateConstructorUsedError; + String get label => throw _privateConstructorUsedError; + String get region => throw _privateConstructorUsedError; + bool get isBoost => throw _privateConstructorUsedError; + + /// Serializes this SnAttachmentDestination to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnAttachmentDestination + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnAttachmentDestinationCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnAttachmentDestinationCopyWith<$Res> { + factory $SnAttachmentDestinationCopyWith(SnAttachmentDestination value, + $Res Function(SnAttachmentDestination) then) = + _$SnAttachmentDestinationCopyWithImpl<$Res, SnAttachmentDestination>; + @useResult + $Res call({int id, String type, String label, String region, bool isBoost}); +} + +/// @nodoc +class _$SnAttachmentDestinationCopyWithImpl<$Res, + $Val extends SnAttachmentDestination> + implements $SnAttachmentDestinationCopyWith<$Res> { + _$SnAttachmentDestinationCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnAttachmentDestination + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? type = null, + Object? label = null, + Object? region = null, + Object? isBoost = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + region: null == region + ? _value.region + : region // ignore: cast_nullable_to_non_nullable + as String, + isBoost: null == isBoost + ? _value.isBoost + : isBoost // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SnAttachmentDestinationImplCopyWith<$Res> + implements $SnAttachmentDestinationCopyWith<$Res> { + factory _$$SnAttachmentDestinationImplCopyWith( + _$SnAttachmentDestinationImpl value, + $Res Function(_$SnAttachmentDestinationImpl) then) = + __$$SnAttachmentDestinationImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int id, String type, String label, String region, bool isBoost}); +} + +/// @nodoc +class __$$SnAttachmentDestinationImplCopyWithImpl<$Res> + extends _$SnAttachmentDestinationCopyWithImpl<$Res, + _$SnAttachmentDestinationImpl> + implements _$$SnAttachmentDestinationImplCopyWith<$Res> { + __$$SnAttachmentDestinationImplCopyWithImpl( + _$SnAttachmentDestinationImpl _value, + $Res Function(_$SnAttachmentDestinationImpl) _then) + : super(_value, _then); + + /// Create a copy of SnAttachmentDestination + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? type = null, + Object? label = null, + Object? region = null, + Object? isBoost = null, + }) { + return _then(_$SnAttachmentDestinationImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + region: null == region + ? _value.region + : region // ignore: cast_nullable_to_non_nullable + as String, + isBoost: null == isBoost + ? _value.isBoost + : isBoost // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnAttachmentDestinationImpl implements _SnAttachmentDestination { + const _$SnAttachmentDestinationImpl( + {this.id = 0, + required this.type, + required this.label, + required this.region, + required this.isBoost}); + + factory _$SnAttachmentDestinationImpl.fromJson(Map json) => + _$$SnAttachmentDestinationImplFromJson(json); + + @override + @JsonKey() + final int id; + @override + final String type; + @override + final String label; + @override + final String region; + @override + final bool isBoost; + + @override + String toString() { + return 'SnAttachmentDestination(id: $id, type: $type, label: $label, region: $region, isBoost: $isBoost)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnAttachmentDestinationImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.type, type) || other.type == type) && + (identical(other.label, label) || other.label == label) && + (identical(other.region, region) || other.region == region) && + (identical(other.isBoost, isBoost) || other.isBoost == isBoost)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, type, label, region, isBoost); + + /// Create a copy of SnAttachmentDestination + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnAttachmentDestinationImplCopyWith<_$SnAttachmentDestinationImpl> + get copyWith => __$$SnAttachmentDestinationImplCopyWithImpl< + _$SnAttachmentDestinationImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnAttachmentDestinationImplToJson( + this, + ); + } +} + +abstract class _SnAttachmentDestination implements SnAttachmentDestination { + const factory _SnAttachmentDestination( + {final int id, + required final String type, + required final String label, + required final String region, + required final bool isBoost}) = _$SnAttachmentDestinationImpl; + + factory _SnAttachmentDestination.fromJson(Map json) = + _$SnAttachmentDestinationImpl.fromJson; + + @override + int get id; + @override + String get type; + @override + String get label; + @override + String get region; + @override + bool get isBoost; + + /// Create a copy of SnAttachmentDestination + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnAttachmentDestinationImplCopyWith<_$SnAttachmentDestinationImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SnAttachmentBoost _$SnAttachmentBoostFromJson(Map json) { + return _SnAttachmentBoost.fromJson(json); +} + +/// @nodoc +mixin _$SnAttachmentBoost { + int get id => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; + DateTime? get deletedAt => throw _privateConstructorUsedError; + int get status => throw _privateConstructorUsedError; + int get destination => throw _privateConstructorUsedError; + int get attachmentId => throw _privateConstructorUsedError; + SnAttachment get attachment => throw _privateConstructorUsedError; + int get account => throw _privateConstructorUsedError; + + /// Serializes this SnAttachmentBoost to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnAttachmentBoost + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnAttachmentBoostCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnAttachmentBoostCopyWith<$Res> { + factory $SnAttachmentBoostCopyWith( + SnAttachmentBoost value, $Res Function(SnAttachmentBoost) then) = + _$SnAttachmentBoostCopyWithImpl<$Res, SnAttachmentBoost>; + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + int status, + int destination, + int attachmentId, + SnAttachment attachment, + int account}); + + $SnAttachmentCopyWith<$Res> get attachment; +} + +/// @nodoc +class _$SnAttachmentBoostCopyWithImpl<$Res, $Val extends SnAttachmentBoost> + implements $SnAttachmentBoostCopyWith<$Res> { + _$SnAttachmentBoostCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnAttachmentBoost + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? status = null, + Object? destination = null, + Object? attachmentId = null, + Object? attachment = null, + Object? account = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as int, + destination: null == destination + ? _value.destination + : destination // ignore: cast_nullable_to_non_nullable + as int, + attachmentId: null == attachmentId + ? _value.attachmentId + : attachmentId // ignore: cast_nullable_to_non_nullable + as int, + attachment: null == attachment + ? _value.attachment + : attachment // ignore: cast_nullable_to_non_nullable + as SnAttachment, + account: null == account + ? _value.account + : account // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } + + /// Create a copy of SnAttachmentBoost + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAttachmentCopyWith<$Res> get attachment { + return $SnAttachmentCopyWith<$Res>(_value.attachment, (value) { + return _then(_value.copyWith(attachment: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SnAttachmentBoostImplCopyWith<$Res> + implements $SnAttachmentBoostCopyWith<$Res> { + factory _$$SnAttachmentBoostImplCopyWith(_$SnAttachmentBoostImpl value, + $Res Function(_$SnAttachmentBoostImpl) then) = + __$$SnAttachmentBoostImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + int status, + int destination, + int attachmentId, + SnAttachment attachment, + int account}); + + @override + $SnAttachmentCopyWith<$Res> get attachment; +} + +/// @nodoc +class __$$SnAttachmentBoostImplCopyWithImpl<$Res> + extends _$SnAttachmentBoostCopyWithImpl<$Res, _$SnAttachmentBoostImpl> + implements _$$SnAttachmentBoostImplCopyWith<$Res> { + __$$SnAttachmentBoostImplCopyWithImpl(_$SnAttachmentBoostImpl _value, + $Res Function(_$SnAttachmentBoostImpl) _then) + : super(_value, _then); + + /// Create a copy of SnAttachmentBoost + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? status = null, + Object? destination = null, + Object? attachmentId = null, + Object? attachment = null, + Object? account = null, + }) { + return _then(_$SnAttachmentBoostImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as int, + destination: null == destination + ? _value.destination + : destination // ignore: cast_nullable_to_non_nullable + as int, + attachmentId: null == attachmentId + ? _value.attachmentId + : attachmentId // ignore: cast_nullable_to_non_nullable + as int, + attachment: null == attachment + ? _value.attachment + : attachment // ignore: cast_nullable_to_non_nullable + as SnAttachment, + account: null == account + ? _value.account + : account // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnAttachmentBoostImpl implements _SnAttachmentBoost { + const _$SnAttachmentBoostImpl( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.status, + required this.destination, + required this.attachmentId, + required this.attachment, + required this.account}); + + factory _$SnAttachmentBoostImpl.fromJson(Map json) => + _$$SnAttachmentBoostImplFromJson(json); + + @override + final int id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + @override + final DateTime? deletedAt; + @override + final int status; + @override + final int destination; + @override + final int attachmentId; + @override + final SnAttachment attachment; + @override + final int account; + + @override + String toString() { + return 'SnAttachmentBoost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, status: $status, destination: $destination, attachmentId: $attachmentId, attachment: $attachment, account: $account)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnAttachmentBoostImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.deletedAt, deletedAt) || + other.deletedAt == deletedAt) && + (identical(other.status, status) || other.status == status) && + (identical(other.destination, destination) || + other.destination == destination) && + (identical(other.attachmentId, attachmentId) || + other.attachmentId == attachmentId) && + (identical(other.attachment, attachment) || + other.attachment == attachment) && + (identical(other.account, account) || other.account == account)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, + deletedAt, status, destination, attachmentId, attachment, account); + + /// Create a copy of SnAttachmentBoost + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith => + __$$SnAttachmentBoostImplCopyWithImpl<_$SnAttachmentBoostImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$SnAttachmentBoostImplToJson( + this, + ); + } +} + +abstract class _SnAttachmentBoost implements SnAttachmentBoost { + const factory _SnAttachmentBoost( + {required final int id, + required final DateTime createdAt, + required final DateTime updatedAt, + required final DateTime? deletedAt, + required final int status, + required final int destination, + required final int attachmentId, + required final SnAttachment attachment, + required final int account}) = _$SnAttachmentBoostImpl; + + factory _SnAttachmentBoost.fromJson(Map json) = + _$SnAttachmentBoostImpl.fromJson; + + @override + int get id; + @override + DateTime get createdAt; + @override + DateTime get updatedAt; + @override + DateTime? get deletedAt; + @override + int get status; + @override + int get destination; + @override + int get attachmentId; + @override + SnAttachment get attachment; + @override + int get account; + + /// Create a copy of SnAttachmentBoost + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/types/attachment.g.dart b/lib/types/attachment.g.dart index e363506..d50f9aa 100644 --- a/lib/types/attachment.g.dart +++ b/lib/types/attachment.g.dart @@ -38,7 +38,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map json) => pool: json['pool'] == null ? null : SnAttachmentPool.fromJson(json['pool'] as Map), - poolId: (json['pool_id'] as num).toInt(), + poolId: (json['pool_id'] as num?)?.toInt(), accountId: (json['account_id'] as num).toInt(), thumbnailId: (json['thumbnail_id'] as num?)?.toInt(), thumbnail: json['thumbnail'] == null @@ -48,6 +48,11 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map json) => compressed: json['compressed'] == null ? null : SnAttachment.fromJson(json['compressed'] as Map), + boosts: (json['boosts'] as List?) + ?.map( + (e) => SnAttachmentBoost.fromJson(e as Map)) + .toList() ?? + const [], usermeta: json['usermeta'] as Map? ?? const {}, metadata: json['metadata'] as Map? ?? const {}, ); @@ -82,6 +87,7 @@ Map _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => 'thumbnail': instance.thumbnail?.toJson(), 'compressed_id': instance.compressedId, 'compressed': instance.compressed?.toJson(), + 'boosts': instance.boosts.map((e) => e.toJson()).toList(), 'usermeta': instance.usermeta, 'metadata': instance.metadata, }; @@ -161,3 +167,54 @@ Map _$$SnAttachmentPoolImplToJson( 'config': instance.config, 'account_id': instance.accountId, }; + +_$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson( + Map json) => + _$SnAttachmentDestinationImpl( + id: (json['id'] as num?)?.toInt() ?? 0, + type: json['type'] as String, + label: json['label'] as String, + region: json['region'] as String, + isBoost: json['is_boost'] as bool, + ); + +Map _$$SnAttachmentDestinationImplToJson( + _$SnAttachmentDestinationImpl instance) => + { + 'id': instance.id, + 'type': instance.type, + 'label': instance.label, + 'region': instance.region, + 'is_boost': instance.isBoost, + }; + +_$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson( + Map json) => + _$SnAttachmentBoostImpl( + 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), + status: (json['status'] as num).toInt(), + destination: (json['destination'] as num).toInt(), + attachmentId: (json['attachment_id'] as num).toInt(), + attachment: + SnAttachment.fromJson(json['attachment'] as Map), + account: (json['account'] as num).toInt(), + ); + +Map _$$SnAttachmentBoostImplToJson( + _$SnAttachmentBoostImpl instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'status': instance.status, + 'destination': instance.destination, + 'attachment_id': instance.attachmentId, + 'attachment': instance.attachment.toJson(), + 'account': instance.account, + }; diff --git a/lib/widgets/attachment/attachment_input.dart b/lib/widgets/attachment/attachment_input.dart index 56f5c83..a3b5bbe 100644 --- a/lib/widgets/attachment/attachment_input.dart +++ b/lib/widgets/attachment/attachment_input.dart @@ -10,8 +10,8 @@ import 'package:surface/widgets/dialog.dart'; class AttachmentInputDialog extends StatefulWidget { final String? title; - - const AttachmentInputDialog({super.key, required this.title}); +final bool? analyzeNow; + const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false}); @override State createState() => _AttachmentInputDialogState(); @@ -53,6 +53,7 @@ class _AttachmentInputDialogState extends State { _thumbnailFile!.path, 'interactive', null, + analyzeNow: widget.analyzeNow ?? false, ); if (!mounted) return; Navigator.pop(context, attachment); @@ -77,7 +78,8 @@ class _AttachmentInputDialogState extends State { controller: _randomIdController, decoration: InputDecoration( labelText: 'fieldAttachmentRandomId'.tr(), - border: const OutlineInputBorder(), + border: const UnderlineInputBorder(), + isDense: true, ), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), diff --git a/lib/widgets/attachment/pending_attachment_boost.dart b/lib/widgets/attachment/pending_attachment_boost.dart new file mode 100644 index 0000000..7fc32a4 --- /dev/null +++ b/lib/widgets/attachment/pending_attachment_boost.dart @@ -0,0 +1,120 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/controllers/post_write_controller.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/attachment.dart'; +import 'package:surface/widgets/dialog.dart'; + +class PendingAttachmentBoostDialog extends StatefulWidget { + final PostWriteMedia media; + + const PendingAttachmentBoostDialog({super.key, required this.media}); + + @override + State createState() => _PendingAttachmentBoostDialogState(); +} + +class _PendingAttachmentBoostDialogState extends State { + List? _regions; + SnAttachmentDestination? _selectedRegion; + + Future _fetchRegions() async { + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/uc/destinations'); + setState(() { + _regions = List.from( + resp.data?.map((e) => SnAttachmentDestination.fromJson(e)) ?? [], + ).cast().where((ele) => ele.isBoost).toList(); + }); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } + } + + bool _isBusy = false; + + Future _performAction() async { + if (_isBusy) return; + if (_selectedRegion == null) return; + + setState(() => _isBusy = true); + + try { + final sn = context.read(); + final resp = await sn.client.post('/cgi/uc/boosts', data: { + 'attachment': widget.media.attachment!.id, + 'destination': _selectedRegion!.id, + }); + if (!mounted) return; + Navigator.pop(context, SnAttachmentBoost.fromJson(resp.data)); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _fetchRegions(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('attachmentBoost').tr(), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('attachmentBoostHint').tr(), + const Gap(16), + Text('attachmentDestinationRegion').tr().fontSize(18), + const Gap(8), + Card( + child: _regions == null + ? const CircularProgressIndicator().center().padding(all: 16) + : Column( + children: _regions!.map( + (ele) { + return RadioListTile( + title: Text(ele.label).tr(), + subtitle: Text( + 'attachmentDestinationRegion${ele.region}'.trExists() + ? 'attachmentDestinationRegion${ele.region}'.tr() + : ele.region, + ), + selected: _selectedRegion == ele, + value: ele, + groupValue: _selectedRegion, + onChanged: (value) { + if (value != null) setState(() => _selectedRegion = value); + }, + ); + }, + ).toList(), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: _isBusy ? null : () { + Navigator.pop(context); + }, + child: Text('dialogDismiss'.tr()), + ), + TextButton( + onPressed: _isBusy ? null : () => _performAction(), + child: Text('dialogConfirm'.tr()), + ), + ], + ); + } +} diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index a5d258b..b83339d 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -18,7 +18,6 @@ import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/config.dart'; -import 'package:surface/providers/link_preview.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/post.dart'; @@ -255,6 +254,10 @@ class PostItem extends StatelessWidget { maxHeight: 560, listPadding: const EdgeInsets.symmetric(horizontal: 12), ), + if (data.body['content'] != null) + LinkPreviewWidget( + text: data.body['content'], + ).padding(horizontal: 4), Container( constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Column( @@ -336,10 +339,6 @@ class PostShareImageWidget extends StatelessWidget { data: data.preload!.attachments!, isFlatted: true, ).padding(horizontal: 16, bottom: 8), - if (data.body['content'] != null) - LinkPreviewWidget( - text: data.body['content'], - ).padding(horizontal: 4), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/widgets/post/post_media_pending_list.dart b/lib/widgets/post/post_media_pending_list.dart index 5cd2855..0d4d0d8 100644 --- a/lib/widgets/post/post_media_pending_list.dart +++ b/lib/widgets/post/post_media_pending_list.dart @@ -21,6 +21,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/widgets/attachment/attachment_input.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart'; +import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; import 'package:surface/widgets/context_menu.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/universal_image.dart'; @@ -93,18 +94,23 @@ class PostMediaPendingList extends StatelessWidget { context: context, builder: (context) => AttachmentInputDialog( title: 'attachmentSetThumbnail'.tr(), + analyzeNow: true, ), ); if (thumbnail == null) return; if (!context.mounted) return; - final attach = context.read(); - final newAttach = await attach.updateOne( - attachments[idx].attachment!, - thumbnailId: thumbnail.id, - ); - - onUpdate!(idx, PostWriteMedia(newAttach)); + try { + final attach = context.read(); + final newAttach = await attach.updateOne( + attachments[idx].attachment!, + thumbnailId: thumbnail.id, + ); + onUpdate!(idx, PostWriteMedia(newAttach)); + } catch (err) { + if (!context.mounted) return; + context.showErrorDialog(err); + } } Future _deleteAttachment(BuildContext context, int idx) async { @@ -124,6 +130,23 @@ class PostMediaPendingList extends StatelessWidget { } } + Future _createBoost(BuildContext context, int idx) async { + if (attachments[idx].attachment == null) return; + + final result = await showDialog( + context: context, + builder: (context) => PendingAttachmentBoostDialog(media: attachments[idx]), + ); + if (result == null) return; + + final newAttach = attachments[idx].attachment!.copyWith( + boosts: [...attachments[idx].attachment!.boosts, result], + ); + final newMedia = PostWriteMedia(newAttach); + + onUpdate!(idx, newMedia); + } + Future _compressVideo(BuildContext context, int idx) async { final result = await showDialog( context: context, @@ -146,6 +169,14 @@ class PostMediaPendingList extends StatelessWidget { _compressVideo(context, idx); }, ), + if (media.attachment != null) + MenuItem( + label: 'attachmentBoost'.tr(), + icon: Symbols.bolt, + onSelected: () { + _createBoost(context, idx); + }, + ), if (media.attachment != null && media.type == SnMediaType.video) MenuItem( label: 'attachmentSetThumbnail'.tr(), @@ -389,11 +420,19 @@ class _PostMediaPendingItem extends StatelessWidget { ], ), ), - if (media.attachment != null && media.attachment!.compressedId != null) + if (media.attachment != null && media.attachment!.boosts.isNotEmpty) Row( children: [ Icon(Symbols.bolt, size: 16), const Gap(4), + Text('attachmentGotBoosted').tr().fontSize(13), + ], + ), + if (media.attachment != null && media.attachment!.compressedId != null) + Row( + children: [ + Icon(Symbols.compress, size: 16), + const Gap(4), Text('attachmentCopyCompressed').tr().fontSize(13), ], ),