♻️ Refactored attachment meta editor
This commit is contained in:
parent
60afc96da2
commit
9d685fa0d9
@ -948,5 +948,8 @@
|
|||||||
"postEditedHint": "edited",
|
"postEditedHint": "edited",
|
||||||
"splashScreenServer": "Server",
|
"splashScreenServer": "Server",
|
||||||
"splashScreenServerName": "Potato",
|
"splashScreenServerName": "Potato",
|
||||||
"splashScreenCaption": "Trying to establishing connection with HyperNet™"
|
"splashScreenCaption": "Trying to establishing connection with HyperNet™",
|
||||||
|
"attachmentEditor": "Attachment editor",
|
||||||
|
"attachmentEditorUnUploadHint": "This attachment is not uploaded, metadata editing is unavailable, and you can crop this attachment.",
|
||||||
|
"attachmentEditorUploadHint": "This attachment is uploaded."
|
||||||
}
|
}
|
||||||
|
@ -945,5 +945,8 @@
|
|||||||
"postEditedHint": "已编辑",
|
"postEditedHint": "已编辑",
|
||||||
"splashScreenServer": "服务器",
|
"splashScreenServer": "服务器",
|
||||||
"splashScreenServerName": "土豆",
|
"splashScreenServerName": "土豆",
|
||||||
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接"
|
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接",
|
||||||
|
"attachmentEditor": "附件编辑器",
|
||||||
|
"attachmentEditorUnUploadHint": "该附件未上传,元数据编辑不可用,同时你可以裁剪本附件。",
|
||||||
|
"attachmentEditorUploadHint": "该附件已上传。"
|
||||||
}
|
}
|
||||||
|
@ -458,7 +458,9 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
|||||||
isBusy: _writeController.isBusy,
|
isBusy: _writeController.isBusy,
|
||||||
onUpload: (int idx) async {
|
onUpload: (int idx) async {
|
||||||
await _writeController.uploadSingleAttachment(
|
await _writeController.uploadSingleAttachment(
|
||||||
context, idx);
|
context,
|
||||||
|
idx,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onInsertLink: (int idx) async {
|
onInsertLink: (int idx) async {
|
||||||
_writeController.contentController.text +=
|
_writeController.contentController.text +=
|
||||||
|
@ -30,6 +30,7 @@ abstract class SnAttachment with _$SnAttachment {
|
|||||||
required String hash,
|
required String hash,
|
||||||
required int destination,
|
required int destination,
|
||||||
required int refCount,
|
required int refCount,
|
||||||
|
String? refUrl,
|
||||||
@Default(0) int contentRating,
|
@Default(0) int contentRating,
|
||||||
@Default(0) int qualityRating,
|
@Default(0) int qualityRating,
|
||||||
required DateTime? cleanedAt,
|
required DateTime? cleanedAt,
|
||||||
|
@ -28,6 +28,7 @@ mixin _$SnAttachment {
|
|||||||
String get hash;
|
String get hash;
|
||||||
int get destination;
|
int get destination;
|
||||||
int get refCount;
|
int get refCount;
|
||||||
|
String? get refUrl;
|
||||||
int get contentRating;
|
int get contentRating;
|
||||||
int get qualityRating;
|
int get qualityRating;
|
||||||
DateTime? get cleanedAt;
|
DateTime? get cleanedAt;
|
||||||
@ -83,6 +84,7 @@ mixin _$SnAttachment {
|
|||||||
other.destination == destination) &&
|
other.destination == destination) &&
|
||||||
(identical(other.refCount, refCount) ||
|
(identical(other.refCount, refCount) ||
|
||||||
other.refCount == refCount) &&
|
other.refCount == refCount) &&
|
||||||
|
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
|
||||||
(identical(other.contentRating, contentRating) ||
|
(identical(other.contentRating, contentRating) ||
|
||||||
other.contentRating == contentRating) &&
|
other.contentRating == contentRating) &&
|
||||||
(identical(other.qualityRating, qualityRating) ||
|
(identical(other.qualityRating, qualityRating) ||
|
||||||
@ -132,6 +134,7 @@ mixin _$SnAttachment {
|
|||||||
hash,
|
hash,
|
||||||
destination,
|
destination,
|
||||||
refCount,
|
refCount,
|
||||||
|
refUrl,
|
||||||
contentRating,
|
contentRating,
|
||||||
qualityRating,
|
qualityRating,
|
||||||
cleanedAt,
|
cleanedAt,
|
||||||
@ -155,7 +158,7 @@ mixin _$SnAttachment {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, 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, refUrl: $refUrl, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +182,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
|||||||
String hash,
|
String hash,
|
||||||
int destination,
|
int destination,
|
||||||
int refCount,
|
int refCount,
|
||||||
|
String? refUrl,
|
||||||
int contentRating,
|
int contentRating,
|
||||||
int qualityRating,
|
int qualityRating,
|
||||||
DateTime? cleanedAt,
|
DateTime? cleanedAt,
|
||||||
@ -231,6 +235,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
|||||||
Object? hash = null,
|
Object? hash = null,
|
||||||
Object? destination = null,
|
Object? destination = null,
|
||||||
Object? refCount = null,
|
Object? refCount = null,
|
||||||
|
Object? refUrl = freezed,
|
||||||
Object? contentRating = null,
|
Object? contentRating = null,
|
||||||
Object? qualityRating = null,
|
Object? qualityRating = null,
|
||||||
Object? cleanedAt = freezed,
|
Object? cleanedAt = freezed,
|
||||||
@ -304,6 +309,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
|||||||
? _self.refCount
|
? _self.refCount
|
||||||
: refCount // ignore: cast_nullable_to_non_nullable
|
: refCount // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
refUrl: freezed == refUrl
|
||||||
|
? _self.refUrl
|
||||||
|
: refUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
contentRating: null == contentRating
|
contentRating: null == contentRating
|
||||||
? _self.contentRating
|
? _self.contentRating
|
||||||
: contentRating // ignore: cast_nullable_to_non_nullable
|
: contentRating // ignore: cast_nullable_to_non_nullable
|
||||||
@ -471,6 +480,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
required this.hash,
|
required this.hash,
|
||||||
required this.destination,
|
required this.destination,
|
||||||
required this.refCount,
|
required this.refCount,
|
||||||
|
this.refUrl,
|
||||||
this.contentRating = 0,
|
this.contentRating = 0,
|
||||||
this.qualityRating = 0,
|
this.qualityRating = 0,
|
||||||
required this.cleanedAt,
|
required this.cleanedAt,
|
||||||
@ -524,6 +534,8 @@ class _SnAttachment extends SnAttachment {
|
|||||||
@override
|
@override
|
||||||
final int refCount;
|
final int refCount;
|
||||||
@override
|
@override
|
||||||
|
final String? refUrl;
|
||||||
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final int contentRating;
|
final int contentRating;
|
||||||
@override
|
@override
|
||||||
@ -623,6 +635,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
other.destination == destination) &&
|
other.destination == destination) &&
|
||||||
(identical(other.refCount, refCount) ||
|
(identical(other.refCount, refCount) ||
|
||||||
other.refCount == refCount) &&
|
other.refCount == refCount) &&
|
||||||
|
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
|
||||||
(identical(other.contentRating, contentRating) ||
|
(identical(other.contentRating, contentRating) ||
|
||||||
other.contentRating == contentRating) &&
|
other.contentRating == contentRating) &&
|
||||||
(identical(other.qualityRating, qualityRating) ||
|
(identical(other.qualityRating, qualityRating) ||
|
||||||
@ -672,6 +685,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
hash,
|
hash,
|
||||||
destination,
|
destination,
|
||||||
refCount,
|
refCount,
|
||||||
|
refUrl,
|
||||||
contentRating,
|
contentRating,
|
||||||
qualityRating,
|
qualityRating,
|
||||||
cleanedAt,
|
cleanedAt,
|
||||||
@ -695,7 +709,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, 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, refUrl: $refUrl, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -721,6 +735,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
|||||||
String hash,
|
String hash,
|
||||||
int destination,
|
int destination,
|
||||||
int refCount,
|
int refCount,
|
||||||
|
String? refUrl,
|
||||||
int contentRating,
|
int contentRating,
|
||||||
int qualityRating,
|
int qualityRating,
|
||||||
DateTime? cleanedAt,
|
DateTime? cleanedAt,
|
||||||
@ -779,6 +794,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
|||||||
Object? hash = null,
|
Object? hash = null,
|
||||||
Object? destination = null,
|
Object? destination = null,
|
||||||
Object? refCount = null,
|
Object? refCount = null,
|
||||||
|
Object? refUrl = freezed,
|
||||||
Object? contentRating = null,
|
Object? contentRating = null,
|
||||||
Object? qualityRating = null,
|
Object? qualityRating = null,
|
||||||
Object? cleanedAt = freezed,
|
Object? cleanedAt = freezed,
|
||||||
@ -852,6 +868,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
|||||||
? _self.refCount
|
? _self.refCount
|
||||||
: refCount // ignore: cast_nullable_to_non_nullable
|
: refCount // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
refUrl: freezed == refUrl
|
||||||
|
? _self.refUrl
|
||||||
|
: refUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
contentRating: null == contentRating
|
contentRating: null == contentRating
|
||||||
? _self.contentRating
|
? _self.contentRating
|
||||||
: contentRating // ignore: cast_nullable_to_non_nullable
|
: contentRating // ignore: cast_nullable_to_non_nullable
|
||||||
|
@ -23,6 +23,7 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
|
|||||||
hash: json['hash'] as String,
|
hash: json['hash'] as String,
|
||||||
destination: (json['destination'] as num).toInt(),
|
destination: (json['destination'] as num).toInt(),
|
||||||
refCount: (json['ref_count'] as num).toInt(),
|
refCount: (json['ref_count'] as num).toInt(),
|
||||||
|
refUrl: json['ref_url'] as String?,
|
||||||
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,
|
||||||
cleanedAt: json['cleaned_at'] == null
|
cleanedAt: json['cleaned_at'] == null
|
||||||
@ -75,6 +76,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
|
|||||||
'hash': instance.hash,
|
'hash': instance.hash,
|
||||||
'destination': instance.destination,
|
'destination': instance.destination,
|
||||||
'ref_count': instance.refCount,
|
'ref_count': instance.refCount,
|
||||||
|
'ref_url': instance.refUrl,
|
||||||
'content_rating': instance.contentRating,
|
'content_rating': instance.contentRating,
|
||||||
'quality_rating': instance.qualityRating,
|
'quality_rating': instance.qualityRating,
|
||||||
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
||||||
|
@ -63,7 +63,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
|||||||
}
|
}
|
||||||
} else if (_file != null) {
|
} else if (_file != null) {
|
||||||
try {
|
try {
|
||||||
final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
|
final place = await attach.chunkedUploadInitialize(
|
||||||
|
await _file!.length(), _file!.name, widget.pool, null);
|
||||||
|
|
||||||
final attachment = await attach.chunkedUploadParts(
|
final attachment = await attach.chunkedUploadParts(
|
||||||
_file!,
|
_file!,
|
||||||
@ -109,11 +110,17 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
leading: const Icon(Symbols.add_photo_alternate),
|
leading: const Icon(Symbols.add_photo_alternate),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
title: Text('addAttachmentFromAlbum').tr(),
|
title: Text('addAttachmentFromAlbum').tr(),
|
||||||
subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
subtitle: _file == null
|
||||||
|
? Text('unset').tr()
|
||||||
|
: Text('waitingForUpload').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_pickMedia();
|
_pickMedia();
|
||||||
},
|
},
|
||||||
|
311
lib/widgets/attachment/pending_attachment_actions.dart
Normal file
311
lib/widgets/attachment/pending_attachment_actions.dart
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:croppy/croppy.dart';
|
||||||
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.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_attachment.dart';
|
||||||
|
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_alt.dart';
|
||||||
|
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||||
|
import 'package:surface/widgets/attachment/pending_attachment_compress.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
|
class PendingAttachmentActionSheet extends StatefulWidget {
|
||||||
|
final PostWriteMedia media;
|
||||||
|
const PendingAttachmentActionSheet({super.key, required this.media});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PendingAttachmentActionSheet> createState() =>
|
||||||
|
_PendingAttachmentActionSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingAttachmentActionSheetState
|
||||||
|
extends State<PendingAttachmentActionSheet> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _cropImage() async {
|
||||||
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
|
? await showCupertinoImageCropper(
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
context,
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
imageProvider: widget.media.getImageProvider(context)!,
|
||||||
|
)
|
||||||
|
: await showMaterialImageCropper(
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
context,
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
imageProvider: widget.media.getImageProvider(context)!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
final rawBytes =
|
||||||
|
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||||
|
.buffer
|
||||||
|
.asUint8List();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final updatedMedia = PostWriteMedia.fromBytes(
|
||||||
|
rawBytes, widget.media.name, widget.media.type);
|
||||||
|
Navigator.pop(context, updatedMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setThumbnail() async {
|
||||||
|
final thumbnail = await showDialog<SnAttachment?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AttachmentInputDialog(
|
||||||
|
title: 'attachmentSetThumbnail'.tr(),
|
||||||
|
pool: 'interactive',
|
||||||
|
analyzeNow: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (thumbnail == null) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final newAttach = await attach.updateOne(widget.media.attachment!,
|
||||||
|
thumbnailId: thumbnail.id);
|
||||||
|
if (mounted) Navigator.pop(context, newAttach);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteAttachment() async {
|
||||||
|
if (_isBusy) return;
|
||||||
|
if (widget.media.attachment == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client
|
||||||
|
.delete('/cgi/uc/attachments/${widget.media.attachment!.id}');
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createBoost() async {
|
||||||
|
final result = await showDialog<SnAttachmentBoost?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingAttachmentBoostDialog(
|
||||||
|
media: widget.media,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
final newAttach = widget.media.attachment!
|
||||||
|
.copyWith(boosts: [...widget.media.attachment!.boosts, result]);
|
||||||
|
final newMedia = PostWriteMedia(newAttach);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, newMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _compressVideo() async {
|
||||||
|
final result = await showDialog<PostWriteMedia?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingVideoCompressDialog(media: widget.media),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setAlt() async {
|
||||||
|
final result = await showDialog<SnAttachment?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingAttachmentAltDialog(media: widget.media),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, PostWriteMedia(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('attachmentEditor')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
if (widget.media.attachment == null)
|
||||||
|
Text('attachmentEditorUnUploadHint')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||||
|
.padding(horizontal: 20, bottom: 8)
|
||||||
|
.opacity(0.8)
|
||||||
|
else
|
||||||
|
Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.media.attachment!.alt),
|
||||||
|
Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text(widget.media.attachment!.size.formatBytes()),
|
||||||
|
Text(
|
||||||
|
widget.media.attachment!.mimetype,
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text('attachmentEditorUploadHint')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||||
|
.opacity(0.8),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16, vertical: 8),
|
||||||
|
).padding(horizontal: 16, bottom: 8),
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
if (widget.media.attachment == null)
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.upload),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('attachmentUpload').tr(),
|
||||||
|
onTap: () => Navigator.pop(context, true),
|
||||||
|
),
|
||||||
|
if (widget.media.type == SnMediaType.video)
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.compress),
|
||||||
|
title: Text('attachmentCompressVideo').tr(),
|
||||||
|
onTap: () => _compressVideo(),
|
||||||
|
),
|
||||||
|
if (widget.media.type == SnMediaType.image)
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.crop),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('crop').tr(),
|
||||||
|
onTap: () => _cropImage(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.delete),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('delete').tr(),
|
||||||
|
onTap: () => Navigator.pop(context, false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.preview),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('preview').tr(),
|
||||||
|
onTap: () {
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(data: [widget.media.attachment!]),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.copy_all),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('attachmentCopyRandomId').tr(),
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: widget.media.attachment!.rid,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.add_link),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('attachmentInsertLink').tr(),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context, 'link');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.bolt),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('attachmentBoost').tr(),
|
||||||
|
onTap: () => _createBoost(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.thumbnail_bar),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('attachmentSetThumbnail').tr(),
|
||||||
|
onTap: () => _setThumbnail(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.description),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('attachmentSetAlt').tr(),
|
||||||
|
onTap: () => _setAlt(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.link_off),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('unlink').tr(),
|
||||||
|
onTap: () => Navigator.pop(context, false),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.delete),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('delete').tr(),
|
||||||
|
onTap: () => _deleteAttachment(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,10 +10,12 @@ class PendingAttachmentAltDialog extends StatefulWidget {
|
|||||||
const PendingAttachmentAltDialog({super.key, required this.media});
|
const PendingAttachmentAltDialog({super.key, required this.media});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
|
State<PendingAttachmentAltDialog> createState() =>
|
||||||
|
_PendingAttachmentAltDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
|
class _PendingAttachmentAltDialogState
|
||||||
|
extends State<PendingAttachmentAltDialog> {
|
||||||
final _contentController = TextEditingController();
|
final _contentController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -63,7 +65,7 @@ class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog>
|
|||||||
controller: _contentController,
|
controller: _contentController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'fieldAttachmentAlt'.tr(),
|
labelText: 'fieldAttachmentAlt'.tr(),
|
||||||
border: const UnderlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
@ -71,9 +73,11 @@ class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog>
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _isBusy ? null : () {
|
onPressed: _isBusy
|
||||||
Navigator.pop(context);
|
? null
|
||||||
},
|
: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
child: Text('dialogDismiss'.tr()),
|
child: Text('dialogDismiss'.tr()),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -14,10 +14,12 @@ class PendingAttachmentBoostDialog extends StatefulWidget {
|
|||||||
const PendingAttachmentBoostDialog({super.key, required this.media});
|
const PendingAttachmentBoostDialog({super.key, required this.media});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState();
|
State<PendingAttachmentBoostDialog> createState() =>
|
||||||
|
_PendingAttachmentBoostDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> {
|
class _PendingAttachmentBoostDialogState
|
||||||
|
extends State<PendingAttachmentBoostDialog> {
|
||||||
List<SnAttachmentDestination>? _regions;
|
List<SnAttachmentDestination>? _regions;
|
||||||
SnAttachmentDestination? _selectedRegion;
|
SnAttachmentDestination? _selectedRegion;
|
||||||
|
|
||||||
@ -84,17 +86,23 @@ class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDia
|
|||||||
children: _regions!.map(
|
children: _regions!.map(
|
||||||
(ele) {
|
(ele) {
|
||||||
return RadioListTile(
|
return RadioListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
title: Text(ele.label).tr(),
|
title: Text(ele.label).tr(),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'attachmentDestinationRegion${ele.region}'.trExists()
|
'attachmentDestinationRegion${ele.region}'.trExists()
|
||||||
? 'attachmentDestinationRegion${ele.region}'.tr()
|
? 'attachmentDestinationRegion${ele.region}'
|
||||||
|
.tr()
|
||||||
: ele.region,
|
: ele.region,
|
||||||
),
|
),
|
||||||
selected: _selectedRegion == ele,
|
selected: _selectedRegion == ele,
|
||||||
value: ele,
|
value: ele,
|
||||||
groupValue: _selectedRegion,
|
groupValue: _selectedRegion,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) setState(() => _selectedRegion = value);
|
if (value != null) {
|
||||||
|
setState(() => _selectedRegion = value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -105,9 +113,11 @@ class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDia
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _isBusy ? null : () {
|
onPressed: _isBusy
|
||||||
Navigator.pop(context);
|
? null
|
||||||
},
|
: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
child: Text('dialogDismiss'.tr()),
|
child: Text('dialogDismiss'.tr()),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: unused_import
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
@ -22,9 +24,9 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_input.dart';
|
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||||
|
import 'package:surface/widgets/attachment/pending_attachment_actions.dart';
|
||||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||||
import 'package:surface/widgets/attachment/pending_attachment_boost.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/dialog.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
@ -50,232 +52,6 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
this.onUpdateBusy,
|
this.onUpdateBusy,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> _cropImage(BuildContext context, int idx) async {
|
|
||||||
final media = attachments[idx];
|
|
||||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
|
||||||
? await showCupertinoImageCropper(
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
context,
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
imageProvider: media.getImageProvider(context)!,
|
|
||||||
)
|
|
||||||
: await showMaterialImageCropper(
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
context,
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
imageProvider: media.getImageProvider(context)!,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == null) return;
|
|
||||||
|
|
||||||
final rawBytes =
|
|
||||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
|
||||||
.buffer
|
|
||||||
.asUint8List();
|
|
||||||
|
|
||||||
if (onUpdate != null) {
|
|
||||||
final updatedMedia = PostWriteMedia.fromBytes(
|
|
||||||
rawBytes,
|
|
||||||
media.name,
|
|
||||||
media.type,
|
|
||||||
);
|
|
||||||
await onUpdate!(idx, updatedMedia);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setThumbnail(BuildContext context, int idx) async {
|
|
||||||
if (idx == -1) {
|
|
||||||
// Thumbnail only can set on video or audio. And thumbnail of the post must be an image, so it's not possible to set thumbnail on the post thumbnail.
|
|
||||||
return;
|
|
||||||
} else if (attachments[idx].attachment == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final thumbnail = await showDialog<SnAttachment?>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AttachmentInputDialog(
|
|
||||||
title: 'attachmentSetThumbnail'.tr(),
|
|
||||||
pool: 'interactive',
|
|
||||||
analyzeNow: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (thumbnail == null) return;
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
|
||||||
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<void> _deleteAttachment(BuildContext context, int idx) async {
|
|
||||||
final media = attachments[idx];
|
|
||||||
if (media.attachment == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
onUpdateBusy?.call(true);
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}');
|
|
||||||
onRemove!(idx);
|
|
||||||
} catch (err) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
onUpdateBusy?.call(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _createBoost(BuildContext context, int idx) async {
|
|
||||||
if (attachments[idx].attachment == null) return;
|
|
||||||
|
|
||||||
final result = await showDialog<SnAttachmentBoost?>(
|
|
||||||
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<void> _compressVideo(BuildContext context, int idx) async {
|
|
||||||
final result = await showDialog<PostWriteMedia?>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PendingVideoCompressDialog(media: attachments[idx]),
|
|
||||||
);
|
|
||||||
if (result == null) return;
|
|
||||||
|
|
||||||
onUpdate!(idx, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setAlt(BuildContext context, int idx) async {
|
|
||||||
final result = await showDialog<SnAttachment?>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
|
|
||||||
);
|
|
||||||
if (result == null) return;
|
|
||||||
|
|
||||||
onUpdate!(idx, PostWriteMedia(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
ContextMenu _createContextMenu(
|
|
||||||
BuildContext context, int idx, PostWriteMedia media) {
|
|
||||||
final canCompressVideo =
|
|
||||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
|
||||||
return ContextMenu(
|
|
||||||
entries: [
|
|
||||||
if (media.attachment == null &&
|
|
||||||
media.type == SnMediaType.video &&
|
|
||||||
canCompressVideo)
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentCompressVideo'.tr(),
|
|
||||||
icon: Symbols.compress,
|
|
||||||
onSelected: () {
|
|
||||||
_compressVideo(context, idx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (media.attachment != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentSetAlt'.tr(),
|
|
||||||
icon: Symbols.description,
|
|
||||||
onSelected: () {
|
|
||||||
_setAlt(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(),
|
|
||||||
icon: Symbols.image,
|
|
||||||
onSelected: () {
|
|
||||||
_setThumbnail(context, idx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (media.attachment == null && onUpload != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentUpload'.tr(),
|
|
||||||
icon: Symbols.upload,
|
|
||||||
onSelected: () {
|
|
||||||
onUpload!(idx);
|
|
||||||
}),
|
|
||||||
if (media.attachment != null && onInsertLink != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentInsertLink'.tr(),
|
|
||||||
icon: Symbols.add_link,
|
|
||||||
onSelected: () {
|
|
||||||
onInsertLink!(idx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (media.type == SnMediaType.image && media.attachment != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'preview'.tr(),
|
|
||||||
icon: Symbols.preview,
|
|
||||||
onSelected: () {
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(data: [media.attachment!]),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (media.type == SnMediaType.image && media.attachment == null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'crop'.tr(),
|
|
||||||
icon: Symbols.crop,
|
|
||||||
onSelected: () => _cropImage(context, idx),
|
|
||||||
),
|
|
||||||
if (media.attachment != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentCopyRandomId'.tr(),
|
|
||||||
icon: Symbols.content_copy,
|
|
||||||
onSelected: () {
|
|
||||||
Clipboard.setData(ClipboardData(text: media.attachment!.rid));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (media.attachment != null && onRemove != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'delete'.tr(),
|
|
||||||
icon: Symbols.delete,
|
|
||||||
onSelected: isBusy ? null : () => _deleteAttachment(context, idx),
|
|
||||||
),
|
|
||||||
if (media.attachment == null && onRemove != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'delete'.tr(),
|
|
||||||
icon: Symbols.delete,
|
|
||||||
onSelected: () {
|
|
||||||
onRemove!(idx);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else if (onRemove != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'unlink'.tr(),
|
|
||||||
icon: Symbols.link_off,
|
|
||||||
onSelected: () {
|
|
||||||
onRemove!(idx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -287,9 +63,27 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
itemCount: attachments.length,
|
itemCount: attachments.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final media = attachments[idx];
|
final media = attachments[idx];
|
||||||
return ContextMenuArea(
|
return GestureDetector(
|
||||||
contextMenu: _createContextMenu(context, idx, media),
|
|
||||||
child: _PostMediaPendingItem(media: media),
|
child: _PostMediaPendingItem(media: media),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingAttachmentActionSheet(
|
||||||
|
media: media,
|
||||||
|
),
|
||||||
|
).then((value) async {
|
||||||
|
if (value is PostWriteMedia) {
|
||||||
|
await onUpdate!(idx, value);
|
||||||
|
}
|
||||||
|
if (value == 'link') {
|
||||||
|
onInsertLink!(idx);
|
||||||
|
} else if (value == false) {
|
||||||
|
onRemove!(idx);
|
||||||
|
} else if (value == true) {
|
||||||
|
onUpload!(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -300,9 +94,7 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
class _PostMediaPendingItem extends StatelessWidget {
|
class _PostMediaPendingItem extends StatelessWidget {
|
||||||
final PostWriteMedia media;
|
final PostWriteMedia media;
|
||||||
|
|
||||||
const _PostMediaPendingItem({
|
const _PostMediaPendingItem({required this.media});
|
||||||
required this.media,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -321,34 +113,36 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: switch (media.type) {
|
child: switch (media.type) {
|
||||||
SnMediaType.image =>
|
SnMediaType.image => LayoutBuilder(
|
||||||
LayoutBuilder(builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Image(
|
return Image(
|
||||||
image: media.getImageProvider(
|
image: media.getImageProvider(
|
||||||
context,
|
context,
|
||||||
width:
|
width:
|
||||||
(constraints.maxWidth * devicePixelRatio).round(),
|
(constraints.maxWidth * devicePixelRatio).round(),
|
||||||
height:
|
height: (constraints.maxHeight * devicePixelRatio)
|
||||||
(constraints.maxHeight * devicePixelRatio).round(),
|
.round(),
|
||||||
)!,
|
)!,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
SnMediaType.video => Stack(
|
SnMediaType.video => Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (media.attachment?.thumbnail != null)
|
if (media.attachment?.thumbnail != null)
|
||||||
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||||
media.attachment!.thumbnail!.rid)),
|
media.attachment!.thumbnail!.rid)),
|
||||||
const Icon(Symbols.videocam,
|
const Icon(
|
||||||
color: Colors.white,
|
Symbols.videocam,
|
||||||
shadows: [
|
color: Colors.white,
|
||||||
Shadow(
|
shadows: [
|
||||||
|
Shadow(
|
||||||
offset: Offset(1, 1),
|
offset: Offset(1, 1),
|
||||||
blurRadius: 8.0,
|
blurRadius: 8.0,
|
||||||
color: Color.fromARGB(255, 0, 0, 0),
|
color: Color.fromARGB(255, 0, 0, 0))
|
||||||
),
|
],
|
||||||
]),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SnMediaType.audio => Stack(
|
SnMediaType.audio => Stack(
|
||||||
@ -357,15 +151,16 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
if (media.attachment?.thumbnail != null)
|
if (media.attachment?.thumbnail != null)
|
||||||
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||||
media.attachment!.thumbnail!.rid)),
|
media.attachment!.thumbnail!.rid)),
|
||||||
const Icon(Symbols.audio_file,
|
const Icon(
|
||||||
color: Colors.white,
|
Symbols.audio_file,
|
||||||
shadows: [
|
color: Colors.white,
|
||||||
Shadow(
|
shadows: [
|
||||||
|
Shadow(
|
||||||
offset: Offset(1, 1),
|
offset: Offset(1, 1),
|
||||||
blurRadius: 8.0,
|
blurRadius: 8.0,
|
||||||
color: Color.fromARGB(255, 0, 0, 0),
|
color: Color.fromARGB(255, 0, 0, 0))
|
||||||
),
|
],
|
||||||
]),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_ => Container(
|
_ => Container(
|
||||||
@ -387,11 +182,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (media.attachment != null)
|
if (media.attachment != null)
|
||||||
Text(
|
Text(media.attachment!.alt,
|
||||||
media.attachment!.alt,
|
maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
else if (media.file != null)
|
else if (media.file != null)
|
||||||
Text(media.file!.name,
|
Text(media.file!.name,
|
||||||
maxLines: 1, overflow: TextOverflow.ellipsis)
|
maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||||
@ -468,11 +260,8 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
final VisualDensity? visualDensity;
|
final VisualDensity? visualDensity;
|
||||||
final Function(Iterable<PostWriteMedia>) onAdd;
|
final Function(Iterable<PostWriteMedia>) onAdd;
|
||||||
|
|
||||||
const AddPostMediaButton({
|
const AddPostMediaButton(
|
||||||
super.key,
|
{super.key, required this.onAdd, this.visualDensity});
|
||||||
required this.onAdd,
|
|
||||||
this.visualDensity,
|
|
||||||
});
|
|
||||||
|
|
||||||
void _takeMedia(bool isVideo) async {
|
void _takeMedia(bool isVideo) async {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
@ -487,17 +276,13 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final result = await picker.pickMultipleMedia();
|
final result = await picker.pickMultipleMedia();
|
||||||
if (result.isEmpty) return;
|
if (result.isEmpty) return;
|
||||||
onAdd(
|
onAdd(result.map((e) => PostWriteMedia.fromFile(e)));
|
||||||
result.map((e) => PostWriteMedia.fromFile(e)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectFile() async {
|
void _selectFile() async {
|
||||||
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
onAdd(
|
onAdd(result.files.map((e) => PostWriteMedia.fromFile(e.xFile)));
|
||||||
result.files.map((e) => PostWriteMedia.fromFile(e.xFile)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _pasteMedia() async {
|
void _pasteMedia() async {
|
||||||
@ -505,10 +290,7 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
if (imageBytes == null) return;
|
if (imageBytes == null) return;
|
||||||
onAdd([
|
onAdd([
|
||||||
PostWriteMedia.fromBytes(
|
PostWriteMedia.fromBytes(
|
||||||
imageBytes,
|
imageBytes, 'attachmentPastedImage'.tr(), SnMediaType.image)
|
||||||
'attachmentPastedImage'.tr(),
|
|
||||||
SnMediaType.image,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -556,19 +338,15 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
final attach = context.read<SnAttachmentProvider>();
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
final attachment = await attach.getOne(randomId);
|
final attachment = await attach.getOne(randomId);
|
||||||
|
|
||||||
onAdd([
|
onAdd([PostWriteMedia(attachment)]);
|
||||||
PostWriteMedia(attachment),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopupMenuButton(
|
return PopupMenuButton(
|
||||||
style: ButtonStyle(visualDensity: visualDensity),
|
style: ButtonStyle(visualDensity: visualDensity),
|
||||||
icon: Icon(
|
icon: Icon(Symbols.add_photo_alternate,
|
||||||
Symbols.add_photo_alternate,
|
color: Theme.of(context).colorScheme.primary),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
if (!kIsWeb &&
|
if (!kIsWeb &&
|
||||||
!Platform.isLinux &&
|
!Platform.isLinux &&
|
||||||
@ -595,7 +373,7 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.videocam),
|
const Icon(Symbols.videocam),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('addAttachmentFromCameraVideo').tr(),
|
Text('addAttachmentFromCameraVideo').tr()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -607,7 +385,7 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.photo_library),
|
const Icon(Symbols.photo_library),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('addAttachmentFromAlbum').tr(),
|
Text('addAttachmentFromAlbum').tr()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -619,7 +397,7 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.file_upload),
|
const Icon(Symbols.file_upload),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('addAttachmentFromFiles').tr(),
|
Text('addAttachmentFromFiles').tr()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -627,13 +405,11 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(children: [
|
||||||
children: [
|
const Icon(Symbols.link),
|
||||||
const Icon(Symbols.link),
|
const Gap(16),
|
||||||
const Gap(16),
|
Text('addAttachmentFromRandomId').tr()
|
||||||
Text('addAttachmentFromRandomId').tr(),
|
]),
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_linkRandomId(context);
|
_linkRandomId(context);
|
||||||
},
|
},
|
||||||
@ -643,7 +419,7 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.content_paste),
|
const Icon(Symbols.content_paste),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('addAttachmentFromClipboard').tr(),
|
Text('addAttachmentFromClipboard').tr()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user