提交 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 web;
class ReadController extends FullLifeCycleController with GetSingleTickerProviderStateMixin{
class ReadController extends FullLifeCycleController
with GetSingleTickerProviderStateMixin {
// 图书id
final String bookId;
// 章节id
......@@ -11,20 +12,40 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
final BookDetailModel bookDetailModel;
// 笔记id 用于我的笔记跳转阅读页对应位置
final String noteId;
ReadController({required this.bookId, required this.chapterId,required this.chapterName,required this.bookDetailModel,required this.noteId});
ReadController(
{required this.bookId,
required this.chapterId,
required this.chapterName,
required this.bookDetailModel,
required this.noteId});
late InAppWebViewController webViewController;
// 目录
List <ChapterModel> chapters = [];
List<ChapterModel> chapters = [];
// 工具栏数组
List <ToolModel> tools = [
ToolModel(tag: 0,name: '目录',selected: false,icon: 'assets/images/category_unselect.png',activeIcon: 'assets/images/category_select.png'),
ToolModel(tag: 1,name: '笔记',selected: false,icon: 'assets/images/note_unselect.png',activeIcon:'assets/images/note_select.png'),
ToolModel(tag: 2,name: '讨论',selected: false,icon: 'assets/images/discuss_unselect.png',activeIcon:'assets/images/discuss_select.png'),
List<ToolModel> tools = [
ToolModel(
tag: 0,
name: '目录',
selected: false,
icon: 'assets/images/category_unselect.png',
activeIcon: 'assets/images/category_select.png'),
ToolModel(
tag: 1,
name: '笔记',
selected: false,
icon: 'assets/images/note_unselect.png',
activeIcon: 'assets/images/note_select.png'),
ToolModel(
tag: 2,
name: '讨论',
selected: false,
icon: 'assets/images/discuss_unselect.png',
activeIcon: 'assets/images/discuss_select.png'),
];
// 默认工具栏选项
late ToolModel toolModel = tools.first;
// 输入框窗口是否展示
bool showChat = false;
bool showChat = false;
// 0 讨论 1 笔记
late int chatType = 0;
// 笔记是否公开
......@@ -41,19 +62,20 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
final TextEditingController contentInput = TextEditingController();
//
// 讨论添加的图片path数组
List <String> discussInputImages= [];
List<String> discussInputImages = [];
// 讨论添加的语音path数组
List <AudioModel> discussInputAudios= [];
List<AudioModel> discussInputAudios = [];
// 笔记标题
String noteTitle = '';
bool _show = false;
bool get show => _show;
// 录音
final FlutterSoundRecorder _mRecorder = FlutterSoundRecorder(logLevel:Level.error);
final FlutterSoundRecorder _mRecorder =
FlutterSoundRecorder(logLevel: Level.error);
// 录音开始
bool startRecording = false;
// 是否存在离线文件
bool existDownFile= false;
bool existDownFile = false;
// 网络状态
bool netStatus = false;
// 当前html名称 0-318.html
......@@ -82,7 +104,6 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
// 本地文件地址
String localHtml5Path = '';
///------------------------------------------ 页面 生命周期--------------------------------------------------------
@override
void onInit() async {
......@@ -102,7 +123,7 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
_getChapters();
netStatus = await Tools.checkCurrentNetStatus();
if(existDownFile && !netStatus){
if (existDownFile && !netStatus) {
webViewController.loadFile(assetFilePath: 'assets/html/read_unline.html');
}
......@@ -125,10 +146,11 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
// await ScreenProtector.preventScreenshotOff();
super.onClose();
}
///------------------------------------------ 页面 生命周期--------------------------------------------------------
/// 选择了底部工具栏
void chooseTool(ToolModel selectedModel){
void chooseTool(ToolModel selectedModel) {
for (var model in tools) {
// 如果当前遍历到的工具是选中的,并且不是点击的工具,则取消选中
if (model.selected && model != selectedModel) {
......@@ -159,8 +181,8 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 是否显示搜索组件
void setShowSearch(bool show){
if(show == false){
void setShowSearch(bool show) {
if (show == false) {
searchInput.text = '';
}
showSearch = show;
......@@ -169,7 +191,7 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
/// 选择了某个章节
void selectChapter(ChapterModel model) {
chapterName = model.name??'';
chapterName = model.name ?? '';
chapterId = model.id.toString();
writeCurrentReadChapterIdToData(chapters);
update();
......@@ -186,7 +208,6 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
flutterTts.setStartHandler(() {
Console.log('-------------Playing-------------------');
});
if (Platform.isAndroid) {
......@@ -214,7 +235,6 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
flutterTts.setErrorHandler((msg) {
Console.log('-------------TTS error-------------------$msg');
});
}
/// 设置参数
......@@ -251,8 +271,11 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
/// 初始化录音组件
Future<void> openTheRecorder() async {
var status = await Access.microphone();
var status = await Access.microphone(
showExplanation: true,
purpose: '用于录制语音笔记,仅用于创建和播放您的录音内容。',
context: Get.context,
);
if (status == false) {
Toast.show('录音权限没有开启无法使用该功能');
return;
......@@ -262,11 +285,11 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy:
AVAudioSessionRouteSharingPolicy.defaultPolicy,
AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
......@@ -290,7 +313,6 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
toFile: recordePath,
audioSource: AudioSource.microphone,
codec: Codec.aacMP4,
);
// _mRecorder?.setSubscriptionDuration(Duration(milliseconds: 100));
// _mRecorder?.onProgress?.listen((e)
......@@ -304,38 +326,40 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 停止录音
void stopRecorder() async{
void stopRecorder() async {
startRecording = false;
final path = await _mRecorder.stopRecorder();
Console.log('stopRecorder-----------path---------------------$path');
if(path!=null && path.isNotEmpty){
if (path != null && path.isNotEmpty) {
var duration = await audioPlayer.setFilePath(path);
Console.log('-----duration---------------------$duration------');
AudioModel audioModel = AudioModel(path: path,duration: Tools.formatDuration(duration!),currentDuration: '0:00:00');
if(audioModel.duration !='0:00:00'){
AudioModel audioModel = AudioModel(
path: path,
duration: Tools.formatDuration(duration!),
currentDuration: '0:00:00');
if (audioModel.duration != '0:00:00') {
discussInputAudios.add(audioModel);
}
}
update();
}
/// 语音文件名称
String generateVoiceFileName(){
String generateVoiceFileName() {
DateTime now = DateTime.now();
String formattedDate = DateFormat('yyyyMMddHHmmss').format(now);
return 'voice_$formattedDate.mp4';
}
/// 重置所有信息
void reset(){
void reset() {
clearAllDiscussInput();
}
/// 播放音频
void playAudio(AudioModel audioModel){
void playAudio(AudioModel audioModel) {
Console.log('-------------播放开始-------------------');
if(audioPlayer.playerState.playing){
if (audioPlayer.playerState.playing) {
audioPlayer.stop();
audioModel.currentDuration = '0:00:00';
}
......@@ -343,11 +367,12 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
audioPlayer.setFilePath(audioModel.path);
audioPlayer.play();
StreamSubscription? positionSubscription;
positionSubscription =audioPlayer.positionStream.listen((position) {
String temp = Tools.formatDuration(position);
Console.log('播放时间---------------------$temp------id-------------${audioModel.path}');
positionSubscription = audioPlayer.positionStream.listen((position) {
String temp = Tools.formatDuration(position);
Console.log(
'播放时间---------------------$temp------id-------------${audioModel.path}');
audioModel.currentDuration = temp;
if(position >= audioPlayer.duration!){
if (position >= audioPlayer.duration!) {
Console.log('---------播放结束-----------');
positionSubscription?.cancel();
}
......@@ -357,35 +382,35 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 添加讨论图片
void addDiscussInputImages(String path){
void addDiscussInputImages(String path) {
discussInputImages.add(path);
Console.log('discussInputImages--------------------------------$path');
update();
}
/// 删除讨论图片
void delDiscussInputImages(String path){
void delDiscussInputImages(String path) {
discussInputImages.remove(path);
Console.log('delDiscussInputImages--------------------------------$path');
update();
}
/// 清空讨论图片
void clearDiscussInputImages(){
void clearDiscussInputImages() {
discussInputImages.clear();
Console.log('clearDiscussInputImages--------------------------------');
update();
}
/// 情况语音
void clearDiscussAudios(){
void clearDiscussAudios() {
discussInputAudios.clear();
Console.log('clearDiscussAudios--------------------------------');
update();
}
/// 清空所有已经填写的数据
void clearAllDiscussInput(){
void clearAllDiscussInput() {
clearDiscussInputImages();
clearDiscussAudios();
titleInput.text = '';
......@@ -397,15 +422,13 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 删除音频
void delAudio(AudioModel audioModel){
void delAudio(AudioModel audioModel) {
discussInputAudios.remove(audioModel);
update();
}
/// 上传文件
Future<String> upload({
required String path
}) async {
Future<String> upload({required String path}) async {
// String result = await CommonAPI.upload(path:path,fileTypes: 'comment');
// return result;
OssTool tool = OssTool('zxts-comment-file');
......@@ -423,58 +446,59 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
final status = await Tools.checkCurrentNetStatus();
if(chatType == 0){
if(contentInput.text.isEmpty && discussInputImages.isEmpty){
if (chatType == 0) {
if (contentInput.text.isEmpty && discussInputImages.isEmpty) {
Toast.show('话题必须填写内容或选择图片');
return false;
}
}
else if(chatType == 1){
if(contentInput.text.isEmpty && discussInputImages.isEmpty && discussInputAudios.isEmpty){
} else if (chatType == 1) {
if (contentInput.text.isEmpty &&
discussInputImages.isEmpty &&
discussInputAudios.isEmpty) {
Toast.show('笔记必须填写内容或选择图片或音频');
return false;
}
}
// 有网情况下 先直传oss 获取到url
if (status){
if (status) {
CustomToast.loading();
// 循环上传图片获取地址
for(String path in discussInputImages){
for (String path in discussInputImages) {
final url = await upload(path: path);
images.add(url);
}
// 循环上传音频获取地址
for(AudioModel model in discussInputAudios){
for (AudioModel model in discussInputAudios) {
final url = await upload(path: model.path);
audios.add(url);
}
CustomToast.dismiss();
}
// 没有网的情况下 存储到本地数据库
else{
for(String path in discussInputImages){
else {
for (String path in discussInputImages) {
images.add(path);
}
// 循环上传音频获取地址
for(AudioModel model in discussInputAudios){
for (AudioModel model in discussInputAudios) {
audios.add(model.path);
}
}
Map<String,dynamic> contentMap = {
'text':contentInput.text,
'audio':audios,
'image':images
Map<String, dynamic> contentMap = {
'text': contentInput.text,
'audio': audios,
'image': images
};
// 话题 只有有网的时候才能发起话题
if (chatType == 0){
if (chatType == 0) {
final result = await addDiscuss(contentMap);
return result;
}
// 笔记 有网没有网都能发起笔记
else if (chatType == 1){
else if (chatType == 1) {
final result = addNote(contentMap);
return result;
}
......@@ -482,27 +506,26 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 添加笔记
Future<bool> addNote(Map<String,dynamic> contentMap) async {
Future<bool> addNote(Map<String, dynamic> contentMap) async {
final status = await Tools.checkCurrentNetStatus();
bool result ;
bool result;
// 有网直接上传接口
if (status){
if (status) {
result = await LibraryAPI.addNote(
bookId: bookId,
chapterId: chapterId,
content: noteTitle,
isOpen: isPublic?'1':'0',
isOpen: isPublic ? '1' : '0',
positioning: notePosition,
noteContent: jsonEncode(contentMap)
);
noteContent: jsonEncode(contentMap));
}
// 没有网存储本地数据库
else{
else {
Map<String, dynamic> data = {
'types': 3,
'book_id': int.parse(bookId),
'chapter_id': int.parse(chapterId),
'is_open': isPublic?1:0,
'is_open': isPublic ? 1 : 0,
'color': '#FF0000',
'content': noteTitle,
'upload': 0,
......@@ -510,13 +533,12 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
'note_content': jsonEncode(contentMap),
'notes_id': 0,
};
result = await SqlManager.insertData(data);
result = await SqlManager.insertData(data);
}
if(result){
if (result) {
Toast.show('笔记发表成功');
}
else{
} else {
Toast.show('笔记发表失败');
}
// 重置所有信息
......@@ -527,28 +549,24 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
/// 发表评论
// {String commentId ='0',String quoteContent ='',String title = ''}
Future<bool> addDiscuss(Map<String,dynamic> contentMap) async{
Future<bool> addDiscuss(Map<String, dynamic> contentMap) async {
final result = await LibraryAPI.addDiscuss(
bookId: bookId,
chapterId: chapterId,
commentId: '0',
quoteContent: noteTitle,
title: titleInput.text,
content: jsonEncode(contentMap)
);
if(result.isNotEmpty){
content: jsonEncode(contentMap));
if (result.isNotEmpty) {
Toast.show('话题发表成功');
}
else{
} else {
Toast.show('话题发表失败');
}
// 重置所有信息
reset();
setShowChat(false);
return result.isNotEmpty?true:false;
return result.isNotEmpty ? true : false;
}
/// 焦点变化
......@@ -563,19 +581,19 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
/// 展示输入框
void setShowChat(bool value) {
showChat = value;
if(value == false){
if (value == false) {
reset();
}
update();
}
/// 显示输入框类型
void setChatType(int type){
void setChatType(int type) {
chatType = type;
}
/// 设置笔记是否公开
void setIsPublic(){
void setIsPublic() {
isPublic = !isPublic;
update();
}
......@@ -586,32 +604,27 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
Future<void> extractZipFileFromCache(String url) async {
// 从缓存中获取 ZIP 文件
var file = await DefaultCacheManager().getSingleFile(url);
if (file != null) {
// 读取 ZIP 文件内容
Uint8List bytes = await file.readAsBytes();
// 解压缩 ZIP 文件
Archive archive = ZipDecoder().decodeBytes(bytes);
// 获取设备上的临时目录
String tempPath = await Tools.getDirectory();
// 将解压缩后的文件保存到临时目录中
for (var file in archive) {
if (file.isFile) {
String fileName = file.name;
String filePath = '$tempPath/$bookId/$fileName';
File(filePath)
..createSync(recursive: true)
..writeAsBytesSync(file.content as List<int>);
Console.log('解压缩文件:$fileName,保存路径:$filePath');
}
// 读取 ZIP 文件内容
Uint8List bytes = await file.readAsBytes();
// 解压缩 ZIP 文件
Archive archive = ZipDecoder().decodeBytes(bytes);
// 获取设备上的临时目录
String tempPath = await Tools.getDirectory();
// 将解压缩后的文件保存到临时目录中
for (var file in archive) {
if (file.isFile) {
String fileName = file.name;
String filePath = '$tempPath/$bookId/$fileName';
File(filePath)
..createSync(recursive: true)
..writeAsBytesSync(file.content as List<int>);
Console.log('解压缩文件:$fileName,保存路径:$filePath');
}
CustomToast.dismiss();
Toast.show('离线成功');
await _isExistFile(bookId);
update();
} else {
Console.log('未找到缓存中的文件或文件不存在');
}
CustomToast.dismiss();
Toast.show('离线成功');
await _isExistFile(bookId);
update();
}
/// 判断是否存在离线文件
......@@ -621,20 +634,17 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
bool directoryExists = await directory.exists();
if (directoryExists) {
Console.log('存在名为 "$bookId" 的文件夹');
existDownFile = await Directory('${directory.path}/$bookId').exists();
}
else {
existDownFile = await Directory('${directory.path}/$bookId').exists();
} else {
Console.log('不存在名为 "$bookId" 的文件夹');
existDownFile = false;
}
update();
return existDownFile;
}
/// 获取对应chapterId文件路径
Future<String> getLocalReadHtml(String chapterId) async{
Future<String> getLocalReadHtml(String chapterId) async {
String docPath = await Tools.getDirectory();
String filePath = '$docPath/$bookId';
Directory directory = Directory(filePath);
......@@ -643,8 +653,8 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
for (var file in files) {
if (file is File && file.path.toLowerCase().endsWith('.html')) {
String fileName = path.basenameWithoutExtension(file.path);
if (fileName.split('-').last == chapterId){
return file.path;
if (fileName.split('-').last == chapterId) {
return file.path;
}
}
}
......@@ -653,7 +663,8 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
/// 获取离线数据
void getOffLineInfo({String direction = ''}) async {
Console.log('-chapterId----------------------$chapterId-----------------------------------------');
Console.log(
'-chapterId----------------------$chapterId-----------------------------------------');
Map<String, dynamic> data = {};
data['chapter_name'] = chapterName;
// 1、根据当前章节id名称 获取内容
......@@ -667,11 +678,10 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
// 4、获取上一章节信息
Map<String, dynamic> upChapter = {};
final upId = await getChapterId(type: 0);
if(upId.isEmpty) {
if (upId.isEmpty) {
data['up_chapter'] = '';
}
else{
String upName = getChapterName(upId);
} else {
String upName = getChapterName(upId);
upChapter['name'] = upName;
upChapter['id'] = int.parse(upId);
String htmlPath = await getLocalReadHtml(upId);
......@@ -679,16 +689,14 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
String deCodeContent = EncryptUtil.aesDecrypt(enCodeContent!);
upChapter['contents'] = deCodeContent;
data['up_chapter'] = upChapter;
}
// 5、获取下一章节信息
Map<String, dynamic> nextChapter = {};
final nextId = await getChapterId(type: 1);
if(nextId.isEmpty) {
if (nextId.isEmpty) {
data['next_chapter'] = '';
}
else{
String nextName = getChapterName(nextId);
} else {
String nextName = getChapterName(nextId);
nextChapter['name'] = nextName;
nextChapter['id'] = int.parse(nextId);
Console.log('nextChapter--------------------------------$nextChapter');
......@@ -704,16 +712,19 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
// 7、 给前端参数
String jsonStr = jsonEncode(data);
Console.log('callbackInFlutterComponent--------------------------------$jsonStr');
webViewController.evaluateJavascript(source: 'callbackInFlutterComponent($jsonStr)');
final result = await SqlManager.updateReadHistoryByBookId(int.parse(bookId), int.parse(chapterId));
Console.log('Sql----readread---存入数据库读到的章节----------------book_id:$bookId-----chapterId:$chapterId---------result:$result--');
Console.log(
'callbackInFlutterComponent--------------------------------$jsonStr');
webViewController.evaluateJavascript(
source: 'callbackInFlutterComponent($jsonStr)');
final result = await SqlManager.updateReadHistoryByBookId(
int.parse(bookId), int.parse(chapterId));
Console.log(
'Sql----readread---存入数据库读到的章节----------------book_id:$bookId-----chapterId:$chapterId---------result:$result--');
}
/// 获取上一章节或下一章节 id
Future<String> getChapterId({required int type}) async{
Future<String> getChapterId({required int type}) async {
String docPath = await Tools.getDirectory();
String filePath = '$docPath/$bookId';
Directory directory = Directory(filePath);
......@@ -721,17 +732,16 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
List<FileSystemEntity> files = directory.listSync(recursive: true);
int findIndex = int.parse(currentHtmlName.split('-').first);
if(type == 0){
if (type == 0) {
findIndex--;
if(findIndex <0){
if (findIndex < 0) {
// Toast.show('前面已没有章节');
// 已到最前
return '';
}
}
else{
} else {
findIndex++;
if(findIndex >files.length -1){
if (findIndex > files.length - 1) {
// 已到最后
return '';
}
......@@ -740,7 +750,7 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
for (var file in files) {
if (file is File && file.path.toLowerCase().endsWith('.html')) {
String fileName = path.basenameWithoutExtension(file.path);
if (int.parse(fileName.split('-').first) == findIndex){
if (int.parse(fileName.split('-').first) == findIndex) {
Console.log('HTML File--------------------------------${file.path}');
String chapterId = fileName.split('-').last;
return chapterId;
......@@ -757,24 +767,23 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
File htmlFile = File(filePath);
String fileContent = await htmlFile.readAsString();
return fileContent;
}
catch (e){
} catch (e) {
Console.log('Error reading file: $e');
return '';
}
}
/// 通过 chapter_id 获取 chapter_name
String getChapterName(String chapterId){
for (ChapterModel model in chapters){
if ('${model.id}' == chapterId){
return model.name??'';
String getChapterName(String chapterId) {
for (ChapterModel model in chapters) {
if ('${model.id}' == chapterId) {
return model.name ?? '';
}
if (model.children !=null){
for (ChapterModel subModel in model.children!){
if ('${subModel.id}' == chapterId){
return subModel.name??'';
}
if (model.children != null) {
for (ChapterModel subModel in model.children!) {
if ('${subModel.id}' == chapterId) {
return subModel.name ?? '';
}
}
}
}
......@@ -782,14 +791,16 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 查询本地笔记
Future<Map<String,dynamic>> queryNewLocalNote() async {
final result = await SqlManager.queryLocalNote(bookId: int.parse(bookId), chapterId: int.parse(chapterId));
Future<Map<String, dynamic>> queryNewLocalNote() async {
final result = await SqlManager.queryLocalNote(
bookId: int.parse(bookId), chapterId: int.parse(chapterId));
return result;
}
/// 查询本地划线高亮笔记
void queryLocalNote() async {
final result = await SqlManager.queryLocalNote(bookId: int.parse(bookId), chapterId: int.parse(chapterId));
final result = await SqlManager.queryLocalNote(
bookId: int.parse(bookId), chapterId: int.parse(chapterId));
Console.log('前端-----------queryLocalNote---------------------$result');
// webViewController.evaluateJavascript(source: 'querySuccessCallBack($result)');
}
......@@ -805,15 +816,19 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 本地删除划线高亮笔记
void delLocalNote({required int noteId,required int id}) async {
final result = await SqlManager.delLocalNote(noteId: noteId,id: id);
void delLocalNote({required int noteId, required int id}) async {
final result = await SqlManager.delLocalNote(noteId: noteId, id: id);
Console.log('前端-----------delLocalNote---------------------$result');
// webViewController.evaluateJavascript(source: 'delSuccessCallBack($result)');
}
/// 修改本地划线高亮笔记
void updateLocalNote({required int notesId,required int id,required Map<String, dynamic> data}) async {
final result = await SqlManager.updateLocalNote(notesId: notesId,id: id, data: data);
void updateLocalNote(
{required int notesId,
required int id,
required Map<String, dynamic> data}) async {
final result =
await SqlManager.updateLocalNote(notesId: notesId, id: id, data: data);
Console.log('前端-----------updateLocalNote---------------------$result');
// webViewController.evaluateJavascript(source: 'updateSuccessCallBack($result)');
}
......@@ -833,30 +848,29 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
/// 写入当前章节
void writeCurrentReadChapterIdToData(List<ChapterModel> data) {
for(ChapterModel cModel in data){
for (ChapterModel cModel in data) {
cModel.currentRead = false;
if(cModel.id == int.parse(chapterId)){
if (cModel.id == int.parse(chapterId)) {
cModel.currentRead = true;
cModel.selected = true;
}
else{
} else {
writeCurrentReadChapterIdToData(cModel.children!);
}
}
ChapterModel? tModel = findChapterById(chapters, int.parse(chapterId));
if(tModel != null){
if (tModel != null) {
updateParentsStatus(chapters, tModel);
}
}
/// 查找章节
ChapterModel? findChapterById(List<ChapterModel> data,num id){
for (ChapterModel cModel in data){
if(cModel.id == id){
ChapterModel? findChapterById(List<ChapterModel> data, num id) {
for (ChapterModel cModel in data) {
if (cModel.id == id) {
return cModel;
}
ChapterModel? tModel = findChapterById(cModel.children!, id);
if(tModel !=null){
if (tModel != null) {
return tModel;
}
}
......@@ -873,13 +887,13 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 寻找父节点
ChapterModel? findParentChapter(List<ChapterModel> data, ChapterModel model){
for (ChapterModel tModel in data){
if(tModel.id == model.pid){
ChapterModel? findParentChapter(List<ChapterModel> data, ChapterModel model) {
for (ChapterModel tModel in data) {
if (tModel.id == model.pid) {
return tModel;
}
ChapterModel? cModel = findParentChapter(tModel.children!, model);
if(cModel != null) {
if (cModel != null) {
return cModel;
}
}
......@@ -887,17 +901,18 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 添加阅读时长
void _addReadTime({required type}) async{
void _addReadTime({required type}) async {
final status = await Tools.checkCurrentNetStatus();
if(status){
if (status) {
LibraryAPI.addReadTime(bookId: bookId, readTypes: type);
}
}
/// 获取离线文件路径
void getBookDown() async{
void getBookDown() async {
final exit = await _isExistFile(bookId);
// 存在离线文件
if (!exit){
if (!exit) {
CustomToast.loading();
final result = await LibraryAPI.getbookDownloadParam(bookId: bookId);
Console.log('----------_getBookDown------------------${result.download}');
......@@ -913,16 +928,14 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
page: _searchPage,
limit: _searchLimit,
bookId: bookId.toString(),
key: searchInput.text
);
key: searchInput.text);
Console.log('--------------------------------');
// 如果是刷新 清理数据
if (isRefresh) searchALlResults.clear();
searchALlResults.addAll(result);
_searchPage ++;
_searchPage++;
_searchNoMore = result.length < _searchLimit;
update();
}
/// 刷新搜索数据
......@@ -954,7 +967,7 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
///------------------------------------------ app 生命周期--------------------------------------------------------
/// 当应用程序从后台切换到前台并变为活动状态时调用。这通常在用户从其他应用程序返回到你的应用程序时发生
void onResumed(){
void onResumed() {
Console.log('onResumed');
webViewController.evaluateJavascript(source: 'activeState("1");');
// open
......@@ -963,7 +976,7 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 当应用程序失去焦点并进入非活动状态时调用。这可能是因为用户切换到其他应用程序或将应用程序最小化
void onPaused(){
void onPaused() {
// close
Console.log('onPaused');
webViewController.evaluateJavascript(source: 'activeState("0");');
......@@ -972,7 +985,7 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 当应用程序失去焦点但仍然可见时调用。通常,在用户切换到另一个应用程序或显示系统对话框时,应用程序可能会处于非活动状态,但仍然是可见的
void onInactive(){
void onInactive() {
Console.log('onInactive');
webViewController.evaluateJavascript(source: 'activeState("0");');
// close
......@@ -981,19 +994,24 @@ class ReadController extends FullLifeCycleController with GetSingleTickerProvide
}
/// 当应用程序被挂起,可能是由于用户关闭应用程序或系统资源不足时调用。在这个状态下,应用程序的代码将不再运行,并且可能被系统终止
void onDetached(){
void onDetached() {
Console.log('onDetached');
webViewController.evaluateJavascript(source: 'activeState("0");');
// close
// 上报阅读结束时间
_addReadTime(type: 'close');
}
///------------------------------------------ app 生命周期--------------------------------------------------------
///------------------------------------------ app 生命周期--------------------------------------------------------
}
class ToolModel {
ToolModel({required this.tag, required this.selected, required this.name,required this.icon,required this.activeIcon});
ToolModel(
{required this.tag,
required this.selected,
required this.name,
required this.icon,
required this.activeIcon});
int tag;
String name;
bool selected;
......@@ -1002,7 +1020,10 @@ class ToolModel {
}
class AudioModel {
AudioModel({required this.path, required this.duration,required this.currentDuration});
AudioModel(
{required this.path,
required this.duration,
required this.currentDuration});
String path;
String duration;
String currentDuration;
......
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,
child: TextButton(
onPressed: () async {
await StorageService.to
.setBool(kLocalPrivacyNoticeShown, true);
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
},
child: const Text('我知道了'),
),
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(true);
}
},
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();
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,36 +421,37 @@ class _UserSetPageState extends State<UserSetPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (!forcedUpgrade)
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),
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(),
),
onTap: () => Navigator.of(context).pop(),
),),
),
Container(
height: 39.5.w, // 设置分割线的高度
width: 1, // 设置分割线的宽度
color: Colours.cLine, // 设置分割线的颜色
),
Padding(padding: EdgeInsets.only(left: 28.w),
child: GestureDetector(
Padding(
padding: EdgeInsets.only(left: 28.w),
child: GestureDetector(
child: Container(
width: 85.w,
color: Colours.cFF,
alignment: Alignment.center,
child: const Text(
child: const Text(
'立即更新',
style: TextStyle(
color: Colours.cAB1941,
......@@ -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 ? '相册权限' : '存储权限',
purpose: purpose,
);
// 用户取消,不申请权限
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;
if (Platform.isAndroid) return true;
return _requestPermission(
permission: Permission.photos,
permissionName: '相册权限',
showExplanation: showExplanation,
purpose: purpose,
context: context,
allowLimited: true,
);
}
/// 相机权限
......@@ -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,39 +12,15 @@ 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;
}
final granted = await Access.photos(
showExplanation: true,
purpose: purpose,
context: context,
);
if (!granted) {
return null;
}
return _imagePicker.pickImage(
source: source,
// maxWidth: maxWidth,
......@@ -52,7 +28,7 @@ abstract class AssetsPicker {
// imageQuality: 100
);
}
/// 拍照
/// [purpose] 权限使用目的说明,用于在申请权限前告知用户
static Future<XFile?> camera({
......@@ -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(
source: source,
// maxWidth: maxWidth,
// maxHeight: maxHeight,
);
}
/// 展示设置弹窗
static _showSettingDialog(BuildContext context,String content) {
showDialog(
final granted = await Access.camera(
showExplanation: true,
purpose: purpose,
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';
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,38 +14,34 @@ 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";
}
_database = await openDatabase(path, version: _version,
onCreate: (Database db, int version) async {
// When creating the db, create the table
// await db.execute("CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)");
// 笔记表
await db.execute("CREATE TABLE $kNoteTable ("
"id INTEGER PRIMARY KEY, "
"types INTEGER, "
"book_id INTEGER, "
"chapter_id INTEGER, "
"notes_id INTEGER, "
"is_open INTEGER, "
"del INTEGER DEFAULT 0, "
"color TEXT, "
"content TEXT, "
"upload INTEGER DEFAULT 0, "
"positioning TEXT, "
"note_content TEXT)"
);
// // 阅读章节表
await db.execute("CREATE TABLE $kReadTable ("
"id INTEGER PRIMARY KEY, "
"book_id INTEGER, "
"chapter_id INTEGER)"
);
});
String path = databasesPath + dbName;
if (Platform.isIOS) {
path = "$databasesPath/$dbName";
}
_database = await openDatabase(path, version: _version,
onCreate: (Database db, int version) async {
// When creating the db, create the table
// await db.execute("CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)");
// 笔记表
await db.execute("CREATE TABLE $kNoteTable ("
"id INTEGER PRIMARY KEY, "
"types INTEGER, "
"book_id INTEGER, "
"chapter_id INTEGER, "
"notes_id INTEGER, "
"is_open INTEGER, "
"del INTEGER DEFAULT 0, "
"color TEXT, "
"content TEXT, "
"upload INTEGER DEFAULT 0, "
"positioning TEXT, "
"note_content TEXT)");
// // 阅读章节表
await db.execute("CREATE TABLE $kReadTable ("
"id INTEGER PRIMARY KEY, "
"book_id INTEGER, "
"chapter_id INTEGER)");
});
}
static Future<Database?> getCurrentDatabase() async {
......@@ -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;
......@@ -92,82 +84,82 @@ class SqlManager {
/// 添加划线高亮笔记
static Future<int> addLocalNote(Map<String, dynamic> data) async {
Database? db = await SqlManager.getCurrentDatabase();
final result = await db?.insert(
final result = await db?.insert(
kNoteTable,
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){
final result = await db?.update(
if (noteId == 0) {
final result = await db?.update(
kNoteTable,
{'del': 1},
where: 'id = ?',
whereArgs: [id],
);
return result??0;
}
else{
final result = await db?.update(
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){
final result = await db?.update(
if (notesId == 0) {
final result = await db?.update(
kNoteTable,
data,
where: 'id = ?',
whereArgs: [id],
);
return result??0;
}
else{
final result = await db?.update(
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
static Future<void> updateUploadStatus() async {
Database? db = await SqlManager.getCurrentDatabase();
final result = await db?.update(
final result = await db?.update(
kNoteTable,
{'upload': 1},
where: 'upload = ?',
......@@ -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,72 +198,68 @@ 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){
Console.log('Sql----------没有当前书籍的数据----------------------');
final result = await db?.insert(
'read_history',
{'chapter_id':chapterId, 'book_id':bookId},
conflictAlgorithm: ConflictAlgorithm.replace,
);
Console.log('Sql----------插入数据结果:$result----------------------');
return result??0;
}
else{
Console.log('Sql----------有当前书籍的数据----------------------');
final result = await db?.update(
'read_history',
{'chapter_id':chapterId},
where: 'book_id = ?',
whereArgs: [bookId],
);
Console.log('Sql----------更新数据结果:$result----------------------');
return result??0;
}
final queryResult = await queryReadHistoryByBookId(bookId);
if (queryResult.isEmpty) {
Console.log('Sql----------没有当前书籍的数据----------------------');
final result = await db?.insert(
'read_history',
{'chapter_id': chapterId, 'book_id': bookId},
conflictAlgorithm: ConflictAlgorithm.replace,
);
Console.log('Sql----------插入数据结果:$result----------------------');
return result ?? 0;
} else {
Console.log('Sql----------有当前书籍的数据----------------------');
final result = await db?.update(
'read_history',
{'chapter_id': chapterId},
where: 'book_id = ?',
whereArgs: [bookId],
);
Console.log('Sql----------更新数据结果:$result----------------------');
return result ?? 0;
}
}
/// 插入数据
static Future<bool> insertData(Map<String, dynamic> data) async {
Database? db = await SqlManager.getCurrentDatabase();
final result = await db?.insert(
final result = await db?.insert(
'members_book_notes',
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() {
......@@ -9,11 +8,11 @@ abstract class Tools {
/// 格式化日期
static String dateFromMS(
int timestamp, {
String pattern = 'yyyy-MM-dd',
bool humanize = false,
}) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp*1000);
int timestamp, {
String pattern = 'yyyy-MM-dd',
bool humanize = false,
}) {
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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论