diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index 4c4436244c..351994354d 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -47,6 +47,12 @@ + + + + diff --git a/frontend/appflowy_flutter/ios/Podfile b/frontend/appflowy_flutter/ios/Podfile index 131b4885ea..5e46cacdb4 100644 --- a/frontend/appflowy_flutter/ios/Podfile +++ b/frontend/appflowy_flutter/ios/Podfile @@ -37,6 +37,16 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + # dart: PermissionGroup.photos + 'PERMISSION_PHOTOS=1', + ] + + end end installer.aggregate_targets.each do |target| diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index ef1354faff..f734473d65 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -63,6 +63,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter - ReachabilitySwift (5.0.0) - SDWebImage (5.14.2): - SDWebImage/Core (= 5.14.2) @@ -98,6 +100,7 @@ DEPENDENCIES: - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) @@ -144,6 +147,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -173,6 +178,7 @@ SPEC CHECKSUMS: keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 @@ -183,6 +189,6 @@ SPEC CHECKSUMS: Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 -PODFILE CHECKSUM: d94f9be27d1db182e9bc77d10f065555d518f127 +PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca COCOAPODS: 1.11.3 diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index 2fae19e29d..aa53cf9b88 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -127,6 +127,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 08FAA63113168DEC7FB74204 /* [CP] Embed Pods Frameworks */, + A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -233,6 +234,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; E790B8FE5609053209ED85CB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart index c11040682f..5a481eaa68 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart @@ -1,53 +1,86 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -///show the dialog to confirm one single action -///[onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog +enum ConfirmDialogActionAlignment { + // The action buttons are aligned vertically + // --------------------- + // | Action Button | + // | Cancel Button | + vertical, + // The action buttons are aligned horizontally + // --------------------- + // | Action Button | Cancel Button | + horizontal, +} + +/// show the dialog to confirm one single action +/// [onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog Future showFlowyMobileConfirmDialog( BuildContext context, { Widget? title, Widget? content, + ConfirmDialogActionAlignment actionAlignment = + ConfirmDialogActionAlignment.horizontal, required String actionButtonTitle, + required VoidCallback? onActionButtonPressed, Color? actionButtonColor, String? cancelButtonTitle, - required void Function()? onActionButtonPressed, - void Function()? onCancelButtonPressed, + Color? cancelButtonColor, + VoidCallback? onCancelButtonPressed, }) async { return showDialog( context: context, builder: (dialogContext) { final foregroundColor = Theme.of(context).colorScheme.onSurface; + final actionButton = TextButton( + child: FlowyText( + actionButtonTitle, + color: actionButtonColor ?? foregroundColor, + ), + onPressed: () { + onActionButtonPressed?.call(); + // we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog. + Navigator.of(dialogContext).pop(); + }, + ); + final cancelButton = TextButton( + child: FlowyText( + cancelButtonTitle ?? LocaleKeys.button_cancel.tr(), + color: cancelButtonColor ?? foregroundColor, + ), + onPressed: () { + onCancelButtonPressed?.call(); + Navigator.of(dialogContext).pop(); + }, + ); + + final actions = switch (actionAlignment) { + ConfirmDialogActionAlignment.horizontal => [ + actionButton, + cancelButton, + ], + ConfirmDialogActionAlignment.vertical => [ + Column( + children: [ + actionButton, + const Divider(height: 1, color: Colors.grey), + cancelButton, + ], + ), + ], + }; + return AlertDialog.adaptive( title: title, content: content, - actions: [ - TextButton( - child: Text( - actionButtonTitle, - style: TextStyle( - color: actionButtonColor ?? foregroundColor, - ), - ), - onPressed: () { - onActionButtonPressed?.call(); - // we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog. - Navigator.of(dialogContext).pop(); - }, - ), - TextButton( - child: Text( - cancelButtonTitle ?? LocaleKeys.button_cancel.tr(), - style: TextStyle( - color: foregroundColor, - ), - ), - onPressed: () { - onCancelButtonPressed?.call(); - Navigator.of(dialogContext).pop(); - }, - ), - ], + contentPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 4.0, + ), + actionsAlignment: MainAxisAlignment.center, + actions: actions, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart index 7feae779cc..0f2635cec9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart @@ -182,14 +182,21 @@ class DocumentCollabAdapter { ); for (final state in values) { // the following code is only for version 1 - if (state.version != 1) { + if (state.version != 1 || state.metadata.isEmpty) { return; } final uid = state.user.uid.toString(); final did = state.user.deviceId; - final metadata = DocumentAwarenessMetadata.fromJson( - jsonDecode(state.metadata), - ); + debugPrint('metadata: ${state.metadata}'); + DocumentAwarenessMetadata metadata; + try { + metadata = DocumentAwarenessMetadata.fromJson( + jsonDecode(state.metadata), + ); + } catch (e) { + Log.error('Failed to parse metadata: $e, ${state.metadata}'); + continue; + } final selectionColor = metadata.selectionColor.tryToColor(); final cursorColor = metadata.cursorColor.tryToColor(); if ((uid == userId && did == deviceId) || diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 3e5cb5416a..0b72fd60f0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -1,21 +1,28 @@ +import 'dart:async'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; import 'package:appflowy/shared/feedback_gesture_detector.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; class PageStyleCoverImage extends StatelessWidget { PageStyleCoverImage({ @@ -114,9 +121,22 @@ class PageStyleCoverImage extends StatelessWidget { } Future _pickImage(BuildContext context) async { - final result = await _imagePicker.pickImage( - source: ImageSource.gallery, - ); + final photoPermission = await _checkPhotoPermission(context); + if (!photoPermission) { + Log.error('Has no permission to access the photo library'); + return; + } + + XFile? result; + try { + result = await _imagePicker.pickImage( + source: ImageSource.gallery, + ); + } catch (e) { + Log.error('Error while picking image: $e'); + return; + } + final path = result?.path; if (path != null && context.mounted) { final String? result; @@ -204,6 +224,54 @@ class PageStyleCoverImage extends StatelessWidget { }, ); } + + Future _checkPhotoPermission(BuildContext context) async { + // check the permission first + final status = await Permission.photos.status; + // if the permission is permanently denied, we should open the app settings + if (status.isPermanentlyDenied && context.mounted) { + unawaited( + showFlowyMobileConfirmDialog( + context, + title: FlowyText.semibold( + LocaleKeys.pageStyle_photoPermissionTitle.tr(), + maxLines: 3, + textAlign: TextAlign.center, + ), + content: FlowyText( + LocaleKeys.pageStyle_photoPermissionDescription.tr(), + maxLines: 5, + textAlign: TextAlign.center, + fontSize: 12.0, + ), + actionAlignment: ConfirmDialogActionAlignment.vertical, + actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(), + actionButtonColor: Colors.blue, + cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(), + cancelButtonColor: Colors.blue, + onActionButtonPressed: () { + openAppSettings(); + }, + ), + ); + + return false; + } else if (status.isDenied) { + // https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937 + Permission permission = Permission.photos; + if (defaultTargetPlatform == TargetPlatform.android && + ApplicationInfo.androidSDKVersion <= 32) { + permission = Permission.storage; + } + // if the permission is denied, we should request the permission + final newStatus = await permission.request(); + if (newStatus.isDenied) { + return false; + } + } + + return true; + } } class _UnsplashCover extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index 55316d2305..64f5914e7c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; @@ -18,6 +16,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; @@ -215,6 +214,8 @@ class _InviteMemberState extends State<_InviteMember> { context .read() .add(WorkspaceMemberEvent.inviteWorkspaceMember(email)); + // clear the email field after inviting + _emailController.clear(); } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 8ff82d3612..e804004a31 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -1329,6 +1329,54 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.dev" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5" + url: "https://pub.dev" + source: hosted + version: "12.0.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + url: "https://pub.dev" + source: hosted + version: "9.4.4" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 68bf48d9e3..4a6972baff 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -134,6 +134,7 @@ dependencies: avatar_stack: ^1.2.0 numerus: ^2.1.2 flutter_animate: ^4.5.0 + permission_handler: ^11.3.1 dev_dependencies: flutter_lints: ^3.0.1 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index c09c4b5e9f..db3c5b14af 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1534,7 +1534,11 @@ "photo": "Photo", "unsplash": "Unsplash", "pageCover": "Page cover", - "none": "None" + "none": "None", + "photoPermissionDescription": "Allow access to the photo library for uploading images.", + "openSettings": "Open Settings", + "photoPermissionTitle": "AppFlowy Would Like to Access Your Photo Library", + "doNotAllow": "Don't Allow" }, "commandPalette": { "placeholder": "Type to search for views...",