✨ Queued upload
This commit is contained in:
		@@ -114,10 +114,12 @@ class PostEditorController extends GetxController {
 | 
				
			|||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      builder: (context) => AttachmentEditorPopup(
 | 
					      builder: (context) => AttachmentEditorPopup(
 | 
				
			||||||
        usage: 'i.attachment',
 | 
					        usage: 'i.attachment',
 | 
				
			||||||
        current: attachments,
 | 
					        initialAttachments: attachments,
 | 
				
			||||||
        onUpdate: (value) {
 | 
					        onAdd: (value) {
 | 
				
			||||||
          attachments.value = value;
 | 
					          attachments.add(value);
 | 
				
			||||||
          attachments.refresh();
 | 
					        },
 | 
				
			||||||
 | 
					        onRemove: (value) {
 | 
				
			||||||
 | 
					          attachments.remove(value);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -163,6 +165,8 @@ class PostEditorController extends GetxController {
 | 
				
			|||||||
    visibleUsers.clear();
 | 
					    visibleUsers.clear();
 | 
				
			||||||
    invisibleUsers.clear();
 | 
					    invisibleUsers.clear();
 | 
				
			||||||
    visibility.value = 0;
 | 
					    visibility.value = 0;
 | 
				
			||||||
 | 
					    publishedAt.value = null;
 | 
				
			||||||
 | 
					    publishedUntil.value = null;
 | 
				
			||||||
    isDraft.value = false;
 | 
					    isDraft.value = false;
 | 
				
			||||||
    isRestoreFromLocal.value = false;
 | 
					    isRestoreFromLocal.value = false;
 | 
				
			||||||
    lastSaveTime.value = null;
 | 
					    lastSaveTime.value = null;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
 | 
				
			|||||||
import 'package:solian/bootstrapper.dart';
 | 
					import 'package:solian/bootstrapper.dart';
 | 
				
			||||||
import 'package:solian/firebase_options.dart';
 | 
					import 'package:solian/firebase_options.dart';
 | 
				
			||||||
import 'package:solian/platform.dart';
 | 
					import 'package:solian/platform.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/attachment_uploader.dart';
 | 
				
			||||||
import 'package:solian/providers/theme_switcher.dart';
 | 
					import 'package:solian/providers/theme_switcher.dart';
 | 
				
			||||||
import 'package:solian/providers/websocket.dart';
 | 
					import 'package:solian/providers/websocket.dart';
 | 
				
			||||||
import 'package:solian/providers/auth.dart';
 | 
					import 'package:solian/providers/auth.dart';
 | 
				
			||||||
@@ -125,5 +126,6 @@ class SolianApp extends StatelessWidget {
 | 
				
			|||||||
    Get.lazyPut(() => ChannelProvider());
 | 
					    Get.lazyPut(() => ChannelProvider());
 | 
				
			||||||
    Get.lazyPut(() => RealmProvider());
 | 
					    Get.lazyPut(() => RealmProvider());
 | 
				
			||||||
    Get.lazyPut(() => ChatCallProvider());
 | 
					    Get.lazyPut(() => ChatCallProvider());
 | 
				
			||||||
 | 
					    Get.lazyPut(() => AttachmentUploaderController());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										165
									
								
								lib/providers/attachment_uploader.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								lib/providers/attachment_uploader.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:solian/models/attachment.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/content/attachment.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AttachmentUploadTask {
 | 
				
			||||||
 | 
					  File file;
 | 
				
			||||||
 | 
					  String usage;
 | 
				
			||||||
 | 
					  Map<String, dynamic>? metadata;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double progress = 0;
 | 
				
			||||||
 | 
					  bool isUploading = false;
 | 
				
			||||||
 | 
					  bool isCompleted = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AttachmentUploadTask({
 | 
				
			||||||
 | 
					    required this.file,
 | 
				
			||||||
 | 
					    required this.usage,
 | 
				
			||||||
 | 
					    this.metadata,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AttachmentUploaderController extends GetxController {
 | 
				
			||||||
 | 
					  RxBool isUploading = false.obs;
 | 
				
			||||||
 | 
					  RxDouble progressOfUpload = 0.0.obs;
 | 
				
			||||||
 | 
					  RxList<AttachmentUploadTask> queueOfUpload = RxList.empty(growable: true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void enqueueTask(AttachmentUploadTask task) {
 | 
				
			||||||
 | 
					    if (isUploading.value) throw Exception('uploading blocked');
 | 
				
			||||||
 | 
					    queueOfUpload.add(task);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void dequeueTask(AttachmentUploadTask task) {
 | 
				
			||||||
 | 
					    if (isUploading.value) throw Exception('uploading blocked');
 | 
				
			||||||
 | 
					    queueOfUpload.remove(task);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Attachment> performSingleTask(int queueIndex) async {
 | 
				
			||||||
 | 
					    isUploading.value = true;
 | 
				
			||||||
 | 
					    progressOfUpload.value = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queueOfUpload[queueIndex].isUploading = true;
 | 
				
			||||||
 | 
					    queueOfUpload.refresh();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final task = queueOfUpload[queueIndex];
 | 
				
			||||||
 | 
					    final result = await _rawUploadAttachment(
 | 
				
			||||||
 | 
					      await task.file.readAsBytes(),
 | 
				
			||||||
 | 
					      task.file.path,
 | 
				
			||||||
 | 
					      task.usage,
 | 
				
			||||||
 | 
					      null,
 | 
				
			||||||
 | 
					      onProgress: (value) {
 | 
				
			||||||
 | 
					        queueOfUpload[queueIndex].progress = value;
 | 
				
			||||||
 | 
					        queueOfUpload.refresh();
 | 
				
			||||||
 | 
					        progressOfUpload.value = value;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queueOfUpload.removeAt(queueIndex);
 | 
				
			||||||
 | 
					    queueOfUpload.refresh();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isUploading.value = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> performUploadQueue({
 | 
				
			||||||
 | 
					    required Function(Attachment item) onData,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    isUploading.value = true;
 | 
				
			||||||
 | 
					    progressOfUpload.value = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var idx = 0; idx < queueOfUpload.length; idx++) {
 | 
				
			||||||
 | 
					      queueOfUpload[idx].isUploading = true;
 | 
				
			||||||
 | 
					      queueOfUpload.refresh();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final task = queueOfUpload[idx];
 | 
				
			||||||
 | 
					      final result = await _rawUploadAttachment(
 | 
				
			||||||
 | 
					        await task.file.readAsBytes(),
 | 
				
			||||||
 | 
					        task.file.path,
 | 
				
			||||||
 | 
					        task.usage,
 | 
				
			||||||
 | 
					        null,
 | 
				
			||||||
 | 
					        onProgress: (value) {
 | 
				
			||||||
 | 
					          queueOfUpload[idx].progress = value;
 | 
				
			||||||
 | 
					          queueOfUpload.refresh();
 | 
				
			||||||
 | 
					          progressOfUpload.value = (idx + value) / queueOfUpload.length;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      progressOfUpload.value = (idx + 1) / queueOfUpload.length;
 | 
				
			||||||
 | 
					      onData(result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      queueOfUpload[idx].isUploading = false;
 | 
				
			||||||
 | 
					      queueOfUpload[idx].isCompleted = false;
 | 
				
			||||||
 | 
					      queueOfUpload.refresh();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queueOfUpload.clear();
 | 
				
			||||||
 | 
					    queueOfUpload.refresh();
 | 
				
			||||||
 | 
					    isUploading.value = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> uploadAttachmentWithCallback(
 | 
				
			||||||
 | 
					    Uint8List data,
 | 
				
			||||||
 | 
					    String path,
 | 
				
			||||||
 | 
					    String usage,
 | 
				
			||||||
 | 
					    Map<String, dynamic>? metadata,
 | 
				
			||||||
 | 
					    Function(Attachment) callback,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    if (isUploading.value) throw Exception('uploading blocked');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isUploading.value = true;
 | 
				
			||||||
 | 
					    final result = await _rawUploadAttachment(
 | 
				
			||||||
 | 
					      data,
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      usage,
 | 
				
			||||||
 | 
					      metadata,
 | 
				
			||||||
 | 
					      onProgress: (progress) {
 | 
				
			||||||
 | 
					        progressOfUpload.value = progress;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    isUploading.value = false;
 | 
				
			||||||
 | 
					    callback(result);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Attachment> uploadAttachment(
 | 
				
			||||||
 | 
					    Uint8List data,
 | 
				
			||||||
 | 
					    String path,
 | 
				
			||||||
 | 
					    String usage,
 | 
				
			||||||
 | 
					    Map<String, dynamic>? metadata,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    if (isUploading.value) throw Exception('uploading blocked');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isUploading.value = true;
 | 
				
			||||||
 | 
					    final result = await _rawUploadAttachment(
 | 
				
			||||||
 | 
					      data,
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      usage,
 | 
				
			||||||
 | 
					      metadata,
 | 
				
			||||||
 | 
					      onProgress: (progress) {
 | 
				
			||||||
 | 
					        progressOfUpload.value = progress;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    isUploading.value = false;
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Attachment> _rawUploadAttachment(
 | 
				
			||||||
 | 
					      Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
 | 
				
			||||||
 | 
					      {Function(double)? onProgress}) async {
 | 
				
			||||||
 | 
					    final AttachmentProvider provider = Get.find();
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final resp = await provider.createAttachment(
 | 
				
			||||||
 | 
					        data,
 | 
				
			||||||
 | 
					        path,
 | 
				
			||||||
 | 
					        usage,
 | 
				
			||||||
 | 
					        metadata,
 | 
				
			||||||
 | 
					        onProgress: onProgress,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      var result = Attachment.fromJson(resp.body);
 | 
				
			||||||
 | 
					      return result;
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      rethrow;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -38,11 +38,8 @@ class AttachmentProvider extends GetConnect {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<Response> createAttachment(
 | 
					  Future<Response> createAttachment(
 | 
				
			||||||
    Uint8List data,
 | 
					      Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
 | 
				
			||||||
    String path,
 | 
					      {Function(double)? onProgress}) async {
 | 
				
			||||||
    String usage,
 | 
					 | 
				
			||||||
    Map<String, dynamic>? metadata,
 | 
					 | 
				
			||||||
  ) async {
 | 
					 | 
				
			||||||
    final AuthProvider auth = Get.find();
 | 
					    final AuthProvider auth = Get.find();
 | 
				
			||||||
    if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
 | 
					    if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,7 +68,13 @@ class AttachmentProvider extends GetConnect {
 | 
				
			|||||||
      if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
 | 
					      if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
 | 
				
			||||||
      'metadata': jsonEncode(metadata),
 | 
					      'metadata': jsonEncode(metadata),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    final resp = await client.post('/attachments', payload);
 | 
					    final resp = await client.post(
 | 
				
			||||||
 | 
					      '/attachments',
 | 
				
			||||||
 | 
					      payload,
 | 
				
			||||||
 | 
					      uploadProgress: (progress) {
 | 
				
			||||||
 | 
					        if (onProgress != null) onProgress(progress);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    if (resp.statusCode != 200) {
 | 
					    if (resp.statusCode != 200) {
 | 
				
			||||||
      throw Exception(resp.bodyString);
 | 
					      throw Exception(resp.bodyString);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -157,6 +157,11 @@ const i18nEnglish = {
 | 
				
			|||||||
  'reactCompleted': 'Your reaction has been added',
 | 
					  'reactCompleted': 'Your reaction has been added',
 | 
				
			||||||
  'reactUncompleted': 'Your reaction has been removed',
 | 
					  'reactUncompleted': 'Your reaction has been removed',
 | 
				
			||||||
  'attachmentUploadBy': 'Upload by',
 | 
					  'attachmentUploadBy': 'Upload by',
 | 
				
			||||||
 | 
					  'attachmentAutoUpload': 'Auto Upload',
 | 
				
			||||||
 | 
					  'attachmentUploadQueue': 'Upload Queue',
 | 
				
			||||||
 | 
					  'attachmentUploadQueueStart': 'Start All',
 | 
				
			||||||
 | 
					  'attachmentAttached': 'Exists Files',
 | 
				
			||||||
 | 
					  'attachmentUploadBlocked': 'Upload blocked, there is currently a task in progress...',
 | 
				
			||||||
  'attachmentAdd': 'Attach attachments',
 | 
					  'attachmentAdd': 'Attach attachments',
 | 
				
			||||||
  'attachmentAddGalleryPhoto': 'Gallery photo',
 | 
					  'attachmentAddGalleryPhoto': 'Gallery photo',
 | 
				
			||||||
  'attachmentAddGalleryVideo': 'Gallery video',
 | 
					  'attachmentAddGalleryVideo': 'Gallery video',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -146,6 +146,11 @@ const i18nSimplifiedChinese = {
 | 
				
			|||||||
  'reactCompleted': '你的反应已被添加',
 | 
					  'reactCompleted': '你的反应已被添加',
 | 
				
			||||||
  'reactUncompleted': '你的反应已被移除',
 | 
					  'reactUncompleted': '你的反应已被移除',
 | 
				
			||||||
  'attachmentUploadBy': '由上传',
 | 
					  'attachmentUploadBy': '由上传',
 | 
				
			||||||
 | 
					  'attachmentAutoUpload': '自动上传',
 | 
				
			||||||
 | 
					  'attachmentUploadQueue': '上传队列',
 | 
				
			||||||
 | 
					  'attachmentUploadQueueStart': '整队上传',
 | 
				
			||||||
 | 
					  'attachmentAttached': '已附附件',
 | 
				
			||||||
 | 
					  'attachmentUploadBlocked': '上传受阻,当前已有任务进行中……',
 | 
				
			||||||
  'attachmentAdd': '附加附件',
 | 
					  'attachmentAdd': '附加附件',
 | 
				
			||||||
  'attachmentAddGalleryPhoto': '相册照片',
 | 
					  'attachmentAddGalleryPhoto': '相册照片',
 | 
				
			||||||
  'attachmentAddGalleryVideo': '相册视频',
 | 
					  'attachmentAddGalleryVideo': '相册视频',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										132
									
								
								lib/widgets/attachments/attachment_attr_editor.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								lib/widgets/attachments/attachment_attr_editor.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_animate/flutter_animate.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:solian/exts.dart';
 | 
				
			||||||
 | 
					import 'package:solian/models/attachment.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/content/attachment.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AttachmentAttrEditorDialog extends StatefulWidget {
 | 
				
			||||||
 | 
					  final Attachment item;
 | 
				
			||||||
 | 
					  final Function(Attachment item) onUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const AttachmentAttrEditorDialog({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.item,
 | 
				
			||||||
 | 
					    required this.onUpdate,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<AttachmentAttrEditorDialog> createState() => _AttachmentAttrEditorDialogState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog> {
 | 
				
			||||||
 | 
					  final _altController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _isBusy = false;
 | 
				
			||||||
 | 
					  bool _isMature = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Attachment?> _updateAttachment() async {
 | 
				
			||||||
 | 
					    final AttachmentProvider provider = Get.find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final resp = await provider.updateAttachment(
 | 
				
			||||||
 | 
					        widget.item.id,
 | 
				
			||||||
 | 
					        _altController.value.text,
 | 
				
			||||||
 | 
					        widget.item.usage,
 | 
				
			||||||
 | 
					        isMature: _isMature,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Get.find<AttachmentProvider>().clearCache(id: widget.item.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					      return Attachment.fromJson(resp.body);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      context.showErrorDialog(e);
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void syncWidget() {
 | 
				
			||||||
 | 
					    _isMature = widget.item.isMature;
 | 
				
			||||||
 | 
					    _altController.text = widget.item.alt;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    syncWidget();
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return AlertDialog(
 | 
				
			||||||
 | 
					      title: Text('attachmentSetting'.tr),
 | 
				
			||||||
 | 
					      content: Container(
 | 
				
			||||||
 | 
					        constraints: const BoxConstraints(minWidth: 400),
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            if (_isBusy)
 | 
				
			||||||
 | 
					              ClipRRect(
 | 
				
			||||||
 | 
					                borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                child: const LinearProgressIndicator().animate().scaleX(),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            const SizedBox(height: 18),
 | 
				
			||||||
 | 
					            TextField(
 | 
				
			||||||
 | 
					              controller: _altController,
 | 
				
			||||||
 | 
					              decoration: InputDecoration(
 | 
				
			||||||
 | 
					                isDense: true,
 | 
				
			||||||
 | 
					                prefixIcon: const Icon(Icons.image_not_supported),
 | 
				
			||||||
 | 
					                border: const OutlineInputBorder(),
 | 
				
			||||||
 | 
					                labelText: 'attachmentAlt'.tr,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onTapOutside: (_) =>
 | 
				
			||||||
 | 
					                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            const SizedBox(height: 8),
 | 
				
			||||||
 | 
					            CheckboxListTile(
 | 
				
			||||||
 | 
					              contentPadding: const EdgeInsets.only(left: 4, right: 18),
 | 
				
			||||||
 | 
					              shape: const RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.all(Radius.circular(10))),
 | 
				
			||||||
 | 
					              title: Text('matureContent'.tr),
 | 
				
			||||||
 | 
					              secondary: const Icon(Icons.visibility_off),
 | 
				
			||||||
 | 
					              value: _isMature,
 | 
				
			||||||
 | 
					              onChanged: (newValue) {
 | 
				
			||||||
 | 
					                setState(() => _isMature = newValue ?? false);
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              controlAffinity: ListTileControlAffinity.leading,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      actionsAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					      actions: <Widget>[
 | 
				
			||||||
 | 
					        Row(
 | 
				
			||||||
 | 
					          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            TextButton(
 | 
				
			||||||
 | 
					              style: TextButton.styleFrom(
 | 
				
			||||||
 | 
					                  foregroundColor:
 | 
				
			||||||
 | 
					                  Theme.of(context).colorScheme.onSurfaceVariant),
 | 
				
			||||||
 | 
					              onPressed: () => Navigator.pop(context),
 | 
				
			||||||
 | 
					              child: Text('cancel'.tr),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            TextButton(
 | 
				
			||||||
 | 
					              child: Text('apply'.tr),
 | 
				
			||||||
 | 
					              onPressed: () {
 | 
				
			||||||
 | 
					                _updateAttachment().then((value) {
 | 
				
			||||||
 | 
					                  if (value != null) {
 | 
				
			||||||
 | 
					                    widget.onUpdate(value);
 | 
				
			||||||
 | 
					                    Navigator.pop(context);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
import 'dart:math' as math;
 | 
					import 'dart:math' as math;
 | 
				
			||||||
import 'dart:typed_data';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:desktop_drop/desktop_drop.dart';
 | 
					import 'package:desktop_drop/desktop_drop.dart';
 | 
				
			||||||
import 'package:dismissible_page/dismissible_page.dart';
 | 
					import 'package:dismissible_page/dismissible_page.dart';
 | 
				
			||||||
@@ -15,20 +14,24 @@ import 'package:path/path.dart' show basename;
 | 
				
			|||||||
import 'package:solian/exts.dart';
 | 
					import 'package:solian/exts.dart';
 | 
				
			||||||
import 'package:solian/models/attachment.dart';
 | 
					import 'package:solian/models/attachment.dart';
 | 
				
			||||||
import 'package:solian/platform.dart';
 | 
					import 'package:solian/platform.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/attachment_uploader.dart';
 | 
				
			||||||
import 'package:solian/providers/auth.dart';
 | 
					import 'package:solian/providers/auth.dart';
 | 
				
			||||||
import 'package:solian/providers/content/attachment.dart';
 | 
					import 'package:solian/providers/content/attachment.dart';
 | 
				
			||||||
 | 
					import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
 | 
				
			||||||
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
 | 
					import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AttachmentEditorPopup extends StatefulWidget {
 | 
					class AttachmentEditorPopup extends StatefulWidget {
 | 
				
			||||||
  final String usage;
 | 
					  final String usage;
 | 
				
			||||||
  final List<int> current;
 | 
					  final List<int> initialAttachments;
 | 
				
			||||||
  final void Function(List<int> data) onUpdate;
 | 
					  final void Function(int) onAdd;
 | 
				
			||||||
 | 
					  final void Function(int) onRemove;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AttachmentEditorPopup({
 | 
					  const AttachmentEditorPopup({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.usage,
 | 
					    required this.usage,
 | 
				
			||||||
    required this.current,
 | 
					    required this.initialAttachments,
 | 
				
			||||||
    required this.onUpdate,
 | 
					    required this.onAdd,
 | 
				
			||||||
 | 
					    required this.onRemove,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -37,6 +40,9 @@ class AttachmentEditorPopup extends StatefulWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
					class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			||||||
  final _imagePicker = ImagePicker();
 | 
					  final _imagePicker = ImagePicker();
 | 
				
			||||||
 | 
					  final AttachmentUploaderController _uploadController = Get.find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _isAutoUpload = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _isBusy = false;
 | 
					  bool _isBusy = false;
 | 
				
			||||||
  bool _isFirstTimeBusy = true;
 | 
					  bool _isFirstTimeBusy = true;
 | 
				
			||||||
@@ -50,24 +56,14 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    final medias = await _imagePicker.pickMultiImage();
 | 
					    final medias = await _imagePicker.pickMultiImage();
 | 
				
			||||||
    if (medias.isEmpty) return;
 | 
					    if (medias.isEmpty) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (final media in medias) {
 | 
					    for (final media in medias) {
 | 
				
			||||||
      final file = File(media.path);
 | 
					      final file = File(media.path);
 | 
				
			||||||
      try {
 | 
					      _enqueueTask(
 | 
				
			||||||
        await _uploadAttachment(
 | 
					        AttachmentUploadTask(file: file, usage: widget.usage),
 | 
				
			||||||
          await file.readAsBytes(),
 | 
					 | 
				
			||||||
          file.path,
 | 
					 | 
				
			||||||
          null,
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      } catch (err) {
 | 
					 | 
				
			||||||
        context.showErrorDialog(err);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _pickVideoToUpload() async {
 | 
					  Future<void> _pickVideoToUpload() async {
 | 
				
			||||||
    final AuthProvider auth = Get.find();
 | 
					    final AuthProvider auth = Get.find();
 | 
				
			||||||
    if (auth.isAuthorized.isFalse) return;
 | 
					    if (auth.isAuthorized.isFalse) return;
 | 
				
			||||||
@@ -75,17 +71,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    final media = await _imagePicker.pickVideo(source: ImageSource.gallery);
 | 
					    final media = await _imagePicker.pickVideo(source: ImageSource.gallery);
 | 
				
			||||||
    if (media == null) return;
 | 
					    if (media == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final file = File(media.path);
 | 
					    final file = File(media.path);
 | 
				
			||||||
 | 
					    _enqueueTask(
 | 
				
			||||||
    try {
 | 
					      AttachmentUploadTask(file: file, usage: widget.usage),
 | 
				
			||||||
      await _uploadAttachment(await file.readAsBytes(), file.path, null);
 | 
					    );
 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _pickFileToUpload() async {
 | 
					  Future<void> _pickFileToUpload() async {
 | 
				
			||||||
@@ -97,21 +86,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    if (result == null) return;
 | 
					    if (result == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    List<File> files = result.paths.map((path) => File(path!)).toList();
 | 
					    List<File> files = result.paths.map((path) => File(path!)).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (final file in files) {
 | 
					    for (final file in files) {
 | 
				
			||||||
      try {
 | 
					      _enqueueTask(
 | 
				
			||||||
        await _uploadAttachment(await file.readAsBytes(), file.path, null);
 | 
					        AttachmentUploadTask(file: file, usage: widget.usage),
 | 
				
			||||||
      } catch (err) {
 | 
					      );
 | 
				
			||||||
        context.showErrorDialog(err);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _takeMediaToUpload(bool isVideo) async {
 | 
					  Future<void> _takeMediaToUpload(bool isVideo) async {
 | 
				
			||||||
    final AuthProvider auth = Get.find();
 | 
					    final AuthProvider auth = Get.find();
 | 
				
			||||||
    if (auth.isAuthorized.isFalse) return;
 | 
					    if (auth.isAuthorized.isFalse) return;
 | 
				
			||||||
@@ -124,50 +107,30 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    if (media == null) return;
 | 
					    if (media == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final file = File(media.path);
 | 
					    final file = File(media.path);
 | 
				
			||||||
    try {
 | 
					    _enqueueTask(
 | 
				
			||||||
      await _uploadAttachment(await file.readAsBytes(), file.path, null);
 | 
					      AttachmentUploadTask(file: file, usage: widget.usage),
 | 
				
			||||||
    } catch (err) {
 | 
					    );
 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _pasteFileToUpload() async {
 | 
					  void _pasteFileToUpload() async {
 | 
				
			||||||
    final data = await Pasteboard.image;
 | 
					    final data = await Pasteboard.image;
 | 
				
			||||||
    if (data == null) return;
 | 
					    if (data == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    if (_uploadController.isUploading.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _uploadAttachment(data, 'Pasted Image', null);
 | 
					    _uploadController.uploadAttachmentWithCallback(
 | 
				
			||||||
 | 
					 | 
				
			||||||
    setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _uploadAttachment(
 | 
					 | 
				
			||||||
      Uint8List data, String path, Map<String, dynamic>? metadata) async {
 | 
					 | 
				
			||||||
    final AttachmentProvider provider = Get.find();
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      context.showSnackbar((PlatformInfo.isWeb
 | 
					 | 
				
			||||||
              ? 'attachmentUploadingWebMode'
 | 
					 | 
				
			||||||
              : 'attachmentUploading')
 | 
					 | 
				
			||||||
          .trParams({'name': basename(path)}));
 | 
					 | 
				
			||||||
      final resp = await provider.createAttachment(
 | 
					 | 
				
			||||||
      data,
 | 
					      data,
 | 
				
			||||||
        path,
 | 
					      'Pasted Image',
 | 
				
			||||||
      widget.usage,
 | 
					      widget.usage,
 | 
				
			||||||
        metadata,
 | 
					      null,
 | 
				
			||||||
      );
 | 
					      (item) {
 | 
				
			||||||
      var result = Attachment.fromJson(resp.body);
 | 
					        widget.onAdd(item.id);
 | 
				
			||||||
      setState(() => _attachments.add(result));
 | 
					        if (mounted) {
 | 
				
			||||||
      widget.onUpdate(_attachments.map((e) => e!.id).toList());
 | 
					          setState(() => _attachments.add(item));
 | 
				
			||||||
      context.clearSnackbar();
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      rethrow;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String _formatBytes(int bytes, {int decimals = 2}) {
 | 
					  String _formatBytes(int bytes, {int decimals = 2}) {
 | 
				
			||||||
@@ -192,21 +155,21 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
  void _revertMetadataList() {
 | 
					  void _revertMetadataList() {
 | 
				
			||||||
    final AttachmentProvider provider = Get.find();
 | 
					    final AttachmentProvider provider = Get.find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (widget.current.isEmpty) {
 | 
					    if (widget.initialAttachments.isEmpty) {
 | 
				
			||||||
      _isFirstTimeBusy = false;
 | 
					      _isFirstTimeBusy = false;
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      _attachments = List.filled(widget.current.length, null);
 | 
					      _attachments = List.filled(widget.initialAttachments.length, null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    int progress = 0;
 | 
					    int progress = 0;
 | 
				
			||||||
    for (var idx = 0; idx < widget.current.length; idx++) {
 | 
					    for (var idx = 0; idx < widget.initialAttachments.length; idx++) {
 | 
				
			||||||
      provider.getMetadata(widget.current[idx]).then((resp) {
 | 
					      provider.getMetadata(widget.initialAttachments[idx]).then((resp) {
 | 
				
			||||||
        progress++;
 | 
					        progress++;
 | 
				
			||||||
        _attachments[idx] = resp;
 | 
					        _attachments[idx] = resp;
 | 
				
			||||||
        if (progress == widget.current.length) {
 | 
					        if (progress == widget.initialAttachments.length) {
 | 
				
			||||||
          setState(() {
 | 
					          setState(() {
 | 
				
			||||||
            _isBusy = false;
 | 
					            _isBusy = false;
 | 
				
			||||||
            _isFirstTimeBusy = false;
 | 
					            _isFirstTimeBusy = false;
 | 
				
			||||||
@@ -230,11 +193,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    showDialog(
 | 
					    showDialog(
 | 
				
			||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      builder: (context) {
 | 
					      builder: (context) {
 | 
				
			||||||
        return AttachmentEditorDialog(
 | 
					        return AttachmentAttrEditorDialog(
 | 
				
			||||||
          item: element,
 | 
					          item: element,
 | 
				
			||||||
          onUpdate: (item) {
 | 
					          onUpdate: (item) {
 | 
				
			||||||
            setState(() => _attachments[index] = item);
 | 
					            setState(() => _attachments[index] = item);
 | 
				
			||||||
            widget.onUpdate(_attachments.map((e) => e!.id).toList());
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -253,6 +215,107 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildQueueEntry(AttachmentUploadTask element, int index) {
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.only(left: 16, right: 8, bottom: 16),
 | 
				
			||||||
 | 
					      child: Card(
 | 
				
			||||||
 | 
					        color: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            SizedBox(
 | 
				
			||||||
 | 
					              height: 54,
 | 
				
			||||||
 | 
					              child: Row(
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  Expanded(
 | 
				
			||||||
 | 
					                    child: Column(
 | 
				
			||||||
 | 
					                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Text(
 | 
				
			||||||
 | 
					                          basename(element.file.path),
 | 
				
			||||||
 | 
					                          overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                          maxLines: 1,
 | 
				
			||||||
 | 
					                          style: const TextStyle(
 | 
				
			||||||
 | 
					                            fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                            fontFamily: 'monospace',
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        FutureBuilder(
 | 
				
			||||||
 | 
					                          future: element.file.length(),
 | 
				
			||||||
 | 
					                          builder: (context, snapshot) {
 | 
				
			||||||
 | 
					                            if (!snapshot.hasData) {
 | 
				
			||||||
 | 
					                              return const Text(
 | 
				
			||||||
 | 
					                                '- Bytes',
 | 
				
			||||||
 | 
					                                style: TextStyle(fontSize: 12),
 | 
				
			||||||
 | 
					                              );
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            return Text(
 | 
				
			||||||
 | 
					                              _formatBytes(snapshot.data!),
 | 
				
			||||||
 | 
					                              style: const TextStyle(fontSize: 12),
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  if (element.isUploading)
 | 
				
			||||||
 | 
					                    SizedBox(
 | 
				
			||||||
 | 
					                      width: 40,
 | 
				
			||||||
 | 
					                      height: 38,
 | 
				
			||||||
 | 
					                      child: Center(
 | 
				
			||||||
 | 
					                        child: SizedBox(
 | 
				
			||||||
 | 
					                          width: 20,
 | 
				
			||||||
 | 
					                          height: 20,
 | 
				
			||||||
 | 
					                          child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					                            strokeWidth: 2.5,
 | 
				
			||||||
 | 
					                            value: element.progress,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  if (element.isCompleted)
 | 
				
			||||||
 | 
					                    const SizedBox(
 | 
				
			||||||
 | 
					                      width: 40,
 | 
				
			||||||
 | 
					                      height: 38,
 | 
				
			||||||
 | 
					                      child: Center(
 | 
				
			||||||
 | 
					                        child: Icon(Icons.check),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  if (!element.isCompleted && !element.isUploading)
 | 
				
			||||||
 | 
					                    IconButton(
 | 
				
			||||||
 | 
					                      color: Colors.green,
 | 
				
			||||||
 | 
					                      icon: const Icon(Icons.play_arrow),
 | 
				
			||||||
 | 
					                      visualDensity: const VisualDensity(horizontal: -4),
 | 
				
			||||||
 | 
					                      onPressed: _uploadController.isUploading.value
 | 
				
			||||||
 | 
					                          ? null
 | 
				
			||||||
 | 
					                          : () {
 | 
				
			||||||
 | 
					                              _uploadController
 | 
				
			||||||
 | 
					                                  .performSingleTask(index)
 | 
				
			||||||
 | 
					                                  .then((r) {
 | 
				
			||||||
 | 
					                                widget.onAdd(r.id);
 | 
				
			||||||
 | 
					                                if (mounted) {
 | 
				
			||||||
 | 
					                                  setState(() => _attachments.add(r));
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                              });
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  if (!element.isCompleted && !element.isUploading)
 | 
				
			||||||
 | 
					                    IconButton(
 | 
				
			||||||
 | 
					                      color: Colors.red,
 | 
				
			||||||
 | 
					                      icon: const Icon(Icons.remove_circle),
 | 
				
			||||||
 | 
					                      visualDensity: const VisualDensity(horizontal: -4),
 | 
				
			||||||
 | 
					                      onPressed: () {
 | 
				
			||||||
 | 
					                        _uploadController.dequeueTask(element);
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ).paddingSymmetric(vertical: 8, horizontal: 16),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Widget _buildListEntry(Attachment element, int index) {
 | 
					  Widget _buildListEntry(Attachment element, int index) {
 | 
				
			||||||
    var fileType = element.mimetype.split('/').firstOrNull;
 | 
					    var fileType = element.mimetype.split('/').firstOrNull;
 | 
				
			||||||
    fileType ??= 'unknown';
 | 
					    fileType ??= 'unknown';
 | 
				
			||||||
@@ -279,7 +342,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
                          maxLines: 1,
 | 
					                          maxLines: 1,
 | 
				
			||||||
                          style: const TextStyle(
 | 
					                          style: const TextStyle(
 | 
				
			||||||
                            fontWeight: FontWeight.bold,
 | 
					                            fontWeight: FontWeight.bold,
 | 
				
			||||||
                              fontFamily: 'monospace'),
 | 
					                            fontFamily: 'monospace',
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        Text(
 | 
					                        Text(
 | 
				
			||||||
                          '${fileType[0].toUpperCase()}${fileType.substring(1)} · ${_formatBytes(element.size)}',
 | 
					                          '${fileType[0].toUpperCase()}${fileType.substring(1)} · ${_formatBytes(element.size)}',
 | 
				
			||||||
@@ -323,12 +387,11 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        onTap: () {
 | 
					                        onTap: () {
 | 
				
			||||||
                          _deleteAttachment(element).then((_) {
 | 
					                          _deleteAttachment(element).then((_) {
 | 
				
			||||||
 | 
					                            widget.onRemove(element.id);
 | 
				
			||||||
                            setState(() => _attachments.removeAt(index));
 | 
					                            setState(() => _attachments.removeAt(index));
 | 
				
			||||||
                              widget.onUpdate(
 | 
					 | 
				
			||||||
                                _attachments.map((e) => e!.id).toList(),
 | 
					 | 
				
			||||||
                              );
 | 
					 | 
				
			||||||
                          });
 | 
					                          });
 | 
				
			||||||
                          }),
 | 
					                        },
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
@@ -340,6 +403,22 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _enqueueTask(AttachmentUploadTask task) {
 | 
				
			||||||
 | 
					    _uploadController.enqueueTask(task);
 | 
				
			||||||
 | 
					    if (_isAutoUpload) {
 | 
				
			||||||
 | 
					      _startUploading();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _startUploading() {
 | 
				
			||||||
 | 
					    _uploadController.performUploadQueue(onData: (r) {
 | 
				
			||||||
 | 
					      widget.onAdd(r.id);
 | 
				
			||||||
 | 
					      if (mounted) {
 | 
				
			||||||
 | 
					        setState(() => _attachments.add(r));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
@@ -353,30 +432,125 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
    return SafeArea(
 | 
					    return SafeArea(
 | 
				
			||||||
      child: DropTarget(
 | 
					      child: DropTarget(
 | 
				
			||||||
        onDragDone: (detail) async {
 | 
					        onDragDone: (detail) async {
 | 
				
			||||||
          setState(() => _isBusy = true);
 | 
					          if (_uploadController.isUploading.value) return;
 | 
				
			||||||
          for (final file in detail.files) {
 | 
					          for (final file in detail.files) {
 | 
				
			||||||
            final data = await file.readAsBytes();
 | 
					            _enqueueTask(
 | 
				
			||||||
            _uploadAttachment(data, file.path, null);
 | 
					              AttachmentUploadTask(file: File(file.path), usage: widget.usage),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
					          crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            Row(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: Row(
 | 
				
			||||||
 | 
					                    mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                    children: [
 | 
					                    children: [
 | 
				
			||||||
                      Text(
 | 
					                      Text(
 | 
				
			||||||
                        'attachmentAdd'.tr,
 | 
					                        'attachmentAdd'.tr,
 | 
				
			||||||
                        style: Theme.of(context).textTheme.headlineSmall,
 | 
					                        style: Theme.of(context).textTheme.headlineSmall,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      const SizedBox(width: 10),
 | 
				
			||||||
 | 
					                      Obx(() {
 | 
				
			||||||
 | 
					                        if (_uploadController.isUploading.value) {
 | 
				
			||||||
 | 
					                          return const SizedBox(
 | 
				
			||||||
 | 
					                            width: 18,
 | 
				
			||||||
 | 
					                            height: 18,
 | 
				
			||||||
 | 
					                            child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					                              strokeWidth: 2.5,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        return const SizedBox();
 | 
				
			||||||
 | 
					                      }),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                Text('attachmentAutoUpload'.tr),
 | 
				
			||||||
 | 
					                const SizedBox(width: 8),
 | 
				
			||||||
 | 
					                Switch(
 | 
				
			||||||
 | 
					                  value: _isAutoUpload,
 | 
				
			||||||
 | 
					                  onChanged: (bool? value) {
 | 
				
			||||||
 | 
					                    if (value != null) {
 | 
				
			||||||
 | 
					                      setState(() => _isAutoUpload = value);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
 | 
					            ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
 | 
				
			||||||
            if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
 | 
					            if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
 | 
				
			||||||
            Expanded(
 | 
					            Expanded(
 | 
				
			||||||
              child: Builder(builder: (context) {
 | 
					              child: CustomScrollView(
 | 
				
			||||||
 | 
					                slivers: [
 | 
				
			||||||
 | 
					                  Obx(() {
 | 
				
			||||||
 | 
					                    if (_uploadController.queueOfUpload.isNotEmpty) {
 | 
				
			||||||
 | 
					                      return SliverPadding(
 | 
				
			||||||
 | 
					                        padding: const EdgeInsets.only(bottom: 8),
 | 
				
			||||||
 | 
					                        sliver: SliverToBoxAdapter(
 | 
				
			||||||
 | 
					                          child: Row(
 | 
				
			||||||
 | 
					                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                'attachmentUploadQueue'.tr,
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              Obx(() {
 | 
				
			||||||
 | 
					                                if (_uploadController.isUploading.value) {
 | 
				
			||||||
 | 
					                                  return const SizedBox();
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                return TextButton(
 | 
				
			||||||
 | 
					                                  child: Text('attachmentUploadQueueStart'.tr),
 | 
				
			||||||
 | 
					                                  onPressed: () {
 | 
				
			||||||
 | 
					                                    _startUploading();
 | 
				
			||||||
 | 
					                                  },
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                              }),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ).paddingOnly(left: 24, right: 24),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return const SliverToBoxAdapter(child: SizedBox());
 | 
				
			||||||
 | 
					                  }),
 | 
				
			||||||
 | 
					                  Obx(() {
 | 
				
			||||||
 | 
					                    if (_uploadController.queueOfUpload.isNotEmpty) {
 | 
				
			||||||
 | 
					                      return SliverPadding(
 | 
				
			||||||
 | 
					                        padding: const EdgeInsets.only(bottom: 8),
 | 
				
			||||||
 | 
					                        sliver: SliverList.builder(
 | 
				
			||||||
 | 
					                          itemCount: _uploadController.queueOfUpload.length,
 | 
				
			||||||
 | 
					                          itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					                            final element =
 | 
				
			||||||
 | 
					                                _uploadController.queueOfUpload[index];
 | 
				
			||||||
 | 
					                            return _buildQueueEntry(element, index);
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return const SliverToBoxAdapter(child: SizedBox());
 | 
				
			||||||
 | 
					                  }),
 | 
				
			||||||
 | 
					                  if (_attachments.isNotEmpty)
 | 
				
			||||||
 | 
					                    SliverPadding(
 | 
				
			||||||
 | 
					                      padding: const EdgeInsets.only(bottom: 8),
 | 
				
			||||||
 | 
					                      sliver: SliverToBoxAdapter(
 | 
				
			||||||
 | 
					                        child: Text(
 | 
				
			||||||
 | 
					                          'attachmentAttached'.tr,
 | 
				
			||||||
 | 
					                          style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					                        ).paddingOnly(left: 24, right: 24),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  if (_attachments.isNotEmpty)
 | 
				
			||||||
 | 
					                    Builder(builder: (context) {
 | 
				
			||||||
                      if (_isFirstTimeBusy && _isBusy) {
 | 
					                      if (_isFirstTimeBusy && _isBusy) {
 | 
				
			||||||
                  return const Center(
 | 
					                        return const SliverFillRemaining(
 | 
				
			||||||
 | 
					                          child: Center(
 | 
				
			||||||
                            child: CircularProgressIndicator(),
 | 
					                            child: CircularProgressIndicator(),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                        );
 | 
					                        );
 | 
				
			||||||
                      }
 | 
					                      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return ListView.builder(
 | 
					                      return SliverList.builder(
 | 
				
			||||||
                        itemCount: _attachments.length,
 | 
					                        itemCount: _attachments.length,
 | 
				
			||||||
                        itemBuilder: (context, index) {
 | 
					                        itemBuilder: (context, index) {
 | 
				
			||||||
                          final element = _attachments[index];
 | 
					                          final element = _attachments[index];
 | 
				
			||||||
@@ -384,10 +558,22 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
                        },
 | 
					                        },
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                    }),
 | 
					                    }),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            const Divider(thickness: 0.3, height: 0.3),
 | 
					            ),
 | 
				
			||||||
            SizedBox(
 | 
					            Obx(() {
 | 
				
			||||||
 | 
					              return IgnorePointer(
 | 
				
			||||||
 | 
					                ignoring: _uploadController.isUploading.value,
 | 
				
			||||||
 | 
					                child: Container(
 | 
				
			||||||
                  height: 64,
 | 
					                  height: 64,
 | 
				
			||||||
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                    border: Border(
 | 
				
			||||||
 | 
					                      top: BorderSide(
 | 
				
			||||||
 | 
					                        color: Theme.of(context).dividerColor,
 | 
				
			||||||
 | 
					                        width: 0.3,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
                  child: SingleChildScrollView(
 | 
					                  child: SingleChildScrollView(
 | 
				
			||||||
                    scrollDirection: Axis.horizontal,
 | 
					                    scrollDirection: Axis.horizontal,
 | 
				
			||||||
                    child: Wrap(
 | 
					                    child: Wrap(
 | 
				
			||||||
@@ -439,136 +625,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
 | 
				
			|||||||
                    ).paddingSymmetric(horizontal: 12),
 | 
					                    ).paddingSymmetric(horizontal: 12),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					                    .animate(
 | 
				
			||||||
 | 
					                      target: _uploadController.isUploading.value ? 0 : 1,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .fade(duration: 100.ms),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
class AttachmentEditorDialog extends StatefulWidget {
 | 
					 | 
				
			||||||
  final Attachment item;
 | 
					 | 
				
			||||||
  final Function(Attachment item) onUpdate;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const AttachmentEditorDialog({
 | 
					 | 
				
			||||||
    super.key,
 | 
					 | 
				
			||||||
    required this.item,
 | 
					 | 
				
			||||||
    required this.onUpdate,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  State<AttachmentEditorDialog> createState() => _AttachmentEditorDialogState();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _AttachmentEditorDialogState extends State<AttachmentEditorDialog> {
 | 
					 | 
				
			||||||
  final _altController = TextEditingController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  bool _isBusy = false;
 | 
					 | 
				
			||||||
  bool _isMature = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<Attachment?> _updateAttachment() async {
 | 
					 | 
				
			||||||
    final AttachmentProvider provider = Get.find();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      final resp = await provider.updateAttachment(
 | 
					 | 
				
			||||||
        widget.item.id,
 | 
					 | 
				
			||||||
        _altController.value.text,
 | 
					 | 
				
			||||||
        widget.item.usage,
 | 
					 | 
				
			||||||
        isMature: _isMature,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      Get.find<AttachmentProvider>().clearCache(id: widget.item.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
      return Attachment.fromJson(resp.body);
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      context.showErrorDialog(e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void syncWidget() {
 | 
					 | 
				
			||||||
    _isMature = widget.item.isMature;
 | 
					 | 
				
			||||||
    _altController.text = widget.item.alt;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void initState() {
 | 
					 | 
				
			||||||
    syncWidget();
 | 
					 | 
				
			||||||
    super.initState();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    return AlertDialog(
 | 
					 | 
				
			||||||
      title: Text('attachmentSetting'.tr),
 | 
					 | 
				
			||||||
      content: Container(
 | 
					 | 
				
			||||||
        constraints: const BoxConstraints(minWidth: 400),
 | 
					 | 
				
			||||||
        child: Column(
 | 
					 | 
				
			||||||
          mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
          children: [
 | 
					 | 
				
			||||||
            if (_isBusy)
 | 
					 | 
				
			||||||
              ClipRRect(
 | 
					 | 
				
			||||||
                borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					 | 
				
			||||||
                child: const LinearProgressIndicator().animate().scaleX(),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            const SizedBox(height: 18),
 | 
					 | 
				
			||||||
            TextField(
 | 
					 | 
				
			||||||
              controller: _altController,
 | 
					 | 
				
			||||||
              decoration: InputDecoration(
 | 
					 | 
				
			||||||
                isDense: true,
 | 
					 | 
				
			||||||
                prefixIcon: const Icon(Icons.image_not_supported),
 | 
					 | 
				
			||||||
                border: const OutlineInputBorder(),
 | 
					 | 
				
			||||||
                labelText: 'attachmentAlt'.tr,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              onTapOutside: (_) =>
 | 
					 | 
				
			||||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            const SizedBox(height: 8),
 | 
					 | 
				
			||||||
            CheckboxListTile(
 | 
					 | 
				
			||||||
              contentPadding: const EdgeInsets.only(left: 4, right: 18),
 | 
					 | 
				
			||||||
              shape: const RoundedRectangleBorder(
 | 
					 | 
				
			||||||
                  borderRadius: BorderRadius.all(Radius.circular(10))),
 | 
					 | 
				
			||||||
              title: Text('matureContent'.tr),
 | 
					 | 
				
			||||||
              secondary: const Icon(Icons.visibility_off),
 | 
					 | 
				
			||||||
              value: _isMature,
 | 
					 | 
				
			||||||
              onChanged: (newValue) {
 | 
					 | 
				
			||||||
                setState(() => _isMature = newValue ?? false);
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              controlAffinity: ListTileControlAffinity.leading,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      actionsAlignment: MainAxisAlignment.spaceBetween,
 | 
					 | 
				
			||||||
      actions: <Widget>[
 | 
					 | 
				
			||||||
        Row(
 | 
					 | 
				
			||||||
          mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
          children: [
 | 
					 | 
				
			||||||
            TextButton(
 | 
					 | 
				
			||||||
              style: TextButton.styleFrom(
 | 
					 | 
				
			||||||
                  foregroundColor:
 | 
					 | 
				
			||||||
                      Theme.of(context).colorScheme.onSurfaceVariant),
 | 
					 | 
				
			||||||
              onPressed: () => Navigator.pop(context),
 | 
					 | 
				
			||||||
              child: Text('cancel'.tr),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            TextButton(
 | 
					 | 
				
			||||||
              child: Text('apply'.tr),
 | 
					 | 
				
			||||||
              onPressed: () {
 | 
					 | 
				
			||||||
                _updateAttachment().then((value) {
 | 
					 | 
				
			||||||
                  if (value != null) {
 | 
					 | 
				
			||||||
                    widget.onUpdate(value);
 | 
					 | 
				
			||||||
                    Navigator.pop(context);
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,7 +112,9 @@ class ChatEvent extends StatelessWidget {
 | 
				
			|||||||
      case 'messages.edit':
 | 
					      case 'messages.edit':
 | 
				
			||||||
        return ChatEventMessageActionLog(
 | 
					        return ChatEventMessageActionLog(
 | 
				
			||||||
          icon: const Icon(Icons.edit_note, size: 16),
 | 
					          icon: const Icon(Icons.edit_note, size: 16),
 | 
				
			||||||
          text: 'messageEditDesc'.trParams({'id': '#${item.id}'}),
 | 
					          text: 'messageEditDesc'.trParams({
 | 
				
			||||||
 | 
					            'id': '#${item.body['related_event']}',
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
          isMerged: isMerged,
 | 
					          isMerged: isMerged,
 | 
				
			||||||
          isHasMerged: isHasMerged,
 | 
					          isHasMerged: isHasMerged,
 | 
				
			||||||
          isQuote: isQuote,
 | 
					          isQuote: isQuote,
 | 
				
			||||||
@@ -120,7 +122,9 @@ class ChatEvent extends StatelessWidget {
 | 
				
			|||||||
      case 'messages.delete':
 | 
					      case 'messages.delete':
 | 
				
			||||||
        return ChatEventMessageActionLog(
 | 
					        return ChatEventMessageActionLog(
 | 
				
			||||||
          icon: const Icon(Icons.cancel_schedule_send, size: 16),
 | 
					          icon: const Icon(Icons.cancel_schedule_send, size: 16),
 | 
				
			||||||
          text: 'messageDeleteDesc'.trParams({'id': '#${item.id}'}),
 | 
					          text: 'messageDeleteDesc'.trParams({
 | 
				
			||||||
 | 
					            'id': '#${item.body['related_event']}',
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
          isMerged: isMerged,
 | 
					          isMerged: isMerged,
 | 
				
			||||||
          isHasMerged: isHasMerged,
 | 
					          isHasMerged: isHasMerged,
 | 
				
			||||||
          isQuote: isQuote,
 | 
					          isQuote: isQuote,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,7 +38,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
 | 
				
			|||||||
  final TextEditingController _textController = TextEditingController();
 | 
					  final TextEditingController _textController = TextEditingController();
 | 
				
			||||||
  final FocusNode _focusNode = FocusNode();
 | 
					  final FocusNode _focusNode = FocusNode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<int> _attachments = List.empty(growable: true);
 | 
					  final List<int> _attachments = List.empty(growable: true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Event? _editTo;
 | 
					  Event? _editTo;
 | 
				
			||||||
  Event? _replyTo;
 | 
					  Event? _replyTo;
 | 
				
			||||||
@@ -48,8 +48,17 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
 | 
				
			|||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      builder: (context) => AttachmentEditorPopup(
 | 
					      builder: (context) => AttachmentEditorPopup(
 | 
				
			||||||
        usage: 'm.attachment',
 | 
					        usage: 'm.attachment',
 | 
				
			||||||
        current: _attachments,
 | 
					        initialAttachments: _attachments,
 | 
				
			||||||
        onUpdate: (value) => _attachments = value,
 | 
					        onAdd: (value) {
 | 
				
			||||||
 | 
					          setState(() {
 | 
				
			||||||
 | 
					            _attachments.add(value);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onRemove: (value) {
 | 
				
			||||||
 | 
					          setState(() {
 | 
				
			||||||
 | 
					            _attachments.remove(value);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user