提交 39ff5237 authored 作者: 王鹏飞's avatar 王鹏飞

feat: Update splash screen privacy notice handling and add system navigator pop on decline

refactor: Remove TTS related files and controller for cleaner codebase fix: Improve microphone permission request with explanation context style: Clean up user settings view and improve toast utility chore: Update asset picker to streamline permission checks fix: Update SQL manager for better database handling and query efficiency chore: Update constants for user agreements with versioning refactor: General code cleanup and formatting across multiple files
上级 67ef61b0
...@@ -2,10 +2,8 @@ ...@@ -2,10 +2,8 @@
package="com.zijing.book.flutterBook"> package="com.zijing.book.flutterBook">
<!-- INTERNET 权限在开发过程中是必需的。具体来说,Flutter 需要它与正在运行的应用程序进行通信,以允许设置断点、提供热重载等功能--> <!-- INTERNET 权限在开发过程中是必需的。具体来说,Flutter 需要它与正在运行的应用程序进行通信,以允许设置断点、提供热重载等功能-->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- STORAGE 权限是用于 TTS 合成到文件的--> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 加入tts服务--> <!-- 加入tts服务-->
<queries> <queries>
<intent> <intent>
......
...@@ -44,8 +44,6 @@ post_install do |installer| ...@@ -44,8 +44,6 @@ post_install do |installer|
'PERMISSION_MICROPHONE=1', 'PERMISSION_MICROPHONE=1',
'PERMISSION_CAMERA=1', 'PERMISSION_CAMERA=1',
'PERMISSION_PHOTOS=1', 'PERMISSION_PHOTOS=1',
'PERMISSION_SPEECH_RECOGNIZER=1',
'PERMISSION_MEDIA_LIBRARY=1',
] ]
end end
end end
......
...@@ -46,16 +46,12 @@ ...@@ -46,16 +46,12 @@
<key>NSAllowsArbitraryLoadsInWebContent</key> <key>NSAllowsArbitraryLoadsInWebContent</key>
<true/> <true/>
</dict> </dict>
<key>NSAppleMusicUsageDescription</key>
<string>清控紫荆数智学堂需要访问媒体</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>清控紫荆数智学堂需要访问相机</string> <string>清控紫荆数智学堂需要访问相机</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>清控紫荆数智学堂需要访问麦克风</string> <string>清控紫荆数智学堂需要访问麦克风</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>清控紫荆数智学堂需要访问照片</string> <string>清控紫荆数智学堂需要访问照片</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>清控紫荆数智学堂需要语言识别</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
......
...@@ -20,12 +20,9 @@ import '../../store/index.dart'; ...@@ -20,12 +20,9 @@ import '../../store/index.dart';
import '../../widgets/index.dart'; import '../../widgets/index.dart';
import '../course/index.dart'; import '../course/index.dart';
part 'view.dart'; part 'view.dart';
part 'controller.dart'; part 'controller.dart';
part 'widgets/cell.dart'; part 'widgets/cell.dart';
part 'widgets/content.dart'; part 'widgets/content.dart';
part 'widgets/subject.dart'; part 'widgets/subject.dart';
part 'widgets/filter.dart'; part 'widgets/filter.dart';
part 'test.dart';
part of library;
class TestPage extends StatefulWidget {
const TestPage({Key? key}) : super(key: key);
@override
State<LibraryPage> createState() => _LibraryPageState();
}
class _TestPageState extends State<TestPage> with AutomaticKeepAliveClientMixin {
//AutomaticKeepAliveClientMixin
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('分类'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(48.w),
child: Row(
children: [
// TabBar放在左侧
const Expanded(
child: TabBar(
tabs: [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
Tab(text: 'Tab 4'),
Tab(text: 'Tab 5'),
],
),
),
// 筛选按钮放在右侧
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () {
// 处理筛选按钮点击事件
},
),
],
),
)
),
body: Column(
children: [
Container(
height: 43, // 设置标签栏的高度
color: Colors.cyan, // 设置标签栏的颜色
// child: // 添加你的横向滑动标签组件
),
Expanded(
child: CustomScrollView(
slivers: [
// // 横向滑动的标签
// SliverToBoxAdapter(
// child: Container(
// height: 43, // 设置标签栏的高度
// color: Colors.grey, // 设置标签栏的颜色
// // child: // 添加你的横向滑动标签组件
// ),
// ),
// 广告位
SliverToBoxAdapter(
child: Container(
height: 100, // 设置广告位的高度
color: Colors.grey, // 设置广告位的颜色
)
),
SliverFillRemaining(
child: TabBarView(
children: [
// Tab 1 对应的内容
Container(
color: Colors.red,
),
// Tab 2 对应的内容
Container(
color: Colors.green,
),
// Tab 3 对应的内容
Container(
color: Colors.blue,
),
Container(
color: Colors.blue,
),
Container(
color: Colors.blue,
),
],
),
),
],
),
),
],
),
);
}
@override
bool get wantKeepAlive => true;
}
part of record;
\ No newline at end of file
library record;
import 'dart:io';
import 'package:audio_session/audio_session.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:flutter_sound/public/flutter_sound_player.dart';
import 'package:flutter_sound/public/flutter_sound_recorder.dart';
import 'package:flutter_sound_platform_interface/flutter_sound_platform_interface.dart';
import 'package:flutter_sound_platform_interface/flutter_sound_recorder_platform_interface.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../utils/index.dart';
part 'view.dart';
part 'controller.dart';
\ No newline at end of file
part of record;
typedef _Fn = void Function();
const theSource = AudioSource.microphone;
class RecordPage extends StatefulWidget {
const RecordPage({Key? key}) : super(key: key);
@override
State<RecordPage> createState() => _RecordPageState();
}
class _RecordPageState extends State<RecordPage> {
final FlutterSoundRecorder? _mRecorder = FlutterSoundRecorder();
final FlutterSoundPlayer? _mPlayer = FlutterSoundPlayer();
final String _mPath = 'tau_file.mp4';
final Codec _codec = Codec.aacMP4;
bool _mRecorderIsInited = false;
@override
void initState() {
openTheRecorder().then((value) {
setState(() {
_mRecorderIsInited = true;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue,
appBar: AppBar(
title: const Text('Recorder'),
),
body: Column(
children: [
Container(
margin: EdgeInsets.all(3.w),
padding: EdgeInsets.all(3.w),
height: 80.w,
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.indigo,
width: 3
)
),
child: Row(
children: [
ElevatedButton(
onPressed: getRecorderFn(),
child: Text(_mRecorder!.isRecording?'stop':'Record')
),
SizedBox(
width: 20.w,
),
Text(_mRecorder!.isRecording? 'Recording in progress' : 'Record is stopped')
],
),
),
Container(
margin: EdgeInsets.all(3.w),
padding: EdgeInsets.all(3.w),
height: 80,
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFFAF0E6),
border: Border.all(
color: Colors.indigo,
width: 3,
),
),
child: Row(children: [
ElevatedButton(
onPressed: (){
},
//color: Colors.white,
//disabledColor: Colors.grey,
child: Text(_mPlayer!.isPlaying ? 'Stop' : 'Play'),
),
const SizedBox(
width: 20,
),
Text(_mPlayer!.isPlaying
? 'Playback in progress'
: 'Player is stopped'),
]),
)
],
),
);
}
Future<void> openTheRecorder() async {
Permission permission = Permission.microphone;
var status = await permission.request();
if (status != PermissionStatus.granted){
throw RecordingPermissionException('Microphone permission not granted');
}else if (status == PermissionStatus.denied) {
requestPermission(permission);
} else if(status == PermissionStatus.permanentlyDenied){
requestPermission(permission);
} else if(status == PermissionStatus.restricted){
requestPermission(permission);
}
await _mRecorder!.openRecorder();
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy:
AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.voiceCommunication,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
_mRecorderIsInited = true;
}
_Fn? getRecorderFn() {
if (!_mRecorderIsInited){
return null;
}
return _mRecorder!.isStopped ? record : stopRecorder;
}
void record() async {
Directory? tempDir = await getExternalStorageDirectory();
var time = DateTime.now().millisecondsSinceEpoch;
const String mPath = 'tau_file.mp4';
String filePath = '${tempDir!.path}/$time$mPath';
_mRecorder!.startRecorder(
toFile: filePath,
codec: _codec,
audioSource: theSource
).then((value) {
setState(() {});
});
}
void stopRecorder() async{
await _mRecorder!.stopRecorder().then((value) async {
Directory? tempDir = await getExternalStorageDirectory();
List<FileSystemEntity> files = tempDir!.listSync();
for (FileSystemEntity file in files) {
Console.log('File: ${file.path}');
}
setState(() {
});
});
}
void requestPermission(Permission permission) async {
PermissionStatus status = await permission.request();
if (status.isPermanentlyDenied){
openAppSettings();
}
}
}
...@@ -2,6 +2,7 @@ library splash_page; ...@@ -2,6 +2,7 @@ library splash_page;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_book/apis/index.dart'; import 'package:flutter_book/apis/index.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
......
...@@ -19,7 +19,11 @@ class _SplashPageState extends State<SplashPage> { ...@@ -19,7 +19,11 @@ class _SplashPageState extends State<SplashPage> {
} }
Future<void> _startLaunchFlow() async { Future<void> _startLaunchFlow() async {
await _showPrivacyNoticeIfNeeded(); final accepted = await _showPrivacyNoticeIfNeeded();
if (!accepted) {
await SystemNavigator.pop();
return;
}
if (!mounted || _navigated) return; if (!mounted || _navigated) return;
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
...@@ -42,12 +46,12 @@ class _SplashPageState extends State<SplashPage> { ...@@ -42,12 +46,12 @@ class _SplashPageState extends State<SplashPage> {
context.pushReplacementNamed(Routes.main); context.pushReplacementNamed(Routes.main);
} }
Future<void> _showPrivacyNoticeIfNeeded() async { Future<bool> _showPrivacyNoticeIfNeeded() async {
if (StorageService.to.getBool(kLocalPrivacyNoticeShown)) { if (StorageService.to.getBool(kLocalPrivacyNoticeShown)) {
return; return true;
} }
await showDialog<void>( final agreed = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (dialogContext) { builder: (dialogContext) {
...@@ -120,23 +124,36 @@ class _SplashPageState extends State<SplashPage> { ...@@ -120,23 +124,36 @@ class _SplashPageState extends State<SplashPage> {
), ),
), ),
actions: [ actions: [
SizedBox( Row(
width: double.infinity, children: [
child: TextButton( Expanded(
onPressed: () async { child: TextButton(
await StorageService.to onPressed: () {
.setBool(kLocalPrivacyNoticeShown, true); Navigator.of(dialogContext).pop(false);
if (dialogContext.mounted) { },
Navigator.of(dialogContext).pop(); child: const Text('不同意'),
} ),
}, ),
child: const Text('我知道了'), Expanded(
), child: TextButton(
onPressed: () async {
await StorageService.to
.setBool(kLocalPrivacyNoticeShown, true);
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop(true);
}
},
child: const Text('同意'),
),
),
],
), ),
], ],
); );
}, },
); );
return agreed ?? false;
} }
@override @override
......
part of tts;
enum TtsState{playing, stopped, paused, continued}
class TTSController extends GetxController {
late FlutterTts flutterTts;
TtsState ttsState = TtsState.stopped;
late String textToSpeak;
@override
void onInit() {
initTts();
super.onInit();
}
initTts() {
flutterTts = FlutterTts();
// 设置语言
flutterTts.setLanguage('zh-Hans-CN');
// 设置语速
flutterTts.setSpeechRate(0.5);
// 设置音量
flutterTts.setVolume(1.0);
// 设置音调
flutterTts.setPitch(1.0);
flutterTts.setStartHandler(() {
ttsState = TtsState.playing;
});
flutterTts.setCompletionHandler(() {
ttsState = TtsState.stopped;
});
flutterTts.setPauseHandler(() {
ttsState = TtsState.paused;
});
flutterTts.setContinueHandler(() {
ttsState = TtsState.continued;
Console.log('继续');
});
flutterTts.setErrorHandler((message) {
Console.log('error:$message');
ttsState = TtsState.stopped;
});
flutterTts.setProgressHandler((text, start, end, word) {
});
}
Future speak(String text) async {
textToSpeak = text;
Console.log(text);
flutterTts.speak(text);
Console.log('读文字');
}
Future stop() async {
update();
}
Future pause() async {
if (ttsState == TtsState.playing){
flutterTts.pause();
ttsState = TtsState.paused;
}else if (ttsState == TtsState.paused || ttsState == TtsState.stopped){
flutterTts.continueHandler!();
}
}
@override
void onClose() {
flutterTts.stop();
super.onClose();
}
}
\ No newline at end of file
library tts;
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:flutter_book/utils/index.dart';
import 'package:get/get.dart';
part 'view.dart';
part 'controller.dart';
\ No newline at end of file
part of tts;
class TestTTSPage extends StatefulWidget {
const TestTTSPage({Key? key}) : super(key: key);
@override
State<TestTTSPage> createState() => _TestTTSPageState();
}
class _TestTTSPageState extends State<TestTTSPage> {
@override
Widget build(BuildContext context) {
return GetBuilder(
init: TTSController(),
builder: (controller) => Scaffold(
appBar: AppBar(
title: const Text('测试语音'),
centerTitle: true,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: Colors.yellow,
height: 150,
width: double.infinity,
// padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
child: const Text('你好我是小助手',textAlign: TextAlign.start,style: TextStyle(
color: Colors.black,
fontSize: 18
),),
),
GestureDetector(
onTap: (){
controller.speak("中华人民共和国(the People's Republic of China),简称“中国”,成立于1949年10月1日 [1],位于亚洲东部,太平洋西岸 [2],是工人阶级领导的、以工农联盟为基础的人民民主专政的社会主义国家 [3],以五星红旗为国旗 [4]、《义勇军进行曲》为国歌 [5],国徽中间是五星照耀下的天安门,周围是谷穗和齿轮 [6] [170],通用语言文字是普通话和规范汉字 [7],首都北京 [8],是一个以汉族为主体、56个民族共同组成的统一的多民族国家。中国陆地面积约960万平方千米,东部和南部大陆海岸线1.8万多千米,海域总面积约473万平方千米 [2]。海域分布有大小岛屿7600多个,其中台湾岛最大,面积35798平方千米 [2]。中国同14国接壤,与8国海上相邻。省级行政区划为23个省、5个自治区、4个直辖市、2个特别行政区。 [2]中国是世界上历史最悠久的国家之一,有着光辉灿烂的文化和光荣的革命传统 [3],世界遗产数量全球领先 [9]。1949年新中国成立后,进入社会主义革命和建设时期,1956年实现向社会主义过渡,此后社会主义建设在探索中曲折发展 [10]。“文化大革命”结束后实行改革开放,沿着中国特色社会主义道路,集中力量进行社会主义现代化建设 [3]。经过长期努力,中国特色社会主义进入了新时代。 [11] [136]中国是世界上人口最多的发展中国家,国土面积居世界第三位,是世界第二大经济体,并持续成为世界经济增长最大的贡献者,2020年经济总量突破100万亿元 [12-13] [125]。中国坚持独立自主的和平外交政策,是联合国安全理事会常任理事国,也是许多国际组织的重要成员,被认为是潜在超级大国之一");
},
child: Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(20),
color: Colors.red,
child: const Text('播放'),
),
),
GestureDetector(
onTap: (){
controller.pause();
},
child: Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(20),
color: Colors.blue,
child: const Text('暂停'),
),
)
],
),
)
);
}
}
...@@ -89,7 +89,11 @@ class UserEditNoteController extends GetxController { ...@@ -89,7 +89,11 @@ class UserEditNoteController extends GetxController {
Future<void> openTheRecorder() async { Future<void> openTheRecorder() async {
if (!initRecorder) { if (!initRecorder) {
// 获取权限 // 获取权限
if (await Access.microphone()) { if (await Access.microphone(
showExplanation: true,
purpose: '用于录制语音笔记,仅用于创建和播放您的录音内容。',
context: Get.context,
)) {
await _mRecorder.openRecorder(); await _mRecorder.openRecorder();
final session = await AudioSession.instance; final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration( await session.configure(AudioSessionConfiguration(
......
...@@ -105,8 +105,7 @@ class _UserSetPageState extends State<UserSetPage> { ...@@ -105,8 +105,7 @@ class _UserSetPageState extends State<UserSetPage> {
content: Wrap( content: Wrap(
children: [ children: [
Container( Container(
margin: margin: EdgeInsets.only(top: 44.w, bottom: 29.w),
EdgeInsets.only(top: 44.w, bottom: 29.w),
// 调整上下间距 // 调整上下间距
child: Center( child: Center(
child: Text( child: Text(
...@@ -157,10 +156,10 @@ class _UserSetPageState extends State<UserSetPage> { ...@@ -157,10 +156,10 @@ class _UserSetPageState extends State<UserSetPage> {
), ),
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
final result = await AccountAPI.logout(); CustomToast.loading();
if (result) { try {
// Toast.show('退出成功'); await AccountAPI.logout();
CustomToast.loading(); } finally {
await UserStore.to.logout(); await UserStore.to.logout();
await Tools.clearData(); await Tools.clearData();
CustomToast.dismiss(); CustomToast.dismiss();
...@@ -241,7 +240,7 @@ class _UserSetPageState extends State<UserSetPage> { ...@@ -241,7 +240,7 @@ class _UserSetPageState extends State<UserSetPage> {
UModel model = await MineAPI.update(); UModel model = await MineAPI.update();
_getModel = model; _getModel = model;
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
int update = verifyVersion(model.version!,packageInfo.version); int update = verifyVersion(model.version!, packageInfo.version);
if (update == 1) { if (update == 1) {
_showUpdateDialog(false); _showUpdateDialog(false);
} }
...@@ -422,36 +421,37 @@ class _UserSetPageState extends State<UserSetPage> { ...@@ -422,36 +421,37 @@ class _UserSetPageState extends State<UserSetPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (!forcedUpgrade) if (!forcedUpgrade)
Padding(padding: EdgeInsets.only(right: 28.w), Padding(
child: GestureDetector( padding: EdgeInsets.only(right: 28.w),
child: child: GestureDetector(
Container( child: Container(
width: 85.w, width: 85.w,
color: Colours.cFF, color: Colours.cFF,
alignment: Alignment.center, alignment: Alignment.center,
child: const Text( child: const Text(
'稍后再说', '稍后再说',
style: TextStyle( style: TextStyle(
color: Colours.c6, color: Colours.c6,
fontSize: 15, fontSize: 15,
fontWeight: Fonts.medium), fontWeight: Fonts.medium),
),
), ),
onTap: () => Navigator.of(context).pop(),
), ),
onTap: () => Navigator.of(context).pop(), ),
),),
Container( Container(
height: 39.5.w, // 设置分割线的高度 height: 39.5.w, // 设置分割线的高度
width: 1, // 设置分割线的宽度 width: 1, // 设置分割线的宽度
color: Colours.cLine, // 设置分割线的颜色 color: Colours.cLine, // 设置分割线的颜色
), ),
Padding(padding: EdgeInsets.only(left: 28.w), Padding(
child: GestureDetector( padding: EdgeInsets.only(left: 28.w),
child: GestureDetector(
child: Container( child: Container(
width: 85.w, width: 85.w,
color: Colours.cFF, color: Colours.cFF,
alignment: Alignment.center, alignment: Alignment.center,
child: const Text( child: const Text(
'立即更新', '立即更新',
style: TextStyle( style: TextStyle(
color: Colours.cAB1941, color: Colours.cAB1941,
...@@ -467,7 +467,6 @@ class _UserSetPageState extends State<UserSetPage> { ...@@ -467,7 +467,6 @@ class _UserSetPageState extends State<UserSetPage> {
}, },
), ),
), ),
], ],
), ),
], ],
...@@ -481,8 +480,6 @@ class _UserSetPageState extends State<UserSetPage> { ...@@ -481,8 +480,6 @@ class _UserSetPageState extends State<UserSetPage> {
); );
} }
/// TODO: 苹果市场app地址 /// TODO: 苹果市场app地址
_appUpdate() { _appUpdate() {
UpdateModel model = UpdateModel( UpdateModel model = UpdateModel(
......
...@@ -10,30 +10,15 @@ abstract class Access { ...@@ -10,30 +10,15 @@ abstract class Access {
String purpose = '', String purpose = '',
BuildContext? context, BuildContext? context,
}) async { }) async {
// 如果需要在申请权限前显示说明,且提供了上下文 if (Platform.isAndroid) return true;
if (showExplanation && context != null && purpose.isNotEmpty) { return _requestPermission(
final bool? shouldRequest = await _showPermissionExplanationDialog( permission: Permission.photos,
context: context, permissionName: '相册权限',
permissionName: Platform.isIOS ? '相册权限' : '存储权限', showExplanation: showExplanation,
purpose: purpose, purpose: purpose,
); context: context,
allowLimited: true,
// 用户取消,不申请权限 );
if (shouldRequest != true) {
return false;
}
}
if (Platform.isIOS) {
final result = await [Permission.photos].request();
return result[Permission.photos] == PermissionStatus.granted ||
result[Permission.photos] == PermissionStatus.limited;
}
if (Platform.isAndroid) {
final result = await [Permission.storage].request();
return result[Permission.storage] == PermissionStatus.granted;
}
return false;
} }
/// 相机权限 /// 相机权限
...@@ -45,22 +30,68 @@ abstract class Access { ...@@ -45,22 +30,68 @@ abstract class Access {
String purpose = '', String purpose = '',
BuildContext? context, BuildContext? context,
}) async { }) async {
// 如果需要在申请权限前显示说明,且提供了上下文 return _requestPermission(
permission: Permission.camera,
permissionName: '相机权限',
showExplanation: showExplanation,
purpose: purpose,
context: context,
);
}
/// 获取麦克风权限
static Future<bool> microphone({
bool showExplanation = false,
String purpose = '',
BuildContext? context,
}) async {
return _requestPermission(
permission: Permission.microphone,
permissionName: '麦克风权限',
showExplanation: showExplanation,
purpose: purpose,
context: context,
);
}
static Future<bool> _requestPermission({
required Permission permission,
required String permissionName,
required bool showExplanation,
required String purpose,
required BuildContext? context,
bool allowLimited = false,
}) async {
final status = await permission.status;
if (_isGranted(status, allowLimited: allowLimited)) {
return true;
}
if (showExplanation && context != null && purpose.isNotEmpty) { if (showExplanation && context != null && purpose.isNotEmpty) {
final bool? shouldRequest = await _showPermissionExplanationDialog( final bool? shouldRequest = await _showPermissionExplanationDialog(
context: context, context: context,
permissionName: '相机权限', permissionName: permissionName,
purpose: purpose, purpose: purpose,
); );
// 用户取消,不申请权限
if (shouldRequest != true) { if (shouldRequest != true) {
return false; return false;
} }
} }
final result = await [Permission.camera].request(); final result = await permission.request();
return result[Permission.camera] == PermissionStatus.granted; final granted = _isGranted(result, allowLimited: allowLimited);
if (!granted &&
context != null &&
context.mounted &&
result.isPermanentlyDenied) {
_showSettingDialog(context, '获取$permissionName');
}
return granted;
}
static bool _isGranted(PermissionStatus status, {bool allowLimited = false}) {
return status == PermissionStatus.granted ||
(allowLimited && status == PermissionStatus.limited);
} }
/// 显示权限使用目的说明对话框 /// 显示权限使用目的说明对话框
...@@ -177,18 +208,111 @@ abstract class Access { ...@@ -177,18 +208,111 @@ abstract class Access {
); );
} }
/// 打开设置 static void _showSettingDialog(BuildContext context, String content) {
static Future<void> setting() async => await openAppSettings(); showDialog(
context: context,
/// 存储权限 builder: (BuildContext dialogContext) {
static Future<bool> storage() async { return WillPopScope(
final result = await [Permission.storage].request(); onWillPop: () => Future.value(true),
return result[Permission.storage] == PermissionStatus.granted; child: AlertDialog(
insetPadding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0.w),
),
actionsPadding: EdgeInsets.zero,
title: Container(
alignment: Alignment.center,
child: Text(
'权限申请',
style: TextStyle(
fontSize: 20.w,
color: Colours.c3,
fontWeight: Fonts.boldSemi),
),
),
actions: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 5.5.w),
child: Text(
content,
style: TextStyle(
fontSize: 15.w,
color: Colours.c9,
fontWeight: Fonts.medium),
),
),
SizedBox(height: 22.w),
Container(
height: 1,
width: 216.5.w,
color: Colours.cLine,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: EdgeInsets.only(right: 28.w),
child: GestureDetector(
onTap: () => Navigator.of(dialogContext).pop(),
child: Container(
width: 85.w,
color: Colours.cFF,
alignment: Alignment.center,
child: const Text(
'取消',
style: TextStyle(
color: Colours.c6,
fontSize: 15,
fontWeight: Fonts.medium),
),
),
),
),
Container(
height: 39.5.w,
width: 1,
color: Colours.cLine,
),
Padding(
padding: EdgeInsets.only(left: 28.w),
child: GestureDetector(
onTap: () {
Navigator.of(dialogContext).pop();
setting();
},
child: Container(
width: 85.w,
color: Colours.cFF,
alignment: Alignment.center,
child: const Text(
'设置',
style: TextStyle(
color: Colours.cAB1941,
fontSize: 15,
fontWeight: Fonts.boldSemi),
),
),
),
),
],
),
],
),
],
),
],
),
);
},
);
} }
/// 获取麦克风权限 /// 打开设置
static Future<bool> microphone() async { static Future<void> setting() async => await openAppSettings();
final result = await [Permission.microphone].request();
return result[Permission.microphone] == PermissionStatus.granted;
}
} }
...@@ -12,39 +12,15 @@ abstract class AssetsPicker { ...@@ -12,39 +12,15 @@ abstract class AssetsPicker {
double maxHeight = 1024, double maxHeight = 1024,
String purpose = '用于从相册中选择图片,用于设置头像、上传图片等功能。', String purpose = '用于从相册中选择图片,用于设置头像、上传图片等功能。',
}) async { }) async {
// 先检查权限状态 final granted = await Access.photos(
bool hasPermission = false; showExplanation: true,
if (Platform.isIOS) { purpose: purpose,
final status = await Permission.photos.status; context: context,
hasPermission = status == PermissionStatus.granted || status == PermissionStatus.limited; );
} else if (Platform.isAndroid) { if (!granted) {
final status = await Permission.storage.status; return null;
hasPermission = status == PermissionStatus.granted;
}
// 如果没有权限,先显示说明对话框,然后申请权限
if (!hasPermission) {
final granted = await Access.photos(
showExplanation: true,
purpose: purpose,
context: context,
);
if (!granted) {
// 用户拒绝或取消,检查是否需要跳转设置
if (context.mounted) {
final status = Platform.isIOS
? await Permission.photos.status
: await Permission.storage.status;
if (status.isPermanentlyDenied) {
_showSettingDialog(context, '获取相册权限');
}
}
return null;
}
} }
return _imagePicker.pickImage( return _imagePicker.pickImage(
source: source, source: source,
// maxWidth: maxWidth, // maxWidth: maxWidth,
...@@ -52,7 +28,7 @@ abstract class AssetsPicker { ...@@ -52,7 +28,7 @@ abstract class AssetsPicker {
// imageQuality: 100 // imageQuality: 100
); );
} }
/// 拍照 /// 拍照
/// [purpose] 权限使用目的说明,用于在申请权限前告知用户 /// [purpose] 权限使用目的说明,用于在申请权限前告知用户
static Future<XFile?> camera({ static Future<XFile?> camera({
...@@ -62,138 +38,25 @@ abstract class AssetsPicker { ...@@ -62,138 +38,25 @@ abstract class AssetsPicker {
double maxHeight = 512, double maxHeight = 512,
String purpose = '用于使用相机拍照,用于设置头像、上传图片等功能。', String purpose = '用于使用相机拍照,用于设置头像、上传图片等功能。',
}) async { }) async {
// 先检查权限状态 final granted = await Access.camera(
final cameraStatus = await Permission.camera.status; showExplanation: true,
bool hasPermission = cameraStatus == PermissionStatus.granted; purpose: purpose,
// 如果没有相机权限,先显示说明对话框,然后申请权限
if (!hasPermission) {
final granted = await Access.camera(
showExplanation: true,
purpose: purpose,
context: context,
);
if (!granted) {
// 用户拒绝或取消,检查是否需要跳转设置
if (context.mounted) {
final status = await Permission.camera.status;
if (status.isPermanentlyDenied) {
_showSettingDialog(context, '获取相机权限');
}
}
return null;
}
}
return _imagePicker.pickImage(
source: source,
// maxWidth: maxWidth,
// maxHeight: maxHeight,
);
}
/// 展示设置弹窗
static _showSettingDialog(BuildContext context,String content) {
showDialog(
context: context, context: context,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () => Future.value(true),
child: AlertDialog(
insetPadding: EdgeInsets.zero, // 设置水平边距
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0.w),
),
// 去除操作按钮区域的内边距
actionsPadding: EdgeInsets.zero,
title: Container(
alignment: Alignment.center,
child: Text('权限申请', style: TextStyle(
fontSize: 20.w,
color: Colours.c3,
fontWeight: Fonts.boldSemi),),
),
actions: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 5.5.w),
child: Text(
content,
style: TextStyle(
fontSize: 15.w,
color: Colours.c9,
fontWeight: Fonts.medium),
),
),
SizedBox(
height: 22.w,
),
Container(
height: 1, // 设置分割线的高度
width: 216.5.w, // 设置分割线的宽度
color: Colours.cLine, // 设置分割线的颜色
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(padding: EdgeInsets.only(right: 28.w),
child: GestureDetector(
child:
Container(
width: 85.w,
color: Colours.cFF,
alignment: Alignment.center,
child: const Text(
'取消',
style: TextStyle(
color: Colours.c6,
fontSize: 15,
fontWeight: Fonts.medium),
),
),
onTap: () => Navigator.of(context).pop(),
),),
Container(
height: 39.5.w, // 设置分割线的高度
width: 1, // 设置分割线的宽度
color: Colours.cLine, // 设置分割线的颜色
),
Padding(padding: EdgeInsets.only(left: 28.w),
child: GestureDetector(
child: Container(
width: 85.w,
color: Colours.cFF,
alignment: Alignment.center,
child: const Text(
'设置',
style: TextStyle(
color: Colours.cAB1941,
fontSize: 15,
fontWeight: Fonts.boldSemi),
),
),
onTap: () {
Access.setting();
},
),
),
],
),
],
),
],
),
],
),
);
},
); );
if (!granted) {
return null;
}
try {
return await _imagePicker.pickImage(
source: source,
// maxWidth: maxWidth,
// maxHeight: maxHeight,
);
} catch (error) {
Console.log('打开相机失败: $error');
Toast.show('打开相机失败,请检查权限后重试');
return null;
}
} }
} }
...@@ -14,11 +14,11 @@ const String kSearchHistory = 'search_history'; ...@@ -14,11 +14,11 @@ const String kSearchHistory = 'search_history';
const String kFailOrder = 'failOrder'; const String kFailOrder = 'failOrder';
const String kNoteTable = 'members_book_notes'; const String kNoteTable = 'members_book_notes';
const String kReadTable = 'read_history'; const String kReadTable = 'read_history';
const String kUserAgreement = '$kHtmlBaseServer/agreement/ser_agreement.html'; const String kUserAgreement = '$kHtmlBaseServer/agreement/ser_agreement.html?v=1';
const String kUserPriAgreement = const String kUserPriAgreement =
'$kHtmlBaseServer/agreement/pri_agreement.html'; '$kHtmlBaseServer/agreement/pri_agreement.html?v=1';
const String kUserRechargeAgreement = const String kUserRechargeAgreement =
'$kHtmlBaseServer/agreement/rec_agreement.html'; '$kHtmlBaseServer/agreement/rec_agreement.html?v=1';
// 错题详情页 html // 错题详情页 html
const String kUserWrongDes = '$kHtmlBaseServer/evaluating_wrong.html'; const String kUserWrongDes = '$kHtmlBaseServer/evaluating_wrong.html';
......
差异被折叠。
...@@ -4,14 +4,8 @@ part of utils; ...@@ -4,14 +4,8 @@ part of utils;
class Toast { class Toast {
/// 展示toast /// 展示toast
static void show(String msg, {int duration = 2000}) { static void show(String msg, {int duration = 2000}) {
if (msg == null) { showToast(msg,
return; duration: Duration(milliseconds: duration), dismissOtherToast: true);
}
showToast(
msg,
duration: Duration(milliseconds: duration),
dismissOtherToast: true
);
} }
/// 取消toast /// 取消toast
......
part of utils; part of utils;
abstract class Tools { abstract class Tools {
/// 取消焦点 /// 取消焦点
static void unfocus() { static void unfocus() {
...@@ -9,11 +8,11 @@ abstract class Tools { ...@@ -9,11 +8,11 @@ abstract class Tools {
/// 格式化日期 /// 格式化日期
static String dateFromMS( static String dateFromMS(
int timestamp, { int timestamp, {
String pattern = 'yyyy-MM-dd', String pattern = 'yyyy-MM-dd',
bool humanize = false, bool humanize = false,
}) { }) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp*1000); final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
if (humanize) { if (humanize) {
final now = DateTime.now(); final now = DateTime.now();
final difference = now.difference(dateTime); final difference = now.difference(dateTime);
...@@ -43,11 +42,11 @@ abstract class Tools { ...@@ -43,11 +42,11 @@ abstract class Tools {
static Future<String> getDirectory() async { static Future<String> getDirectory() async {
// getTemporaryDirectory // getTemporaryDirectory
final directory = await getTemporaryDirectory(); final directory = await getTemporaryDirectory();
return directory!.path; return directory.path;
} }
/// 语音文件名称 /// 语音文件名称
static String generateVoiceFileName(){ static String generateVoiceFileName() {
DateTime now = DateTime.now(); DateTime now = DateTime.now();
String formattedDate = DateFormat('yyyyMMddHHmmss').format(now); String formattedDate = DateFormat('yyyyMMddHHmmss').format(now);
return 'voice_$formattedDate.mp4'; return 'voice_$formattedDate.mp4';
...@@ -63,7 +62,7 @@ abstract class Tools { ...@@ -63,7 +62,7 @@ abstract class Tools {
/// 判断当前网络状态 /// 判断当前网络状态
static Future<bool> checkCurrentNetStatus() async { static Future<bool> checkCurrentNetStatus() async {
final connectivityResult = await (Connectivity().checkConnectivity()); final connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none){ if (connectivityResult == ConnectivityResult.none) {
return false; return false;
} }
return true; return true;
...@@ -108,7 +107,3 @@ abstract class Tools { ...@@ -108,7 +107,3 @@ abstract class Tools {
// } // }
// } // }
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论