From 2737768a60a5547e3e9b429b7e26269779130afc Mon Sep 17 00:00:00 2001 From: Krille Fear Date: Sun, 28 Nov 2021 11:43:36 +0100 Subject: [PATCH] feat: Drag&Drop to send multiple files on desktop and web --- CHANGELOG.md | 1 + lib/pages/chat/chat.dart | 27 +- lib/pages/chat/chat_view.dart | 392 +++++++++--------- .../send_file_dialog.dart | 0 .../send_location_dialog.dart | 0 lib/pages/chat_list/chat_list_item.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 7 + pubspec.yaml | 2 +- 10 files changed, 247 insertions(+), 189 deletions(-) rename lib/pages/{new_private_chat => chat}/send_file_dialog.dart (100%) rename lib/pages/{new_private_chat => chat}/send_location_dialog.dart (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aeb6236..b7224708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - feat: Send reactions to multiple events - feat: Speed up app start - feat: Use SalomonBottomBar +- feat: Drag&Drop to send multiple files on desktop and web - fix: Adjust color - fix: Automatic key requests - fix: Bootstrap loop diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index e6045a1e..dfb50773 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -6,6 +6,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -29,8 +30,8 @@ import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import '../../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart'; import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart'; -import '../new_private_chat/send_file_dialog.dart'; -import '../new_private_chat/send_location_dialog.dart'; +import 'send_file_dialog.dart'; +import 'send_location_dialog.dart'; import 'sticker_picker_dialog.dart'; class Chat extends StatefulWidget { @@ -60,6 +61,28 @@ class ChatController extends State { Timer typingCoolDown; Timer typingTimeout; bool currentlyTyping = false; + bool dragging = false; + + void onDragEntered(_) => setState(() => dragging = true); + void onDragExited(_) => setState(() => dragging = false); + void onDragDone(DropDoneDetails details) async { + setState(() => dragging = false); + for (final url in details.urls) { + final file = File.fromUri(url); + final bytes = await file.readAsBytes(); + await showDialog( + context: context, + useRootNavigator: false, + builder: (c) => SendFileDialog( + file: MatrixFile( + bytes: bytes, + name: file.path.split('/').last, + ).detectFileType, + room: room, + ), + ); + } + } List selectedEvents = []; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 1dd83e9b..82e49c80 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; @@ -168,202 +169,223 @@ class ChatView extends StatelessWidget { ) : null, backgroundColor: Theme.of(context).colorScheme.surface, - body: Stack( - children: [ - if (Matrix.of(context).wallpaper != null) - Image.file( - Matrix.of(context).wallpaper, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - ), - SafeArea( - child: Column( - children: [ - TombstoneDisplay(controller), - Expanded( - child: FutureBuilder( - future: controller.getTimeline(), - builder: (BuildContext context, snapshot) { - if (snapshot.hasError) { - SentryController.captureException( - snapshot.error, - StackTrace.current, - ); - } - if (controller.timeline == null) { - return const Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 2), - ); - } + body: DropTarget( + onDragDone: controller.onDragDone, + onDragEntered: controller.onDragEntered, + onDragExited: controller.onDragExited, + child: Stack( + children: [ + if (Matrix.of(context).wallpaper != null) + Image.file( + Matrix.of(context).wallpaper, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), + SafeArea( + child: Column( + children: [ + TombstoneDisplay(controller), + Expanded( + child: FutureBuilder( + future: controller.getTimeline(), + builder: (BuildContext context, snapshot) { + if (snapshot.hasError) { + SentryController.captureException( + snapshot.error, + StackTrace.current, + ); + } + if (controller.timeline == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2), + ); + } - // create a map of eventId --> index to greatly improve performance of - // ListView's findChildIndexCallback - final thisEventsKeyMap = {}; - for (var i = 0; - i < controller.filteredEvents.length; - i++) { - thisEventsKeyMap[ - controller.filteredEvents[i].eventId] = i; - } - return ListView.custom( - padding: EdgeInsets.only( - top: 16, - bottom: 4, - left: horizontalPadding, - right: horizontalPadding, - ), - reverse: true, - controller: controller.scrollController, - keyboardDismissBehavior: PlatformInfos.isIOS - ? ScrollViewKeyboardDismissBehavior.onDrag - : ScrollViewKeyboardDismissBehavior.manual, - childrenDelegate: SliverChildBuilderDelegate( - (BuildContext context, int i) { - return i == controller.filteredEvents.length + 1 - ? controller.timeline.isRequestingHistory - ? const Center( - child: CircularProgressIndicator - .adaptive(strokeWidth: 2), - ) - : controller.canLoadMore - ? Center( - child: OutlinedButton( - style: - OutlinedButton.styleFrom( - backgroundColor: Theme.of( - context) - .scaffoldBackgroundColor, + // create a map of eventId --> index to greatly improve performance of + // ListView's findChildIndexCallback + final thisEventsKeyMap = {}; + for (var i = 0; + i < controller.filteredEvents.length; + i++) { + thisEventsKeyMap[ + controller.filteredEvents[i].eventId] = i; + } + return ListView.custom( + padding: EdgeInsets.only( + top: 16, + bottom: 4, + left: horizontalPadding, + right: horizontalPadding, + ), + reverse: true, + controller: controller.scrollController, + keyboardDismissBehavior: PlatformInfos.isIOS + ? ScrollViewKeyboardDismissBehavior.onDrag + : ScrollViewKeyboardDismissBehavior.manual, + childrenDelegate: SliverChildBuilderDelegate( + (BuildContext context, int i) { + return i == + controller.filteredEvents.length + 1 + ? controller.timeline.isRequestingHistory + ? const Center( + child: CircularProgressIndicator + .adaptive(strokeWidth: 2), + ) + : controller.canLoadMore + ? Center( + child: OutlinedButton( + style: OutlinedButton + .styleFrom( + backgroundColor: Theme.of( + context) + .scaffoldBackgroundColor, + ), + onPressed: controller + .requestHistory, + child: Text(L10n.of(context) + .loadMore), ), - onPressed: - controller.requestHistory, - child: Text(L10n.of(context) - .loadMore), - ), - ) - : Container() - : i == 0 - ? Column( - mainAxisSize: MainAxisSize.min, - children: [ - SeenByRow(controller), - TypingIndicators(controller), - ], - ) - : AutoScrollTag( - key: ValueKey(controller - .filteredEvents[i - 1].eventId), - index: i - 1, - controller: - controller.scrollController, - child: Swipeable( + ) + : Container() + : i == 0 + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + SeenByRow(controller), + TypingIndicators(controller), + ], + ) + : AutoScrollTag( key: ValueKey(controller .filteredEvents[i - 1] .eventId), - background: const Padding( - padding: EdgeInsets.symmetric( - horizontal: 12.0), - child: Center( - child: Icon( - Icons.reply_outlined), + index: i - 1, + controller: + controller.scrollController, + child: Swipeable( + key: ValueKey(controller + .filteredEvents[i - 1] + .eventId), + background: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 12.0), + child: Center( + child: Icon( + Icons.reply_outlined), + ), ), - ), - direction: - SwipeDirection.endToStart, - onSwipe: (direction) => - controller.replyAction( - replyTo: controller - .filteredEvents[ - i - 1]), - child: Message( - controller - .filteredEvents[i - 1], - onInfoTab: - controller.showEventInfo, - onAvatarTab: (Event event) => - showModalBottomSheet( - context: context, - builder: (c) => - UserBottomSheet( - user: event.sender, - outerContext: context, - onMention: () => controller - .sendController - .text += - '${event.sender.mention} ', + direction: + SwipeDirection.endToStart, + onSwipe: (direction) => + controller.replyAction( + replyTo: controller + .filteredEvents[ + i - 1]), + child: Message( + controller + .filteredEvents[i - 1], + onInfoTab: controller + .showEventInfo, + onAvatarTab: (Event event) => + showModalBottomSheet( + context: context, + builder: (c) => + UserBottomSheet( + user: event.sender, + outerContext: + context, + onMention: () => controller + .sendController + .text += + '${event.sender.mention} ', + ), ), - ), - unfold: controller.unfold, - onSelect: controller - .onSelectMessage, - scrollToEventId: (String eventId) => controller - .scrollToEventId(eventId), - longPressSelect: controller - .selectedEvents.isEmpty, - selected: controller.selectedEvents.any((e) => - e.eventId == - controller - .filteredEvents[i - 1] - .eventId), - timeline: controller.timeline, - nextEvent: i < - controller - .filteredEvents - .length - ? controller.filteredEvents[i] - : null), - ), - ); - }, - childCount: controller.filteredEvents.length + 2, - findChildIndexCallback: (key) => - controller.findChildIndexCallback( - key, thisEventsKeyMap), + unfold: controller.unfold, + onSelect: controller + .onSelectMessage, + scrollToEventId: + (String eventId) => + controller.scrollToEventId( + eventId), + longPressSelect: controller + .selectedEvents.isEmpty, + selected: controller + .selectedEvents + .any((e) => + e.eventId == + controller + .filteredEvents[i - 1] + .eventId), + timeline: controller.timeline, + nextEvent: i < controller.filteredEvents.length ? controller.filteredEvents[i] : null), + ), + ); + }, + childCount: + controller.filteredEvents.length + 2, + findChildIndexCallback: (key) => + controller.findChildIndexCallback( + key, thisEventsKeyMap), + ), + ); + }, + ), + ), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5), + alignment: Alignment.center, + child: Material( + borderRadius: const BorderRadius.only( + bottomLeft: + Radius.circular(AppConfig.borderRadius), + bottomRight: + Radius.circular(AppConfig.borderRadius), + ), + elevation: 6, + shadowColor: Theme.of(context) + .secondaryHeaderColor + .withAlpha(100), + clipBehavior: Clip.hardEdge, + color: + Theme.of(context).appBarTheme.backgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], ), - ); - }, - ), - ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join) - Container( - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5), - alignment: Alignment.center, - child: Material( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(AppConfig.borderRadius), - bottomRight: - Radius.circular(AppConfig.borderRadius), - ), - elevation: 6, - shadowColor: Theme.of(context) - .secondaryHeaderColor - .withAlpha(100), - clipBehavior: Clip.hardEdge, - color: Theme.of(context).appBarTheme.backgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], ), ), - ), - ], + ], + ), ), - ), - ], + if (controller.dragging) + Container( + color: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.9), + alignment: Alignment.center, + child: const Icon( + Icons.upload_outlined, + size: 100, + ), + ), + ], + ), ), ), ), diff --git a/lib/pages/new_private_chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart similarity index 100% rename from lib/pages/new_private_chat/send_file_dialog.dart rename to lib/pages/chat/send_file_dialog.dart diff --git a/lib/pages/new_private_chat/send_location_dialog.dart b/lib/pages/chat/send_location_dialog.dart similarity index 100% rename from lib/pages/new_private_chat/send_location_dialog.dart rename to lib/pages/chat/send_location_dialog.dart diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 2a02c095..b82210a8 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -13,7 +13,7 @@ import 'package:fluffychat/utils/room_status_extension.dart'; import '../../utils/date_time_extension.dart'; import '../../widgets/avatar.dart'; import '../../widgets/matrix.dart'; -import '../new_private_chat/send_file_dialog.dart'; +import '../chat/send_file_dialog.dart'; enum ArchivedRoomAction { delete, rejoin } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 0a1f5713..c4fdeb3b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index d646db05..a5046f8f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop file_selector_linux flutter_secure_storage url_launcher_linux diff --git a/pubspec.lock b/pubspec.lock index 72944153..b6c3dbd7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -246,6 +246,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.3" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" desktop_notifications: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1fea0874..5a06fadf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: cached_network_image: ^3.1.0 chewie: ^1.2.2 cupertino_icons: any + desktop_drop: ^0.2.0 desktop_notifications: ">=0.4.0 <0.5.0" # Version 0.5.0 breaks web builds: https://github.com/canonical/dbus.dart/issues/250 email_validator: ^2.0.1 emoji_picker_flutter: ^1.0.7 @@ -23,7 +24,6 @@ dependencies: file_picker_cross: ^4.5.0 flutter: sdk: flutter - # From this fix: https://github.com/g123k/flutter_app_badger/pull/47 flutter_app_badger: ^1.3.0 flutter_app_lock: ^2.0.0 flutter_blurhash: ^0.6.0