Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
ethan
/
appframe
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit b24210eb
authored
2026-11-04 09:41:33 +0800
by
tanghuan
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
权限提示调整
1 parent
ac90d982
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
128 additions
and
69 deletions
lib/bloc/web_cubit.dart
lib/config/constant.dart
lib/ui/widgets/permission/permission_dialog_shell.dart
lib/ui/widgets/permission/permission_intro_dialog.dart
lib/ui/widgets/permission/permission_settings_dialog.dart
lib/bloc/web_cubit.dart
View file @
b24210e
...
...
@@ -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
_confirmCamera
AndGallery
Intro
(
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
,
permiss
ion
))
{
if
(!
await
showPermissionIntroDialog
(
context
,
title:
title
,
description:
descript
ion
))
{
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
_confirmCamera
AndMicrophoneAndGallery
Intro
(
context
))
{
var
cancelResp
=
{
'unique'
:
unique
,
'cmd'
:
cmd
,
'data'
:
null
,
'errMsg'
:
'cancel'
};
_sendResponse
(
cancelResp
);
return
;
...
...
lib/config/constant.dart
View file @
b24210e
...
...
@@ -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
;
...
...
lib/ui/widgets/permission/permission_dialog_shell.dart
View file @
b24210e
...
...
@@ -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
'
;
}
/// 权限类弹窗的纯样式骨架
///
/// 只负责渐变背景、盾牌插画、标题、说明以及主次按钮的排版与视觉,
...
...
lib/ui/widgets/permission/permission_intro_dialog.dart
View file @
b24210e
...
...
@@ -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:
permiss
ion
),
builder:
(
BuildContext
context
)
=>
PermissionIntroDialog
(
title:
title
,
description:
descript
ion
),
);
return
result
==
true
;
}
lib/ui/widgets/permission/permission_settings_dialog.dart
View file @
b24210e
...
...
@@ -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
]
??
_fallbackD
escription
,
title:
title
,
description:
d
escription
,
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:
permiss
ion
),
builder:
(
BuildContext
context
)
=>
PermissionSettingsDialog
(
title:
title
,
description:
descript
ion
),
);
}
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment