diff --git a/lib/exts.dart b/lib/exts.dart index 5f15dde..8bf713c 100644 --- a/lib/exts.dart +++ b/lib/exts.dart @@ -8,6 +8,10 @@ extension SolianExtenions on BuildContext { )); } + void clearSnackbar() { + ScaffoldMessenger.of(this).clearSnackBars(); + } + Future showErrorDialog(dynamic exception) { return showDialog( useRootNavigator: true, diff --git a/lib/translations.dart b/lib/translations.dart index 2222883..d5c9bcc 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -99,12 +99,16 @@ class SolianMessages extends Translations { 'attachmentAddGalleryVideo': 'Gallery video', 'attachmentAddCameraPhoto': 'Capture photo', 'attachmentAddCameraVideo': 'Capture video', + 'attachmentAddClipboard': 'Paste file', 'attachmentAddFile': 'Attach file', 'attachmentSetting': 'Adjust attachment', 'attachmentAlt': 'Alternative text', 'attachmentLoadFailed': 'Load Attachment Failed', 'attachmentLoadFailedCaption': 'Something went wrong during loading the attachment metadata...', + 'attachmentUploading': 'Uploading @name...', + 'attachmentUploadingWebMode': + 'Uploading @name... Due to browser\'s limitation, calculate attachment information may cause some lag...', 'realm': 'Realm', 'realms': 'Realms', 'realmOrganizing': 'Organize a realm', @@ -325,11 +329,15 @@ class SolianMessages extends Translations { 'attachmentAddGalleryVideo': '相册视频', 'attachmentAddCameraPhoto': '拍摄图片', 'attachmentAddCameraVideo': '拍摄视频', + 'attachmentAddClipboard': '粘贴文件', 'attachmentAddFile': '附加文件', 'attachmentSetting': '调整附件', 'attachmentAlt': '替代文字', 'attachmentLoadFailed': '加载失败', 'attachmentLoadFailedCaption': '有错误发生于加载附件元数据的过程中了…', + 'attachmentUploading': '上传附件 @name 中…', + 'attachmentUploadingWebMode': + '上传附件 @name 中… 由于浏览器单线程限制,计算所需资源可能会导致界面卡顿…', 'realm': '领域', 'realms': '领域', 'realmOrganizing': '组织领域', diff --git a/lib/widgets/attachments/attachment_publish.dart b/lib/widgets/attachments/attachment_publish.dart index 0a3b119..14703c9 100644 --- a/lib/widgets/attachments/attachment_publish.dart +++ b/lib/widgets/attachments/attachment_publish.dart @@ -1,19 +1,20 @@ import 'dart:async'; import 'dart:io'; -import 'dart:isolate'; import 'dart:math' as math; import 'dart:typed_data'; -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:get/get.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:path/path.dart' show basename; import 'package:solian/exts.dart'; import 'package:solian/models/attachment.dart'; +import 'package:solian/platform.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/attachment.dart'; +import 'package:super_clipboard/super_clipboard.dart'; import 'package:super_drag_and_drop/super_drag_and_drop.dart'; class AttachmentPublishPopup extends StatefulWidget { @@ -153,10 +154,48 @@ class _AttachmentPublishPopupState extends State { setState(() => _isBusy = false); } + void pasteFileToUpload() async { + final clipboard = SystemClipboard.instance; + if (clipboard == null) return; + final reader = await clipboard.read(); + for (final format in Formats.standardFormats.whereType()) { + if (reader.canProvide(format)) { + reader.getFile(format, (file) async { + final data = await file.readAll(); + await uploadAttachment( + data, + file.fileName ?? 'unknown', + await calculateBytesSha256(data), + ); + }); + } + } + } + + void handlePasteFile(ClipboardReadEvent event) async { + final reader = await event.getClipboardReader(); + for (final format in Formats.standardFormats.whereType()) { + if (reader.canProvide(format)) { + reader.getFile(format, (file) async { + final data = await file.readAll(); + await uploadAttachment( + data, + file.fileName ?? 'unknown', + await calculateBytesSha256(data), + ); + }); + } + } + } + Future uploadAttachment(Uint8List data, String path, String hash, {double? ratio}) async { final AttachmentProvider provider = Get.find(); try { + context.showSnackbar((PlatformInfo.isWeb + ? 'attachmentUploadingWebMode' + : 'attachmentUploading') + .trParams({'name': basename(path)})); final resp = await provider.createAttachment( data, path, @@ -167,6 +206,7 @@ class _AttachmentPublishPopupState extends State { var result = Attachment.fromJson(resp.body); setState(() => _attachments.add(result)); widget.onUpdate(_attachments.map((e) => e!.id).toList()); + context.clearSnackbar(); } catch (err) { rethrow; } @@ -222,11 +262,13 @@ class _AttachmentPublishPopupState extends State { void initState() { super.initState(); revertMetadataList(); + ClipboardEvents.instance?.registerPasteEventListener(handlePasteFile); } @override void dispose() { super.dispose(); + ClipboardEvents.instance?.unregisterPasteEventListener(handlePasteFile); } @override @@ -256,11 +298,9 @@ class _AttachmentPublishPopupState extends State { final data = await file.readAll(); await uploadAttachment( data, - file.fileName ?? 'attachment', + file.fileName ?? 'unknown', await calculateBytesSha256(data), ); - }, onError: (error) { - print('Error reading value $error'); }); } } @@ -359,6 +399,13 @@ class _AttachmentPublishPopupState extends State { alignment: WrapAlignment.center, runAlignment: WrapAlignment.center, children: [ + if (PlatformInfo.isDesktop) + ElevatedButton.icon( + icon: const Icon(Icons.paste), + label: Text('attachmentAddClipboard'.tr), + style: const ButtonStyle(visualDensity: density), + onPressed: () => pasteFileToUpload(), + ), ElevatedButton.icon( icon: const Icon(Icons.add_photo_alternate), label: Text('attachmentAddGalleryPhoto'.tr), diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index c8f8f99..41605a8 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -134,15 +134,17 @@ class _PostItemState extends State { size: 16, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), ), - Text( - 'postRepliedNotify'.trParams( - {'username': '@${widget.item.replyTo!.author.name}'}, - ), - style: TextStyle( - color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.75), - ), - ).paddingOnly(left: 6), + Expanded( + child: Text( + 'postRepliedNotify'.trParams( + {'username': '@${widget.item.replyTo!.author.name}'}, + ), + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.75), + ), + ).paddingOnly(left: 6), + ), ], ).paddingOnly(left: 12), Card( @@ -167,15 +169,17 @@ class _PostItemState extends State { size: 16, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), ), - Text( - 'postRepostedNotify'.trParams( - {'username': '@${widget.item.repostTo!.author.name}'}, - ), - style: TextStyle( - color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.75), - ), - ).paddingOnly(left: 6), + Expanded( + child: Text( + 'postRepostedNotify'.trParams( + {'username': '@${widget.item.repostTo!.author.name}'}, + ), + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.75), + ), + ).paddingOnly(left: 6), + ), ], ).paddingOnly(left: 12), Card( diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 4920538..33debae 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -11,10 +11,10 @@ PODS: - Firebase/Messaging (10.27.0): - Firebase/CoreOnly - FirebaseMessaging (~> 10.27.0) - - firebase_core (3.1.0): + - firebase_core (3.1.1): - Firebase/CoreOnly (~> 10.27.0) - FlutterMacOS - - firebase_messaging (15.0.1): + - firebase_messaging (15.0.2): - Firebase/CoreOnly (~> 10.27.0) - Firebase/Messaging (~> 10.27.0) - firebase_core @@ -76,6 +76,8 @@ PODS: - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - irondash_engine_context (0.0.1): + - FlutterMacOS - livekit_client (2.2.0): - FlutterMacOS - WebRTC-SDK (= 114.5735.10) @@ -92,6 +94,8 @@ PODS: - Flutter - FlutterMacOS - PromisesObjC (2.4.0) + - protocol_handler_macos (0.0.1): + - FlutterMacOS - Sentry/HybridSDK (8.29.0) - sentry_flutter (8.3.0): - Flutter @@ -103,6 +107,8 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS + - super_native_extensions (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - video_player_avfoundation (0.0.1): @@ -122,13 +128,16 @@ DEPENDENCIES: - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) @@ -166,6 +175,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos FlutterMacOS: :path: Flutter/ephemeral + irondash_engine_context: + :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos livekit_client: :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos macos_window_utils: @@ -174,12 +185,16 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + protocol_handler_macos: + :path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos sentry_flutter: :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + super_native_extensions: + :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos video_player_avfoundation: @@ -192,8 +207,8 @@ SPEC CHECKSUMS: device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 Firebase: 26b040b20866a55f55eb3611b9fcf3ae64816b86 - firebase_core: 3cf30a8be4c850f7192bf9597f5939d0c3af706b - firebase_messaging: 122ef6eefe1c2bb8abe454da56377d19c916c357 + firebase_core: df33f64432203e675060e475de6ea8127eb8398a + firebase_messaging: b8921fa4cbbbebab66e98b181a69f2af0692a820 FirebaseCore: a2b95ae4ce7c83ceecfbbbe3b6f1cddc7415a808 FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698 FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e @@ -204,16 +219,19 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 livekit_client: 9b39e0f1b8e1a8ec794bb72a4f9bbfc28c959ece macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 nanopb: 438bc412db1928dac798aa6fd75726007be04262 package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa Sentry: 016de45ee5ce5fca2a829996f1bfafeb5e62e8b4 sentry_flutter: 5fb57c5b7e6427a9dc1fedde4269eb65823982d4 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib index b2b309a..70bc316 100644 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -332,14 +332,14 @@ - + - + - + - + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 9215a01..31750fb 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -7,7 +7,7 @@ class MainFlutterWindow: NSWindow { let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) - self.minSize = NSSize(width: 380, height: 600) + self.minSize = NSSize(width: 380, height: 540) RegisterGeneratedPlugins(registry: flutterViewController) diff --git a/pubspec.lock b/pubspec.lock index ccec9b5..2167303 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1478,7 +1478,7 @@ packages: source: hosted version: "3.1.2" super_clipboard: - dependency: transitive + dependency: "direct main" description: name: super_clipboard sha256: cdab725bac26655ebd189f4d202d694a8cbc1c21e0f0478ccd7829c71716f09b diff --git a/pubspec.yaml b/pubspec.yaml index d8601b6..726db86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: protocol_handler: ^0.2.0 markdown: ^7.2.2 super_drag_and_drop: ^0.8.17 + super_clipboard: ^0.8.17 dev_dependencies: flutter_test: