提交 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 @@
package="com.zijing.book.flutterBook">
<!-- INTERNET 权限在开发过程中是必需的。具体来说,Flutter 需要它与正在运行的应用程序进行通信,以允许设置断点、提供热重载等功能-->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- STORAGE 权限是用于 TTS 合成到文件的-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 加入tts服务-->
<queries>
<intent>
......
......@@ -44,8 +44,6 @@ post_install do |installer|
'PERMISSION_MICROPHONE=1',
'PERMISSION_CAMERA=1',
'PERMISSION_PHOTOS=1',
'PERMISSION_SPEECH_RECOGNIZER=1',
'PERMISSION_MEDIA_LIBRARY=1',
]
end
end
......
......@@ -46,16 +46,12 @@
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>清控紫荆数智学堂需要访问媒体</string>
<key>NSCameraUsageDescription</key>
<string>清控紫荆数智学堂需要访问相机</string>
<key>NSMicrophoneUsageDescription</key>
<string>清控紫荆数智学堂需要访问麦克风</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>清控紫荆数智学堂需要访问照片</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>清控紫荆数智学堂需要语言识别</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
......
......@@ -20,12 +20,9 @@ import '../../store/index.dart';
import '../../widgets/index.dart';
import '../course/index.dart';
part 'view.dart';
part 'controller.dart';
part 'widgets/cell.dart';
part 'widgets/content.dart';
part 'widgets/subject.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;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_book/apis/index.dart';
import 'package:go_router/go_router.dart';
......
......@@ -19,7 +19,11 @@ class _SplashPageState extends State<SplashPage> {
}
Future<void> _startLaunchFlow() async {
await _showPrivacyNoticeIfNeeded();
final accepted = await _showPrivacyNoticeIfNeeded();
if (!accepted) {
await SystemNavigator.pop();
return;
}
if (!mounted || _navigated) return;
await Future.delayed(const Duration(seconds: 2));
......@@ -42,12 +46,12 @@ class _SplashPageState extends State<SplashPage> {
context.pushReplacementNamed(Routes.main);
}
Future<void> _showPrivacyNoticeIfNeeded() async {
Future<bool> _showPrivacyNoticeIfNeeded() async {
if (StorageService.to.getBool(kLocalPrivacyNoticeShown)) {
return;
return true;
}
await showDialog<void>(
final agreed = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
......@@ -120,23 +124,36 @@ class _SplashPageState extends State<SplashPage> {
),
),
actions: [
SizedBox(
width: double.infinity,
Row(
children: [
Expanded(
child: TextButton(
onPressed: () {
Navigator.of(dialogContext).pop(false);
},
child: const Text('不同意'),
),
),
Expanded(
child: TextButton(
onPressed: () async {
await StorageService.to
.setBool(kLocalPrivacyNoticeShown, true);
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
Navigator.of(dialogContext).pop(true);
}
},
child: const Text('我知道了'),
child: const Text('同意'),
),
),
],
),
],
);
},
);
return agreed ?? false;
}
@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 {
Future<void> openTheRecorder() async {
if (!initRecorder) {
// 获取权限
if (await Access.microphone()) {
if (await Access.microphone(
showExplanation: true,
purpose: '用于录制语音笔记,仅用于创建和播放您的录音内容。',
context: Get.context,
)) {
await _mRecorder.openRecorder();
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
......
......@@ -105,8 +105,7 @@ class _UserSetPageState extends State<UserSetPage> {
content: Wrap(
children: [
Container(
margin:
EdgeInsets.only(top: 44.w, bottom: 29.w),
margin: EdgeInsets.only(top: 44.w, bottom: 29.w),
// 调整上下间距
child: Center(
child: Text(
......@@ -157,10 +156,10 @@ class _UserSetPageState extends State<UserSetPage> {
),
GestureDetector(
onTap: () async {
final result = await AccountAPI.logout();
if (result) {
// Toast.show('退出成功');
CustomToast.loading();
try {
await AccountAPI.logout();
} finally {
await UserStore.to.logout();
await Tools.clearData();
CustomToast.dismiss();
......@@ -241,7 +240,7 @@ class _UserSetPageState extends State<UserSetPage> {
UModel model = await MineAPI.update();
_getModel = model;
final packageInfo = await PackageInfo.fromPlatform();
int update = verifyVersion(model.version!,packageInfo.version);
int update = verifyVersion(model.version!, packageInfo.version);
if (update == 1) {
_showUpdateDialog(false);
}
......@@ -422,10 +421,10 @@ class _UserSetPageState extends State<UserSetPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (!forcedUpgrade)
Padding(padding: EdgeInsets.only(right: 28.w),
Padding(
padding: EdgeInsets.only(right: 28.w),
child: GestureDetector(
child:
Container(
child: Container(
width: 85.w,
color: Colours.cFF,
alignment: Alignment.center,
......@@ -438,14 +437,15 @@ class _UserSetPageState extends State<UserSetPage> {
),
),
onTap: () => Navigator.of(context).pop(),
),),
),
),
Container(
height: 39.5.w, // 设置分割线的高度
width: 1, // 设置分割线的宽度
color: Colours.cLine, // 设置分割线的颜色
),
Padding(padding: EdgeInsets.only(left: 28.w),
Padding(
padding: EdgeInsets.only(left: 28.w),
child: GestureDetector(
child: Container(
width: 85.w,
......@@ -467,7 +467,6 @@ class _UserSetPageState extends State<UserSetPage> {
},
),
),
],
),
],
......@@ -481,8 +480,6 @@ class _UserSetPageState extends State<UserSetPage> {
);
}
/// TODO: 苹果市场app地址
_appUpdate() {
UpdateModel model = UpdateModel(
......
......@@ -10,30 +10,15 @@ abstract class Access {
String purpose = '',
BuildContext? context,
}) async {
// 如果需要在申请权限前显示说明,且提供了上下文
if (showExplanation && context != null && purpose.isNotEmpty) {
final bool? shouldRequest = await _showPermissionExplanationDialog(
context: context,
permissionName: Platform.isIOS ? '相册权限' : '存储权限',
if (Platform.isAndroid) return true;
return _requestPermission(
permission: Permission.photos,
permissionName: '相册权限',
showExplanation: showExplanation,
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 {
String purpose = '',
BuildContext? context,
}) 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) {
final bool? shouldRequest = await _showPermissionExplanationDialog(
context: context,
permissionName: '相机权限',
permissionName: permissionName,
purpose: purpose,
);
// 用户取消,不申请权限
if (shouldRequest != true) {
return false;
}
}
final result = await [Permission.camera].request();
return result[Permission.camera] == PermissionStatus.granted;
final result = await permission.request();
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 {
);
}
/// 打开设置
static Future<void> setting() async => await openAppSettings();
/// 存储权限
static Future<bool> storage() async {
final result = await [Permission.storage].request();
return result[Permission.storage] == PermissionStatus.granted;
static void _showSettingDialog(BuildContext context, String content) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
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(
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 {
final result = await [Permission.microphone].request();
return result[Permission.microphone] == PermissionStatus.granted;
}
/// 打开设置
static Future<void> setting() async => await openAppSettings();
}
......@@ -12,38 +12,14 @@ abstract class AssetsPicker {
double maxHeight = 1024,
String purpose = '用于从相册中选择图片,用于设置头像、上传图片等功能。',
}) async {
// 先检查权限状态
bool hasPermission = false;
if (Platform.isIOS) {
final status = await Permission.photos.status;
hasPermission = status == PermissionStatus.granted || status == PermissionStatus.limited;
} else if (Platform.isAndroid) {
final status = await Permission.storage.status;
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(
source: source,
......@@ -62,138 +38,25 @@ abstract class AssetsPicker {
double maxHeight = 512,
String purpose = '用于使用相机拍照,用于设置头像、上传图片等功能。',
}) async {
// 先检查权限状态
final cameraStatus = await Permission.camera.status;
bool hasPermission = cameraStatus == PermissionStatus.granted;
// 如果没有相机权限,先显示说明对话框,然后申请权限
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(
try {
return await _imagePicker.pickImage(
source: source,
// maxWidth: maxWidth,
// maxHeight: maxHeight,
);
} catch (error) {
Console.log('打开相机失败: $error');
Toast.show('打开相机失败,请检查权限后重试');
return null;
}
/// 展示设置弹窗
static _showSettingDialog(BuildContext context,String content) {
showDialog(
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();
},
),
),
],
),
],
),
],
),
],
),
);
},
);
}
}
......@@ -14,11 +14,11 @@ const String kSearchHistory = 'search_history';
const String kFailOrder = 'failOrder';
const String kNoteTable = 'members_book_notes';
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 =
'$kHtmlBaseServer/agreement/pri_agreement.html';
'$kHtmlBaseServer/agreement/pri_agreement.html?v=1';
const String kUserRechargeAgreement =
'$kHtmlBaseServer/agreement/rec_agreement.html';
'$kHtmlBaseServer/agreement/rec_agreement.html?v=1';
// 错题详情页 html
const String kUserWrongDes = '$kHtmlBaseServer/evaluating_wrong.html';
......
part of utils;
class SqlManager {
static const _version = 1;
static const _name= "zi_jing_app_flutter.db";
static const _name = "zi_jing_app_flutter.db";
static Database? _database;
///初始化
static init() async {
// open the database
......@@ -17,7 +14,6 @@ class SqlManager {
// var databasesPath = await Tools.getDirectory();
String dbName = _name;
Console.log('Sql-----------databasesPath---------$databasesPath');
if(databasesPath != null) {
String path = databasesPath + dbName;
if (Platform.isIOS) {
path = "$databasesPath/$dbName";
......@@ -39,17 +35,14 @@ class SqlManager {
"content TEXT, "
"upload INTEGER DEFAULT 0, "
"positioning TEXT, "
"note_content TEXT)"
);
"note_content TEXT)");
// // 阅读章节表
await db.execute("CREATE TABLE $kReadTable ("
"id INTEGER PRIMARY KEY, "
"book_id INTEGER, "
"chapter_id INTEGER)"
);
"chapter_id INTEGER)");
});
}
}
static Future<Database?> getCurrentDatabase() async {
if (_database == null) {
......@@ -63,25 +56,24 @@ class SqlManager {
await _database?.close();
}
/// 查询划线高亮笔记
static Future<Map<String, dynamic>> queryLocalNote({required int bookId,required int chapterId}) async {
static Future<Map<String, dynamic>> queryLocalNote(
{required int bookId, required int chapterId}) async {
Database? db = await SqlManager.getCurrentDatabase();
Map<String, dynamic> returnMap = {};
// 划线
List<Map<String, dynamic>>? lineResult = await db?.query(
kNoteTable,
columns: ['types', 'chapter_id','positioning','content','color','id'],
columns: ['types', 'chapter_id', 'positioning', 'content', 'color', 'id'],
where: 'book_id = ? and chapter_id = ? and types = ? and del = ?',
whereArgs: [bookId, chapterId,1,0],
whereArgs: [bookId, chapterId, 1, 0],
);
// 高亮
List<Map<String, dynamic>>? colorResult = await db?.query(
kNoteTable,
columns: ['types', 'chapter_id','positioning','content','color','id'],
columns: ['types', 'chapter_id', 'positioning', 'content', 'color', 'id'],
where: 'book_id = ? and chapter_id = ? and types = ? and del = ?',
whereArgs: [bookId, chapterId,2,0],
whereArgs: [bookId, chapterId, 2, 0],
);
returnMap['line_list'] = lineResult;
returnMap['color_line'] = colorResult;
......@@ -97,71 +89,71 @@ class SqlManager {
data,
conflictAlgorithm: ConflictAlgorithm.replace,
);
return result??0;
return result ?? 0;
}
/// 删除划线高亮笔记
static Future<int> delLocalNote({required int noteId,required int id}) async {
static Future<int> delLocalNote(
{required int noteId, required int id}) async {
Database? db = await SqlManager.getCurrentDatabase();
if(noteId == 0){
if (noteId == 0) {
final result = await db?.update(
kNoteTable,
{'del': 1},
where: 'id = ?',
whereArgs: [id],
);
return result??0;
}
else{
return result ?? 0;
} else {
final result = await db?.update(
kNoteTable,
{'del': 1},
where: 'notes_id = ?',
whereArgs: [noteId],
);
return result??0;
return result ?? 0;
}
}
/// 修改划线高亮笔记
static Future<int> updateLocalNote({required int notesId,required int id,required Map<String, dynamic> data}) async {
static Future<int> updateLocalNote(
{required int notesId,
required int id,
required Map<String, dynamic> data}) async {
Database? db = await SqlManager.getCurrentDatabase();
if(notesId ==0){
if (notesId == 0) {
final result = await db?.update(
kNoteTable,
data,
where: 'id = ?',
whereArgs: [id],
);
return result??0;
}
else{
return result ?? 0;
} else {
final result = await db?.update(
kNoteTable,
data,
where: 'notes_id = ?',
whereArgs: [notesId],
);
return result??0;
return result ?? 0;
}
}
/// 查询所有没有上传的数据
static Future<List<Map<String, dynamic>>> queryNoUploadData() async {
Database? db = await SqlManager.getCurrentDatabase();
List<Map<String, dynamic>>? results = await db?.query(
kNoteTable,
where: 'upload = ? and del = ?',
whereArgs: [0,0],
whereArgs: [0, 0],
);
// List<Map<String, dynamic>>? results = await db?.query(
// 'members_book_notes',
// where: 'del = ?',
// whereArgs: [0],
// );
return results ??[];
return results ?? [];
}
/// 将所有 upload 为 0 的数据的 upload 字段值更新为 1
......@@ -175,8 +167,9 @@ class SqlManager {
);
Console.log('Sql---------------更新数据----------------$result');
}
/// 将上传成功的id数据的notes_id 更新为服务器对应数值 并设置upload 为1
static Future<void> updateNotesId(List<Map<String,dynamic>> data) async {
static Future<void> updateNotesId(List<Map<String, dynamic>> data) async {
Database? db = await SqlManager.getCurrentDatabase();
// 构建批量更新的SQL语句
String sql = 'UPDATE members_book_notes SET notes_id = CASE id ';
......@@ -205,57 +198,54 @@ class SqlManager {
db?.delete(kNoteTable);
db?.delete(kReadTable);
}
/// 根据 book_id 查询当前读到的 章节
static Future<String> queryReadHistoryByBookId(int bookId) async {
try {
Database? db = await SqlManager.getCurrentDatabase();
if (!db!.isOpen){
if (!db!.isOpen) {
await init();
}
List<Map<String, dynamic>>? results = await db?.query(
List<Map<String, dynamic>> results = await db.query(
kReadTable,
where: 'book_id = ?',
whereArgs: [bookId],
);
return results?.first['chapter_id'].toString() ?? '';
}
catch(e){
return results.isNotEmpty ? results.first['chapter_id'].toString() : '';
} catch (e) {
Console.log('Error querying read history by book id: $e');
return '';
}
}
/// 根据 book_id 写入当前读到的 章节
static Future<int> updateReadHistoryByBookId(int bookId ,int chapterId) async {
static Future<int> updateReadHistoryByBookId(
int bookId, int chapterId) async {
Database? db = await SqlManager.getCurrentDatabase();
final queryResult = await queryReadHistoryByBookId(bookId);
if (queryResult.isEmpty){
if (queryResult.isEmpty) {
Console.log('Sql----------没有当前书籍的数据----------------------');
final result = await db?.insert(
'read_history',
{'chapter_id':chapterId, 'book_id':bookId},
{'chapter_id': chapterId, 'book_id': bookId},
conflictAlgorithm: ConflictAlgorithm.replace,
);
Console.log('Sql----------插入数据结果:$result----------------------');
return result??0;
}
else{
return result ?? 0;
} else {
Console.log('Sql----------有当前书籍的数据----------------------');
final result = await db?.update(
'read_history',
{'chapter_id':chapterId},
{'chapter_id': chapterId},
where: 'book_id = ?',
whereArgs: [bookId],
);
Console.log('Sql----------更新数据结果:$result----------------------');
return result??0;
return result ?? 0;
}
}
/// 插入数据
static Future<bool> insertData(Map<String, dynamic> data) async {
Database? db = await SqlManager.getCurrentDatabase();
......@@ -264,13 +254,12 @@ class SqlManager {
data,
conflictAlgorithm: ConflictAlgorithm.replace,
);
if (result !=null){
if (result != null) {
return true;
}
return false;
}
/// 查询所有数据
static Future<List<Map<String, dynamic>>?> queryAllData() async {
Database? db = await SqlManager.getCurrentDatabase();
......@@ -285,7 +274,7 @@ class SqlManager {
where: 'id = ?',
whereArgs: [id],
);
return results?.first??{};
return results?.first ?? {};
}
/// 更新数据
......@@ -308,5 +297,4 @@ class SqlManager {
whereArgs: [id],
);
}
}
......@@ -4,14 +4,8 @@ part of utils;
class Toast {
/// 展示toast
static void show(String msg, {int duration = 2000}) {
if (msg == null) {
return;
}
showToast(
msg,
duration: Duration(milliseconds: duration),
dismissOtherToast: true
);
showToast(msg,
duration: Duration(milliseconds: duration), dismissOtherToast: true);
}
/// 取消toast
......
part of utils;
abstract class Tools {
/// 取消焦点
static void unfocus() {
......@@ -13,7 +12,7 @@ abstract class Tools {
String pattern = 'yyyy-MM-dd',
bool humanize = false,
}) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp*1000);
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
if (humanize) {
final now = DateTime.now();
final difference = now.difference(dateTime);
......@@ -43,11 +42,11 @@ abstract class Tools {
static Future<String> getDirectory() async {
// getTemporaryDirectory
final directory = await getTemporaryDirectory();
return directory!.path;
return directory.path;
}
/// 语音文件名称
static String generateVoiceFileName(){
static String generateVoiceFileName() {
DateTime now = DateTime.now();
String formattedDate = DateFormat('yyyyMMddHHmmss').format(now);
return 'voice_$formattedDate.mp4';
......@@ -63,7 +62,7 @@ abstract class Tools {
/// 判断当前网络状态
static Future<bool> checkCurrentNetStatus() async {
final connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none){
if (connectivityResult == ConnectivityResult.none) {
return false;
}
return true;
......@@ -108,7 +107,3 @@ abstract class Tools {
// }
// }
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论