From ec69b3487708d6f86da8aec9d37258407a215dca Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 3 May 2024 00:09:33 +0800 Subject: [PATCH] :sparkles: Better attachments upload, and supports files --- ios/Podfile.lock | 50 +++++++ lib/i18n/app_en.arb | 6 +- lib/i18n/app_zh.arb | 4 +- lib/providers/chat.dart | 6 + lib/screens/chat/channel/editor.dart | 4 +- lib/screens/chat/chat.dart | 3 +- lib/screens/posts/comment_editor.dart | 3 +- lib/screens/posts/moment_editor.dart | 13 +- lib/widgets/chat/message_editor.dart | 4 +- lib/widgets/posts/attachment_editor.dart | 175 ++++++++++++----------- pubspec.lock | 8 ++ pubspec.yaml | 1 + 12 files changed, 176 insertions(+), 101 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6989be7..cebb63b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,6 +4,40 @@ PODS: - FlutterMacOS - device_info_plus (0.0.1): - Flutter + - DKImagePickerController/Core (4.3.8): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.8) + - DKImagePickerController/PhotoGallery (4.3.8): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.8) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter - Flutter (1.0.0) - flutter_local_notifications (0.0.1): - Flutter @@ -32,6 +66,10 @@ PODS: - Flutter - screen_brightness_ios (0.1.0): - Flutter + - SDWebImage (5.19.1): + - SDWebImage/Core (= 5.19.1) + - SDWebImage/Core (5.19.1) + - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter - volume_controller (0.0.1): @@ -45,6 +83,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) @@ -65,6 +104,10 @@ DEPENDENCIES: SPEC REPOS: trunk: + - DKImagePickerController + - DKPhotoGallery + - SDWebImage + - SwiftyGif - WebRTC-SDK EXTERNAL SOURCES: @@ -72,6 +115,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/connectivity_plus/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter flutter_local_notifications: @@ -110,6 +155,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + DKImagePickerController: a7836546cfdfe014171694f643a7d575bc8ace7f + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be @@ -123,6 +171,8 @@ SPEC CHECKSUMS: path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 + SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 diff --git a/lib/i18n/app_en.arb b/lib/i18n/app_en.arb index d795295..6fd321a 100644 --- a/lib/i18n/app_en.arb +++ b/lib/i18n/app_en.arb @@ -59,10 +59,10 @@ "comment": "Comment", "attachment": "Attachment", "attachmentAdd": "Add new attachment", - "pickPhoto": "Gallery photo", + "pickMedia": "Gallery media", + "pickFile": "Device file", "takePhoto": "Capture photo", - "pickVideo": "Gallery video", - "takeVideo": "Record video", + "takeVideo": "Capture video", "newMoment": "Record a moment", "newComment": "Leave a comment", "connectingServer": "Connecting to server...", diff --git a/lib/i18n/app_zh.arb b/lib/i18n/app_zh.arb index 0ed30cc..c50360a 100644 --- a/lib/i18n/app_zh.arb +++ b/lib/i18n/app_zh.arb @@ -59,9 +59,9 @@ "comment": "评论", "attachment": "附件", "attachmentAdd": "附加新附件", - "pickPhoto": "相册照片", + "pickMedia": "相册媒体", + "pickFile": "设备文件", "takePhoto": "拍摄照片", - "pickVideo": "相册视频", "takeVideo": "拍摄视频", "newMoment": "记录时刻", "newComment": "留下评论", diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart index f9ce2d4..091cd74 100644 --- a/lib/providers/chat.dart +++ b/lib/providers/chat.dart @@ -107,6 +107,12 @@ class ChatProvider extends ChangeNotifier { isCallShown = state; notifyListeners(); } + + void unFocus() { + currentCall = null; + focusChannel = null; + notifyListeners(); + } } class ChatCallInstance { diff --git a/lib/screens/chat/channel/editor.dart b/lib/screens/chat/channel/editor.dart index e256a94..34c1fc4 100644 --- a/lib/screens/chat/channel/editor.dart +++ b/lib/screens/chat/channel/editor.dart @@ -86,7 +86,7 @@ class _ChannelEditorScreenState extends State { @override Widget build(BuildContext context) { final editingBanner = MaterialBanner( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), leading: const Icon(Icons.edit_note), backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), dividerColor: const Color.fromARGB(1, 0, 0, 0), @@ -111,6 +111,7 @@ class _ChannelEditorScreenState extends State { child: Column( children: [ _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), + widget.editing != null ? editingBanner : Container(), ListTile( title: Text(AppLocalizations.of(context)!.chatChannelUsage), subtitle: Text(AppLocalizations.of(context)!.chatChannelUsageCaption), @@ -174,7 +175,6 @@ class _ChannelEditorScreenState extends State { ), ), ), - widget.editing != null ? editingBanner : Container(), ], ), ); diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 921fe9d..4fba98a 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -249,8 +249,7 @@ class _ChatScreenWidgetState extends State { @override void deactivate() { - _chat.focusChannel = null; - _chat.ongoingCall = null; + _chat.unFocus(); super.deactivate(); } } diff --git a/lib/screens/posts/comment_editor.dart b/lib/screens/posts/comment_editor.dart index ca74d96..8ae661a 100644 --- a/lib/screens/posts/comment_editor.dart +++ b/lib/screens/posts/comment_editor.dart @@ -107,7 +107,7 @@ class _CommentEditorScreenState extends State { final auth = context.read(); final editingBanner = MaterialBanner( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), leading: const Icon(Icons.edit_note), backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), dividerColor: const Color.fromARGB(1, 0, 0, 0), @@ -175,6 +175,7 @@ class _CommentEditorScreenState extends State { ), widget.editing != null ? editingBanner : Container(), Container( + constraints: const BoxConstraints(minHeight: 56), decoration: BoxDecoration( border: Border( top: BorderSide(width: 0.3, color: Theme.of(context).dividerColor), diff --git a/lib/screens/posts/moment_editor.dart b/lib/screens/posts/moment_editor.dart index 8549d22..6884d14 100644 --- a/lib/screens/posts/moment_editor.dart +++ b/lib/screens/posts/moment_editor.dart @@ -97,7 +97,7 @@ class _MomentEditorScreenState extends State { final auth = context.read(); final editingBanner = MaterialBanner( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), leading: const Icon(Icons.edit_note), backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), dividerColor: const Color.fromARGB(1, 0, 0, 0), @@ -121,9 +121,7 @@ class _MomentEditorScreenState extends State { ], child: Column( children: [ - _isSubmitting - ? const LinearProgressIndicator().animate().scaleX() - : Container(), + _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), FutureBuilder( future: auth.getProfiles(), builder: (context, snapshot) { @@ -155,16 +153,15 @@ class _MomentEditorScreenState extends State { keyboardType: TextInputType.multiline, controller: _textController, decoration: InputDecoration.collapsed( - hintText: - AppLocalizations.of(context)!.postContentPlaceholder, + hintText: AppLocalizations.of(context)!.postContentPlaceholder, ), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), ), ), widget.editing != null ? editingBanner : Container(), Container( + constraints: const BoxConstraints(minHeight: 56), decoration: BoxDecoration( border: Border( top: BorderSide(width: 0.3, color: Theme.of(context).dividerColor), diff --git a/lib/widgets/chat/message_editor.dart b/lib/widgets/chat/message_editor.dart index c91fb46..c72e63c 100644 --- a/lib/widgets/chat/message_editor.dart +++ b/lib/widgets/chat/message_editor.dart @@ -106,7 +106,7 @@ class _ChatMessageEditorState extends State { @override Widget build(BuildContext context) { final editingBanner = MaterialBanner( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 20), + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), leading: const Icon(Icons.edit_note), backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), dividerColor: const Color.fromARGB(1, 0, 0, 0), @@ -120,7 +120,7 @@ class _ChatMessageEditorState extends State { ); final replyingBanner = MaterialBanner( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 20), + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), leading: const Icon(Icons.reply), backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), dividerColor: const Color.fromARGB(1, 0, 0, 0), diff --git a/lib/widgets/posts/attachment_editor.dart b/lib/widgets/posts/attachment_editor.dart index 9929892..b9c02ea 100755 --- a/lib/widgets/posts/attachment_editor.dart +++ b/lib/widgets/posts/attachment_editor.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math' as math; import 'package:crypto/crypto.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:http/http.dart'; @@ -41,51 +42,84 @@ class _AttachmentEditorState extends State { showModalBottomSheet( context: context, builder: (context) => AttachmentEditorMethodPopup( - pickImage: () => pickImageToUpload(context, ImageSource.gallery), - takeImage: () => pickImageToUpload(context, ImageSource.camera), - pickVideo: () => pickVideoToUpload(context, ImageSource.gallery), - takeVideo: () => pickVideoToUpload(context, ImageSource.camera), + pickMedia: () => pickMediaToUpload(context), + pickFile: () => pickFileToUpload(context), + takeImage: () => takeMediaToUpload(context, false), + takeVideo: () => takeMediaToUpload(context, true), ), ); } - Future pickImageToUpload( - BuildContext context, ImageSource source) async { + Future pickMediaToUpload(BuildContext context) async { final auth = context.read(); if (!await auth.isAuthorized()) return; - final image = await _imagePicker.pickImage(source: source); - if (image == null) return; + final medias = await _imagePicker.pickMultipleMedia(); + if (medias.isEmpty) return; setState(() => _isSubmitting = true); - final file = File(image.path); - final hashcode = await calculateSha256(file); - - if (Navigator.canPop(context)) { - Navigator.pop(context); + bool isPopped = false; + for (final media in medias) { + final file = File(media.path); + final hashcode = await calculateSha256(file); + try { + await uploadAttachment(file, hashcode); + } catch (err) { + context.showErrorDialog(err); + } + if (!isPopped && Navigator.canPop(context)) { + Navigator.pop(context); + isPopped = true; + } } - try { - await uploadAttachment(file, hashcode); - } catch (err) { - context.showErrorDialog(err); - } finally { - setState(() => _isSubmitting = false); - } + setState(() => _isSubmitting = false); } - Future pickVideoToUpload( - BuildContext context, ImageSource source) async { + Future pickFileToUpload(BuildContext context) async { final auth = context.read(); if (!await auth.isAuthorized()) return; - final image = await _imagePicker.pickVideo(source: source); - if (image == null) return; + FilePickerResult? result = await FilePicker.platform.pickFiles(allowMultiple: true); + if (result == null) return; + + List files = result.paths.map((path) => File(path!)).toList(); setState(() => _isSubmitting = true); - final file = File(image.path); + bool isPopped = false; + for (final file in files) { + final hashcode = await calculateSha256(file); + try { + await uploadAttachment(file, hashcode); + } catch (err) { + context.showErrorDialog(err); + } + if (!isPopped && Navigator.canPop(context)) { + Navigator.pop(context); + isPopped = true; + } + } + + setState(() => _isSubmitting = false); + } + + Future takeMediaToUpload(BuildContext context, bool isVideo) async { + final auth = context.read(); + if (!await auth.isAuthorized()) return; + + XFile? media; + if (isVideo) { + media = await _imagePicker.pickVideo(source: ImageSource.camera); + } else { + media = await _imagePicker.pickImage(source: ImageSource.camera); + } + if (media == null) return; + + setState(() => _isSubmitting = true); + + final file = File(media.path); final hashcode = await calculateSha256(file); if (Navigator.canPop(context)) { @@ -96,16 +130,15 @@ class _AttachmentEditorState extends State { await uploadAttachment(file, hashcode); } catch (err) { context.showErrorDialog(err); - } finally { - setState(() => _isSubmitting = false); } + + setState(() => _isSubmitting = false); } Future uploadAttachment(File file, String hashcode) async { final auth = context.read(); - final req = MultipartRequest( - 'POST', getRequestUri(widget.provider, '/api/attachments')); + final req = MultipartRequest('POST', getRequestUri(widget.provider, '/api/attachments')); req.files.add(await MultipartFile.fromPath('attachment', file.path)); req.fields['hashcode'] = hashcode; @@ -121,12 +154,10 @@ class _AttachmentEditorState extends State { } } - Future disposeAttachment( - BuildContext context, Attachment item, int index) async { + Future disposeAttachment(BuildContext context, Attachment item, int index) async { final auth = context.read(); - final req = MultipartRequest('DELETE', - getRequestUri(widget.provider, '/api/attachments/${item.id}')); + final req = MultipartRequest('DELETE', getRequestUri(widget.provider, '/api/attachments/${item.id}')); setState(() => _isSubmitting = true); var res = await auth.client!.send(req); @@ -167,17 +198,7 @@ class _AttachmentEditorState extends State { if (bytes == 0) return '0 Bytes'; const k = 1024; final dm = decimals < 0 ? 0 : decimals; - final sizes = [ - 'Bytes', - 'KiB', - 'MiB', - 'GiB', - 'TiB', - 'PiB', - 'EiB', - 'ZiB', - 'YiB' - ]; + final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; final i = (math.log(bytes) / math.log(k)).floor().toInt(); return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; } @@ -195,8 +216,7 @@ class _AttachmentEditorState extends State { return Column( children: [ Container( - padding: - const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), + padding: const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -215,9 +235,7 @@ class _AttachmentEditorState extends State { builder: (context, snapshot) { if (snapshot.hasData && snapshot.data == true) { return TextButton( - onPressed: _isSubmitting - ? null - : () => viewAttachMethods(context), + onPressed: _isSubmitting ? null : () => viewAttachMethods(context), style: TextButton.styleFrom(shape: const CircleBorder()), child: const Icon(Icons.add_circle), ); @@ -229,16 +247,14 @@ class _AttachmentEditorState extends State { ], ), ), - _isSubmitting - ? const LinearProgressIndicator().animate().scaleX() - : Container(), + _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), Expanded( - child: ListView.separated( + child: ListView.builder( itemCount: _attachments.length, itemBuilder: (context, index) { var element = _attachments[index]; return Container( - padding: const EdgeInsets.only(left: 16, right: 8), + padding: const EdgeInsets.only(left: 16, right: 8, bottom: 16), child: Row( children: [ Expanded( @@ -263,14 +279,12 @@ class _AttachmentEditorState extends State { foregroundColor: Colors.red, ), child: const Icon(Icons.delete), - onPressed: () => - disposeAttachment(context, element, index), + onPressed: () => disposeAttachment(context, element, index), ), ], ), ); }, - separatorBuilder: (context, index) => const Divider(thickness: 0.3), ), ), ], @@ -279,16 +293,16 @@ class _AttachmentEditorState extends State { } class AttachmentEditorMethodPopup extends StatelessWidget { - final Function pickImage; + final Function pickMedia; + final Function pickFile; final Function takeImage; - final Function pickVideo; final Function takeVideo; const AttachmentEditorMethodPopup({ super.key, - required this.pickImage, + required this.pickMedia, + required this.pickFile, required this.takeImage, - required this.pickVideo, required this.takeVideo, }); @@ -319,15 +333,28 @@ class AttachmentEditorMethodPopup extends StatelessWidget { children: [ InkWell( borderRadius: BorderRadius.circular(8), - onTap: () => pickImage(), + onTap: () => pickMedia(), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.add_photo_alternate, - color: Colors.indigo), + const Icon(Icons.photo_library, color: Colors.indigo), const SizedBox(height: 8), - Text(AppLocalizations.of(context)!.pickPhoto), + Text(AppLocalizations.of(context)!.pickMedia), + ], + ), + ), + ), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => pickFile(), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.file_present, color: Colors.amber), + const SizedBox(height: 8), + Text(AppLocalizations.of(context)!.pickFile), ], ), ), @@ -339,27 +366,13 @@ class AttachmentEditorMethodPopup extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.camera_alt, color: Colors.indigo), + const Icon(Icons.camera_alt, color: Colors.teal), const SizedBox(height: 8), Text(AppLocalizations.of(context)!.takePhoto), ], ), ), ), - InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => pickVideo(), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.camera, color: Colors.teal), - const SizedBox(height: 8), - Text(AppLocalizations.of(context)!.pickVideo), - ], - ), - ), - ), InkWell( borderRadius: BorderRadius.circular(8), onTap: () => takeVideo(), @@ -367,7 +380,7 @@ class AttachmentEditorMethodPopup extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.video_call, color: Colors.teal), + const Icon(Icons.movie, color: Colors.blue), const SizedBox(height: 8), Text(AppLocalizations.of(context)!.takeVideo), ], diff --git a/pubspec.lock b/pubspec.lock index 21490df..e6b85e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + url: "https://pub.dev" + source: hosted + version: "8.0.3" file_selector_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ff44fea..728a7f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: wakelock_plus: ^1.2.4 flutter_local_notifications: ^17.1.0 draggable_float_widget: ^0.1.0 + file_picker: ^8.0.3 dev_dependencies: flutter_test: