Commit b24210eb by tanghuan

权限提示调整

1 parent ac90d982
......@@ -977,7 +977,11 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
if (await _checkGalleryPermanentlyDenied()) {
var resp = {'unique': unique, 'cmd': cmd, 'data': null, 'errMsg': 'no auth'};
_sendResponse(resp);
showPermissionSettingsDialog(context, '相册权限');
showPermissionSettingsDialog(
context,
title: '"班小二APP"需要访问您的相册',
description: '未开启相册权限,无法选择图片。您可以在"设置"中开启后重试。',
);
return;
}
......@@ -1030,15 +1034,19 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
void _chooseImageFromCamera(BuildContext context, String unique, String cmd) async {
// 检查是否已被永久拒绝,此时需要对用户进行引导
if (await _checkCameraPermanentlyDenied()) {
if (await _checkCameraPermanentlyDenied() || await _checkGalleryPermanentlyDenied()) {
var resp = {'unique': unique, 'cmd': cmd, 'data': null, 'errMsg': 'no auth'};
_sendResponse(resp);
showPermissionSettingsDialog(context, '相机权限');
showPermissionSettingsDialog(
context,
title: '“班小二APP”需要访问您的相机、相册',
description: '未开启相机权限或相册权限,无法拍摄或选择图片。您可以在"设置"中开启后重试。',
);
return;
}
// 首次使用相机前告知用户用途,用户暂不开启则直接返回
if (!await _confirmCameraIntro(context)) {
// 首次使用相机和相册前告知用户用途,用户暂不开启则直接返回
if (!await _confirmCameraAndGalleryIntro(context)) {
var cancelResp = {'unique': unique, 'cmd': cmd, 'data': null, 'errMsg': 'cancel'};
_sendResponse(cancelResp);
return;
......@@ -1129,7 +1137,6 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
return;
}
final status = await Permission.microphone.status;
if (!context.mounted || isClosed || _isClosing || completer.isCompleted) {
return;
}
......@@ -1137,13 +1144,22 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
return;
}
if (status == PermissionStatus.permanentlyDenied) {
await showPermissionSettingsDialog(context, '麦克风权限');
if (await _checkMicrophonePermanentlyDenied()) {
await showPermissionSettingsDialog(
context,
title: '"班小二APP"需要使用麦克风',
description: '未开启麦克风权限,无法录音评测。您可以在"设置"中开启后重试。',
);
errMsg = 'no auth';
return;
}
final agreed = await _confirmPermissionIntro(context, '麦克风权限', Constant.hasShownMicrophoneIntroKey);
final agreed = await _confirmPermissionIntro(
context,
Constant.hasShownMicrophoneIntroKey,
title: '"班小二APP"需要使用麦克风',
description: '为了方便您完成跟读、朗读、背诵等评测及在线音频作业录制,我们需要使用您的麦克风录制音频。',
);
if (!context.mounted || isClosed || _isClosing || completer.isCompleted) {
return;
}
......@@ -1176,7 +1192,12 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
/// 已记录标记则直接放行;用户点击「暂不开启」时不写入标记,下次仍会再次提示。
/// 返回 true 表示可以继续后续的权限申请与业务处理。
///
Future<bool> _confirmPermissionIntro(BuildContext context, String permission, String preferenceKey) async {
Future<bool> _confirmPermissionIntro(
BuildContext context,
String preferenceKey, {
required String title,
required String description,
}) async {
if (!context.mounted || isClosed || _isClosing) {
return false;
}
......@@ -1185,7 +1206,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
return true;
}
if (!await showPermissionIntroDialog(context, permission)) {
if (!await showPermissionIntroDialog(context, title: title, description: description)) {
return false;
}
if (!context.mounted || isClosed || _isClosing) {
......@@ -1201,14 +1222,48 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
/// 首次使用相机前的用途说明
///
Future<bool> _confirmCameraIntro(BuildContext context) {
return _confirmPermissionIntro(context, '相机权限', Constant.hasShownCameraIntroKey);
return _confirmPermissionIntro(
context,
Constant.hasShownCameraIntroKey,
title: '“班小二APP”需要访问您的相机、相册',
description: '为了方便您完成作业/打卡提交、纸本听写、作文、口算、错题上传、头像设置等学习任务,我们需要使用您的相机拍摄照片,并访问您的相册以保存拍摄的图片。',
);
}
///
/// 首次使用相册前的用途说明
///
Future<bool> _confirmGalleryIntro(BuildContext context) {
return _confirmPermissionIntro(context, '相册权限', Constant.hasShownGalleryIntroKey);
return _confirmPermissionIntro(
context,
Constant.hasShownGalleryIntroKey,
title: '“班小二APP”需要访问您的相册',
description: '为了方便您完成作业/打卡提交、纸本听写、作文、口算、错题上传、头像设置等学习任务,我们需要访问您的相册选择图片、视频上传。',
);
}
///
/// 首次使用相机和相册前的用途说明
///
Future<bool> _confirmCameraAndGalleryIntro(BuildContext context) {
return _confirmPermissionIntro(
context,
Constant.hasShownCameraAndGalleryIntroKey,
title: '“班小二APP”需要访问您的相机、相册',
description: '为了方便您完成作业/打卡提交、纸本听写、作文、口算、错题上传、头像设置等学习任务,我们需要使用您的相机拍摄照片,并访问您的相册以保存拍摄的图片。',
);
}
///
/// 首次使用相机、麦克风和相册前的用途说明
///
Future<bool> _confirmCameraAndMicrophoneAndGalleryIntro(BuildContext context) {
return _confirmPermissionIntro(
context,
Constant.hasShownCameraAndMicrophoneAndGalleryIntroKey,
title: '“班小二APP”需要访问您的相机、麦克风、相册',
description: '为了方便您录制视频并提交作业/打卡任务,我们需要使用您的相机进行拍摄,使用您的麦克风录制视频声音,并访问您的相册以保存录制的视频。',
);
}
///
......@@ -1239,6 +1294,14 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
return PermissionStatus.permanentlyDenied == status;
}
///
/// 检测麦克风权限是否被永久拒绝
///
Future<bool> _checkMicrophonePermanentlyDenied() async {
PermissionStatus status = await Permission.microphone.status;
return PermissionStatus.permanentlyDenied == status;
}
void setChooseVideoCmdFlag(bool chooseVideoCmdFlag, String chooseVideoCmdMessage) {
emit(state.copyWith(chooseVideoCmdFlag: chooseVideoCmdFlag, chooseVideoCmdMessage: chooseVideoCmdMessage));
}
......@@ -1289,7 +1352,11 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
if (await _checkGalleryPermanentlyDenied()) {
var resp = {'unique': unique, 'cmd': cmd, 'data': null, 'errMsg': 'no auth'};
_sendResponse(resp);
showPermissionSettingsDialog(context, '相册权限');
showPermissionSettingsDialog(
context,
title: '“班小二APP”需要访问您的相册',
description: '未开启相册权限,无法选择视频。您可以在"设置"中开启后重试。',
);
return;
}
......@@ -1340,15 +1407,19 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
void _chooseVideoFromCamera(BuildContext context, int maxDuration, String unique, String cmd) async {
// 检查是否已被永久拒绝,此时需要对用户进行引导
if (await _checkCameraPermanentlyDenied()) {
if (await _checkCameraPermanentlyDenied() || await _checkMicrophonePermanentlyDenied() || await _checkGalleryPermanentlyDenied()) {
var resp = {'unique': unique, 'cmd': cmd, 'data': null, 'errMsg': 'no auth'};
_sendResponse(resp);
showPermissionSettingsDialog(context, '相机权限');
showPermissionSettingsDialog(
context,
title: '“班小二APP”需要访问您的相机、麦克风、相册',
description: '未开启相机权限、或麦克风权限、或相册权限,无法拍摄。您可以在"设置"中开启后重试。',
);
return;
}
// 首次使用相机前告知用户用途,用户暂不开启则直接返回
if (!await _confirmCameraIntro(context)) {
if (!await _confirmCameraAndMicrophoneAndGalleryIntro(context)) {
var cancelResp = {'unique': unique, 'cmd': cmd, 'data': null, 'errMsg': 'cancel'};
_sendResponse(cancelResp);
return;
......
......@@ -130,9 +130,15 @@ class Constant {
/// 首次使用相册前,是否已显示相册用途说明弹窗(仅在用户点击「继续」时写入)
static const String hasShownGalleryIntroKey = 'has_shown_gallery_intro';
/// 首次使用相机和相册前,是否已显示相机和相册用途说明弹窗(仅在用户点击「继续」时写入)
static const String hasShownCameraAndGalleryIntroKey = 'has_shown_camera_and_gallery_intro';
/// 首次使用麦克风前,是否已显示麦克风用途说明弹窗(仅在用户点击「继续」时写入)
static const String hasShownMicrophoneIntroKey = 'has_shown_microphone_intro';
/// 首次使用相机和麦克风和相册前,是否已显示相机和麦克风和相册用途说明弹窗(仅在用户点击「继续」时写入)
static const String hasShownCameraAndMicrophoneAndGalleryIntroKey = 'has_shown_camera_and_microphone_and_gallery_intro';
/// 测试阶段使用
static const bool needIM = false;
static const bool needUpgrade = true;
......
......@@ -2,26 +2,6 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
/// 弹窗标题中使用的应用名称
const String permissionDialogAppName = '班小二';
/// 去掉「权限」后缀,得到归一化后的权限名称(兼容「相机」与「相机权限」两种写法)
String normalizePermissionName(String permission) {
return permission.endsWith('权限') ? permission.substring(0, permission.length - 2) : permission;
}
/// 标题句式中与默认写法不同的权限(相册为「访问」,其余为「使用」)
const Map<String, String> _titleSuffixes = {
'相册': '需要访问您的相册',
};
/// 权限弹窗标题,例如:「班小二APP」需要使用相机、「班小二APP」需要访问您的相册
String permissionDialogTitle(String permission) {
final name = normalizePermissionName(permission);
final suffix = _titleSuffixes[name] ?? '需要使用$name';
return '"${permissionDialogAppName}APP"$suffix';
}
/// 权限类弹窗的纯样式骨架
///
/// 只负责渐变背景、盾牌插画、标题、说明以及主次按钮的排版与视觉,
......
......@@ -7,26 +7,23 @@ import 'permission_dialog_shell.dart';
/// 返回 `true` 表示用户点击「继续」,可以继续进行权限申请与后续业务;
/// 返回 `false` 表示用户点击「暂不开启」或点击遮罩关闭。
class PermissionIntroDialog extends StatelessWidget {
const PermissionIntroDialog({super.key, required this.permission});
const PermissionIntroDialog({
super.key,
required this.title,
required this.description,
});
/// 权限名称,兼容「相机」与「相机权限」两种写法
final String permission;
/// 外部传入的完整标题,原样展示
final String title;
/// 各权限的用途说明
static const Map<String, String> _descriptions = {
'相机': '为了方便您完成作业/打卡提交、纸本听写、作文、口算、错题上传、头像设置等学习任务,我们需要使用您的相机拍摄照片或视频。',
'相册': '为了方便您完成作业/打卡提交、纸本听写、作文、口算、错题上传、头像设置等学习任务,我们需要访问您的相册选择图片、视频上传。',
'麦克风': '为了方便您完成跟读、朗读、背诵等评测及在线音频作业录制,我们需要使用您的麦克风录制音频。',
};
static const String _fallbackDescription = '为了方便您正常使用相关功能,我们需要使用您的';
/// 外部传入的完整说明,原样展示
final String description;
@override
Widget build(BuildContext context) {
final name = normalizePermissionName(permission);
return PermissionDialogShell(
title: permissionDialogTitle(permission),
description: _descriptions[name] ?? '$_fallbackDescription$name。',
title: title,
description: description,
primaryText: '继续',
onPrimary: () => Navigator.of(context).pop(true),
secondaryText: '暂不开启',
......@@ -38,10 +35,14 @@ class PermissionIntroDialog extends StatelessWidget {
/// 展示首次使用权限前的用途说明弹窗
///
/// 返回 `true` 表示用户同意继续。
Future<bool> showPermissionIntroDialog(BuildContext context, String permission) async {
Future<bool> showPermissionIntroDialog(
BuildContext context, {
required String title,
required String description,
}) async {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) => PermissionIntroDialog(permission: permission),
builder: (BuildContext context) => PermissionIntroDialog(title: title, description: description),
);
return result == true;
}
......@@ -8,26 +8,23 @@ import 'permission_dialog_shell.dart';
/// 与首次使用的用途说明弹窗共用样式骨架,
/// 差异在于文案面向「已拒绝」场景,主按钮直接跳转系统应用设置页。
class PermissionSettingsDialog extends StatelessWidget {
const PermissionSettingsDialog({super.key, required this.permission});
const PermissionSettingsDialog({
super.key,
required this.title,
required this.description,
});
/// 权限名称,兼容「相机」与「相机权限」两种写法
final String permission;
/// 外部传入的完整标题,原样展示
final String title;
/// 各权限被拒绝后的引导说明
static const Map<String, String> _descriptions = {
'相机': '未开启相机权限,无法拍摄。您可以在"设置"中开启后重试。',
'相册': '未开启相册权限,无法选择图片。您可以在"设置"中开启后重试。',
'麦克风': '未开启麦克风权限,无法录音评测。您可以在"设置"中开启后重试。',
};
static const String _fallbackDescription = '未开启对应权限,无法使用相关功能。您可以在"设置"中开启后重试。';
/// 外部传入的完整说明,原样展示
final String description;
@override
Widget build(BuildContext context) {
final name = normalizePermissionName(permission);
return PermissionDialogShell(
title: permissionDialogTitle(permission),
description: _descriptions[name] ?? _fallbackDescription,
title: title,
description: description,
primaryText: '去设置',
onPrimary: () {
Navigator.of(context).pop();
......@@ -41,9 +38,13 @@ class PermissionSettingsDialog extends StatelessWidget {
}
/// 展示权限被永久拒绝后的「去设置」引导弹窗
Future<void> showPermissionSettingsDialog(BuildContext context, String permission) {
Future<void> showPermissionSettingsDialog(
BuildContext context, {
required String title,
required String description,
}) {
return showDialog<void>(
context: context,
builder: (BuildContext context) => PermissionSettingsDialog(permission: permission),
builder: (BuildContext context) => PermissionSettingsDialog(title: title, description: description),
);
}
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!