From 0f82dc988e68445264f2d78c2773e3eb1cf706b9 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 13 Aug 2024 16:32:54 +0100 Subject: [PATCH 1/3] add open_filex --- app/pubspec.lock | 20 ++++++++++++++------ app/pubspec.yaml | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index 7344a3181374..9296e26994ff 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -797,26 +797,26 @@ packages: dependency: transitive description: name: flutter_local_notifications - sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f url: "https://pub.dev" source: hosted - version: "17.1.2" + version: "17.2.2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" flutter_localizations: dependency: "direct main" description: flutter @@ -1509,6 +1509,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: ba425ea49affd0a98a234aa9344b9ea5d4c4f7625a1377961eae9fe194c3d523 + url: "https://pub.dev" + source: hosted + version: "4.5.0" package_config: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f37edac3c80a..b26374b12b40 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -132,6 +132,7 @@ dependencies: shake_detector: path: ../packages/shake_detector + open_filex: ^4.5.0 dev_dependencies: flutter_test: From 5350f95d52d7da1b7f707988246eaebeeac4d0f0 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 13 Aug 2024 17:55:11 +0100 Subject: [PATCH 2/3] Add Phosphor Icons --- app/pubspec.lock | 8 ++++++++ app/pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/app/pubspec.lock b/app/pubspec.lock index 9296e26994ff..b1635ba2287c 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1629,6 +1629,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + phosphor_flutter: + dependency: "direct main" + description: + name: phosphor_flutter + sha256: "8a14f238f28a0b54842c5a4dc20676598dd4811fcba284ed828bd5a262c11fde" + url: "https://pub.dev" + source: hosted + version: "2.1.0" photo_view: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index b26374b12b40..8a8bc3ab1dd2 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -133,6 +133,7 @@ dependencies: shake_detector: path: ../packages/shake_detector open_filex: ^4.5.0 + phosphor_flutter: ^2.1.0 dev_dependencies: flutter_test: From 42f9870aedbda93521a81c6cf0a23490f27e5ab8 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 13 Aug 2024 17:55:34 +0100 Subject: [PATCH 3/3] Implement new open-or-share bottom drawer --- app/lib/common/widgets/download_button.dart | 40 ------- app/lib/common/widgets/image_dialog.dart | 14 +-- app/lib/common/widgets/video_dialog.dart | 14 +-- .../attachments/widgets/attachment_item.dart | 10 +- .../attachments/widgets/views/file_view.dart | 19 +-- .../chat/widgets/file_message_builder.dart | 10 +- .../events/pages/event_details_page.dart | 53 +++------ .../features/files/actions/download_file.dart | 24 ++++ .../features/files/actions/file_share.dart | 109 ++++++++++++++++++ .../files/widgets/share_file_button.dart | 18 +++ app/lib/l10n/app_en.arb | 6 + 11 files changed, 187 insertions(+), 130 deletions(-) delete mode 100644 app/lib/common/widgets/download_button.dart create mode 100644 app/lib/features/files/actions/download_file.dart create mode 100644 app/lib/features/files/actions/file_share.dart create mode 100644 app/lib/features/files/widgets/share_file_button.dart diff --git a/app/lib/common/widgets/download_button.dart b/app/lib/common/widgets/download_button.dart deleted file mode 100644 index 6ef4c7a839ed..000000000000 --- a/app/lib/common/widgets/download_button.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:io'; -import 'package:path/path.dart'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; - -Future downloadFile(BuildContext context, File file) async { - final lang = L10n.of(context); - final filename = basename(file.path); - String? outputFile = await FilePicker.platform.saveFile( - dialogTitle: lang.downloadFileDialogTitle, - fileName: filename, - ); - - if (outputFile != null) { - await file.copy(outputFile); - EasyLoading.showToast(lang.downloadFileSuccess(outputFile)); - } -} - -class DownloadButton extends StatelessWidget { - final File file; - - const DownloadButton({super.key, required this.file}); - - @override - Widget build(BuildContext context) { - if (Platform.isAndroid) { - // Saving unfortunately crashes on Android at the moment - // FIXME: https://github.com/acterglobal/a3/issues/1803 - return const SizedBox.shrink(); - } - return IconButton( - onPressed: () => downloadFile(context, file), - icon: const Icon(Icons.download_rounded), - ); - } -} diff --git a/app/lib/common/widgets/image_dialog.dart b/app/lib/common/widgets/image_dialog.dart index be4985c12a18..9dd4ddd99963 100644 --- a/app/lib/common/widgets/image_dialog.dart +++ b/app/lib/common/widgets/image_dialog.dart @@ -1,10 +1,8 @@ import 'dart:io'; -import 'package:acter/common/themes/app_theme.dart'; -import 'package:acter/common/widgets/download_button.dart'; +import 'package:acter/features/files/widgets/share_file_button.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:share_plus/share_plus.dart'; import 'package:zoom_hover_pinch_image/zoom_hover_pinch_image.dart'; class ImageDialog extends ConsumerWidget { @@ -19,7 +17,6 @@ class ImageDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final canShare = !isDesktop; return Dialog( insetPadding: EdgeInsets.zero, child: Scaffold( @@ -32,14 +29,7 @@ class ImageDialog extends ConsumerWidget { overflow: TextOverflow.ellipsis, ), actions: [ - if (canShare) - IconButton( - onPressed: () { - Share.shareXFiles([XFile(imageFile.path)]); - }, - icon: const Icon(Icons.share), - ), - DownloadButton(file: imageFile), + ShareFileButton(file: imageFile), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close), diff --git a/app/lib/common/widgets/video_dialog.dart b/app/lib/common/widgets/video_dialog.dart index 4a45b5f97bf2..ef59a5e6ee1d 100644 --- a/app/lib/common/widgets/video_dialog.dart +++ b/app/lib/common/widgets/video_dialog.dart @@ -1,9 +1,7 @@ import 'dart:io'; -import 'package:acter/common/themes/app_theme.dart'; import 'package:acter/common/widgets/acter_video_player.dart'; -import 'package:acter/common/widgets/download_button.dart'; +import 'package:acter/features/files/widgets/share_file_button.dart'; import 'package:flutter/material.dart'; -import 'package:share_plus/share_plus.dart'; class VideoDialog extends StatelessWidget { final String title; @@ -17,7 +15,6 @@ class VideoDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final canShare = !isDesktop; return Dialog( insetPadding: EdgeInsets.zero, child: Container( @@ -37,14 +34,7 @@ class VideoDialog extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - if (canShare) - IconButton( - onPressed: () { - Share.shareXFiles([XFile(videoFile.path)]); - }, - icon: const Icon(Icons.share), - ), - DownloadButton(file: videoFile), + ShareFileButton(file: videoFile), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close), diff --git a/app/lib/features/attachments/widgets/attachment_item.dart b/app/lib/features/attachments/widgets/attachment_item.dart index 951b0044672c..b240379d067a 100644 --- a/app/lib/features/attachments/widgets/attachment_item.dart +++ b/app/lib/features/attachments/widgets/attachment_item.dart @@ -1,19 +1,17 @@ import 'package:acter/common/actions/redact_content.dart'; import 'package:acter/common/models/attachment_media_state/attachment_media_state.dart'; import 'package:acter/common/models/types.dart'; -import 'package:acter/common/themes/app_theme.dart'; import 'package:acter/common/utils/utils.dart'; -import 'package:acter/common/widgets/download_button.dart'; import 'package:acter/common/widgets/image_dialog.dart'; import 'package:acter/common/widgets/video_dialog.dart'; import 'package:acter/features/attachments/providers/attachment_providers.dart'; +import 'package:acter/features/files/actions/file_share.dart'; import 'package:acter/features/pins/actions/attachment_leading_icon.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' show Attachment; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:share_plus/share_plus.dart'; // Attachment item UI class AttachmentItem extends ConsumerWidget { @@ -169,11 +167,7 @@ class AttachmentItem extends ConsumerWidget { } // If attachment is downloaded and file or others else { - if (isDesktop) { - downloadFile(context, mediaState.mediaFile!); - } else { - Share.shareXFiles([XFile(mediaState.mediaFile!.path)]); - } + openFileShareDialog(context: context, file: mediaState.mediaFile!); } } } diff --git a/app/lib/features/attachments/widgets/views/file_view.dart b/app/lib/features/attachments/widgets/views/file_view.dart index 9e055dc5fee6..0f8157b701be 100644 --- a/app/lib/features/attachments/widgets/views/file_view.dart +++ b/app/lib/features/attachments/widgets/views/file_view.dart @@ -1,12 +1,10 @@ import 'package:acter/common/models/attachment_media_state/attachment_media_state.dart'; -import 'package:acter/common/themes/app_theme.dart'; -import 'package:acter/common/widgets/download_button.dart'; import 'package:acter/features/attachments/providers/attachment_providers.dart'; +import 'package:acter/features/files/actions/file_share.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' show Attachment; import 'package:flutter/material.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:share_plus/share_plus.dart'; class FileView extends ConsumerWidget { final Attachment attachment; @@ -47,11 +45,7 @@ class FileView extends ConsumerWidget { return InkWell( onTap: () async { if (mediaState.mediaFile != null) { - if (isDesktop) { - downloadFile(context, mediaState.mediaFile!); - } else { - Share.shareXFiles([XFile(mediaState.mediaFile!.path)]); - } + openFileShareDialog(context: context, file: mediaState.mediaFile!); } else { ref .read(attachmentMediaStateProvider(attachment).notifier) @@ -97,11 +91,10 @@ class FileView extends ConsumerWidget { return InkWell( onTap: openView! ? () async { - if (isDesktop) { - downloadFile(context, mediaState.mediaFile!); - } else { - Share.shareXFiles([XFile(mediaState.mediaFile!.path)]); - } + openFileShareDialog( + context: context, + file: mediaState.mediaFile!, + ); } : null, child: ClipRRect( diff --git a/app/lib/features/chat/widgets/file_message_builder.dart b/app/lib/features/chat/widgets/file_message_builder.dart index 316a37dec992..5554f310e95d 100644 --- a/app/lib/features/chat/widgets/file_message_builder.dart +++ b/app/lib/features/chat/widgets/file_message_builder.dart @@ -1,14 +1,12 @@ import 'package:acter/common/models/types.dart'; -import 'package:acter/common/themes/app_theme.dart'; -import 'package:acter/common/widgets/download_button.dart'; import 'package:acter/features/chat/models/media_chat_state/media_chat_state.dart'; import 'package:acter/features/chat/providers/chat_providers.dart'; +import 'package:acter/features/files/actions/file_share.dart'; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:share_plus/share_plus.dart'; class FileMessageBuilder extends ConsumerWidget { final types.FileMessage message; @@ -31,11 +29,7 @@ class FileMessageBuilder extends ConsumerWidget { return InkWell( onTap: () async { if (mediaState.mediaFile != null) { - if (isDesktop) { - downloadFile(context, mediaState.mediaFile!); - } else { - Share.shareXFiles([XFile(mediaState.mediaFile!.path)]); - } + openFileShareDialog(context: context, file: mediaState.mediaFile!); } else { await ref .read(mediaChatStateProvider(messageInfo).notifier) diff --git a/app/lib/features/events/pages/event_details_page.dart b/app/lib/features/events/pages/event_details_page.dart index bac992c3f720..e091b22d53aa 100644 --- a/app/lib/features/events/pages/event_details_page.dart +++ b/app/lib/features/events/pages/event_details_page.dart @@ -1,8 +1,9 @@ +import 'dart:io'; + import 'package:acter/common/actions/redact_content.dart'; import 'package:acter/common/actions/report_content.dart'; import 'package:acter/common/providers/common_providers.dart'; import 'package:acter/common/providers/room_providers.dart'; -import 'package:acter/common/themes/app_theme.dart'; import 'package:acter/common/utils/utils.dart'; import 'package:acter/features/events/actions/get_event_type.dart'; import 'package:acter/features/events/widgets/change_date_sheet.dart'; @@ -19,6 +20,7 @@ import 'package:acter/features/events/utils/events_utils.dart'; import 'package:acter/features/events/widgets/event_date_widget.dart'; import 'package:acter/features/events/widgets/participants_list.dart'; import 'package:acter/features/events/widgets/skeletons/event_details_skeleton_widget.dart'; +import 'package:acter/features/files/actions/file_share.dart'; import 'package:acter/features/home/providers/client_providers.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/space/widgets/member_avatar.dart'; @@ -26,7 +28,6 @@ import 'package:acter_avatar/acter_avatar.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:atlas_icons/atlas_icons.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -35,7 +36,7 @@ import 'package:jiffy/jiffy.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' show join; import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; final _log = Logger('a3::event::details'); @@ -419,51 +420,29 @@ class _EventDetailPageConsumerState extends ConsumerState { } Widget _buildShareAction(CalendarEvent calendarEvent) { - return PopupMenuButton( - icon: const Icon(Icons.share), - itemBuilder: (context) => [ - PopupMenuItem( - onTap: () => onShareEvent(calendarEvent), - child: Row( - children: [ - const Icon(Icons.share), - const SizedBox(width: 10), - Text(L10n.of(context).shareIcal), - ], - ), - ), - ], + return IconButton( + icon: PhosphorIcon(PhosphorIcons.shareFat()), + onPressed: () => onShareEvent(calendarEvent), ); } Future onShareEvent(CalendarEvent event) async { try { final filename = event.title().replaceAll(RegExp(r'[^A-Za-z0-9_-]'), '_'); - - if (isDesktop) { - String? outputFile = await FilePicker.platform.saveFile( - dialogTitle: 'Please select where to store the file', - fileName: '$filename.ics', - ); - - if (outputFile != null) { - // User canceled the picker - event.icalForSharing(outputFile); - EasyLoading.showToast('File saved to $outputFile'); - } - return; - } - final tempDir = await getTemporaryDirectory(); final icalPath = join(tempDir.path, '$filename.ics'); event.icalForSharing(icalPath); - await Share.shareXFiles([ - XFile( - icalPath, + if (context.mounted) { + await openFileShareDialog( + // ignore: use_build_context_synchronously + context: context, + // ignore: use_build_context_synchronously + header: Text(L10n.of(context).shareIcal), + file: File(icalPath), mimeType: 'text/calendar', - ), - ]); + ); + } } catch (error, stack) { _log.severe('Creating iCal Share Event failed:', error, stack); // ignore: use_build_context_synchronously diff --git a/app/lib/features/files/actions/download_file.dart b/app/lib/features/files/actions/download_file.dart new file mode 100644 index 000000000000..0a245a62d2d9 --- /dev/null +++ b/app/lib/features/files/actions/download_file.dart @@ -0,0 +1,24 @@ +import 'dart:io'; +import 'package:path/path.dart'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +Future downloadFile(BuildContext context, File file) async { + final lang = L10n.of(context); + final filename = basename(file.path); + String? outputFile = await FilePicker.platform.saveFile( + dialogTitle: lang.downloadFileDialogTitle, + fileName: filename, + ); + + if (outputFile == null) { + return false; + } + + await file.copy(outputFile); + EasyLoading.showToast(lang.downloadFileSuccess(outputFile)); + return true; +} diff --git a/app/lib/features/files/actions/file_share.dart b/app/lib/features/files/actions/file_share.dart new file mode 100644 index 000000000000..e2427fdd95e2 --- /dev/null +++ b/app/lib/features/files/actions/file_share.dart @@ -0,0 +1,109 @@ +import 'dart:io'; + +import 'package:acter/features/files/actions/download_file.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:share_plus/share_plus.dart'; + +Future openFileShareDialog({ + required BuildContext context, + required File file, + Widget? header, + String? mimeType, + List? beforeOptions, + List? afterOptions, +}) async { + await showModalBottomSheet( + showDragHandle: true, + useSafeArea: true, + context: context, + isDismissible: true, + constraints: const BoxConstraints(maxHeight: 300), + builder: (context) => _FileOptionsDialog( + file: file, + header: header, + beforeOptions: beforeOptions, + afterOptions: afterOptions, + mimeType: mimeType, + ), + ); +} + +class _FileOptionsDialog extends StatelessWidget { + final File file; + final String? mimeType; + final Widget? header; + final List? beforeOptions; + final List? afterOptions; + + const _FileOptionsDialog({ + required this.file, + this.header, + this.afterOptions, + this.beforeOptions, + this.mimeType, + }); + + @override + Widget build(BuildContext context) { + final lang = L10n.of(context); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + constraints: const BoxConstraints( + maxWidth: 600, + minWidth: 300, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + header ?? const SizedBox.shrink(), + if (header != null) const SizedBox(height: 16.0), + if (beforeOptions?.isNotEmpty == true) ...beforeOptions!, + TextButton.icon( + onPressed: () async { + final result = await OpenFilex.open(file.absolute.path); + if (result.type == ResultType.done) { + // done, close this dialog + if (context.mounted) { + Navigator.pop(context); + } + } + }, + label: Text(lang.openFile), + icon: PhosphorIcon(PhosphorIcons.fileArrowUp()), + ), + TextButton.icon( + onPressed: () async { + final result = await Share.shareXFiles( + [XFile(file.path, mimeType: mimeType)],); + if (result.status == ShareResultStatus.success) { + // done, close this dialog + if (context.mounted) { + Navigator.pop(context); + } + } + }, + label: Text(lang.shareFile), + icon: PhosphorIcon(PhosphorIcons.shareNetwork()), + ), + if (!Platform.isAndroid) // crashes on Android for some reason ... + TextButton.icon( + onPressed: () async { + if (await downloadFile(context, file)) { + // done, close this dialog + if (context.mounted) { + Navigator.pop(context); + } + } + }, + label: Text(lang.saveFileAs), + icon: PhosphorIcon(PhosphorIcons.downloadSimple()), + ), + if (afterOptions?.isNotEmpty == true) ...afterOptions!, + ], + ), + ); + } +} diff --git a/app/lib/features/files/widgets/share_file_button.dart b/app/lib/features/files/widgets/share_file_button.dart new file mode 100644 index 000000000000..bbf32c02bc1f --- /dev/null +++ b/app/lib/features/files/widgets/share_file_button.dart @@ -0,0 +1,18 @@ +import 'dart:io'; +import 'package:acter/features/files/actions/file_share.dart'; +import 'package:flutter/material.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +class ShareFileButton extends StatelessWidget { + final File file; + + const ShareFileButton({super.key, required this.file}); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () => openFileShareDialog(context: context, file: file), + icon: PhosphorIcon(PhosphorIcons.shareFat()), + ); + } +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 6d69abbbd4c9..b991e55ce35d 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -956,6 +956,12 @@ "@sasVerified": {}, "save": "Save", "@save": {}, + "saveFileAs": "Save file as", + "@saveFileAs" : {}, + "openFile": "Open", + "@openFile" : {}, + "shareFile": "Share", + "@shareFile" : {}, "saveChanges": "Save Changes", "@saveChanges": {}, "savingCode": "Saving code",