diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 9c8242dd2e..8b7c2796b9 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2792,6 +2792,7 @@ "downloadImageSuccess": "Image saved to Pictures", "@downloadImageSuccess": {}, "downloadImageError": "Error saving image", + "downloadFileError": "Error downloading file", "@downloadImageError": {}, "downloadFileInWeb": "File saved to {directory}", "@downloadFileInWeb": { diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index ab65dd7510..fc63e52540 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -2696,6 +2696,7 @@ "@acceptInvite": {}, "downloadImageError": "Erreur d'enregistrement de l'image", "@downloadImageError": {}, + "downloadFileError": "Erreur de téléchargement du fichier", "externalContactMessage": "Certains des utilisateurs que vous souhaitez ajouter ne figurent pas dans vos contacts. Voulez-vous les inviter ?", "@externalContactMessage": {}, "appLanguage": "Langue de l'application", diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index d585d5dd32..c030f8f853 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -78,7 +78,7 @@ abstract class AppRoutes { path: '/home', pageBuilder: (context, state) => defaultPageBuilder( context, - PlatformInfos.isMobile + PlatformInfos.isMobile || PlatformInfos.isLinux ? const TwakeWelcome() : AutoHomeserverPicker( loggedOut: state.extra is bool ? state.extra as bool? : null, diff --git a/lib/domain/model/extensions/xfile_extension.dart b/lib/domain/model/extensions/xfile_extension.dart new file mode 100644 index 0000000000..684045c614 --- /dev/null +++ b/lib/domain/model/extensions/xfile_extension.dart @@ -0,0 +1,24 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:matrix/matrix.dart'; + +extension XFileExtension on XFile { + Future toMatrixFile() async { + return MatrixFile.fromMimeType( + bytes: await readAsBytes(), + mimeType: mimeType, + name: name, + filePath: path, + sizeInBytes: await length(), + ); + } + + Future toPlatformFile() async { + return PlatformFile.fromMap({ + 'name': name, + 'path': path, + 'bytes': await readAsBytes(), + 'size': await length(), + }); + } +} diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index bca0fa0442..b948ae7a16 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -67,7 +67,7 @@ class BootstrapDialogState extends State { if (PlatformInfos.isAndroid) { return L10n.of(context)!.storeInAndroidKeystore; } - if (PlatformInfos.isIOS || PlatformInfos.isMacOS) { + if (PlatformInfos.isIOS || PlatformInfos.isMacOS || PlatformInfos.isLinux) { return L10n.of(context)!.storeInAppleKeyChain; } return L10n.of(context)!.storeSecurlyOnThisDevice; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index bd5d9ad3f3..c3157ea555 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1284,6 +1284,9 @@ class ChatController extends State void onSendFileClick(BuildContext context) async { if (PlatformInfos.isMobile) { _showMediaPicker(context); + } else if (PlatformInfos.isDesktop) { + final matrixFiles = await pickFilesFromDesktop(); + sendFileOnWebAction(context, room: room, matrixFilesList: matrixFiles); } else { final matrixFiles = await pickFilesFromSystem(); sendFileOnWebAction(context, room: room, matrixFilesList: matrixFiles); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 1514143825..275d77602e 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -241,7 +241,7 @@ class SelectionTextContainer extends StatelessWidget { @override Widget build(BuildContext context) { - if (!PlatformInfos.isWeb) { + if (PlatformInfos.isMobile) { return child; } diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 0557ad104b..3e6765f8c6 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -5,10 +5,12 @@ import 'package:fluffychat/pages/chat/chat_input_row_web.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/shortcuts.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:matrix/matrix.dart'; @@ -98,35 +100,57 @@ class ChatInputRow extends StatelessWidget { ); } - InputBar _buildInputBar(BuildContext context) { - return InputBar( - typeAheadKey: controller.chatComposerTypeAheadKey, - rawKeyboardFocusNode: controller.rawKeyboardListenerFocusNode, - room: controller.room!, - minLines: 1, - maxLines: 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: TextInputType.multiline, - textInputAction: null, - onSubmitted: (_) => controller.onInputBarSubmitted(), - suggestionsController: controller.suggestionsController, - typeAheadFocusNode: controller.inputFocus, - controller: controller.sendController, - focusSuggestionController: controller.focusSuggestionController, - suggestionScrollController: controller.suggestionScrollController, - showEmojiPickerNotifier: controller.showEmojiPickerNotifier, - decoration: InputDecoration( - hintText: L10n.of(context)!.chatMessage, - hintMaxLines: 1, - hintStyle: Theme.of(context) - .textTheme - .bodyLarge - ?.merge( - Theme.of(context).inputDecorationTheme.hintStyle, - ) - .copyWith(letterSpacing: -0.15), + Widget _buildInputBar(BuildContext context) { + return Shortcuts( + shortcuts: { + LogicalKeySet( + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.keyA, + ): const SelectAllIntent(), + LogicalKeySet( + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.keyE, + ): const OnEmojiActionIntent(), + }, + child: Actions( + actions: >{ + SelectAllIntent: CallbackAction( + onInvoke: (_) => controller.selectAll(), + ), + OnEmojiActionIntent: CallbackAction( + onInvoke: (_) => controller.onEmojiAction(), + ), + }, + child: InputBar( + typeAheadKey: controller.chatComposerTypeAheadKey, + rawKeyboardFocusNode: controller.rawKeyboardListenerFocusNode, + room: controller.room!, + minLines: 1, + maxLines: 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: TextInputType.multiline, + textInputAction: null, + onSubmitted: (_) => controller.onInputBarSubmitted(), + suggestionsController: controller.suggestionsController, + typeAheadFocusNode: controller.inputFocus, + controller: controller.sendController, + focusSuggestionController: controller.focusSuggestionController, + suggestionScrollController: controller.suggestionScrollController, + showEmojiPickerNotifier: controller.showEmojiPickerNotifier, + decoration: InputDecoration( + hintText: L10n.of(context)!.chatMessage, + hintMaxLines: 1, + hintStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.merge( + Theme.of(context).inputDecorationTheme.hintStyle, + ) + .copyWith(letterSpacing: -0.15), + ), + onChanged: controller.onInputBarChanged, + ), ), - onChanged: controller.onInputBarChanged, ); } } diff --git a/lib/pages/chat/chat_input_row_mobile.dart b/lib/pages/chat/chat_input_row_mobile.dart index 0b8e310d67..c1efea26f4 100644 --- a/lib/pages/chat/chat_input_row_mobile.dart +++ b/lib/pages/chat/chat_input_row_mobile.dart @@ -2,9 +2,7 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; typedef OnTapEmojiAction = void Function(); @@ -48,44 +46,34 @@ class ChatInputRowMobile extends StatelessWidget { Expanded( child: inputBar, ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.keyE, - }, - onKeysPressed: onEmojiAction, - helpLabel: L10n.of(context)!.emojis, - child: InkWell( - onTap: onEmojiAction, - hoverColor: Colors.transparent, - child: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, + InkWell( + onTap: onEmojiAction, + hoverColor: Colors.transparent, + child: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: ValueListenableBuilder( + valueListenable: emojiPickerNotifier, + builder: (context, showEmojiPicker, child) { + return TwakeIconButton( + paddingAll: + ChatInputRowStyle.chatInputRowPaddingBtnMobile, + tooltip: L10n.of(context)!.emojis, + onTap: showEmojiPicker ? onKeyboardAction : onEmojiAction, + icon: showEmojiPicker ? Icons.keyboard : Icons.tag_faces, ); }, - child: ValueListenableBuilder( - valueListenable: emojiPickerNotifier, - builder: (context, showEmojiPicker, child) { - return TwakeIconButton( - paddingAll: - ChatInputRowStyle.chatInputRowPaddingBtnMobile, - tooltip: L10n.of(context)!.emojis, - onTap: - showEmojiPicker ? onKeyboardAction : onEmojiAction, - icon: - showEmojiPicker ? Icons.keyboard : Icons.tag_faces, - ); - }, - ), ), ), ), diff --git a/lib/pages/chat/chat_input_row_web.dart b/lib/pages/chat/chat_input_row_web.dart index 5448c2e5c8..5bf160c3c7 100644 --- a/lib/pages/chat/chat_input_row_web.dart +++ b/lib/pages/chat/chat_input_row_web.dart @@ -2,9 +2,7 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -54,39 +52,33 @@ class ChatInputRowWeb extends StatelessWidget { Expanded( child: inputBar, ), - KeyBoardShortcuts( - keysToPress: {LogicalKeyboardKey.altLeft, LogicalKeyboardKey.keyE}, - onKeysPressed: onEmojiAction, - helpLabel: L10n.of(context)!.emojis, - child: InkWell( - onTap: onEmojiAction, - hoverColor: Colors.transparent, - child: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, + InkWell( + onTap: onEmojiAction, + hoverColor: Colors.transparent, + child: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: ValueListenableBuilder( + valueListenable: emojiPickerNotifier, + builder: (context, showEmojiPicker, child) { + return TwakeIconButton( + paddingAll: ChatInputRowStyle.chatInputRowPaddingBtnMobile, + tooltip: L10n.of(context)!.emojis, + onTap: showEmojiPicker ? onKeyboardAction : onEmojiAction, + icon: showEmojiPicker ? Icons.keyboard : Icons.tag_faces, ); }, - child: ValueListenableBuilder( - valueListenable: emojiPickerNotifier, - builder: (context, showEmojiPicker, child) { - return TwakeIconButton( - paddingAll: - ChatInputRowStyle.chatInputRowPaddingBtnMobile, - tooltip: L10n.of(context)!.emojis, - onTap: showEmojiPicker ? onKeyboardAction : onEmojiAction, - icon: showEmojiPicker ? Icons.keyboard : Icons.tag_faces, - ); - }, - ), ), ), ), diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index d2b0b17b91..d21cd91f53 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -6,10 +6,12 @@ import 'package:fluffychat/pages/chat/chat_view_body.dart'; import 'package:fluffychat/pages/chat/chat_view_style.dart'; import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/utils/shortcuts.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_state_layer.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -103,105 +105,120 @@ class ChatView extends StatelessWidget with MessageContentMixin { return Focus( focusNode: controller.chatFocusNode, - child: GestureDetector( - onTapDown: (_) => controller.setReadMarker(), - behavior: HitTestBehavior.opaque, - child: StreamBuilder( - stream: controller.room!.onUpdate.stream - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) => FutureBuilder( - future: controller.loadTimelineFuture, - builder: (BuildContext context, snapshot) { - return Scaffold( - backgroundColor: LinagoraSysColors.material().onPrimary, - appBar: AppBar( - backgroundColor: LinagoraSysColors.material().onPrimary, - automaticallyImplyLeading: false, - toolbarHeight: AppConfig.toolbarHeight(context), - title: Padding( - padding: ChatViewStyle.paddingLeading(context), - child: Row( - children: [ - _buildLeading(context), - Expanded( - child: ChatAppBarTitle( - selectedEvents: controller.selectedEvents, - room: controller.room, - isArchived: controller.isArchived, - sendController: controller.sendController, - connectivityResultStream: controller - .networkConnectionService - .connectivity - .onConnectivityChanged, - actions: _appBarActions(context), - onPushDetails: controller.onPushDetails, - roomName: controller.roomName, - ), - ), - ], - ), - ), - actions: [ - if (!controller.selectMode) - Padding( - padding: ChatViewStyle.paddingTrailing(context), + child: Shortcuts( + shortcuts: { + LogicalKeySet( + LogicalKeyboardKey.control, + LogicalKeyboardKey.keyV, + ): const PasteIntent(), + }, + child: Actions( + actions: >{ + PasteIntent: CallbackAction( + onInvoke: (intent) => controller.paste(), + ), + }, + child: GestureDetector( + onTapDown: (_) => controller.setReadMarker(), + behavior: HitTestBehavior.opaque, + child: StreamBuilder( + stream: controller.room!.onUpdate.stream + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) => FutureBuilder( + future: controller.loadTimelineFuture, + builder: (BuildContext context, snapshot) { + return Scaffold( + backgroundColor: LinagoraSysColors.material().onPrimary, + appBar: AppBar( + backgroundColor: LinagoraSysColors.material().onPrimary, + automaticallyImplyLeading: false, + toolbarHeight: AppConfig.toolbarHeight(context), + title: Padding( + padding: ChatViewStyle.paddingLeading(context), child: Row( children: [ - IconButton( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - onPressed: controller.toggleSearch, - icon: const Icon(Icons.search), - ), - if (!controller.room!.isDirectChat) - Builder( - builder: (context) => TwakeIconButton( - icon: Icons.more_vert, - tooltip: L10n.of(context)!.more, - onTapDown: (tapDownDetails) => - controller.handleAppbarMenuAction( - context, - tapDownDetails, - ), - preferBelow: false, - ), + _buildLeading(context), + Expanded( + child: ChatAppBarTitle( + selectedEvents: controller.selectedEvents, + room: controller.room, + isArchived: controller.isArchived, + sendController: controller.sendController, + connectivityResultStream: controller + .networkConnectionService + .connectivity + .onConnectivityChanged, + actions: _appBarActions(context), + onPushDetails: controller.onPushDetails, + roomName: controller.roomName, ), + ), ], ), ), - ], - bottom: PreferredSize( - preferredSize: const Size(double.infinity, 1), - child: Container( - color: LinagoraStateLayer( - LinagoraSysColors.material().surfaceTint, - ).opacityLayer1, - height: 1, - ), - ), - ), - floatingActionButton: ValueListenableBuilder( - valueListenable: controller.showScrollDownButtonNotifier, - builder: (context, showScrollDownButton, _) { - if (showScrollDownButton && - controller.selectedEvents.isEmpty && - controller.replyEventNotifier.value == null) { - return Padding( - padding: const EdgeInsets.only(bottom: 56.0), - child: FloatingActionButton( - onPressed: controller.scrollDown, - mini: true, - child: const Icon(Icons.arrow_downward_outlined), + actions: [ + if (!controller.selectMode) + Padding( + padding: ChatViewStyle.paddingTrailing(context), + child: Row( + children: [ + IconButton( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + onPressed: controller.toggleSearch, + icon: const Icon(Icons.search), + ), + if (!controller.room!.isDirectChat) + Builder( + builder: (context) => TwakeIconButton( + icon: Icons.more_vert, + tooltip: L10n.of(context)!.more, + onTapDown: (tapDownDetails) => + controller.handleAppbarMenuAction( + context, + tapDownDetails, + ), + preferBelow: false, + ), + ), + ], + ), + ), + ], + bottom: PreferredSize( + preferredSize: const Size(double.infinity, 1), + child: Container( + color: LinagoraStateLayer( + LinagoraSysColors.material().surfaceTint, + ).opacityLayer1, + height: 1, ), - ); - } - return const SizedBox(); - }, - ), - body: _buildBody(), - ); - }, + ), + ), + floatingActionButton: ValueListenableBuilder( + valueListenable: controller.showScrollDownButtonNotifier, + builder: (context, showScrollDownButton, _) { + if (showScrollDownButton && + controller.selectedEvents.isEmpty && + controller.replyEventNotifier.value == null) { + return Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: controller.scrollDown, + mini: true, + child: const Icon(Icons.arrow_downward_outlined), + ), + ); + } + return const SizedBox(); + }, + ), + body: _buildBody(), + ); + }, + ), + ), ), ), ), diff --git a/lib/pages/chat/events/message/multi_platform_message_container.dart b/lib/pages/chat/events/message/multi_platform_message_container.dart index d6a03bcd5d..fbe6b75919 100644 --- a/lib/pages/chat/events/message/multi_platform_message_container.dart +++ b/lib/pages/chat/events/message/multi_platform_message_container.dart @@ -18,7 +18,7 @@ class MultiPlatformsMessageContainer extends StatelessWidget { @override Widget build(BuildContext context) { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return MouseRegion( child: child, onHover: (event) { diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 9e367c14d8..a61e7ea654 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -101,7 +101,7 @@ class MessageContent extends StatelessWidget return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (!PlatformInfos.isWeb) ...[ + if (PlatformInfos.isMobile) ...[ MessageDownloadContent( event, ), @@ -126,7 +126,7 @@ class MessageContent extends StatelessWidget return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (!PlatformInfos.isWeb) ...[ + if (PlatformInfos.isMobile || PlatformInfos.isDesktop) ...[ MessageDownloadContent( event, ), @@ -317,7 +317,7 @@ class _MessageImageBuilder extends StatelessWidget { return matrixFile != null && matrixFile.filePath != null && matrixFile is MatrixImageFile && - !PlatformInfos.isWeb; + !PlatformInfos.isWebOrDesktop; } } diff --git a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart index 324db4a8e4..0f69d4a881 100644 --- a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart +++ b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart @@ -87,7 +87,8 @@ class ChatAdaptiveScaffoldBuilderController builder: (_) => Stack( children: [ body!, - if (rightColumnType != null && PlatformInfos.isWeb) + if (rightColumnType != null && + PlatformInfos.isWebOrDesktop) widget.rightBuilder( this, isInStack: true, diff --git a/lib/pages/chat_blank/chat_blank.dart b/lib/pages/chat_blank/chat_blank.dart index 3620c84da5..7fce4f6fbc 100644 --- a/lib/pages/chat_blank/chat_blank.dart +++ b/lib/pages/chat_blank/chat_blank.dart @@ -60,6 +60,7 @@ class _ChatBlankNotChat extends StatelessWidget { style: Theme.of(context).textTheme.headlineLarge?.copyWith( fontWeight: FontWeight.w600, ), + textAlign: TextAlign.center, ), ), _ChatBlankRichText(context: context), diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart index 1ca9f2278d..a1dd6015ea 100644 --- a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart @@ -130,7 +130,7 @@ class _VideoItem extends StatelessWidget { Future _onTapVideo(BuildContext context) async { final result = await Navigator.of( context, - rootNavigator: PlatformInfos.isWeb, + rootNavigator: PlatformInfos.isWebOrDesktop, ).push( HeroPageRoute( builder: (context) { diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index e141149380..3dca912866 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -28,7 +28,7 @@ class ChatListHeader extends StatelessWidget { Container( height: ChatListHeaderStyle.searchBarContainerHeight, padding: ChatListHeaderStyle.searchInputPadding, - child: PlatformInfos.isWeb + child: PlatformInfos.isWebOrDesktop ? _normalModeWidgetWeb(context) : _normalModeWidgetsMobile(context), ), diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index 61ea3c69fb..65851003ff 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -286,7 +286,7 @@ class _MessageContent extends StatelessWidget { Widget build(BuildContext context) { switch (event.messageType) { case MessageTypes.File: - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return MessageDownloadContentWeb(event, highlightText: searchWord); } else { return MessageDownloadContent(event, highlightText: searchWord); diff --git a/lib/pages/dialer/dialer.dart b/lib/pages/dialer/dialer.dart index c1a5cfd739..a306bbda79 100644 --- a/lib/pages/dialer/dialer.dart +++ b/lib/pages/dialer/dialer.dart @@ -186,7 +186,10 @@ class MyCallingPage extends State { void _playCallSound() async { const path = 'assets/sounds/call.ogg'; - if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isMacOS) { + if (kIsWeb || + PlatformInfos.isMobile || + PlatformInfos.isMacOS || + PlatformInfos.isLinux) { final player = AudioPlayer(); await player.setAsset(path); player.play(); diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index eaa40b8a17..0dececb5a4 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -45,7 +45,7 @@ class ImageViewerController extends State { @override void initState() { super.initState(); - if (!PlatformInfos.isWeb && widget.event != null) { + if (!PlatformInfos.isWebOrDesktop && widget.event != null) { handleDownloadFile(widget.event!); } } diff --git a/lib/pages/image_viewer/image_viewer_style.dart b/lib/pages/image_viewer/image_viewer_style.dart index 3d88427473..ecc0f9b314 100644 --- a/lib/pages/image_viewer/image_viewer_style.dart +++ b/lib/pages/image_viewer/image_viewer_style.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; class ImageViewerStyle { static const double minScaleInteractiveViewer = 1.0; static const double maxScaleInteractiveViewer = 10.0; - static double? appBarHeight = PlatformInfos.isWeb ? 56 : null; + static double? appBarHeight = PlatformInfos.isWebOrDesktop ? 56 : null; static EdgeInsetsGeometry paddingTopAppBar = EdgeInsetsDirectional.only( - top: PlatformInfos.isWeb ? 0 : 56, + top: PlatformInfos.isWebOrDesktop ? 0 : 56, ); } diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 58742ba159..b4c0445ff4 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -69,7 +69,7 @@ class ImageViewerView extends StatelessWidget { backgroundColor: Colors.black, body: GestureDetector( onTap: () { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { Navigator.of(context).pop(); } else { controller.showAppbarPreview.toggle(); @@ -101,7 +101,7 @@ class _ImageWidget extends StatelessWidget { @override Widget build(BuildContext context) { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return FutureBuilder( future: event.downloadAndDecryptAttachment( getThumbnail: true, diff --git a/lib/pages/image_viewer/media_viewer_app_bar_view.dart b/lib/pages/image_viewer/media_viewer_app_bar_view.dart index b8a724bc37..aca9ace062 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_view.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_view.dart @@ -92,7 +92,7 @@ class MediaViewerAppbarView extends StatelessWidget { menuChildren: [ ContextMenuItemImageViewer( icon: Icons.file_download_outlined, - title: PlatformInfos.isWeb + title: PlatformInfos.isWebOrDesktop ? L10n.of(context)!.saveFile : L10n.of(context)!.saveToGallery, onTap: () { diff --git a/lib/pages/new_group/widget/selected_participants_list.dart b/lib/pages/new_group/widget/selected_participants_list.dart index 6ded008627..979c62f2c1 100644 --- a/lib/pages/new_group/widget/selected_participants_list.dart +++ b/lib/pages/new_group/widget/selected_participants_list.dart @@ -45,7 +45,7 @@ class _SelectedParticipantsListState extends State { padding: SelectedParticipantsListStyle.paddingAll, child: Wrap( spacing: 8.0, - runSpacing: PlatformInfos.isWeb ? 4.0 : 0.0, + runSpacing: PlatformInfos.isWebOrDesktop ? 4.0 : 0.0, children: contactsNotifier.contactsList.map((contact) { return InputChip( shape: RoundedRectangleBorder( diff --git a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart index fb43918231..644f05692c 100644 --- a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart @@ -120,8 +120,7 @@ class EmotesSettingsView extends StatelessWidget { final image = controller.pack!.images[imageCode]!; final textEditingController = TextEditingController(); textEditingController.text = imageCode; - final useShortCuts = - (PlatformInfos.isWeb || PlatformInfos.isDesktop); + final useShortCuts = PlatformInfos.isWebOrDesktop; return ListTile( leading: Container( width: 180.0, diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index ada07b9f99..e1b20ba300 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/room/upload_content_state.dart'; import 'package:fluffychat/domain/app_state/settings/update_profile_failure.dart'; import 'package:fluffychat/domain/app_state/settings/update_profile_success.dart'; +import 'package:fluffychat/domain/model/extensions/xfile_extension.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_for_web_interactor.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; @@ -33,6 +34,8 @@ import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:wechat_camera_picker/wechat_camera_picker.dart'; class SettingsProfile extends StatefulWidget { @@ -180,7 +183,43 @@ class SettingsProfileController extends State ), ); Logs().d( - 'SettingsProfile::_getImageOnWeb(): AvatarWebNotifier - $result', + 'SettingsProfile::_getImageOnWeb(): AvatarNotifier - $result', + ); + } + } + + void _getImageOnDesktop( + BuildContext context, + ) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + ); + final String initialDirectory = + (await getApplicationDocumentsDirectory()).path; + + final XFile? result = await openFile( + initialDirectory: initialDirectory, + acceptedTypeGroups: [typeGroup], + ); + + Logs().d( + 'SettingsProfile::_getImageOnDesktop(): FilePickerResult - ${result?.path}', + ); + + if (result == null) { + return; + } else { + if (!isEditedProfileNotifier.value) { + isEditedProfileNotifier.toggle(); + } + settingsProfileUIState.value = Right( + GetAvatarInBytesUIStateSuccess( + filePickerResult: FilePickerResult([await result.toPlatformFile()]), + ), + ); + Logs().d( + 'SettingsProfile::_getImageOnDesktop(): AvatarNotifier - $result', ); } } @@ -190,6 +229,10 @@ class SettingsProfileController extends State _getImageOnWeb(context); return; } + if (PlatformInfos.isDesktop) { + _getImageOnDesktop(context); + return; + } final currentPermissionPhotos = await getCurrentMediaPermission(); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart index c3d36bcc7b..184f03d67a 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -93,7 +93,7 @@ class SettingsProfileViewMobile extends StatelessWidget { ); } if (success is GetAvatarInBytesUIStateSuccess && - PlatformInfos.isWeb) { + PlatformInfos.isWebOrDesktop) { if (success.filePickerResult == null || success.filePickerResult?.files.single.bytes == null) { @@ -160,7 +160,7 @@ class SettingsProfileViewMobile extends StatelessWidget { ) { return GestureDetector( onTap: () { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { menuController.isOpen ? menuController.close() : menuController.open(); diff --git a/lib/presentation/mixins/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart index 9f5ec79a41..ea93279ce9 100644 --- a/lib/presentation/mixins/connect_page_mixin.dart +++ b/lib/presentation/mixins/connect_page_mixin.dart @@ -24,6 +24,8 @@ mixin ConnectPageMixin { static const redirectPublicPlatformOnWeb = 'post_login_redirect_url'; + static const linowsRedirectUrl = 'http://localhost:60665'; + bool supportsFlow({ required BuildContext context, required String flowType, @@ -37,7 +39,8 @@ mixin ConnectPageMixin { bool supportsSso(BuildContext context) => (PlatformInfos.isMobile || PlatformInfos.isWeb || - PlatformInfos.isMacOS) && + PlatformInfos.isMacOS || + PlatformInfos.isLinux) && supportsFlow(context: context, flowType: 'm.login.sso'); bool supportsLogin(BuildContext context) => @@ -58,6 +61,9 @@ mixin ConnectPageMixin { AppConfig.homeserver.isNotEmpty; String _getRedirectUrlScheme(String redirectUrl) { + if (PlatformInfos.isLinuxOrWindows) { + return linowsRedirectUrl; + } return Uri.parse(redirectUrl).scheme; } @@ -106,7 +112,8 @@ mixin ConnectPageMixin { redirectUrl: redirectUrl, ); final urlScheme = _getRedirectUrlScheme(redirectUrl); - return await FlutterWebAuth2.authenticate( + + return FlutterWebAuth2.authenticate( url: url, callbackUrlScheme: urlScheme, options: const FlutterWebAuth2Options( @@ -215,6 +222,7 @@ mixin ConnectPageMixin { } String _generateRedirectUrl(String homeserver) { + if (PlatformInfos.isLinuxOrWindows) return linowsRedirectUrl; if (kIsWeb) { String? homeserverParam = ''; if (homeserver.isNotEmpty) { diff --git a/lib/presentation/mixins/handle_clipboard_action_mixin.dart b/lib/presentation/mixins/handle_clipboard_action_mixin.dart index c203eb39b3..5b7b4f9a34 100644 --- a/lib/presentation/mixins/handle_clipboard_action_mixin.dart +++ b/lib/presentation/mixins/handle_clipboard_action_mixin.dart @@ -22,17 +22,36 @@ mixin HandleClipboardActionMixin on PasteImageMixin { ClipboardEvents.instance?.unregisterPasteEventListener(_onPasteEvent); } + void selectAll() { + sendController.selection = TextSelection( + baseOffset: 0, + extentOffset: sendController.text.length, + ); + } + + Future paste() async { + final clipboard = SystemClipboard.instance; + if (clipboard != null) { + final reader = await clipboard.read(); + _paste(reader); + } + } + void _onPasteEvent(ClipboardReadEvent event) async { if (chatFocusNode.hasFocus != true) { return; } final clipboardReader = await event.getClipboardReader(); + _paste(clipboardReader); + } + + void _paste(ClipboardReader reader) async { if (await TwakeClipboard.instance - .isReadableImageFormat(clipboardReader: clipboardReader) && + .isReadableImageFormat(clipboardReader: reader) && room != null) { - await pasteImage(context, room!, clipboardReader: clipboardReader); + await pasteImage(context, room!, clipboardReader: reader); } else { - sendController.pasteText(clipboardReader: clipboardReader); + sendController.pasteText(clipboardReader: reader); } } } diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart index 9bd069b01f..9c0e9ed9c2 100644 --- a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart +++ b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart @@ -151,7 +151,7 @@ mixin MediaViewerAppBarMixin on SaveMediaToGalleryAndroidMixin { BuildContext context, Event? event, ) { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { event?.saveFile(context); } else { if (event != null) { diff --git a/lib/presentation/mixins/paste_image_mixin.dart b/lib/presentation/mixins/paste_image_mixin.dart index 7dff5f4421..b18abc9f0d 100644 --- a/lib/presentation/mixins/paste_image_mixin.dart +++ b/lib/presentation/mixins/paste_image_mixin.dart @@ -20,7 +20,7 @@ mixin PasteImageMixin { return; } List? matrixFiles; - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { matrixFiles = await TwakeClipboard.instance .pasteImagesUsingBytes(reader: clipboardReader); } @@ -43,7 +43,7 @@ mixin PasteImageMixin { .toList(); await showDialog( context: context, - useRootNavigator: PlatformInfos.isWeb, + useRootNavigator: PlatformInfos.isWebOrDesktop, builder: (context) { return SendFileDialog( room: room, diff --git a/lib/presentation/mixins/play_video_action_mixin.dart b/lib/presentation/mixins/play_video_action_mixin.dart index 507c948c28..6a5cc77889 100644 --- a/lib/presentation/mixins/play_video_action_mixin.dart +++ b/lib/presentation/mixins/play_video_action_mixin.dart @@ -29,11 +29,12 @@ mixin PlayVideoActionMixin { }, ); if (isReplacement) { - Navigator.of(context, rootNavigator: PlatformInfos.isWeb).pushReplacement( + Navigator.of(context, rootNavigator: PlatformInfos.isWebOrDesktop) + .pushReplacement( pageRoute, ); } else { - Navigator.of(context, rootNavigator: PlatformInfos.isWeb).push( + Navigator.of(context, rootNavigator: PlatformInfos.isWebOrDesktop).push( pageRoute, ); } diff --git a/lib/presentation/mixins/send_files_mixin.dart b/lib/presentation/mixins/send_files_mixin.dart index 1b7fa60c87..756e2393c3 100644 --- a/lib/presentation/mixins/send_files_mixin.dart +++ b/lib/presentation/mixins/send_files_mixin.dart @@ -1,12 +1,14 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/extensions/platform_file/platform_file_extension.dart'; +import 'package:fluffychat/domain/model/extensions/xfile_extension.dart'; import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/images_picker/images_picker.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; @@ -68,6 +70,24 @@ mixin SendFilesMixin { return result.files.map((file) => file.toMatrixFileOnWeb()).toList(); } + Future> pickFilesFromDesktop() async { + final String initialDirectory = + (await getApplicationDocumentsDirectory()).path; + final List xFiles = + await openFiles(initialDirectory: initialDirectory); + + if (xFiles.isEmpty) return []; + + final matrixFiles = []; + + for (final xFile in xFiles) { + final matrixFile = await xFile.toMatrixFile(); + matrixFiles.add(matrixFile); + } + + return matrixFiles; + } + void onPickerTypeClick({ required BuildContext context, Room? room, diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 7b9cf5418c..17cfa360e4 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -107,7 +107,8 @@ abstract class ClientManager { AuthenticationTypes.password, if (PlatformInfos.isMobile || PlatformInfos.isWeb || - PlatformInfos.isMacOS) + PlatformInfos.isMacOS || + PlatformInfos.isLinux) AuthenticationTypes.sso, }, nativeImplementations: nativeImplementations, diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart index a283163d0a..6f666c8f72 100644 --- a/lib/utils/manager/download_manager/download_manager.dart +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -98,6 +98,7 @@ class DownloadManager { required Event event, bool getThumbnail = false, bool isFirstPriority = false, + bool isTemporary = true, }) async { _initDownloadFileInfo(event); final streamController = _eventIdMapDownloadFileInfo[event.eventId] @@ -131,6 +132,7 @@ class DownloadManager { streamController: streamController, cancelToken: cancelToken, isFirstPriority: isFirstPriority, + isTemporary: isTemporary, ); } @@ -140,6 +142,7 @@ class DownloadManager { required StreamController> streamController, required CancelToken cancelToken, bool isFirstPriority = false, + bool isTemporary = true, }) { if (PlatformInfos.isWeb) { _addTaskToWorkerQueueWeb( @@ -157,6 +160,7 @@ class DownloadManager { streamController, cancelToken, isFirstPriority: isFirstPriority, + isTemporary: isTemporary, ); } @@ -166,6 +170,7 @@ class DownloadManager { StreamController> streamController, CancelToken cancelToken, { bool isFirstPriority = false, + bool isTemporary = true, }) { workingQueue.addTask( Task( @@ -176,6 +181,7 @@ class DownloadManager { getThumbnail: getThumbnail, downloadStreamController: streamController, cancelToken: cancelToken, + isTemporary: isTemporary, ); } catch (e) { Logs().e('DownloadManager::download(): $e'); diff --git a/lib/utils/manager/storage_directory_manager.dart b/lib/utils/manager/storage_directory_manager.dart index a45902f8f5..24ff934e02 100644 --- a/lib/utils/manager/storage_directory_manager.dart +++ b/lib/utils/manager/storage_directory_manager.dart @@ -13,7 +13,8 @@ class StorageDirectoryManager { static StorageDirectoryManager get instance => _instance; - Future getFileStoreDirectory() async { + Future getFileStoreDirectory({bool isTemporary = true}) async { + if (!isTemporary) return (await getDownloadsDirectory())!.path; try { try { return (await getTemporaryDirectory()).path; @@ -28,10 +29,14 @@ class StorageDirectoryManager { Future getFilePathInAppDownloads({ required String eventId, required String fileName, + bool isTemporary = true, }) async { - final fileStoreDirectory = - await StorageDirectoryManager.instance.getFileStoreDirectory(); - return '$fileStoreDirectory/$eventId/$fileName'; + final fileStoreDirectory = await StorageDirectoryManager.instance + .getFileStoreDirectory(isTemporary: isTemporary); + if (isTemporary) { + return '$fileStoreDirectory/$eventId/$fileName'; + } + return '$fileStoreDirectory/${AppConfig.applicationName}/$fileName'; } Future getTwakeDownloadsFolderInDevice() async { @@ -71,9 +76,13 @@ class StorageDirectoryManager { Future getDecryptedFilePath({ required String eventId, required String fileName, + bool isTemporary = true, }) async { - final fileStoreDirectory = - await StorageDirectoryManager.instance.getFileStoreDirectory(); - return '$fileStoreDirectory/$eventId/decrypted-$fileName'; + final fileStoreDirectory = await StorageDirectoryManager.instance + .getFileStoreDirectory(isTemporary: isTemporary); + if (isTemporary) { + return '$fileStoreDirectory/$eventId/decrypted-$fileName'; + } + return '$fileStoreDirectory/${AppConfig.applicationName}/decrypted-$fileName'; } } diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 1b8483cd54..924c9adcce 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -43,11 +43,13 @@ extension DownloadFileExtension on Event { bool getThumbnail = false, CancelToken? cancelToken, required String filename, + bool isTemporary = true, }) async { final attachment = File( await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ), ); final downloadLink = mxcUrl.getDownloadLink(room.client); @@ -155,6 +157,7 @@ extension DownloadFileExtension on Event { required String filename, bool getThumbnail = false, StreamController>? streamController, + bool isTemporary = true, }) async { streamController?.add( const Right( @@ -168,6 +171,7 @@ extension DownloadFileExtension on Event { await StorageDirectoryManager.instance.getDecryptedFilePath( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ), getThumbnail: getThumbnail, ); @@ -180,6 +184,7 @@ extension DownloadFileExtension on Event { await StorageDirectoryManager.instance.getDecryptedFilePath( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ), ).copySync(savePath); streamController?.add( @@ -260,6 +265,7 @@ extension DownloadFileExtension on Event { StreamController>? downloadStreamController, ProgressCallback? progressCallback, CancelToken? cancelToken, + bool isTemporary = true, }) async { if (!canContainAttachment()) { throw ("getFileInfo: This event has the type '$type' and so it can't contain an attachment."); @@ -289,6 +295,7 @@ extension DownloadFileExtension on Event { await StorageDirectoryManager.instance.getDecryptedFilePath( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ); final decryptedFile = File(decryptedPath); @@ -308,13 +315,17 @@ extension DownloadFileExtension on Event { return downloadOrRetrieveAttachment( mxcUrl, - await StorageDirectoryManager.instance - .getFilePathInAppDownloads(eventId: eventId, fileName: filename), + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: filename, + isTemporary: isTemporary, + ), downloadStreamController: downloadStreamController, getThumbnail: getThumbnail, progressCallback: progressCallback, cancelToken: cancelToken, filename: filename, + isTemporary: isTemporary, ); } } diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 9c78247ab3..3c002d485c 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -185,7 +185,7 @@ extension LocalizedBody on Event { bool get isPinned => room.pinnedEventIds.contains(eventId); Future copy(BuildContext context, Timeline timeline) async { - if (messageType == MessageTypes.Image && PlatformInfos.isWeb) { + if (messageType == MessageTypes.Image && PlatformInfos.isWebOrDesktop) { final matrixFile = getMatrixFile() ?? await downloadAndDecryptAttachment( getThumbnail: true, diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 1930cbd93c..9edad87c96 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -16,7 +16,7 @@ import 'package:file_saver/file_saver.dart'; extension MatrixFileExtension on MatrixFile { Future downloadFile(BuildContext context) async { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return await downloadFileInWeb(context); } @@ -51,11 +51,21 @@ extension MatrixFileExtension on MatrixFile { name: name, bytes: bytes, ); + + TwakeSnackBar.show( + context, + L10n.of(context)!.fileSavedToDownloads, + ); return '$directory/$name'; } catch (e) { Logs().e( "MatrixFileExtension()::downloadFileInWeb()::Error: $e", ); + + TwakeSnackBar.show( + context, + L10n.of(context)!.downloadImageError, + ); } return null; } diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index d6ee76d22f..2f4ab9467d 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -32,6 +32,10 @@ abstract class PlatformInfos { static bool get isDesktop => isLinux || isWindows || isMacOS; + static bool get isLinuxOrWindows => isLinux || isWindows; + + static bool get isWebOrDesktop => isWeb || isDesktop; + static bool get usesTouchscreen => !isMobile; static bool get platformCanRecord => (isMobile || isMacOS); diff --git a/lib/utils/shortcuts.dart b/lib/utils/shortcuts.dart new file mode 100644 index 0000000000..739f123078 --- /dev/null +++ b/lib/utils/shortcuts.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +class OnEmojiActionIntent extends Intent { + const OnEmojiActionIntent(); +} + +class SelectAllIntent extends Intent { + const SelectAllIntent(); +} + +class PasteIntent extends Intent { + const PasteIntent(); +} diff --git a/lib/utils/voip/user_media_manager.dart b/lib/utils/voip/user_media_manager.dart index 874da93ee1..f6b747efe5 100644 --- a/lib/utils/voip/user_media_manager.dart +++ b/lib/utils/voip/user_media_manager.dart @@ -19,7 +19,7 @@ class UserMediaManager { Future startRingingTone() async { if (PlatformInfos.isMobile) { await FlutterRingtonePlayer.playRingtone(volume: 80); - } else if ((kIsWeb || PlatformInfos.isMacOS) && + } else if ((kIsWeb || PlatformInfos.isMacOS || PlatformInfos.isLinux) && _assetsAudioPlayer != null) { const path = 'assets/sounds/phone.ogg'; final player = _assetsAudioPlayer = AudioPlayer(); diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart index 317052f76a..29448bd301 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart @@ -33,7 +33,7 @@ class AdaptiveScaffoldAppBar extends StatelessWidget { children: [ const _LeadingAppBarWidget(), if (AppConfig.appGridDashboardAvailable && - PlatformInfos.isWeb) + PlatformInfos.isWebOrDesktop) const Expanded( child: AppGridDashboard(), ), diff --git a/lib/widgets/mixins/download_file_on_mobile_mixin.dart b/lib/widgets/mixins/download_file_on_mobile_mixin.dart index 4a2a438835..17e1ae39a4 100644 --- a/lib/widgets/mixins/download_file_on_mobile_mixin.dart +++ b/lib/widgets/mixins/download_file_on_mobile_mixin.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -70,6 +71,7 @@ mixin DownloadFileOnMobileMixin on State { await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: event.eventId, fileName: event.filename, + isTemporary: !PlatformInfos.isDesktop, ); final file = File(filePath); if (await file.exists() && await file.length() == event.getFileSize()) { @@ -119,6 +121,7 @@ mixin DownloadFileOnMobileMixin on State { downloadFileStateNotifier.value = const DownloadingPresentationState(); downloadManager.download( event: event, + isTemporary: !PlatformInfos.isDesktop, ); _trySetupDownloadingStreamSubcription(); } diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index 6af3307dfc..21bf5c5008 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_failure.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_loading.dart'; @@ -16,7 +18,7 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; -import 'package:open_file/open_file.dart'; +import 'package:open_app_file/open_app_file.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; @@ -174,6 +176,13 @@ mixin HandleDownloadAndPreviewFileMixin { ); return; } + + if (PlatformInfos.isDesktop) { + _openDownloadedFileOnDesktop( + filePath: filePath, + mimeType: mimeType, + ); + } } void _openDownloadedFileForPreviewAndroid({ @@ -184,9 +193,9 @@ mixin HandleDownloadAndPreviewFileMixin { await Share.shareXFiles([XFile(filePath)]); return; } - final openResults = await OpenFile.open( + final openResults = await OpenAppFile.open( filePath, - type: mimeType, + mimeType: mimeType, uti: DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]) .value, ); @@ -207,12 +216,41 @@ mixin HandleDownloadAndPreviewFileMixin { Logs().d( 'ChatController:_openDownloadedFileForPreviewIos(): $filePath', ); - await OpenFile.open( + await OpenAppFile.open( filePath, - type: mimeType, + mimeType: mimeType, ); } + void _openDownloadedFileOnDesktop({ + required String filePath, + required String? mimeType, + }) async { + Logs().d( + 'ChatController:_openDownloadedFileOnDesktop(): $filePath', + ); + final downloadDirectory = await getDownloadsDirectory(); + try { + await OpenAppFile.open( + filePath, + mimeType: mimeType, + ); + } catch (e) { + Logs().e( + 'ChatController:_openDownloadedFileOnDesktop(): $e', + ); + if (downloadDirectory == null) { + return; + } + if (PlatformInfos.isLinux || PlatformInfos.isMacOS) { + Process.run('open', [downloadDirectory.path]); + } + if (PlatformInfos.isWindows) { + Process.run('explorer', [downloadDirectory.path]); + } + } + } + Future previewPdfWeb( BuildContext context, Event event, { diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index c0ede95b7a..21498cd125 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -217,8 +217,10 @@ class _MxcImageState extends State { void _onTap(BuildContext context) async { if (widget.onTapPreview != null) { widget.onTapPreview!(); - final result = - await Navigator.of(context, rootNavigator: PlatformInfos.isWeb).push( + final result = await Navigator.of( + context, + rootNavigator: PlatformInfos.isWebOrDesktop, + ).push( HeroPageRoute( builder: (context) { return InteractiveViewerGallery( diff --git a/lib/widgets/video_viewer_style.dart b/lib/widgets/video_viewer_style.dart index 07d9425fa3..7420582ae8 100644 --- a/lib/widgets/video_viewer_style.dart +++ b/lib/widgets/video_viewer_style.dart @@ -14,7 +14,7 @@ class VideoViewerStyle { bottom: 8.0 + MediaQuery.of(context).viewPadding.bottom, ); - static EdgeInsets backButtonMargin(context) => PlatformInfos.isWeb + static EdgeInsets backButtonMargin(context) => PlatformInfos.isWebOrDesktop ? const EdgeInsets.only(top: 8.0, left: 16.0) : EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top); diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 5b46b21570..3661e82da3 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "fluffychat") +set(BINARY_NAME "Twake") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "chat.fluffy.fluffychat") +set(APPLICATION_ID "app.twake.linux.chat") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/linux/my_application.cc b/linux/my_application.cc index 4f0f0c0980..fb9683ab46 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -53,14 +53,14 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "fluffychat"); + gtk_header_bar_set_title(header_bar, "Twake Chat"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "fluffychat"); + gtk_window_set_title(window, "Twake Chat"); } - gtk_window_set_default_size(window, 864, 600); + gtk_window_set_default_size(window, 1280, 720); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); diff --git a/pubspec.lock b/pubspec.lock index 6d22a13ddc..30f1f8ed1a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -619,7 +619,7 @@ packages: source: hosted version: "0.2.12" file_selector: - dependency: "direct overridden" + dependency: "direct main" description: name: file_selector sha256: "1d2fde93dddf634a9c3c0faa748169d7ac0d83757135555707e52f02c017ad4f" @@ -1022,26 +1022,27 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: f2afec1f1762c040a349ea2a588e32f442da5d0db3494a52a929a97c9e550bc5 + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "9.0.0" flutter_secure_storage_linux: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" - url: "https://pub.dev" - source: hosted + path: flutter_secure_storage_linux + ref: develop + resolved-ref: "27d3e2e69123f0c712919ad392e15830741e4383" + url: "https://github.com/tomekit/flutter_secure_storage.git" + source: git version: "1.2.0" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: ff0768a6700ea1d9620e03518e2e25eac86a8bd07ca3556e9617bfa5ace4bd00 + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -1804,14 +1805,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - open_file: + open_app_file: dependency: "direct main" description: - name: open_file - sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20 - url: "https://pub.dev" - source: hosted - version: "3.3.2" + path: "." + ref: HEAD + resolved-ref: "7054e90c4632af0a47be93d1b8891dc499bc5d6d" + url: "git@github.com:aws1313/open_app_file.git" + source: git + version: "4.0.1" overflow_view: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 49176b0ff7..fe881ff602 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,7 +89,7 @@ dependencies: flutter_olm: ^1.2.0 flutter_openssl_crypto: ^0.1.0 flutter_ringtone_player: ^3.1.1 - flutter_secure_storage: ^7.0.1 + flutter_secure_storage: ^9.0.0 flutter_svg: ^0.22.0 flutter_typeahead: ^5.1.0 flutter_web_auth_2: ^3.1.1 @@ -150,7 +150,6 @@ dependencies: tuple: ^2.0.2 lottie: ^2.3.2 wechat_camera_picker: 4.2.1 - open_file: ^3.3.2 mime: ^1.0.4 async: ^2.11.0 cached_network_image: ^3.2.3 @@ -175,6 +174,10 @@ dependencies: flutter_portal: 1.1.4 external_path: 1.0.3 gal: 2.3.0 + open_app_file: + git: + url: git@github.com:aws1313/open_app_file.git + file_selector: ^0.9.2+2 dev_dependencies: build_runner: ^2.3.3 @@ -245,6 +248,12 @@ dependency_overrides: git: url: https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git ref: main + # https://github.com/mogol/flutter_secure_storage/issues/616 + flutter_secure_storage_linux: + git: + url: https://github.com/tomekit/flutter_secure_storage.git + ref: develop + path: flutter_secure_storage_linux geolocator_android: hosted: name: geolocator_android