feat: request permission again if the user denied the photo permission (#5251)

* fix: clear the email field after sending email

* fix: ask permission before picking image

* feat: improve photo permission UI design

* chore: update translations

* fix: android photo permission

* chore: update translations

* fix: awareness meta data decode error
This commit is contained in:
Lucas.Xu 2024-05-06 11:27:55 +08:00 committed by GitHub
parent d52042fa4f
commit 266a2a53ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 244 additions and 42 deletions

View File

@ -47,6 +47,12 @@
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Permission to read files from external storage (outside application container).
As of Android 12 this permission no longer has any effect. Instead use the
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READM_MEDIA_AUDIO permissions. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Permissions to read media files. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />

View File

@ -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|

View File

@ -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

View File

@ -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;

View File

@ -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<T?> showFlowyMobileConfirmDialog<T>(
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,
);
},
);

View File

@ -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) ||

View File

@ -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<void> _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<bool> _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 {

View File

@ -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<WorkspaceMemberBloc>()
.add(WorkspaceMemberEvent.inviteWorkspaceMember(email));
// clear the email field after inviting
_emailController.clear();
}
}

View File

@ -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:

View File

@ -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

View File

@ -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...",