From 51a63f4ab1a9aa5a6801d59d078c6016a2726bc2 Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:38:49 +0800 Subject: [PATCH 1/8] refactor: move some value from Global to AppData class Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/funcs/fsrs_func.dart | 24 +- lib/funcs/local_pk_server.dart | 21 +- lib/funcs/ui.dart | 24 +- lib/funcs/utili.dart | 4 +- lib/main.dart | 13 +- lib/package_replacement/fake_dart_io.dart | 4 +- lib/package_replacement/storage.dart | 2 +- lib/pages/home_page.dart | 13 +- lib/pages/learning_page.dart | 17 +- lib/pages/setting_page.dart | 12 +- .../learning_pages/fsrs_pages.dart | 13 +- .../learning_pages/learning_pages_build.dart | 27 +- .../setting_pages/about_page.dart | 2 +- .../setting_pages/data_download_page.dart | 4 +- .../setting_pages/debug_page.dart | 10 +- .../setting_pages/model_download_page.dart | 3 +- .../setting_pages/questions_setting_page.dart | 2 +- .../setting_pages/sync_page.dart | 8 +- .../test_pages/listening_test_page.dart | 4 +- .../test_pages/local_pk_page.dart | 2 +- lib/vars/global.dart | 312 +++++++++--------- 21 files changed, 278 insertions(+), 243 deletions(-) diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index 9019885..1fc94bc 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:arabic_learning/vars/global.dart'; import 'package:flutter/foundation.dart'; import 'package:fsrs/fsrs.dart'; import 'package:logging/logging.dart'; @@ -7,23 +8,26 @@ import 'package:logging/logging.dart'; import 'package:arabic_learning/package_replacement/storage.dart'; class FSRS { - late final SharedPreferences prefs; + // 作为单例 + static final FSRS _instance = FSRS._internal(); + factory FSRS() => _instance; + FSRS._internal(); + late FSRSConfig config; - late final Logger logger; + + final Logger logger = Logger("FSRS"); // index != cardId; cardId = wordId = the index of word in global.wordData[words] - bool init({required SharedPreferences outerPrefs}) { - prefs = outerPrefs; - logger = Logger('FSRS'); + bool init() { logger.fine("构建FSRS模块"); - - if(!prefs.containsKey("fsrsData")) { + AppData appData = AppData(); + if(!appData.storage.containsKey("fsrsData")) { logger.info("未发现FSRS配置,加载默认配置"); config = FSRSConfig(); - prefs.setString("fsrsData", jsonEncode(config.toMap())); + appData.storage.setString("fsrsData", jsonEncode(config.toMap())); return false; } else { - config = FSRSConfig.buildFromMap(jsonDecode(prefs.getString("fsrsData")!)); + config = FSRSConfig.buildFromMap(jsonDecode(appData.storage.getString("fsrsData")!)); logger.info("FSRS配置加载完成"); } @@ -34,7 +38,7 @@ class FSRS { void save() async { logger.info("正在保存FSRS配置"); - prefs.setString("fsrsData", jsonEncode(config.toMap())); + AppData().storage.setString("fsrsData", jsonEncode(config.toMap())); } void createScheduler({required SharedPreferences prefs}) { diff --git a/lib/funcs/local_pk_server.dart b/lib/funcs/local_pk_server.dart index 1eafea0..22f6c05 100644 --- a/lib/funcs/local_pk_server.dart +++ b/lib/funcs/local_pk_server.dart @@ -11,7 +11,6 @@ import 'package:logging/logging.dart'; import 'package:arabic_learning/vars/statics_var.dart'; import 'package:flutter/foundation.dart' show ChangeNotifier; import 'package:flutter_webrtc/flutter_webrtc.dart'; -import 'package:provider/provider.dart'; class PKServer with ChangeNotifier{ final Logger logger = Logger("PKServer"); @@ -38,7 +37,6 @@ class PKServer with ChangeNotifier{ List selectableSource = []; ClassSelection? classSelection; late int rndSeed; - late Global global; Duration? delay; bool preparedP1 = false; bool preparedP2 = false; @@ -50,7 +48,6 @@ class PKServer with ChangeNotifier{ Future initHost(bool isHoster, BuildContext context, {String? offer}) async { isServer = isHoster; - global = context.read(); try { logger.info("正在初始化WebRTC"); @@ -187,10 +184,12 @@ class PKServer with ChangeNotifier{ void _questVersionCheck() async => _channel!.send(RTCDataChannelMessage(json.encode({"step": 0, "version": StaticsVar.appVersion}))); void _questDictExchange() async { + AppData appData = AppData(); _channel!.send(RTCDataChannelMessage( json.encode({ "step": 1, - "dictSum": List.generate(global.wordData.classes.length, (int index) => global.wordData.classes[index].getHash(global.wordData.words), growable: false) + "dictSum": List.generate(appData.wordData.classes.length, (int index) => + appData.wordData.classes[index].getHash(appData.wordData.words), growable: false) }) )); } @@ -208,7 +207,7 @@ class PKServer with ChangeNotifier{ void setPrepare() { preparedP1 = true; pkState = PKState( - testWords: getSelectedWords(global.wordData, classSelection!.selectedClass, doShuffle: true, shuffleSeed: rndSeed), + testWords: getSelectedWords(AppData().wordData, classSelection!.selectedClass, doShuffle: true, shuffleSeed: rndSeed), selfProgress: [], sideProgress: [] ); @@ -265,6 +264,8 @@ class PKServer with ChangeNotifier{ return; } + AppData appData = AppData(); + switch(step){ /// 版本号检查结果 from Client case 0 when data.containsKey("accepted"): { @@ -298,8 +299,8 @@ class PKServer with ChangeNotifier{ delay = DateTime.parse(data["time"]).difference(DateTime.now()); List sumList = data["dictSum"]; selectableSource.clear(); - for(SourceItem source in global.wordData.classes) { - if(sumList.contains(source.getHash(global.wordData.words))) selectableSource.add(source); + for(SourceItem source in appData.wordData.classes) { + if(sumList.contains(source.getHash(appData.wordData.words))) selectableSource.add(source); logger.fine("[$packageid] 计算得到${source.sourceJsonFileName}在哈希中有匹配"); } pageController!.nextPage(duration: Durations.medium2, curve: StaticsVar.curve); @@ -315,8 +316,8 @@ class PKServer with ChangeNotifier{ logger.fine("[$packageid] 进行词库检查"); List sumList = data["dictSum"]; selectableSource.clear(); - for(SourceItem source in global.wordData.classes) { - if(sumList.contains(source.getHash(global.wordData.words))) { + for(SourceItem source in appData.wordData.classes) { + if(sumList.contains(source.getHash(appData.wordData.words))) { selectableSource.add(source); logger.fine("[$packageid] 计算得到${source.sourceJsonFileName}在哈希中有匹配"); } @@ -325,7 +326,7 @@ class PKServer with ChangeNotifier{ _channel!.send(RTCDataChannelMessage(json.encode({ "step": 1, "accepted": true, - "dictSum": List.generate(selectableSource.length, (int index) => selectableSource[index].getHash(global.wordData.words)), + "dictSum": List.generate(selectableSource.length, (int index) => selectableSource[index].getHash(appData.wordData.words)), "time": DateTime.now().toIso8601String() }))); pageController!.nextPage(duration: Durations.medium2, curve: StaticsVar.curve); diff --git a/lib/funcs/ui.dart b/lib/funcs/ui.dart index 418fa84..1db9fe9 100644 --- a/lib/funcs/ui.dart +++ b/lib/funcs/ui.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:ui'; +import 'package:arabic_learning/funcs/fsrs_func.dart'; import 'package:arabic_learning/vars/config_structure.dart' show ClassItem, SourceItem, WordItem, ClassSelection; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -39,15 +40,16 @@ import 'package:arabic_learning/funcs/utili.dart'; Future popSelectClasses(BuildContext context, {bool withCache = false, bool withReviewChoose = true, List? forceSelectRange}) async { context.read().uiLogger.info("弹出课程选择(ClassSelectPage),withCache: $withCache"); final List beforeSelectedClasses = []; + AppData appData = AppData(); if(withCache) { if(forceSelectRange != null) throw Exception("popSelectClasses不允许forceSelectRange时使用withCache"); - final String tpcPrefs = context.read().prefs.getString("tempConfig") ?? jsonEncode(StaticsVar.tempConfig); + final String tpcPrefs = appData.storage.getString("tempConfig") ?? jsonEncode(StaticsVar.tempConfig); final List> cacheList = (jsonDecode(tpcPrefs)["SelectedClasses"] as List) .cast() .map((e) => e.cast().toList()) .toList(); for(List cachedClass in cacheList) { - for(SourceItem sourceItem in context.read().wordData.classes){ + for(SourceItem sourceItem in appData.wordData.classes){ if(sourceItem.sourceJsonFileName != cachedClass[0]){ continue; } @@ -60,21 +62,23 @@ Future popSelectClasses(BuildContext context, {bool withCache = } context.read().uiLogger.fine("已缓存课程选择: $beforeSelectedClasses"); } + ClassSelection? selectedClasses = await showModalBottomSheet( context: context, shape: RoundedRectangleBorder(side: BorderSide(width: 1.0, color: Theme.of(context).colorScheme.onSurface.withAlpha(150)), borderRadius: StaticsVar.br), isDismissible: false, - isScrollControlled: context.read().isWideScreen, + isScrollControlled: appData.isWideScreen, enableDrag: true, builder: (BuildContext context) { return ClassSelectPage(beforeSelectedClasses: beforeSelectedClasses, withReviewChoose: withReviewChoose); } ); + if(withCache && selectedClasses != null && context.mounted) { - final String tpcPrefs = context.read().prefs.getString("tempConfig") ?? jsonEncode(StaticsVar.tempConfig); + final String tpcPrefs = appData.storage.getString("tempConfig") ?? jsonEncode(StaticsVar.tempConfig); Map tpcMap = jsonDecode(tpcPrefs); tpcMap["SelectedClasses"] = selectedClasses; - context.read().prefs.setString("tempConfig", jsonEncode(tpcMap)); + appData.storage.setString("tempConfig", jsonEncode(tpcMap)); context.read().uiLogger.info("课程选择缓存完成"); } if(context.mounted) context.read().uiLogger.fine("选择的课程: $selectedClasses"); @@ -99,7 +103,7 @@ List classesSelectionList(BuildContext context, Function (ClassItem) onC if(forceSelectRange != null) { sourcesList = forceSelectRange; } else { - sourcesList = context.read().wordData.classes; + sourcesList = AppData().wordData.classes; } List widgetList = []; for (SourceItem source in sourcesList) { @@ -627,7 +631,7 @@ class ClassSelectPage extends StatelessWidget { final MediaQueryData mediaQuery = MediaQuery.of(context); ClassSelection classSelection = ClassSelection( selectedClass: beforeSelectedClasses.toList(), - countInReview: context.read().globalFSRS.config.enabled + countInReview: FSRS().config.enabled ); void addClass(ClassItem classInfo) { classSelection.selectedClass.add(classInfo); @@ -665,7 +669,7 @@ class ClassSelectPage extends StatelessWidget { Switch( value: classSelection.countInReview, onChanged: (value){ - if(value == true && !context.read().globalFSRS.config.enabled) { + if(value == true && !FSRS().config.enabled) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("请先启用复习系统"))); return ; } @@ -799,7 +803,7 @@ class _ChoiceQuestions extends State { // showingMode 0: 1 Row, 1: 2 Rows, 2: 4 Rows if(showingMode == -1){ context.read().uiLogger.fine("未指定布局,开始计算"); - showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width, context.read().isWideScreen); + showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width, AppData().isWideScreen); context.read().uiLogger.info("最终采用布局方案: $showingMode"); } return Material( @@ -1035,7 +1039,7 @@ class _ListeningQuestion extends State { int showingMode = widget.bottonLayout; if(showingMode == -1){ context.read().uiLogger.fine("未指定布局,开始计算"); - showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width, context.read().isWideScreen); + showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width, AppData().isWideScreen); context.read().uiLogger.info("最终采用布局方案: $showingMode"); } return Material( diff --git a/lib/funcs/utili.dart b/lib/funcs/utili.dart index 82117a1..0fff46e 100644 --- a/lib/funcs/utili.dart +++ b/lib/funcs/utili.dart @@ -94,7 +94,7 @@ Future> playTextToSpeech(String text, BuildContext context, {doubl // 2: sherpa-onnx } else if (context.read().globalConfig.audio.audioSource == 2) { context.read().logger.info("[TTS]配置使用 sherpa_onnx TTS"); - if(context.read().vitsTTS == null) { + if(AppData().vitsTTS == null) { context.read().logger.warning("[TTS]sherpa_onnx 未加载"); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -108,7 +108,7 @@ Future> playTextToSpeech(String text, BuildContext context, {doubl final cacheFile = io.File("${basePath.path}/temp.wav"); if(cacheFile.existsSync()) cacheFile.deleteSync(); if(!context.mounted) return [false, ""]; - final audio = context.read().vitsTTS!.generate(text: text, speed: speed); + final audio = AppData().vitsTTS!.generate(text: text, speed: speed); final ok = sherpa_onnx.writeWave( filename: cacheFile.path, samples: audio.samples, diff --git a/lib/main.dart b/lib/main.dart index 1517a82..221ed40 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,7 @@ import 'package:logging/logging.dart'; import 'package:arabic_learning/funcs/ui.dart'; import 'package:arabic_learning/funcs/utili.dart'; -import 'package:arabic_learning/vars/global.dart' show Global; +import 'package:arabic_learning/vars/global.dart' show AppData, Global; import 'package:arabic_learning/vars/license_storage.dart' show LicenseVars; import 'package:arabic_learning/vars/statics_var.dart' show StaticsVar; import 'package:arabic_learning/pages/home_page.dart'; @@ -90,7 +90,7 @@ class MyApp extends StatelessWidget { Container( decoration: BoxDecoration( image: DecorationImage( - image: MemoryImage(context.read().stella!), + image: MemoryImage(AppData().stella!), fit: BoxFit.cover, ), ), @@ -244,7 +244,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { context.read().uiLogger.fine("构建 MyHomePage"); final gob = context.watch(); - if(gob.firstStart) { + if(AppData().isFirstStart) { context.read().uiLogger.info("构建首次启动页面"); return Scaffold( body: PopScope( @@ -292,7 +292,8 @@ class _MyHomePageState extends State { onPressed: () async { if(controller.text.isNotEmpty){ context.read().uiLogger.info("用户同意协议,签署名:${controller.text}"); - context.read().acceptAggrement(controller.text); + context.read().globalConfig = context.read().globalConfig.copyWith(user: controller.text); + context.read().updateSetting(refresh: true); } else { context.read().uiLogger.info("用户未填写名称"); ScaffoldMessenger.of(context).showSnackBar( @@ -384,10 +385,10 @@ class _MyHomePageState extends State { builder: (context, constraints) { // 根据屏幕宽度决定使用哪种布局 if (constraints.maxWidth > _desktopBreakpoint) { - Provider.of(context, listen: false).isWideScreen = true; + AppData().isWideScreen = true; return _buildDesktopLayout(context); } else { - Provider.of(context, listen: false).isWideScreen = false; + AppData().isWideScreen = false; return _buildMobileLayout(context); } }, diff --git a/lib/package_replacement/fake_dart_io.dart b/lib/package_replacement/fake_dart_io.dart index d0560a0..078bb17 100644 --- a/lib/package_replacement/fake_dart_io.dart +++ b/lib/package_replacement/fake_dart_io.dart @@ -21,7 +21,9 @@ class File { } class Directory { - Directory(String filePath); + String path; + + Directory(this.path); Future create({required bool recursive}) async {} } diff --git a/lib/package_replacement/storage.dart b/lib/package_replacement/storage.dart index c28a387..edb95cf 100644 --- a/lib/package_replacement/storage.dart +++ b/lib/package_replacement/storage.dart @@ -61,7 +61,7 @@ class SharedPreferences { } Future setString(String key, String value) async { - logger.finer("设置键$key,值$value"); + // logger.finest("设置键$key,值$value"); if (type) { return prefs.setString(key, value); } else { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 2c2c5bf..787455e 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -18,7 +18,7 @@ class HomePage extends StatelessWidget { context.read().uiLogger.fine("构建 HomePage"); final themeColor = Theme.of(context).colorScheme; final MediaQueryData mediaQuery = MediaQuery.of(context); - final FSRS fsrs = context.read().globalFSRS; + final FSRS fsrs = FSRS(); return Column( children: [ DailyWord(), @@ -132,7 +132,7 @@ class HomePage extends StatelessWidget { children: [ Text('单词总数', style: TextStyle(fontSize: 12.0)), SizedBox(height: mediaQuery.size.height * 0.03), - Text(context.read().wordCount.toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)) + Text(AppData().wordCount.toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)) ], ), ), @@ -163,15 +163,16 @@ class _DailyWord extends State { Random rnd = Random(seed); late WordItem data; late String dailyWord; - if(context.read().wordCount != 0) { - data = context.read().wordData.words[rnd.nextInt(context.read().wordCount)]; + AppData appData = AppData(); + if(appData.wordCount != 0) { + data = appData.wordData.words[rnd.nextInt(appData.wordCount)]; dailyWord = data.arabic; } return ElevatedButton( onPressed: () async { if(playing) return; - if(context.read().wordCount != 0) { + if(appData.wordCount != 0) { playing = true; late List temp; temp = await playTextToSpeech(dailyWord, context); @@ -202,7 +203,7 @@ class _DailyWord extends State { FittedBox( fit: BoxFit.scaleDown, child: Column( - children: context.read().wordCount == 0 ? [Text("当前未导入词库数据\n请点此以跳转设置页面导入")] + children: AppData().wordCount == 0 ? [Text("当前未导入词库数据\n请点此以跳转设置页面导入")] : [ Text( data.arabic, diff --git a/lib/pages/learning_page.dart b/lib/pages/learning_page.dart index 8153463..49e1e83 100644 --- a/lib/pages/learning_page.dart +++ b/lib/pages/learning_page.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:arabic_learning/funcs/fsrs_func.dart'; import 'package:arabic_learning/sub_pages_builder/setting_pages/questions_setting_page.dart' show QuestionsSettingPage; import 'package:arabic_learning/vars/config_structure.dart'; import 'package:flutter/material.dart'; @@ -90,14 +91,14 @@ class LearningPage extends StatelessWidget { ], ), SizedBox(height: mediaQuery.size.height * 0.05), - if(context.read().globalFSRS.config.pushAmount != 0) ElevatedButton.icon( + if(FSRS().config.pushAmount != 0) ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.onPrimary.withAlpha(150), fixedSize: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.15), shape: RoundedRectangleBorder(borderRadius: StaticsVar.br), ), onPressed: (){ - if(context.read().wordData.words.isEmpty) { + if(AppData().wordData.words.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("词库为空,无法推送!请先导入词库"), duration: Duration(seconds: 1),), ); @@ -108,10 +109,10 @@ class LearningPage extends StatelessWidget { final Set pushWords = {}; final Random rnd = Random(seed); int tries = 0; - while(pushWords.length < context.read().globalFSRS.config.pushAmount && tries < context.read().globalFSRS.config.pushAmount * 10){ - int chosen = rnd.nextInt(context.read().wordData.words.length); - if(!context.read().globalFSRS.isContained(chosen)) { - pushWords.add(context.read().wordData.words.elementAt(chosen)); + while(pushWords.length < FSRS().config.pushAmount && tries < FSRS().config.pushAmount * 10){ + int chosen = rnd.nextInt(AppData().wordData.words.length); + if(!FSRS().isContained(chosen)) { + pushWords.add(AppData().wordData.words.elementAt(chosen)); } tries++; } @@ -125,7 +126,7 @@ class LearningPage extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => FSRSLearningPage(fsrs: context.read().globalFSRS, words: pushWords.toList()) + builder: (context) => FSRSLearningPage(fsrs: FSRS(), words: pushWords.toList()) ) ); }, @@ -161,7 +162,7 @@ Future shiftToStudy(BuildContext context) async { context.read().uiLogger.info("准备转向学习页面"); final ClassSelection classSelection = await popSelectClasses(context, withCache: false, withReviewChoose: true); if(classSelection.selectedClass.isEmpty || !context.mounted) return; - final List words = getSelectedWords(context.read().wordData, classSelection.selectedClass, doShuffle: false, doDouble: false); + final List words = getSelectedWords(AppData().wordData, classSelection.selectedClass, doShuffle: false, doDouble: false); context.read().uiLogger.info("完成单词挑拣,共${words.length}个"); if(words.isEmpty) return; context.read().uiLogger.info("跳转: LearningPage => InLearningPage"); diff --git a/lib/pages/setting_page.dart b/lib/pages/setting_page.dart index 5e3a964..4272dde 100644 --- a/lib/pages/setting_page.dart +++ b/lib/pages/setting_page.dart @@ -193,7 +193,7 @@ class _SettingPage extends State { Icon(Icons.download, size: 24.0), SizedBox(width: mediaQuery.size.width * 0.01), Expanded(child: Text("导入词库数据")), - Text("词库中现有: ${context.read().wordCount}"), + Text("词库中现有: ${AppData().wordCount}"), SizedBox(width: mediaQuery.size.width * 0.02), ], ), @@ -241,7 +241,7 @@ class _SettingPage extends State { try{ context.read().uiLogger.fine("文件读取完成,开始解析"); Map jsonData = json.decode(jsonString); - Provider.of(context, listen: false).importData(jsonData, platformFile.name); + AppData().importDictData(jsonData, platformFile.name); alart(context, "文件 \"${platformFile.name}\" \n已导入。"); context.read().uiLogger.info("文件解析成功"); } catch (e) { @@ -334,7 +334,13 @@ class _SettingPage extends State { items: [ DropdownMenuItem(value: 0, child: Text("系统文本转语音", overflow: TextOverflow.ellipsis,)), DropdownMenuItem(value: 1, child: Text("请求TextReadTTS.com的语音", overflow: TextOverflow.ellipsis)), - DropdownMenuItem(value: 2, enabled: kIsWeb ? false : (context.read().modelTTSDownloaded ? true : false), child: Text("神经网络合成语音", overflow: TextOverflow.ellipsis, style: TextStyle(color: kIsWeb ? Colors.grey : (context.read().modelTTSDownloaded ? null : Colors.grey))),), + DropdownMenuItem(value: 2, + enabled: kIsWeb ? false : (AppData().modelTTSDownloaded ? true : false), + child: Text("神经网络合成语音", + overflow: TextOverflow.ellipsis, + style: TextStyle(color: kIsWeb ? Colors.grey : (AppData().modelTTSDownloaded ? null : Colors.grey)) + ), + ) ], isExpanded: true, ), diff --git a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart index f95b0e0..b4910a3 100644 --- a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart +++ b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart @@ -18,7 +18,7 @@ class ForeFSRSSettingPage extends StatelessWidget { @override Widget build(BuildContext context) { context.read().uiLogger.info("构建 ForeFSRSSettingPage"); - final FSRS fsrs = context.read().globalFSRS; + final FSRS fsrs = FSRS(); if(fsrs.config.enabled && !forceChoosing) { return MainFSRSPage(fsrs: fsrs); } @@ -235,7 +235,7 @@ class ForeFSRSSettingPage extends StatelessWidget { shape: RoundedRectangleBorder(borderRadius: StaticsVar.br) ), onPressed: (){ - fsrs.createScheduler(prefs: context.read().prefs); + fsrs.createScheduler(prefs: AppData().storage); alart(context, "设置完成,重新进入规律学习页面即可开始", onConfirmed: (){Navigator.popUntil(context, (route) => route.isFirst);}); }, icon: Icon(Icons.done), @@ -357,7 +357,8 @@ class _FSRSReviewCardPage extends State { Widget build(BuildContext context) { context.read().uiLogger.info("构建 FSRSReviewCardPage"); MediaQueryData mediaQuery = MediaQuery.of(context); - final List wordData = context.read().wordData.words; + AppData appData = AppData(); + final List wordData = appData.wordData.words; late final int correct; // 防止重建后选项丢失 @@ -366,9 +367,9 @@ class _FSRSReviewCardPage extends State { options = const ["记得很清楚", "还记得", "回忆困难", "忘了"]; correct = -1; } else { - List optionWords = getRandomWords(4, context.read().wordData, include: wordData[widget.wordID], preferClass: !widget.fsrs.config.preferSimilar, rnd: widget.rnd); + List optionWords = getRandomWords(4, appData.wordData, include: wordData[widget.wordID], preferClass: !widget.fsrs.config.preferSimilar, rnd: widget.rnd); options = List.generate(4, (int index) => optionWords[index].chinese, growable: false); - correct = options!.indexOf(context.read().wordData.words[widget.wordID].chinese); + correct = options!.indexOf(appData.wordData.words[widget.wordID].chinese); } } @@ -466,7 +467,7 @@ class _FSRSLearningPageState extends State { void initState() { final Random rnd = Random(); for(WordItem word in widget.words) { - List optionWords = getRandomWords(4, context.read().wordData, include: word, preferClass: !widget.fsrs.config.preferSimilar, rnd: rnd); + List optionWords = getRandomWords(4, AppData().wordData, include: word, preferClass: !widget.fsrs.config.preferSimilar, rnd: rnd); List option = List.generate(4, (int index) => optionWords[index].chinese, growable: false); options.add(option); } diff --git a/lib/sub_pages_builder/learning_pages/learning_pages_build.dart b/lib/sub_pages_builder/learning_pages/learning_pages_build.dart index ca7f09e..db4e1a3 100644 --- a/lib/sub_pages_builder/learning_pages/learning_pages_build.dart +++ b/lib/sub_pages_builder/learning_pages/learning_pages_build.dart @@ -41,9 +41,9 @@ class _InLearningPageState extends State { void onSolve({required WordItem targetWord, required bool isCorrect, required int takentime, - required FSRS fsrs, bool isTypingQuestion = false}){ if(isCorrect) correctCount++; + FSRS fsrs = FSRS(); if(widget.countInReview && fsrs.config.enabled) { if(fsrs.isContained(targetWord.id)){ if(isTypingQuestion) { @@ -70,7 +70,7 @@ class _InLearningPageState extends State { TestItem.buildTestItem( wordItem, questionsSetting.questionSections[sectionIndex], - context.read().wordData, + AppData().wordData, questionsSetting.preferSimilar, rnd ) @@ -171,7 +171,7 @@ class _InLearningPageState extends State { ), body: Center( child: PageView.builder( - scrollDirection: Provider.of(context).isWideScreen ? Axis.vertical : Axis.horizontal, + scrollDirection: AppData().isWideScreen ? Axis.vertical : Axis.horizontal, physics: NeverScrollableScrollPhysics(), // itemCount: testList.length, controller: controller, @@ -217,7 +217,7 @@ class _InLearningPageState extends State { if(!ans) { Future.delayed(Duration(seconds: 1), (){if(context.mounted) viewAnswer(context, testItem.testWord);}); } - onSolve(targetWord: testItem.testWord, isCorrect: ans, takentime: DateTime.now().difference(quizStart).inMilliseconds, fsrs: context.read().globalFSRS); + onSolve(targetWord: testItem.testWord, isCorrect: ans, takentime: DateTime.now().difference(quizStart).inMilliseconds); Future.delayed(Duration(milliseconds: 700) ,(){setState(() { clicked = true; });}); @@ -249,10 +249,10 @@ class _InLearningPageState extends State { clicked = true; }); if(text == testItem.testWord.arabic) { - onSolve(targetWord: testItem.testWord, isCorrect: true, takentime: DateTime.now().difference(quizStart).inMilliseconds, fsrs: context.read().globalFSRS, isTypingQuestion: true); + onSolve(targetWord: testItem.testWord, isCorrect: true, takentime: DateTime.now().difference(quizStart).inMilliseconds); return true; } else { - onSolve(targetWord: testItem.testWord, isCorrect: false, takentime: DateTime.now().difference(quizStart).inMilliseconds, fsrs: context.read().globalFSRS, isTypingQuestion: true); + onSolve(targetWord: testItem.testWord, isCorrect: false, takentime: DateTime.now().difference(quizStart).inMilliseconds); viewAnswer(context, testItem.testWord); return false; } @@ -288,7 +288,7 @@ class _InLearningPageState extends State { if(!ans) { Future.delayed(Duration(seconds: 1), (){if(context.mounted) viewAnswer(context, testItem.testWord);}); } - onSolve(targetWord: testItem.testWord, isCorrect: ans, takentime: DateTime.now().difference(quizStart).inMilliseconds, fsrs: context.read().globalFSRS); + onSolve(targetWord: testItem.testWord, isCorrect: ans, takentime: DateTime.now().difference(quizStart).inMilliseconds); Future.delayed(Duration(milliseconds: 700) ,(){setState(() { clicked = true; });}); @@ -789,16 +789,17 @@ class _WordCardOverViewLayout extends State { @override Widget build(BuildContext context) { MediaQueryData mediaQuery = MediaQuery.of(context); + AppData appData = AppData(); return ListView.builder( physics: allowJsonScorll ? null : NeverScrollableScrollPhysics(), controller: jsonController, - itemCount: context.read().wordData.classes.length + 1, + itemCount: appData.wordData.classes.length + 1, itemBuilder: (context, jsonIndex) { - if(jsonIndex == context.read().wordData.classes.length) { + if(jsonIndex == appData.wordData.classes.length) { return SizedBox(height: mediaQuery.size.height); } - final SourceItem jsonSource = context.read().wordData.classes[jsonIndex]; + final SourceItem jsonSource = appData.wordData.classes[jsonIndex]; return ExpansionTile( title: Text(jsonSource.sourceJsonFileName.trim()), minTileHeight: 64, @@ -861,7 +862,7 @@ class _WordCardOverViewLayout extends State { return Container( margin: EdgeInsets.all(8.0), child: WordCard( - word: context.read().wordData.words[classItem.wordIndexs[index]], + word: appData.wordData.words[classItem.wordIndexs[index]], useMask: false, width: mediaQuery.size.width / (context.read().globalConfig.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : context.read().globalConfig.learning.overviewForceColumn), height: mediaQuery.size.width / (context.read().globalConfig.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : context.read().globalConfig.learning.overviewForceColumn), @@ -898,7 +899,7 @@ class WordLookupLayout extends StatelessWidget { threshold: 4~/(lookfor.length * 0.5 + 1) // 输入越多 容差越小 )); // 从BK树找 - for(WordItem word in context.read().wordData.words) { + for(WordItem word in AppData().wordData.words) { if(match.contains(word)) continue; if(word.arabic.removeAracicExtensionPart().contains(lookfor.removeAracicExtensionPart())) { match.add(word); @@ -913,7 +914,7 @@ class WordLookupLayout extends StatelessWidget { getLevenshtein(lookfor.removeAracicExtensionPart(), a.arabic.removeAracicExtensionPart()) - getLevenshtein(lookfor.removeAracicExtensionPart(), b.arabic.removeAracicExtensionPart()) ); } else { - for(WordItem word in context.read().wordData.words) { + for(WordItem word in AppData().wordData.words) { if(match.contains(word)) continue; if(word.chinese.contains(lookfor)) { match.add(word); diff --git a/lib/sub_pages_builder/setting_pages/about_page.dart b/lib/sub_pages_builder/setting_pages/about_page.dart index 6e00d5b..598f6c3 100644 --- a/lib/sub_pages_builder/setting_pages/about_page.dart +++ b/lib/sub_pages_builder/setting_pages/about_page.dart @@ -59,7 +59,7 @@ class AboutPage extends StatelessWidget { ExpansionTile( title: Text("调试信息"), children: [ - TextContainer(text: "Storage Type: ${context.read().prefs.type ? "SharedPreferences" : "IndexDB"}"), + TextContainer(text: "Storage Type: ${AppData().storage.type ? "SharedPreferences" : "IndexDB"}"), ], ) ], diff --git a/lib/sub_pages_builder/setting_pages/data_download_page.dart b/lib/sub_pages_builder/setting_pages/data_download_page.dart index 447ee49..59a55cc 100644 --- a/lib/sub_pages_builder/setting_pages/data_download_page.dart +++ b/lib/sub_pages_builder/setting_pages/data_download_page.dart @@ -58,7 +58,7 @@ Future> downloadList(BuildContext context) async{ context.read().uiLogger.info("线上词库获取成功"); for(var f in json) { if(f["type"] == "file") { - bool downloaded = context.read().wordData.classes.any((SourceItem source) => source.sourceJsonFileName == f["name"]); + bool downloaded = AppData().wordData.classes.any((SourceItem source) => source.sourceJsonFileName == f["name"]); bool inDownloading = false; list.add(StatefulBuilder( builder: (context, setLocalState) { @@ -81,7 +81,7 @@ Future> downloadList(BuildContext context) async{ var response = await dio.getUri(Uri.parse(f["download_url"])); if(!context.mounted) return ; if(response.statusCode == 200) { - context.read().importData(jsonDecode(response.data) as Map, f["name"]); + AppData().importDictData(jsonDecode(response.data) as Map, f["name"]); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("下载成功: ${f["name"]}"), )); diff --git a/lib/sub_pages_builder/setting_pages/debug_page.dart b/lib/sub_pages_builder/setting_pages/debug_page.dart index b7634d6..a4eebf1 100644 --- a/lib/sub_pages_builder/setting_pages/debug_page.dart +++ b/lib/sub_pages_builder/setting_pages/debug_page.dart @@ -92,14 +92,18 @@ class _DebugPage extends State { children: [ Column( children: List.generate( - context.watch().internalLogCapture.length, + AppData().internalLogCapture.length, (index){ - final String logLine = context.read().internalLogCapture[context.read().internalLogCapture.length - index - 1]; + final String logLine = AppData().internalLogCapture[AppData().internalLogCapture.length - index - 1]; return Container( width: MediaQuery.of(context).size.width * 0.9, decoration: BoxDecoration( color: Theme.of(context).colorScheme.onPrimary, - borderRadius: index == 0 ? BorderRadius.vertical(top: Radius.circular(10.0)) : index == context.watch().internalLogCapture.length-1 ? BorderRadius.vertical(bottom: Radius.circular(10.0)) : BorderRadius.all(Radius.circular(5.0)) + borderRadius: index == 0 + ? BorderRadius.vertical(top: Radius.circular(10.0)) + : index == AppData().internalLogCapture.length-1 + ? BorderRadius.vertical(bottom: Radius.circular(10.0)) + : BorderRadius.all(Radius.circular(5.0)) ), margin: EdgeInsets.all(2.0), padding: EdgeInsets.all(4.0), diff --git a/lib/sub_pages_builder/setting_pages/model_download_page.dart b/lib/sub_pages_builder/setting_pages/model_download_page.dart index b3beb6d..ac6fbfc 100644 --- a/lib/sub_pages_builder/setting_pages/model_download_page.dart +++ b/lib/sub_pages_builder/setting_pages/model_download_page.dart @@ -78,12 +78,11 @@ class _ModelDownload extends State { }); await extractTarBz2('${basePath.path}/arabicLearning/tts/temp.tar.bz2', "${basePath.path}/arabicLearning/tts/model/"); if(!context.mounted) return; - context.read().loadTTS(); + AppData().loadTTS(context.read().globalConfig.audio.playRate); context.read().uiLogger.info("模型下载完成"); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("下载完成"))); setState(() { progress = 4; - context.read().modelTTSDownloaded = true; }); if(io.File('${basePath.path}/arabicLearning/tts/temp.tar.bz2').existsSync()){ io.File('${basePath.path}/arabicLearning/tts/temp.tar.bz2').delete(); diff --git a/lib/sub_pages_builder/setting_pages/questions_setting_page.dart b/lib/sub_pages_builder/setting_pages/questions_setting_page.dart index 43886fb..5ffbd1d 100644 --- a/lib/sub_pages_builder/setting_pages/questions_setting_page.dart +++ b/lib/sub_pages_builder/setting_pages/questions_setting_page.dart @@ -82,7 +82,7 @@ class _QuestionsSettingPage extends State { appBar: AppBar(title: Text("题型配置")), body: Column( children: [ - if(!context.read().isWideScreen) TextContainer(text: "长按可拖动排序", style: TextStyle(color: Colors.grey), animated: true), + if(!AppData().isWideScreen) TextContainer(text: "长按可拖动排序", style: TextStyle(color: Colors.grey), animated: true), Expanded( child: ReorderableListView( onReorder: (oldIndex, newIndex) { diff --git a/lib/sub_pages_builder/setting_pages/sync_page.dart b/lib/sub_pages_builder/setting_pages/sync_page.dart index 32d1d88..81dc20c 100644 --- a/lib/sub_pages_builder/setting_pages/sync_page.dart +++ b/lib/sub_pages_builder/setting_pages/sync_page.dart @@ -123,7 +123,7 @@ class _DataSyncPage extends State { }); try{ if(!webdav.isReachable) await webdav.connect(); - if(context.mounted) await webdav.upload(context.read().prefs); + if(context.mounted) await webdav.upload(AppData().storage); } catch (e) { if(!context.mounted) return; alart(context, e.toString()); @@ -161,7 +161,7 @@ class _DataSyncPage extends State { }); try{ if(!webdav.isReachable) await webdav.connect(); - if(context.mounted) await webdav.download(context.read().prefs); + if(context.mounted) await webdav.download(AppData().storage); if(context.mounted) context.read().conveySetting(); } catch (e) { if(!context.mounted) return; @@ -204,7 +204,7 @@ class _DataSyncPage extends State { dialogTitle: "导出数据", lockParentWindow: true, fileName: "export.json", - bytes: utf8.encode(jsonEncode(context.read().prefs.export())), + bytes: utf8.encode(jsonEncode(AppData().storage.export())), ) != null) { if(context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( @@ -262,7 +262,7 @@ class _DataSyncPage extends State { if (!context.mounted) return; try{ context.read().uiLogger.fine("备份数据读取完成,开始解析"); - context.read().prefs.recovery(jsonDecode(jsonString)); + AppData().storage.recovery(jsonDecode(jsonString)); if(context.mounted) context.read().conveySetting(); alart(context, "备份数据 \"${platformFile.name}\" \n已恢复\n部分设置可能需要软件重启后才能生效"); context.read().uiLogger.info("备份数据 \"${platformFile.name}\" \n已导入。"); diff --git a/lib/sub_pages_builder/test_pages/listening_test_page.dart b/lib/sub_pages_builder/test_pages/listening_test_page.dart index 533038a..6ac203c 100644 --- a/lib/sub_pages_builder/test_pages/listening_test_page.dart +++ b/lib/sub_pages_builder/test_pages/listening_test_page.dart @@ -26,7 +26,7 @@ class _ForeListeningSettingPage extends State { Widget build(BuildContext context) { context.read().uiLogger.info("构建 ForeListeningSettingPage"); MediaQueryData mediaQuery = MediaQuery.of(context); - int wordCount = getSelectedWords(context.read().wordData, selectedClasses.selectedClass).length; + int wordCount = getSelectedWords(AppData().wordData, selectedClasses.selectedClass).length; return Scaffold( appBar: AppBar( title: Text('自主听写预设置'), @@ -210,7 +210,7 @@ class _ForeListeningSettingPage extends State { playTimes: playTimes, interval: interval, intervalBetweenWords: intervalBetweenWords, - words: getSelectedWords(context.read().wordData, selectedClasses.selectedClass, doShuffle: true) + words: getSelectedWords(AppData().wordData, selectedClasses.selectedClass, doShuffle: true) ) ) ); diff --git a/lib/sub_pages_builder/test_pages/local_pk_page.dart b/lib/sub_pages_builder/test_pages/local_pk_page.dart index b1e6663..fa53cd9 100644 --- a/lib/sub_pages_builder/test_pages/local_pk_page.dart +++ b/lib/sub_pages_builder/test_pages/local_pk_page.dart @@ -517,7 +517,7 @@ class _PKOngoingPage extends State { void initState() { Random rnd = Random(context.read().rndSeed); for(WordItem wordItem in context.read().pkState.testWords) { - List optionWords = getRandomWords(4, context.read().wordData, allowRepet: false, include: wordItem, shuffle: true, rnd: rnd); + List optionWords = getRandomWords(4, AppData().wordData, allowRepet: false, include: wordItem, shuffle: true, rnd: rnd); choiceOptions.add(List.generate(4, (int index) => optionWords[index].chinese)); } super.initState(); diff --git a/lib/vars/global.dart b/lib/vars/global.dart index 341f957..8e922b0 100644 --- a/lib/vars/global.dart +++ b/lib/vars/global.dart @@ -20,87 +20,78 @@ class Global with ChangeNotifier { bool backupFontLoaded = false; bool inited = false; //是否初始化完成 - List internalLogCapture = []; - Uint8List? stella; String? arFont; String? zhFont; - late bool firstStart; // 是否为第一次使用 - late bool updateLogRequire; //是否需要显示更新日志 - late bool isWideScreen; // 设备是否是宽屏幕 - late final SharedPreferences prefs; // 储存实例 - late FSRS globalFSRS; - late ThemeData themeData; - bool modelTTSDownloaded = false; - late DictData wordData; - int get wordCount => wordData.words.length; - sherpa_onnx.OfflineTts? vitsTTS; + bool updateLogRequire = false; //是否需要显示更新日志 - Config globalConfig = Config(); + ThemeData get themeData => ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: StaticsVar.themeList[globalConfig.regular.theme], + brightness: globalConfig.regular.darkMode ? Brightness.dark : Brightness.light, + ), + fontFamily: zhFont, + ); + + late Config globalConfig; Future init() async { logger.info("类收到初始化请求,当前初始化状态为 $inited"); if(inited) return false; - logger.info("类开始初始化"); - prefs = await SharedPreferences.getInstance(); - firstStart = prefs.getString("settingData") == null; - if(firstStart) { + logger.info("开始类初始化"); + + AppData appData = AppData(); + await appData.init(); + + if(appData.isFirstStart) { logger.info("首次启动检测为真"); - updateLogRequire = false; - await prefs.setString("wordData", jsonEncode({"Words": [], "Classes": {}})); - wordData = DictData(words: [], classes: []); - logger.info("首次启动: 配置表初始化完成"); - globalFSRS = FSRS()..init(outerPrefs: prefs); - await postInit(); + globalConfig = Config(); + await refreshApp(); } else { - await conveySetting(); + conveySetting(); + await updateSetting(); } + inited = true; logger.info("初始化完成"); return true; } // 预处理一些版本更新的配置文件兼容 - Future conveySetting() async { + void conveySetting() { logger.info("处理配置文件"); - // 在配置文件加载完成前可以做的 - wordData = DictData.buildFromMap(jsonDecode(prefs.getString("wordData")!)); - if(!BKSearch.isReady) BKSearch.init(wordData.words); - globalFSRS = FSRS()..init(outerPrefs: prefs); - - Config oldConfig = Config.buildFromMap(jsonDecode(prefs.getString("settingData")!)); + Config oldConfig = Config.buildFromMap(jsonDecode(AppData().storage.getString("settingData")!)); if(oldConfig.lastVersion != globalConfig.lastVersion) { logger.info("检测到当前版本与上次启动版本不同"); updateLogRequire = true; oldConfig=oldConfig.copyWith(lastVersion: globalConfig.lastVersion); - } else { - updateLogRequire = false; } globalConfig = oldConfig; logger.info("配置文件合成完成"); - await updateSetting(); } // 更新配置到存储中 Future updateSetting({Map? settingData, bool refresh = true}) async { logger.info("保存配置文件中"); if(settingData != null) globalConfig = Config.buildFromMap(settingData); - prefs.setString("settingData", jsonEncode(globalConfig.toMap())); - if(refresh) await postInit(); + AppData().storage.setString("settingData", jsonEncode(globalConfig.toMap())); + if(refresh) await refreshApp(); } - void loadFont() async { + Future loadFont() async { if(backupFontLoaded) return; - backupFontLoaded = true; try{ final ByteData bundle = await rootBundle.load("assets/fonts/zh/NotoSansSC-Medium.ttf"); final FontLoader loader = FontLoader(StaticsVar.zhBackupFont)..addFont(Future.value(bundle)); - loader.load(); + await loader.load(); } catch (e) { - backupFontLoaded = false; + logger.severe("无法加载备用字体"); + return; } + backupFontLoaded = true; notifyListeners(); } @@ -114,63 +105,25 @@ class Global with ChangeNotifier { if(globalConfig.debug.enableInternalLog){ Logger.root.level = Level.ALL; const List levelList = [Level.ALL, Level.FINEST, Level.FINER, Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE, Level.SHOUT, Level.OFF]; + AppData appData = AppData(); Logger.root.onRecord.listen((record) async { if(record.level < levelList[globalConfig.debug.internalLevel]) return; - internalLogCapture.add('${record.time}-[${record.loggerName}][${record.level.name}]: ${record.message}'); + appData.internalLogCapture.add('${record.time}-[${record.loggerName}][${record.level.name}]: ${record.message}'); }); } } - Future postInit() async { + Future refreshApp() async { logger.info("应用设置中"); + AppData appData = AppData(); + if(globalConfig.audio.audioSource == 2) await appData.loadTTS(globalConfig.audio.playRate); + if(globalConfig.egg.stella) await appData.loadEggs(); changeLoggerBehavior(); - await loadTTS(); - await loadEggs(); updateTheme(); notifyListeners(); logger.info("应用设置完成"); } - // load TTS model if any - Future loadTTS() async { - if(kIsWeb || vitsTTS != null || globalConfig.audio.audioSource != 2) return; - logger.info("TTS: 加载本地TTS中"); - final basePath = await path_provider.getApplicationDocumentsDirectory(); - if(io.File("${basePath.path}/${StaticsVar.modelPath}/ar_JO-kareem-medium.onnx").existsSync()){ - modelTTSDownloaded = true; - sherpa_onnx.initBindings(); - final vits = sherpa_onnx.OfflineTtsVitsModelConfig( - model: "${basePath.path}/${StaticsVar.modelPath}/ar_JO-kareem-medium.onnx", - // lexicon: '${basePath.path}/${StaticsVar.modelPath}/', - dataDir: "${basePath.path}/${StaticsVar.modelPath}/espeak-ng-data", - tokens: '${basePath.path}/${StaticsVar.modelPath}/tokens.txt', - lengthScale: 1 / globalConfig.audio.playRate, - ); - // kokoro = sherpa_onnx.OfflineTtsKokoroModelConfig(); - final modelConfig = sherpa_onnx.OfflineTtsModelConfig( - vits: vits, - numThreads: 2, - debug: false, - provider: 'cpu', - ); - - final config = sherpa_onnx.OfflineTtsConfig( - model: modelConfig, - maxNumSenetences: 1, - ); - - vitsTTS = sherpa_onnx.OfflineTts(config); - logger.info("TTS: 本地TTS加载完成"); - } - } - - Future loadEggs() async { - if(globalConfig.egg.stella && stella == null){ - final rawString = await rootBundle.loadString("assets/eggs/s.txt"); - stella = base64Decode(rawString.replaceAll('\n', '').replaceAll('\r', '').trim()); - } - } - void updateTheme() { logger.info("更新主题中"); if(globalConfig.regular.font == 2) { @@ -184,52 +137,123 @@ class Global with ChangeNotifier { arFont = null; zhFont = null; } - themeData = ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: StaticsVar.themeList[globalConfig.regular.theme], - brightness: globalConfig.regular.darkMode ? Brightness.dark : Brightness.light, - ), - fontFamily: zhFont, - ); } + + void updateLearningStreak(){ + final int nowDate = DateTime.now().difference(DateTime(2025, 11, 1)).inDays; + if (nowDate == globalConfig.learning.lastDate) return; + logger.info("保存学习进度中"); + // 以 2025/11/1 为基准计算天数(因为这个bug是这天修的:} ) + if (nowDate - globalConfig.learning.lastDate > 1) { + globalConfig = globalConfig.copyWith(learning: globalConfig.learning.copyWith(startDate: nowDate)); + } + globalConfig = globalConfig.copyWith(learning: globalConfig.learning.copyWith(lastDate: nowDate)); + updateSetting(refresh: false); + logger.info("学习进度保存完成"); + } +} - void acceptAggrement(String name) { - firstStart = false; - globalConfig = globalConfig.copyWith(user: name); - prefs.setString("settingData", jsonEncode(globalConfig.toMap())); - notifyListeners(); +class AppData { + // 作为单例 + static final AppData _instance = AppData._internal(); + factory AppData() => _instance; + AppData._internal(); + + bool inited = false; + Logger logger = Logger("AppData"); + + List internalLogCapture = []; + Uint8List? stella; + bool isWideScreen = false; + + late final SharedPreferences storage; + late final io.Directory basePath; + late FSRS fsrs; + late DictData wordData; + sherpa_onnx.OfflineTts? vitsTTS; + + int get wordCount => wordData.words.length; + bool get isFirstStart => storage.getString("settingData") == null; + bool get modelTTSDownloaded => io.File("${basePath.path}/${StaticsVar.modelPath}/ar_JO-kareem-medium.onnx").existsSync(); + + Future init() async { + if(inited) return; + storage = await SharedPreferences.getInstance(); + basePath = (await path_provider.getApplicationDocumentsDirectory()) as io.Directory; + + wordData = DictData.buildFromMap(jsonDecode(storage.getString("wordData")!)); + if(!BKSearch.isReady) BKSearch.init(wordData.words); + FSRS().init(); + inited = true; + } + + Future initStorageValue() async { + await storage.setString("wordData", jsonEncode({"Words": [], "Classes": {}})); + wordData = DictData(words: [], classes: []); + logger.info("配置表初始化完成"); + } + + // load TTS model if any + Future loadTTS(double playRate) async { + if(kIsWeb || vitsTTS != null || modelTTSDownloaded) return; + logger.info("TTS: 加载本地TTS中"); + sherpa_onnx.initBindings(); + final vits = sherpa_onnx.OfflineTtsVitsModelConfig( + model: "${basePath.path}/${StaticsVar.modelPath}/ar_JO-kareem-medium.onnx", + dataDir: "${basePath.path}/${StaticsVar.modelPath}/espeak-ng-data", + tokens: '${basePath.path}/${StaticsVar.modelPath}/tokens.txt', + lengthScale: 1 / playRate, + ); + final modelConfig = sherpa_onnx.OfflineTtsModelConfig( + vits: vits, + numThreads: 2, + debug: false, + provider: 'cpu', + ); + final config = sherpa_onnx.OfflineTtsConfig( + model: modelConfig, + maxNumSenetences: 1, + ); + + vitsTTS = sherpa_onnx.OfflineTts(config); + logger.info("TTS: 本地TTS加载完成"); } + Future loadEggs() async { + if(stella == null){ + final rawString = await rootBundle.loadString("assets/eggs/s.txt"); + stella = base64Decode(rawString); + } + } - // Non-Format Data: - // { - // "ClassName": [ - // { - // "chinese": {Chinese}, - // "arabic": {arabic}, - // "explanation": {explanation} - // }, ... - // ] - // } - // Format Data: - // { - // "Words" : [ - // { - // "arabic": {arabic}, - // "chinese": {Chinese}, - // "explanation": {explanation}, - // "subClass": {ClassName}, - // "learningProgress": {times} //int - // }, ... - // ], - // "Classes": { - // "SourceJsonFileName": { - // "ClassName": [wordINDEX], - // } - // } - // } - DictData dataFormater(Map data, DictData exData, String sourceName) { + /// Non-Format Data: + /// { + /// "ClassName": [ + /// { + /// "chinese": {Chinese}, + /// "arabic": {arabic}, + /// "explanation": {explanation} + /// }, ... + /// ] + /// } + /// Format Data: + /// { + /// "Words" : [ + /// { + /// "arabic": {arabic}, + /// "chinese": {Chinese}, + /// "explanation": {explanation}, + /// "subClass": {ClassName}, + /// "learningProgress": {times} //int + /// }, ... + /// ], + /// "Classes": { + /// "SourceJsonFileName": { + /// "ClassName": [wordINDEX], + /// } + /// } + /// } + DictData dataFormater(Map data, DictData existData, String sourceName) { logger.info("开始词汇格式化"); // Use Maps for O(1) lookup speed instead of O(N) List.indexOf @@ -237,23 +261,23 @@ class Global with ChangeNotifier { Map pureWordMap = {}; List chineseList = []; - for(int i = 0; i < exData.words.length; i++) { - WordItem x = exData.words[i]; + for(int i = 0; i < existData.words.length; i++) { + WordItem x = existData.words[i]; rawWordMap[x.arabic] = i; pureWordMap[x.arabic.removeAracicExtensionPart().trim()] = i; chineseList.add(x.chinese); // Keep list for indexing since it maps 1:1 with word id } - int counter = exData.words.length; + int counter = existData.words.length; SourceItem? exSource; // 查找已有数据中是否有同名的源数据组 - for(SourceItem x in exData.classes) { + for(SourceItem x in existData.classes) { if(x.sourceJsonFileName == sourceName) exSource = x; } if(exSource == null){ - exData.classes.add(SourceItem(sourceJsonFileName: sourceName, subClasses: [])); - exSource = exData.classes.last; + existData.classes.add(SourceItem(sourceJsonFileName: sourceName, subClasses: [])); + exSource = existData.classes.last; } for(var className in data.keys){ @@ -282,7 +306,7 @@ class Global with ChangeNotifier { } exClass.wordIndexs.add(counter); - exData.words.add( + existData.words.add( WordItem( arabic: word["arabic"], chinese: word["chinese"], @@ -298,28 +322,14 @@ class Global with ChangeNotifier { } exSource.subClasses.add(exClass); } - return exData; + return existData; } - void importData(Map data, String source) { + void importDictData(Map importData, String source) { logger.info("收到词汇导入请求"); - wordData = dataFormater(data, wordData, source); - prefs.setString("wordData", jsonEncode(wordData.toMap())); + wordData = dataFormater(importData, wordData, source); + storage.setString("wordData", jsonEncode(wordData.toMap())); BKSearch.init(wordData.words); // 重新建树 logger.info("词汇导入完成"); - notifyListeners(); } - - void updateLearningStreak(){ - final int nowDate = DateTime.now().difference(DateTime(2025, 11, 1)).inDays; - if (nowDate == globalConfig.learning.lastDate) return; - logger.info("保存学习进度中"); - // 以 2025/11/1 为基准计算天数(因为这个bug是这天修的:} ) - if (nowDate - globalConfig.learning.lastDate > 1) { - globalConfig = globalConfig.copyWith(learning: globalConfig.learning.copyWith(startDate: nowDate)); - } - globalConfig = globalConfig.copyWith(learning: globalConfig.learning.copyWith(lastDate: nowDate)); - updateSetting(refresh: false); - logger.info("学习进度保存完成"); - } -} +} \ No newline at end of file From 505278c1f1224b7f150f22373fef93807f5b3893 Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:50:17 +0800 Subject: [PATCH 2/8] feat: remove some optional args in utili Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/funcs/ui.dart | 4 ++-- lib/funcs/utili.dart | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/funcs/ui.dart b/lib/funcs/ui.dart index 1db9fe9..c0ad78c 100644 --- a/lib/funcs/ui.dart +++ b/lib/funcs/ui.dart @@ -803,7 +803,7 @@ class _ChoiceQuestions extends State { // showingMode 0: 1 Row, 1: 2 Rows, 2: 4 Rows if(showingMode == -1){ context.read().uiLogger.fine("未指定布局,开始计算"); - showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width, AppData().isWideScreen); + showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width); context.read().uiLogger.info("最终采用布局方案: $showingMode"); } return Material( @@ -1039,7 +1039,7 @@ class _ListeningQuestion extends State { int showingMode = widget.bottonLayout; if(showingMode == -1){ context.read().uiLogger.fine("未指定布局,开始计算"); - showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width, AppData().isWideScreen); + showingMode = calculateButtonBoxLayout(widget.choices, mediaQuery.size.width); context.read().uiLogger.info("最终采用布局方案: $showingMode"); } return Material( diff --git a/lib/funcs/utili.dart b/lib/funcs/utili.dart index 0fff46e..d66969c 100644 --- a/lib/funcs/utili.dart +++ b/lib/funcs/utili.dart @@ -134,8 +134,9 @@ Future> playTextToSpeech(String text, BuildContext context, {doubl return [true, ""]; } -int calculateButtonBoxLayout(List possible, double width, bool isWideScreen){ +int calculateButtonBoxLayout(List possible, double width){ // showingMode 0: 1 Row, 1: 2 Rows, 2: 4 Rows + bool isWideScreen = AppData().isWideScreen; for(int i = 1; i < 4; i++) { if(possible[i].length * 16 > width * (isWideScreen ? 0.21 : 0.8)){ if (isWideScreen) { From 51879b1839adf1867653019fb017166bf8101f01 Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:50:37 +0800 Subject: [PATCH 3/8] fix: globalconfig not initilized Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/vars/global.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/vars/global.dart b/lib/vars/global.dart index 8e922b0..d8c6379 100644 --- a/lib/vars/global.dart +++ b/lib/vars/global.dart @@ -33,7 +33,7 @@ class Global with ChangeNotifier { fontFamily: zhFont, ); - late Config globalConfig; + Config globalConfig = Config(); Future init() async { @@ -46,7 +46,6 @@ class Global with ChangeNotifier { if(appData.isFirstStart) { logger.info("首次启动检测为真"); - globalConfig = Config(); await refreshApp(); } else { conveySetting(); From deaea0058f6c33dbb281ea8168af5f6632cf1727 Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:58:59 +0800 Subject: [PATCH 4/8] fix: remove extra log listener Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/main.dart b/lib/main.dart index 221ed40..64af10b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,6 +24,7 @@ import 'package:workmanager/workmanager.dart' show Workmanager, Constraints; void main() async { Logger.root.level = kDebugMode ? Level.ALL : Level.OFF; if (kDebugMode){ + Logger.root.clearListeners(); Logger.root.onRecord.listen((record) { if(record.loggerName == "BKTree") return; // bk树不要刷屏 debugPrint('${record.time}-[${record.loggerName}][${record.level.name}]: ${record.message}'); From a0b84c2d0ec4b2a26c7287095d485ef571e9893e Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:03:23 +0800 Subject: [PATCH 5/8] refactor: move config to appdata Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- assets/help/announce.md | 26 +++++ lib/funcs/ui.dart | 20 ++-- lib/funcs/utili.dart | 57 +++------- lib/main.dart | 103 +++++++++--------- lib/pages/home_page.dart | 45 ++++---- lib/pages/learning_page.dart | 2 + lib/pages/setting_page.dart | 40 +++---- .../learning_pages/learning_pages_build.dart | 26 ++--- .../setting_pages/about_page.dart | 2 +- .../setting_pages/debug_page.dart | 14 +-- .../setting_pages/model_download_page.dart | 2 +- .../setting_pages/open_source_licenses.dart | 6 +- .../setting_pages/questions_setting_page.dart | 32 +++--- .../setting_pages/sync_page.dart | 26 ++--- .../test_pages/listening_test_page.dart | 4 +- lib/vars/global.dart | 37 +++---- 16 files changed, 224 insertions(+), 218 deletions(-) create mode 100644 assets/help/announce.md diff --git a/assets/help/announce.md b/assets/help/announce.md new file mode 100644 index 0000000..6b2fb8b --- /dev/null +++ b/assets/help/announce.md @@ -0,0 +1,26 @@ +# 软件说明 + +**欢迎使用本软件,请先阅读使用说明。** + +软件开源地址:[https://github.com/OctagonalStar/arabic_learning](https://github.com/OctagonalStar/arabic_learning) + +## 声明 + +由于该软件目前还处在开发阶段,有一些bug是不可避免的。所以在正式使用该软件前你应当阅读并理解以下条款: + +1. 该软件仅供学习使用,**请勿用于商业用途**。 +2. 该软件**不会**对你的阿拉伯语成绩做出任何担保~,若你出现阿拉伯语成绩不理想的情况请先考虑自己的问题 :)~ +3. 由于软件在不同系统上运行可能存在兼容性问题,软件出错造成的任何损失(包含精神损伤),软件作者和其他贡献者不会担负任何责任 +4. 你知晓并理解如果你错误地使用软件(如使用错误的数据集)造成的任何后果,~乃至二次宇宙大爆炸,~都需要你自行承担 +5. 其他在*GNU AFFERO GENERAL PUBLIC LICENSE (Version 3)*开源协议下的条款 + +## 网页端特别声明 + +1. 由于网页端的一些限制,该软件**不一定**能按照预期工作 +2. 软件使用中所有的数据均保存在浏览器缓存中,不会进行任何用户数据上传。清空网站缓存可导致数据永久丢失 +3. 该网页部署于Github Pages,由Github Action自动构建,所以网站会不定期进行热更新,且版本快于发布版 +4. 由于(3)的原因,你可以由此更早地体验到新版功能,但也可能遇到新bug,如果遇到了相关的bug请在Github上提交issue +5. 由于Github Pages服务器地区不可控,我**完全不能**保证你是否能正常链接网站 +6. **网站展示效果不代表实际app发布版效果** + +若你已理解并接受上述条款,请向下翻页,并在底部输入框中填写你的名字,并点击“我没异议”按钮以确认。 diff --git a/lib/funcs/ui.dart b/lib/funcs/ui.dart index c0ad78c..236cd4d 100644 --- a/lib/funcs/ui.dart +++ b/lib/funcs/ui.dart @@ -511,7 +511,7 @@ class WordCard extends StatelessWidget { icon: const Icon(Icons.volume_up, size: 24.0), label: FittedBox(child: Text(word.arabic, style: TextStyle(fontSize: 64.0, fontFamily: context.read().arFont))), onPressed: (){ - playTextToSpeech(word.arabic, context); + playTextToSpeech(word.arabic); }, ), Stack( @@ -829,11 +829,12 @@ class _ChoiceQuestions extends State { setLocalState(() { playing = true; }); - late List temp; - temp = await playTextToSpeech(widget.mainWord, context); - if(!temp[0] && context.mounted) { - alart(context, temp[1]); + try { + await playTextToSpeech(widget.mainWord); + } catch (e) { + if(context.mounted) alart(context, e.toString()); } + setLocalState(() { playing = false; }); @@ -1064,11 +1065,12 @@ class _ListeningQuestion extends State { setLocalState(() { playing = true; }); - late List temp; - temp = await playTextToSpeech(widget.mainWord, context); - if(!temp[0] && context.mounted) { - alart(context, temp[1]); + try { + await playTextToSpeech(widget.mainWord); + } catch (e) { + if(context.mounted) alart(context, e.toString()); } + setLocalState(() { playing = false; }); diff --git a/lib/funcs/utili.dart b/lib/funcs/utili.dart index d66969c..51191ad 100644 --- a/lib/funcs/utili.dart +++ b/lib/funcs/utili.dart @@ -5,7 +5,6 @@ import 'package:archive/archive.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart' as path_provider; -import 'package:provider/provider.dart'; import 'package:dio/dio.dart'; import 'package:just_audio/just_audio.dart'; import 'package:bk_tree/bk_tree.dart'; @@ -44,70 +43,46 @@ List getSelectedWords(DictData wordData , List selectedClas return ans; } -Future> playTextToSpeech(String text, BuildContext context, {double? speed}) async { - // return [bool isSuccessed?, String errorInfo]; - speed ??= context.read().globalConfig.audio.playRate; - context.read().logger.info("[TTS]请求: 文本: [$text]"); +Future playTextToSpeech(String text, {double? speed}) async { + speed ??= AppData().config.audio.playRate; // 0: System TTS - if (context.read().globalConfig.audio.audioSource == 0) { - context.read().logger.info("[TTS]配置使用系统TTS"); + if (AppData().config.audio.audioSource == 0) { FlutterTts flutterTts = FlutterTts(); - if(!(await flutterTts.getLanguages).toString().contains("ar") && context.mounted) { - context.read().logger.warning("[TTS]用户设备不支持AR语言TTS"); - return [false, "你的设备似乎未安装阿拉伯语语言或不支持阿拉伯语文本转语音功能,语音可能无法正常播放。\n你可以在 设置-常见问题 中找到可能的解决方案"]; + if(!(await flutterTts.getLanguages).toString().contains("ar")) { + throw Exception("你的设备似乎未安装阿拉伯语语言或不支持阿拉伯语文本转语音功能,语音可能无法正常播放。\n你可以在 设置-常见问题 中找到可能的解决方案"); } await flutterTts.setLanguage("ar"); await flutterTts.setPitch(1.0); - if(!context.mounted) return [false, ""]; await flutterTts.setSpeechRate(speed / 2); await flutterTts.speak(text); await Future.delayed(Duration(seconds: 2)); - if(!context.mounted) return [false, ""]; - context.read().logger.fine("[TTS]系统TTS阅读完成"); // 1: TextReadTTS - } else if (context.read().globalConfig.audio.audioSource == 1) { - context.read().logger.info("[TTS]配置使用API进行TTS"); + } else if (AppData().config.audio.audioSource == 1) { try { - context.read().logger.fine("[TTS]正在获取"); final response = await Dio().getUri(Uri.parse("https://textreadtts.com/tts/convert?accessKey=FREE&language=arabic&speaker=speaker2&text=$text")).timeout(Duration(seconds: 8), onTimeout: () => throw Exception("请求超时")); if (response.statusCode == 200) { - if(response.data["code"] == 1 && context.mounted) { - context.read().logger.fine("[TTS]API音频获取失败,文本长度超过API限制"); - return [false, "API音源请求失败:\n错误信息:文本长度超过API限制"]; + if(response.data["code"] == 1) { + throw Exception("API音源请求失败:\n错误信息:文本长度超过API限制"); } await StaticsVar.player.setUrl(response.data["audio"]); - if(!context.mounted) return [false, ""]; await StaticsVar.player.setSpeed(speed); await StaticsVar.player.play(); await Future.delayed(Duration(seconds: 2)); - if(context.mounted) context.read().logger.fine("[TTS]API TTS阅读完成"); } else { - if(context.mounted) context.read().logger.severe("[TTS]网络获取错误 ${response.statusCode}"); - return [false, "API音源请求失败:\n错误码:${response.statusCode.toString()}"]; + throw Exception("API音源请求失败:\n错误码:${response.statusCode.toString()}"); } } catch (e) { - if(context.mounted) context.read().logger.severe("[TTS]API错误 $e"); - return [false, "API音源请求失败:\n错误信息:${e.toString()}"]; + throw Exception("API音源请求失败:\n错误信息:${e.toString()}"); } // 2: sherpa-onnx - } else if (context.read().globalConfig.audio.audioSource == 2) { - context.read().logger.info("[TTS]配置使用 sherpa_onnx TTS"); - if(AppData().vitsTTS == null) { - context.read().logger.warning("[TTS]sherpa_onnx 未加载"); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('神经网络音频模型尚未就绪,请等待...'), - duration: Duration(seconds: 2), - ), - ); - } + } else if (AppData().config.audio.audioSource == 2) { + if(AppData().vitsTTS == null) throw Exception("神经网络音频模型尚未就绪"); try { final basePath = await path_provider.getApplicationCacheDirectory(); final cacheFile = io.File("${basePath.path}/temp.wav"); if(cacheFile.existsSync()) cacheFile.deleteSync(); - if(!context.mounted) return [false, ""]; final audio = AppData().vitsTTS!.generate(text: text, speed: speed); final ok = sherpa_onnx.writeWave( filename: cacheFile.path, @@ -121,17 +96,13 @@ Future> playTextToSpeech(String text, BuildContext context, {doubl StaticsVar.player.play(); await Future.delayed(duration); if(cacheFile.existsSync()) cacheFile.deleteSync(); - if(context.mounted) context.read().logger.fine("[TTS]sherpa_onnx TTS阅读完成"); } else { - context.read().logger.severe("[TTS]sherpa_onnx 无法将音频写入文件"); - return [false, "神经网络音频合成失败\n错误信息:无法将音频写入文件"]; + throw Exception("神经网络音频合成失败\n错误信息:无法将音频写入文件"); } } catch (e) { - if(context.mounted) context.read().logger.severe("[TTS]sherpa_onnx 错误: $e"); - return [false, "神经网络音频合成失败\n错误信息:${e.toString()}"]; + throw Exception("sherpa_onnx 错误: $e"); } } - return [true, ""]; } int calculateButtonBoxLayout(List possible, double width){ diff --git a/lib/main.dart b/lib/main.dart index 64af10b..23cb01a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'package:logging/logging.dart'; import 'package:arabic_learning/funcs/ui.dart'; import 'package:arabic_learning/funcs/utili.dart'; import 'package:arabic_learning/vars/global.dart' show AppData, Global; -import 'package:arabic_learning/vars/license_storage.dart' show LicenseVars; import 'package:arabic_learning/vars/statics_var.dart' show StaticsVar; import 'package:arabic_learning/pages/home_page.dart'; import 'package:arabic_learning/pages/learning_page.dart'show LearningPage; @@ -78,28 +77,27 @@ class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { - context.read().uiLogger.fine("收到应用层构建请求"); + context.read().uiLogger.info("收到应用层构建请求"); return context.watch().inited? MaterialApp( title: StaticsVar.appName, themeMode: ThemeMode.system, theme: context.read().themeData, - home: context.read().globalConfig.egg.stella - ? Scaffold( - body: Stack( - children: [ - Container( - decoration: BoxDecoration( - image: DecorationImage( - image: MemoryImage(AppData().stella!), - fit: BoxFit.cover, - ), + home: AppData().config.egg.stella + ? Stack( + children: [ + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: MemoryImage(AppData().stella!), + fit: BoxFit.cover, ), ), - const MyHomePage(title: StaticsVar.appName), - ], - ),) - : const MyHomePage(title: StaticsVar.appName), + ), + const MyHomePage(), + ], + ) + : const MyHomePage(), ) : Material(child: Container(width: double.infinity, height: double.infinity, color: Colors.black ,child: Center(child: CircularProgressIndicator()))); } @@ -107,8 +105,8 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - final String title; + const MyHomePage({super.key}); + @override State createState() => _MyHomePageState(); } @@ -117,7 +115,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { late List _pageList; final PageController _pageController = PageController(initialPage: 0); - bool disPlayedFirst = false; + final TextEditingController controller = TextEditingController(); // 判断是否为桌面端的阈值(可根据需要调整) static const double _desktopBreakpoint = 600; @@ -240,11 +238,11 @@ class _MyHomePageState extends State { ); } - final TextEditingController controller = TextEditingController(); @override Widget build(BuildContext context) { context.read().uiLogger.fine("构建 MyHomePage"); - final gob = context.watch(); + final global = context.watch(); + if(AppData().isFirstStart) { context.read().uiLogger.info("构建首次启动页面"); return Scaffold( @@ -254,13 +252,13 @@ class _MyHomePageState extends State { Expanded( child: ListView( children: [ - SelectableText('欢迎使用本软件,请先阅读使用说明。', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.redAccent, fontSize: 36)), - SelectableText("软件开源地址:https://github.com/OctagonalStar/arabic_learning"), - SelectableText(LicenseVars.noMyDutyAnnouce), - SelectableText("若你已理解并接受上述条款,请向下翻页,并在底部输入框中填写你的名字,并点击“我没异议”按钮以确认。"), - SizedBox(height: MediaQuery.of(context).size.height * 0.1), - if(kIsWeb) SelectableText(LicenseVars.theWebSpecialAnnouce), - Text('招募软件图标ing\n有想法或者有现有设计可以联系我', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.redAccent, fontSize: 18)), + FutureBuilder( + future: rootBundle.loadString('assets/help/audio.md'), + initialData: "加载中...", + builder: (context, asyncSnapshot) { + return MarkdownBody(data: asyncSnapshot.data!); + } + ), SizedBox(height: MediaQuery.of(context).size.height), TextField( controller: controller, @@ -293,7 +291,7 @@ class _MyHomePageState extends State { onPressed: () async { if(controller.text.isNotEmpty){ context.read().uiLogger.info("用户同意协议,签署名:${controller.text}"); - context.read().globalConfig = context.read().globalConfig.copyWith(user: controller.text); + AppData().config = AppData().config.copyWith(user: controller.text); context.read().updateSetting(refresh: true); } else { context.read().uiLogger.info("用户未填写名称"); @@ -315,15 +313,17 @@ class _MyHomePageState extends State { ), ); } + if(io.Platform.isAndroid) { FlutterLocalNotificationsPlugin() .resolvePlatformSpecificImplementation() ?.requestNotificationsPermission(); } + // 更新日志通知 - if(gob.updateLogRequire) { + if(global.updateLogRequire) { context.read().uiLogger.info("预定更新日志通知"); - gob.updateLogRequire = false; + global.updateLogRequire = false; Future.delayed(Duration(seconds: 1), () async { late final String changeLog; changeLog = await rootBundle.loadString('CHANGELOG.md'); @@ -336,30 +336,29 @@ class _MyHomePageState extends State { isScrollControlled: true, builder: (context) { context.read().uiLogger.info("构建更新日志通知"); - return Material( - child: Column( - children: [ - TextContainer(text: "更新内容 软件版本: ${StaticsVar.appVersion.zfill(6)}"), - SizedBox( - height: MediaQuery.of(context).size.height * 0.8, - child: Markdown(data: changeLog) + return Column( + children: [ + TextContainer(text: "更新内容 软件版本: ${StaticsVar.appVersion.zfill(6)}"), + SizedBox( + height: MediaQuery.of(context).size.height * 0.8, + child: Markdown(data: changeLog) + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + fixedSize: Size(double.infinity, MediaQuery.of(context).size.height * 0.07) ), - ElevatedButton( - style: ElevatedButton.styleFrom( - fixedSize: Size(double.infinity, MediaQuery.of(context).size.height * 0.07) - ), - onPressed: () { - Navigator.pop(context); - }, - child: Text("知道了") - ) - ], - ) + onPressed: () { + Navigator.pop(context); + }, + child: Text("知道了") + ) + ], ); }, ); }); } + _pageList = [ HomePage(), LearningPage(), @@ -367,13 +366,13 @@ class _MyHomePageState extends State { SettingPage() ]; return Scaffold( - backgroundColor: context.read().globalConfig.egg.stella ? Colors.transparent : null, + backgroundColor: AppData().config.egg.stella ? Colors.transparent : null, appBar: AppBar( centerTitle: true, backgroundColor: Theme.of(context).colorScheme.inversePrimary.withAlpha(150), - title: Text(widget.title), + title: Text(StaticsVar.appName), actions: [ - if(kIsWeb && !gob.globalConfig.regular.hideAppDownloadButton) ElevatedButton.icon( + if(kIsWeb && !AppData().config.regular.hideAppDownloadButton) ElevatedButton.icon( icon: Icon(Icons.add_to_home_screen), label: Text('下载APP版本'), onPressed: () { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 787455e..8be059c 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -13,12 +13,13 @@ import 'package:arabic_learning/funcs/fsrs_func.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); + @override Widget build(BuildContext context) { context.read().uiLogger.fine("构建 HomePage"); - final themeColor = Theme.of(context).colorScheme; - final MediaQueryData mediaQuery = MediaQuery.of(context); - final FSRS fsrs = FSRS(); + MediaQueryData mediaQuery = MediaQuery.of(context); + FSRS fsrs = FSRS(); + return Column( children: [ DailyWord(), @@ -32,10 +33,10 @@ class HomePage extends StatelessWidget { margin: EdgeInsets.all(4.0), padding: EdgeInsets.all(16.0), decoration: BoxDecoration( - color: themeColor.secondaryContainer.withAlpha(150), + color: Theme.of(context).colorScheme.secondaryContainer.withAlpha(150), boxShadow: [ BoxShadow( - color: themeColor.surfaceBright.withAlpha(150), + color: Theme.of(context).colorScheme.surfaceBright.withAlpha(150), offset: Offset(2, 4), blurRadius: 8.0, ), @@ -49,13 +50,13 @@ class HomePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text('连胜天数', style: TextStyle(fontSize: 12.0)), - context.read().globalConfig.learning.lastDate == DateTime.now().difference(DateTime(2025, 11, 1)).inDays + AppData().config.learning.lastDate == DateTime.now().difference(DateTime(2025, 11, 1)).inDays ? Icon(Icons.done, size: 15.0, color: Colors.tealAccent) - : Icon(Icons.error_outline, size: 15.0, color: Colors.amber, semanticLabel: "今天还没学习~"), + : Icon(Icons.error_outline, size: 15.0, color: Colors.amber), ], ), SizedBox(height: mediaQuery.size.height * 0.03), - Text(getStrokeDays(context.read().globalConfig.learning).toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)), + Text(getStrokeDays(AppData().config.learning).toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)), ], ), ), @@ -65,10 +66,10 @@ class HomePage extends StatelessWidget { margin: EdgeInsets.all(4.0), padding: EdgeInsets.all(16.0), decoration: BoxDecoration( - color: themeColor.secondaryContainer.withAlpha(150), + color: Theme.of(context).colorScheme.secondaryContainer.withAlpha(150), boxShadow: [ BoxShadow( - color: themeColor.surfaceBright.withAlpha(150), + color: Theme.of(context).colorScheme.surfaceBright.withAlpha(150), offset: Offset(2, 4), blurRadius: 8.0, ), @@ -94,10 +95,10 @@ class HomePage extends StatelessWidget { margin: EdgeInsets.all(4.0), padding: EdgeInsets.all(16.0), decoration: BoxDecoration( - color: themeColor.secondaryContainer.withAlpha(150), + color: Theme.of(context).colorScheme.secondaryContainer.withAlpha(150), boxShadow: [ BoxShadow( - color: themeColor.surfaceBright.withAlpha(150), + color: Theme.of(context).colorScheme.surfaceBright.withAlpha(150), offset: Offset(2, 4), blurRadius: 8.0, ), @@ -118,10 +119,10 @@ class HomePage extends StatelessWidget { margin: EdgeInsets.all(4.0), padding: EdgeInsets.all(16.0), decoration: BoxDecoration( - color: themeColor.secondaryContainer.withAlpha(150), + color: Theme.of(context).colorScheme.secondaryContainer.withAlpha(150), boxShadow: [ BoxShadow( - color: themeColor.surfaceBright.withAlpha(150), + color: Theme.of(context).colorScheme.surfaceBright.withAlpha(150), offset: Offset(2, 4), blurRadius: 8.0, ), @@ -173,13 +174,17 @@ class _DailyWord extends State { onPressed: () async { if(playing) return; if(appData.wordCount != 0) { - playing = true; - late List temp; - temp = await playTextToSpeech(dailyWord, context); - if(!temp[0] && context.mounted) { - alart(context, temp[1]); + setState(() { + playing = true; + }); + try { + await playTextToSpeech(dailyWord); + } catch (e) { + if(context.mounted) alart(context, e.toString()); } - playing = false; + setState(() { + playing = false; + }); } else { context.read().uiLogger.info("跳转: DailyWord => SettingPage"); Navigator.of(context).push(MaterialPageRoute(builder: (context) => Scaffold(appBar: AppBar(title: Text("设置")) , body: SettingPage()))); diff --git a/lib/pages/learning_page.dart b/lib/pages/learning_page.dart index 49e1e83..6b125f0 100644 --- a/lib/pages/learning_page.dart +++ b/lib/pages/learning_page.dart @@ -15,10 +15,12 @@ import 'package:arabic_learning/sub_pages_builder/learning_pages/learning_pages_ class LearningPage extends StatelessWidget { const LearningPage({super.key}); + @override Widget build(BuildContext context) { context.read().uiLogger.fine("构建 LearningPage"); final mediaQuery = MediaQuery.of(context); + return Column( children: [ SizedBox(height: mediaQuery.size.height * 0.05), diff --git a/lib/pages/setting_page.dart b/lib/pages/setting_page.dart index 4272dde..085349d 100644 --- a/lib/pages/setting_page.dart +++ b/lib/pages/setting_page.dart @@ -87,6 +87,8 @@ class _SettingPage extends State { List regularSetting(BuildContext context) { MediaQueryData mediaQuery = MediaQuery.of(context); + AppData appData = AppData(); + return [ Row( children: [ @@ -94,7 +96,7 @@ class _SettingPage extends State { SizedBox(width: mediaQuery.size.width * 0.01), Expanded(child: Text("主题颜色:")), DropdownButton( - value: context.watch().globalConfig.regular.theme, + value: appData.config.regular.theme, items: const [ DropdownMenuItem(value: 0, child: Text('樱粉')), DropdownMenuItem(value: 1, child: Text('海蓝')), @@ -110,8 +112,8 @@ class _SettingPage extends State { ], onChanged: (value) async { context.read().uiLogger.info("更新主题颜色: $value"); - context.read().globalConfig = context.read().globalConfig.copyWith( - regular: context.read().globalConfig.regular.copyWith(theme: value) + AppData().config = AppData().config.copyWith( + regular: AppData().config.regular.copyWith(theme: value) ); context.read().updateSetting(); }, @@ -124,11 +126,11 @@ class _SettingPage extends State { SizedBox(width: mediaQuery.size.width * 0.01), Expanded(child: Text("深色模式:")), Switch( - value: context.watch().globalConfig.regular.darkMode, + value: appData.config.regular.darkMode, onChanged: (value) { context.read().uiLogger.info("更新深色模式设置: $value"); - context.read().globalConfig = context.read().globalConfig.copyWith( - regular: context.read().globalConfig.regular.copyWith(darkMode: value) + AppData().config = AppData().config.copyWith( + regular: AppData().config.regular.copyWith(darkMode: value) ); context.read().updateSetting(); }, @@ -141,7 +143,7 @@ class _SettingPage extends State { SizedBox(width: mediaQuery.size.width * 0.01), Expanded(child: Text("字体设置:")), DropdownButton( - value: context.watch().globalConfig.regular.font, + value: appData.config.regular.font, items: [ DropdownMenuItem(value: 0, child: Text('默认字体')), DropdownMenuItem(value: 1, child: Text('仅阿语使用备用字体')), @@ -154,8 +156,8 @@ class _SettingPage extends State { SnackBar(content: Text("网页版加载中文字体需要较长时间,请先耐心等待"), duration: Duration(seconds: 3),), ); } - context.read().globalConfig = context.read().globalConfig.copyWith( - regular: context.read().globalConfig.regular.copyWith(font: value) + AppData().config = AppData().config.copyWith( + regular: AppData().config.regular.copyWith(font: value) ); Provider.of(context, listen: false).updateSetting(); }, @@ -168,11 +170,11 @@ class _SettingPage extends State { SizedBox(width: mediaQuery.size.width * 0.01), Expanded(child: Text("隐藏网页版右上角APP下载按钮")), Switch( - value: context.read().globalConfig.regular.hideAppDownloadButton, + value: AppData().config.regular.hideAppDownloadButton, onChanged: (value) { context.read().uiLogger.info("更新网页端APP下载按钮隐藏设置: $value"); - context.read().globalConfig = context.read().globalConfig.copyWith( - regular: context.read().globalConfig.regular.copyWith(hideAppDownloadButton: value) + AppData().config = AppData().config.copyWith( + regular: AppData().config.regular.copyWith(hideAppDownloadButton: value) ); context.read().updateSetting(); }, @@ -322,12 +324,12 @@ class _SettingPage extends State { ], ), DropdownButton( - value: context.read().globalConfig.audio.audioSource, + value: AppData().config.audio.audioSource, onChanged: (value) { context.read().uiLogger.info("更新音频接口: $value"); if(value == 1) alart(context, "警告: \n来自\"TextReadTTS.com\"的音频不支持发音符号,且只能合成40字以内的文本。\n开启此功能请知悉。"); - context.read().globalConfig = context.read().globalConfig.copyWith( - audio: context.read().globalConfig.audio.copyWith(audioSource: value) + AppData().config = AppData().config.copyWith( + audio: AppData().config.audio.copyWith(audioSource: value) ); context.read().updateSetting(); }, @@ -363,15 +365,15 @@ class _SettingPage extends State { ) ), Slider( - value: context.read().globalConfig.audio.playRate, + value: AppData().config.audio.playRate, min: 0.5, max: 1.5, divisions: 10, - label: "${context.read().globalConfig.audio.playRate}", + label: "${AppData().config.audio.playRate}", onChanged: (value) { setState(() { - context.read().globalConfig = context.read().globalConfig.copyWith( - audio: context.read().globalConfig.audio.copyWith(playRate: value) + AppData().config = AppData().config.copyWith( + audio: AppData().config.audio.copyWith(playRate: value) ); }); }, diff --git a/lib/sub_pages_builder/learning_pages/learning_pages_build.dart b/lib/sub_pages_builder/learning_pages/learning_pages_build.dart index db4e1a3..2d82a83 100644 --- a/lib/sub_pages_builder/learning_pages/learning_pages_build.dart +++ b/lib/sub_pages_builder/learning_pages/learning_pages_build.dart @@ -61,7 +61,7 @@ class _InLearningPageState extends State { @override void initState() { // 加载测试词 - final SubQuizConfig questionsSetting = context.read().globalConfig.quiz.zhar; + final SubQuizConfig questionsSetting = AppData().config.quiz.zhar; List> questionsInSections = List.generate(questionsSetting.questionSections.length, (_) => []); for(int sectionIndex = 0; sectionIndex < questionsSetting.questionSections.length; sectionIndex++) { @@ -663,7 +663,7 @@ class _WordCardOverViewPage extends State { onSubmitted: (text) { setState(() {}); }, - onChanged: context.read().globalConfig.learning.wordLookupRealtime ? (text) { + onChanged: AppData().config.learning.wordLookupRealtime ? (text) { setState(() {}); } : null, ), @@ -688,8 +688,8 @@ class _WordCardOverViewPage extends State { }, builder: (context) { - int forceCloumn = context.read().globalConfig.learning.overviewForceColumn; - bool lookupRealtime = context.read().globalConfig.learning.wordLookupRealtime; + int forceCloumn = AppData().config.learning.overviewForceColumn; + bool lookupRealtime = AppData().config.learning.wordLookupRealtime; return StatefulBuilder( builder: (context, setLocalState) { return Column( @@ -736,8 +736,8 @@ class _WordCardOverViewPage extends State { ), onPressed: (){ setState(() { - context.read().globalConfig = context.read().globalConfig.copyWith( - learning: context.read().globalConfig.learning.copyWith( + AppData().config = AppData().config.copyWith( + learning: AppData().config.learning.copyWith( overviewForceColumn: forceCloumn, wordLookupRealtime: lookupRealtime ) @@ -857,15 +857,15 @@ class _WordCardOverViewLayout extends State { height: mediaQuery.size.height * 0.8, child: GridView.builder( itemCount: classItem.wordIndexs.length, - gridDelegate: context.read().globalConfig.learning.overviewForceColumn == 0 ? SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: mediaQuery.size.width ~/ 300) : SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: context.read().globalConfig.learning.overviewForceColumn), + gridDelegate: AppData().config.learning.overviewForceColumn == 0 ? SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: mediaQuery.size.width ~/ 300) : SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: AppData().config.learning.overviewForceColumn), itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(8.0), child: WordCard( word: appData.wordData.words[classItem.wordIndexs[index]], useMask: false, - width: mediaQuery.size.width / (context.read().globalConfig.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : context.read().globalConfig.learning.overviewForceColumn), - height: mediaQuery.size.width / (context.read().globalConfig.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : context.read().globalConfig.learning.overviewForceColumn), + width: mediaQuery.size.width / (AppData().config.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : AppData().config.learning.overviewForceColumn), + height: mediaQuery.size.width / (AppData().config.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : AppData().config.learning.overviewForceColumn), ), ); } @@ -932,7 +932,7 @@ class WordLookupLayout extends StatelessWidget { } context.read().uiLogger.finer("单词检索结果: $match"); - if(!context.read().globalConfig.learning.wordLookupRealtime){ + if(!AppData().config.learning.wordLookupRealtime){ Future.delayed(Durations.medium1, () { if(context.mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -944,15 +944,15 @@ class WordLookupLayout extends StatelessWidget { return GridView.builder( itemCount: match.length, - gridDelegate: context.read().globalConfig.learning.overviewForceColumn == 0 ? SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: mediaQuery.size.width ~/ 300) : SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: context.read().globalConfig.learning.overviewForceColumn), + gridDelegate: AppData().config.learning.overviewForceColumn == 0 ? SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: mediaQuery.size.width ~/ 300) : SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: AppData().config.learning.overviewForceColumn), itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(8.0), child: WordCard( word: match[index], useMask: false, - width: mediaQuery.size.width / (context.read().globalConfig.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : context.read().globalConfig.learning.overviewForceColumn), - height: mediaQuery.size.width / (context.read().globalConfig.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : context.read().globalConfig.learning.overviewForceColumn), + width: mediaQuery.size.width / (AppData().config.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : AppData().config.learning.overviewForceColumn), + height: mediaQuery.size.width / (AppData().config.learning.overviewForceColumn == 0 ? (mediaQuery.size.width ~/ 300) : AppData().config.learning.overviewForceColumn), ), ); } diff --git a/lib/sub_pages_builder/setting_pages/about_page.dart b/lib/sub_pages_builder/setting_pages/about_page.dart index 598f6c3..91dbc7f 100644 --- a/lib/sub_pages_builder/setting_pages/about_page.dart +++ b/lib/sub_pages_builder/setting_pages/about_page.dart @@ -39,7 +39,7 @@ class AboutPage extends StatelessWidget { ), ), TextContainer(text: "LICENSE"), - TextContainer(text: "Copyright (C) <2025> \n该软件通过GNU GENERAL PUBLIC LICENSE (Version 3)协议授权给 \"${context.read().globalConfig.user}\",协议内容详见开放源代码许可页面"), + TextContainer(text: "Copyright (C) <2025> \n该软件通过GNU GENERAL PUBLIC LICENSE (Version 3)协议授权给 \"${AppData().config.user}\",协议内容详见开放源代码许可页面"), ElevatedButton.icon( style: ElevatedButton.styleFrom( fixedSize: Size.fromHeight(MediaQuery.of(context).size.height * 0.1) diff --git a/lib/sub_pages_builder/setting_pages/debug_page.dart b/lib/sub_pages_builder/setting_pages/debug_page.dart index a4eebf1..fd3b655 100644 --- a/lib/sub_pages_builder/setting_pages/debug_page.dart +++ b/lib/sub_pages_builder/setting_pages/debug_page.dart @@ -31,7 +31,7 @@ class _DebugPage extends State { body: ListView( controller: controller, children: [ - TextContainer(text: "该页面为软件调试/测试和bug反馈使用,非必要请勿开启日志捕获,以免性能损耗", style: TextStyle(color: Colors.redAccent), animated: true), + TextContainer(text: "该页面为软件调试/测试和bug反馈使用,非必要请勿开启日志捕获,以免性能损耗", style: TextStyle(color: Colors.redAccent)), Container( decoration: BoxDecoration( borderRadius: StaticsVar.br, @@ -43,10 +43,10 @@ class _DebugPage extends State { Icon(Icons.logo_dev), Expanded(child: Text("启用软件内日志捕获")), Switch( - value: context.watch().globalConfig.debug.enableInternalLog, + value: AppData().config.debug.enableInternalLog, onChanged: (value){ - context.read().globalConfig = context.read().globalConfig.copyWith( - debug: context.read().globalConfig.debug.copyWith(enableInternalLog: value) + AppData().config = AppData().config.copyWith( + debug: AppData().config.debug.copyWith(enableInternalLog: value) ); context.read().updateSetting(); } @@ -76,10 +76,10 @@ class _DebugPage extends State { DropdownMenuItem(value: 7,child: Text("Level.SHOUT")), DropdownMenuItem(value: 8,child: Text("Level.OFF")), ], - value: context.watch().globalConfig.debug.internalLevel, + value:AppData().config.debug.internalLevel, onChanged: (value) { - context.read().globalConfig = context.read().globalConfig.copyWith( - debug: context.read().globalConfig.debug.copyWith(internalLevel: value) + AppData().config = AppData().config.copyWith( + debug: AppData().config.debug.copyWith(internalLevel: value) ); context.read().updateSetting(); } diff --git a/lib/sub_pages_builder/setting_pages/model_download_page.dart b/lib/sub_pages_builder/setting_pages/model_download_page.dart index ac6fbfc..425c90f 100644 --- a/lib/sub_pages_builder/setting_pages/model_download_page.dart +++ b/lib/sub_pages_builder/setting_pages/model_download_page.dart @@ -78,7 +78,7 @@ class _ModelDownload extends State { }); await extractTarBz2('${basePath.path}/arabicLearning/tts/temp.tar.bz2', "${basePath.path}/arabicLearning/tts/model/"); if(!context.mounted) return; - AppData().loadTTS(context.read().globalConfig.audio.playRate); + AppData().loadTTS(AppData().config.audio.playRate); context.read().uiLogger.info("模型下载完成"); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("下载完成"))); setState(() { diff --git a/lib/sub_pages_builder/setting_pages/open_source_licenses.dart b/lib/sub_pages_builder/setting_pages/open_source_licenses.dart index 565610a..f041b68 100644 --- a/lib/sub_pages_builder/setting_pages/open_source_licenses.dart +++ b/lib/sub_pages_builder/setting_pages/open_source_licenses.dart @@ -79,7 +79,7 @@ class OpenSourceLicensePage extends StatelessWidget { ), children: [ TextContainer(text: app.license??""), - if(context.read().globalConfig.regular.theme == 10) ElevatedButton.icon( + if(AppData().config.regular.theme == 10) ElevatedButton.icon( onPressed: () { context.read().uiLogger.warning("触发彩蛋 #00s"); alart(context, "荏苒的时光足以使沧海化为桑田...", delayConfirm: Duration(seconds: 3), @@ -98,8 +98,8 @@ class OpenSourceLicensePage extends StatelessWidget { onConfirmed: (){ alart(context, "注:你开启了一项彩蛋功能\n若要关闭请再次点击此按钮\n请*手动*重启软件以应用更改...", delayConfirm: Duration(seconds: 3), onConfirmed: (){ - context.read().globalConfig = context.read().globalConfig.copyWith( - egg: context.read().globalConfig.egg.copyWith(stella: !context.read().globalConfig.egg.stella) + AppData().config = AppData().config.copyWith( + egg: AppData().config.egg.copyWith(stella: !AppData().config.egg.stella) ); context.read().updateSetting(); SystemNavigator.pop(); diff --git a/lib/sub_pages_builder/setting_pages/questions_setting_page.dart b/lib/sub_pages_builder/setting_pages/questions_setting_page.dart index 5ffbd1d..04bf25f 100644 --- a/lib/sub_pages_builder/setting_pages/questions_setting_page.dart +++ b/lib/sub_pages_builder/setting_pages/questions_setting_page.dart @@ -18,9 +18,9 @@ class _QuestionsSettingPage extends State { static const Map castMap = {0: "单词卡片学习", 1: "中译阿 选择题", 2: "阿译中 选择题", 3: "中译阿 拼写题", 4: "听力题"}; void _updateConfig() { - context.read().globalConfig = context.read().globalConfig.copyWith( - quiz: context.read().globalConfig.quiz.copyWith( - zhar: context.read().globalConfig.quiz.zhar.copyWith( + AppData().config = AppData().config.copyWith( + quiz: AppData().config.quiz.copyWith( + zhar: AppData().config.quiz.zhar.copyWith( questionSections: selectedTypes!.cast() ) ) @@ -31,7 +31,7 @@ class _QuestionsSettingPage extends State { Widget build(BuildContext context) { context.read().uiLogger.info("构建 QuestionsSettingPage:$selectedTypes"); late final SubQuizConfig section; - section = context.read().globalConfig.quiz.zhar; + section = AppData().config.quiz.zhar; MediaQueryData mediaQuery = MediaQuery.of(context); selectedTypes ??= List.from(section.questionSections); List listTiles = []; @@ -104,9 +104,9 @@ class _QuestionsSettingPage extends State { onChanged: (value) { context.read().uiLogger.info("题型内题目乱序: $value"); setState(() { - context.read().globalConfig = context.read().globalConfig.copyWith( - quiz: context.read().globalConfig.quiz.copyWith( - zhar: context.read().globalConfig.quiz.zhar.copyWith( + AppData().config = AppData().config.copyWith( + quiz: AppData().config.quiz.copyWith( + zhar: AppData().config.quiz.zhar.copyWith( shuffleInternaly: value ) ) @@ -124,9 +124,9 @@ class _QuestionsSettingPage extends State { onChanged: (value) { context.read().uiLogger.info("题型乱序: $value"); setState(() { - context.read().globalConfig = context.read().globalConfig.copyWith( - quiz: context.read().globalConfig.quiz.copyWith( - zhar: context.read().globalConfig.quiz.zhar.copyWith( + AppData().config = AppData().config.copyWith( + quiz: AppData().config.quiz.copyWith( + zhar: AppData().config.quiz.zhar.copyWith( shuffleExternaly: value ) ) @@ -144,9 +144,9 @@ class _QuestionsSettingPage extends State { onChanged: (value) { context.read().uiLogger.info("全局乱序: $value"); setState(() { - context.read().globalConfig = context.read().globalConfig.copyWith( - quiz: context.read().globalConfig.quiz.copyWith( - zhar: context.read().globalConfig.quiz.zhar.copyWith( + AppData().config = AppData().config.copyWith( + quiz: AppData().config.quiz.copyWith( + zhar: AppData().config.quiz.zhar.copyWith( shuffleGlobally: value ) ) @@ -164,9 +164,9 @@ class _QuestionsSettingPage extends State { onChanged: (value) { context.read().uiLogger.info("偏好相似: $value"); setState(() { - context.read().globalConfig = context.read().globalConfig.copyWith( - quiz: context.read().globalConfig.quiz.copyWith( - zhar: context.read().globalConfig.quiz.zhar.copyWith( + AppData().config = AppData().config.copyWith( + quiz: AppData().config.quiz.copyWith( + zhar: AppData().config.quiz.zhar.copyWith( preferSimilar: value ) ) diff --git a/lib/sub_pages_builder/setting_pages/sync_page.dart b/lib/sub_pages_builder/setting_pages/sync_page.dart index 81dc20c..b4f92f3 100644 --- a/lib/sub_pages_builder/setting_pages/sync_page.dart +++ b/lib/sub_pages_builder/setting_pages/sync_page.dart @@ -28,12 +28,12 @@ class _DataSyncPage extends State { @override Widget build(BuildContext context) { context.read().uiLogger.info("构建 DataSyncPage"); - enabled ??= context.read().globalConfig.webSync.enabled; + enabled ??= AppData().config.webSync.enabled; context.read().uiLogger.fine("获取WebDAV实例"); final WebDAV webdav = WebDAV( - uri: context.read().globalConfig.webSync.account.uri, - user: context.read().globalConfig.webSync.account.userName, - password: context.read().globalConfig.webSync.account.passWord + uri: AppData().config.webSync.account.uri, + user: AppData().config.webSync.account.userName, + password: AppData().config.webSync.account.passWord ); MediaQueryData mediaQuery = MediaQuery.of(context); @@ -69,12 +69,12 @@ class _DataSyncPage extends State { Row( children: [ Text("联通性检查: "), - if(context.read().globalConfig.webSync.account.uri.isEmpty) Text("未绑定", style: Theme.of(context).textTheme.labelSmall), + if(AppData().config.webSync.account.uri.isEmpty) Text("未绑定", style: Theme.of(context).textTheme.labelSmall), FutureBuilder( future: WebDAV.test( - context.read().globalConfig.webSync.account.uri, - context.read().globalConfig.webSync.account.userName, - password: context.read().globalConfig.webSync.account.passWord + AppData().config.webSync.account.uri, + AppData().config.webSync.account.userName, + password: AppData().config.webSync.account.passWord ), builder: (context, snapshot) { if(snapshot.hasError) { @@ -293,9 +293,9 @@ Future popAccountSetting(BuildContext context) async { await showDialog>( context: context, builder: (BuildContext context) { - uriController.text = context.read().globalConfig.webSync.account.uri; - accountController.text = context.read().globalConfig.webSync.account.userName; - passwdController.text = context.read().globalConfig.webSync.account.passWord; + uriController.text = AppData().config.webSync.account.uri; + accountController.text = AppData().config.webSync.account.userName; + passwdController.text = AppData().config.webSync.account.passWord; return AlertDialog( title: Text("设置WebDAV同步"), content: Column( @@ -367,8 +367,8 @@ Future popAccountSetting(BuildContext context) async { alart(context, e.toString()); return; } - context.read().globalConfig = context.read().globalConfig.copyWith( - webSync: context.read().globalConfig.webSync.copyWith( + AppData().config = AppData().config.copyWith( + webSync: AppData().config.webSync.copyWith( account: SyncAccountConfig( uri: uriController.text, userName: accountController.text, diff --git a/lib/sub_pages_builder/test_pages/listening_test_page.dart b/lib/sub_pages_builder/test_pages/listening_test_page.dart index 6ac203c..c421d77 100644 --- a/lib/sub_pages_builder/test_pages/listening_test_page.dart +++ b/lib/sub_pages_builder/test_pages/listening_test_page.dart @@ -49,7 +49,7 @@ class _ForeListeningSettingPage extends State { IconButton( onPressed: () { context.read().uiLogger.info("进行听写发音测试"); - playTextToSpeech("وَ", context); + playTextToSpeech("وَ"); }, icon: Icon(Icons.volume_up, size: 100) ), @@ -398,7 +398,7 @@ class _MainListeningPageState extends State { counter = "-"; }); if(!context.mounted) return; - await playTextToSpeech(x.arabic, context, speed: widget.playRate); + await playTextToSpeech(x.arabic, speed: widget.playRate); // await Future.delayed(Duration(seconds: 1)); setState((){ state = "播放间隔中..."; diff --git a/lib/vars/global.dart b/lib/vars/global.dart index d8c6379..875b8c0 100644 --- a/lib/vars/global.dart +++ b/lib/vars/global.dart @@ -27,13 +27,11 @@ class Global with ChangeNotifier { ThemeData get themeData => ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( - seedColor: StaticsVar.themeList[globalConfig.regular.theme], - brightness: globalConfig.regular.darkMode ? Brightness.dark : Brightness.light, + seedColor: StaticsVar.themeList[AppData().config.regular.theme], + brightness: AppData().config.regular.darkMode ? Brightness.dark : Brightness.light, ), fontFamily: zhFont, ); - - Config globalConfig = Config(); Future init() async { @@ -62,21 +60,21 @@ class Global with ChangeNotifier { logger.info("处理配置文件"); Config oldConfig = Config.buildFromMap(jsonDecode(AppData().storage.getString("settingData")!)); - if(oldConfig.lastVersion != globalConfig.lastVersion) { + if(oldConfig.lastVersion != AppData().config.lastVersion) { logger.info("检测到当前版本与上次启动版本不同"); updateLogRequire = true; - oldConfig=oldConfig.copyWith(lastVersion: globalConfig.lastVersion); + oldConfig=oldConfig.copyWith(lastVersion: AppData().config.lastVersion); } - globalConfig = oldConfig; + AppData().config = oldConfig; logger.info("配置文件合成完成"); } // 更新配置到存储中 Future updateSetting({Map? settingData, bool refresh = true}) async { logger.info("保存配置文件中"); - if(settingData != null) globalConfig = Config.buildFromMap(settingData); - AppData().storage.setString("settingData", jsonEncode(globalConfig.toMap())); + if(settingData != null) AppData().config = Config.buildFromMap(settingData); + AppData().storage.setString("settingData", jsonEncode(AppData().config.toMap())); if(refresh) await refreshApp(); } @@ -101,12 +99,12 @@ class Global with ChangeNotifier { debugPrint('${record.time}-[${record.loggerName}][${record.level.name}]: ${record.message}'); }); } - if(globalConfig.debug.enableInternalLog){ + if(AppData().config.debug.enableInternalLog){ Logger.root.level = Level.ALL; const List levelList = [Level.ALL, Level.FINEST, Level.FINER, Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE, Level.SHOUT, Level.OFF]; AppData appData = AppData(); Logger.root.onRecord.listen((record) async { - if(record.level < levelList[globalConfig.debug.internalLevel]) return; + if(record.level < levelList[AppData().config.debug.internalLevel]) return; appData.internalLogCapture.add('${record.time}-[${record.loggerName}][${record.level.name}]: ${record.message}'); }); } @@ -115,8 +113,8 @@ class Global with ChangeNotifier { Future refreshApp() async { logger.info("应用设置中"); AppData appData = AppData(); - if(globalConfig.audio.audioSource == 2) await appData.loadTTS(globalConfig.audio.playRate); - if(globalConfig.egg.stella) await appData.loadEggs(); + if(AppData().config.audio.audioSource == 2) await appData.loadTTS(AppData().config.audio.playRate); + if(AppData().config.egg.stella) await appData.loadEggs(); changeLoggerBehavior(); updateTheme(); notifyListeners(); @@ -125,11 +123,11 @@ class Global with ChangeNotifier { void updateTheme() { logger.info("更新主题中"); - if(globalConfig.regular.font == 2) { + if(AppData().config.regular.font == 2) { arFont = StaticsVar.arBackupFont; zhFont = StaticsVar.zhBackupFont; loadFont(); - } else if(globalConfig.regular.font == 1) { + } else if(AppData().config.regular.font == 1) { arFont = StaticsVar.arBackupFont; zhFont = null; } else { @@ -140,13 +138,13 @@ class Global with ChangeNotifier { void updateLearningStreak(){ final int nowDate = DateTime.now().difference(DateTime(2025, 11, 1)).inDays; - if (nowDate == globalConfig.learning.lastDate) return; + if (nowDate == AppData().config.learning.lastDate) return; logger.info("保存学习进度中"); // 以 2025/11/1 为基准计算天数(因为这个bug是这天修的:} ) - if (nowDate - globalConfig.learning.lastDate > 1) { - globalConfig = globalConfig.copyWith(learning: globalConfig.learning.copyWith(startDate: nowDate)); + if (nowDate - AppData().config.learning.lastDate > 1) { + AppData().config = AppData().config.copyWith(learning: AppData().config.learning.copyWith(startDate: nowDate)); } - globalConfig = globalConfig.copyWith(learning: globalConfig.learning.copyWith(lastDate: nowDate)); + AppData().config = AppData().config.copyWith(learning: AppData().config.learning.copyWith(lastDate: nowDate)); updateSetting(refresh: false); logger.info("学习进度保存完成"); } @@ -164,6 +162,7 @@ class AppData { List internalLogCapture = []; Uint8List? stella; bool isWideScreen = false; + Config config = Config(); late final SharedPreferences storage; late final io.Directory basePath; From 45f3cfbe9e96f16b0867179b9f70f2cd7bd06489 Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:21:01 +0800 Subject: [PATCH 6/8] refactor: reset MyApp build logic Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/main.dart | 54 +++++++++++++++++++++++++------------------- lib/vars/global.dart | 6 +---- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 23cb01a..5ade125 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -67,7 +67,7 @@ void main() async { // await global.init(); runApp( ChangeNotifierProvider( - create: (context) => Global()..init(), + create: (context) => Global(), child: MyApp(), ), ); @@ -75,31 +75,39 @@ void main() async { class MyApp extends StatelessWidget { const MyApp({super.key}); + @override Widget build(BuildContext context) { - context.read().uiLogger.info("收到应用层构建请求"); - return context.watch().inited? - MaterialApp( - title: StaticsVar.appName, - themeMode: ThemeMode.system, - theme: context.read().themeData, - home: AppData().config.egg.stella - ? Stack( - children: [ - Container( - decoration: BoxDecoration( - image: DecorationImage( - image: MemoryImage(AppData().stella!), - fit: BoxFit.cover, + context.read().uiLogger.warning("收到应用层构建请求"); + return FutureBuilder( + future: context.read().init(), + initialData: false, + builder: (context, asyncSnapshot) { + if(!(asyncSnapshot.data??false)) { + return Material(child: Container(width: double.infinity, height: double.infinity, color: Colors.black ,child: Center(child: CircularProgressIndicator()))); + } + return MaterialApp( + title: StaticsVar.appName, + themeMode: ThemeMode.system, + theme: context.read().themeData, + home: AppData().config.egg.stella + ? Stack( + children: [ + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: MemoryImage(AppData().stella!), + fit: BoxFit.cover, + ), + ), ), - ), - ), - const MyHomePage(), - ], - ) - : const MyHomePage(), - ) - : Material(child: Container(width: double.infinity, height: double.infinity, color: Colors.black ,child: Center(child: CircularProgressIndicator()))); + const MyHomePage(), + ], + ) + : const MyHomePage(), + ); + } + ); } } diff --git a/lib/vars/global.dart b/lib/vars/global.dart index 875b8c0..b733968 100644 --- a/lib/vars/global.dart +++ b/lib/vars/global.dart @@ -19,7 +19,6 @@ class Global with ChangeNotifier { final Logger logger = Logger("Global"); bool backupFontLoaded = false; - bool inited = false; //是否初始化完成 String? arFont; String? zhFont; bool updateLogRequire = false; //是否需要显示更新日志 @@ -35,9 +34,7 @@ class Global with ChangeNotifier { Future init() async { - logger.info("类收到初始化请求,当前初始化状态为 $inited"); - if(inited) return false; - logger.info("开始类初始化"); + logger.info("开始全局控制类初始化"); AppData appData = AppData(); await appData.init(); @@ -50,7 +47,6 @@ class Global with ChangeNotifier { await updateSetting(); } - inited = true; logger.info("初始化完成"); return true; } From 0731ee34a319544690383357da37c275aada7136 Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:08:03 +0800 Subject: [PATCH 7/8] fix: init error Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/main.dart | 60 ++++++++++++++++++++++++-------------------- lib/vars/global.dart | 10 +++++--- pubspec.yaml | 1 + 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 5ade125..cc458b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -86,25 +86,28 @@ class MyApp extends StatelessWidget { if(!(asyncSnapshot.data??false)) { return Material(child: Container(width: double.infinity, height: double.infinity, color: Colors.black ,child: Center(child: CircularProgressIndicator()))); } - return MaterialApp( - title: StaticsVar.appName, - themeMode: ThemeMode.system, - theme: context.read().themeData, - home: AppData().config.egg.stella - ? Stack( - children: [ - Container( - decoration: BoxDecoration( - image: DecorationImage( - image: MemoryImage(AppData().stella!), - fit: BoxFit.cover, + return Consumer( + builder: (context, global, child) => MaterialApp( + title: StaticsVar.appName, + themeMode: ThemeMode.system, + theme: global.themeData, + home: AppData().config.egg.stella + ? Stack( + children: [ + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: MemoryImage(AppData().stella!), + fit: BoxFit.cover, + ), ), ), - ), - const MyHomePage(), - ], - ) - : const MyHomePage(), + child!, + ], + ) + : child, + ), + child: const MyHomePage(), ); } ); @@ -121,7 +124,6 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { - late List _pageList; final PageController _pageController = PageController(initialPage: 0); final TextEditingController controller = TextEditingController(); @@ -177,7 +179,12 @@ class _MyHomePageState extends State { setState(() {}); }, // physics: const NeverScrollableScrollPhysics(), // 禁用滑动 - children: _pageList, + children: [ + HomePage(), + LearningPage(), + TestPage(), + SettingPage() + ], ), ), ], @@ -197,7 +204,12 @@ class _MyHomePageState extends State { onPageChanged: (index) { setState(() {}); }, - children: _pageList, + children: [ + HomePage(), + LearningPage(), + TestPage(), + SettingPage() + ], ), ), // 底部导航栏 @@ -261,7 +273,7 @@ class _MyHomePageState extends State { child: ListView( children: [ FutureBuilder( - future: rootBundle.loadString('assets/help/audio.md'), + future: rootBundle.loadString('assets/help/announce.md'), initialData: "加载中...", builder: (context, asyncSnapshot) { return MarkdownBody(data: asyncSnapshot.data!); @@ -367,12 +379,6 @@ class _MyHomePageState extends State { }); } - _pageList = [ - HomePage(), - LearningPage(), - TestPage(), - SettingPage() - ]; return Scaffold( backgroundColor: AppData().config.egg.stella ? Colors.transparent : null, appBar: AppBar( diff --git a/lib/vars/global.dart b/lib/vars/global.dart index b733968..23e2a06 100644 --- a/lib/vars/global.dart +++ b/lib/vars/global.dart @@ -38,9 +38,11 @@ class Global with ChangeNotifier { AppData appData = AppData(); await appData.init(); + FSRS().init(); if(appData.isFirstStart) { logger.info("首次启动检测为真"); + appData.initStorageValue(); await refreshApp(); } else { conveySetting(); @@ -175,9 +177,11 @@ class AppData { storage = await SharedPreferences.getInstance(); basePath = (await path_provider.getApplicationDocumentsDirectory()) as io.Directory; - wordData = DictData.buildFromMap(jsonDecode(storage.getString("wordData")!)); - if(!BKSearch.isReady) BKSearch.init(wordData.words); - FSRS().init(); + if(!isFirstStart) { + wordData = DictData.buildFromMap(jsonDecode(storage.getString("wordData")!)); + if(!BKSearch.isReady) BKSearch.init(wordData.words); + FSRS().init(); + } inited = true; } diff --git a/pubspec.yaml b/pubspec.yaml index 26c909e..6786b1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ flutter: - assets/eggs/s.txt - assets/fonts/zh/NotoSansSC-Medium.ttf - assets/help/audio.md + - assets/help/announce.md fonts: - family: Vazirmatn fonts: From c346acd4464cf20461edbd94189990343064fe0d Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:32:10 +0800 Subject: [PATCH 8/8] feat: add indeveloping banner Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- .../setting_pages/about_page.dart | 25 ++++++++----------- .../setting_pages/debug_page.dart | 6 +++++ .../setting_pages/sync_page.dart | 1 - .../test_pages/local_pk_page.dart | 3 ++- lib/vars/license_storage.dart | 17 ------------- 5 files changed, 18 insertions(+), 34 deletions(-) diff --git a/lib/sub_pages_builder/setting_pages/about_page.dart b/lib/sub_pages_builder/setting_pages/about_page.dart index 91dbc7f..dd26c8d 100644 --- a/lib/sub_pages_builder/setting_pages/about_page.dart +++ b/lib/sub_pages_builder/setting_pages/about_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:provider/provider.dart'; import 'package:arabic_learning/vars/statics_var.dart'; -import 'package:arabic_learning/vars/license_storage.dart'; import 'package:arabic_learning/vars/global.dart'; import 'package:arabic_learning/funcs/ui.dart'; import 'package:arabic_learning/sub_pages_builder/setting_pages/open_source_licenses.dart'; @@ -22,7 +23,7 @@ class AboutPage extends StatelessWidget { TextContainer(text: "关于"), TextContainer(text: "该软件仅供学习使用,请勿用于商业用途。\n该软件基于GNU AFFERO GENERAL PUBLIC LICENSE (Version 3)协议开源,协议原文详见页面底部。", style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)), TextContainer(text: "目前该软件主要由 OctagonalStar(别问为什么写网名) 开发,如果有什么问题或者提议都欢迎提issue(或者线下真实?)。\n该软件 ,主要是为了帮助大家掌握阿语词汇"), - TextContainer(text: "免责声明"), + TextContainer(text: "声明"), Container( margin: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0), @@ -30,13 +31,13 @@ class AboutPage extends StatelessWidget { color: Theme.of(context).colorScheme.onSecondary, borderRadius: StaticsVar.br, ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(LicenseVars.noMyDutyAnnouce), - ], - ), + child: FutureBuilder( + future: rootBundle.loadString('assets/help/announce.md'), + initialData: "加载中...", + builder: (context, asyncSnapshot) { + return MarkdownBody(data: asyncSnapshot.data!); + } + ) ), TextContainer(text: "LICENSE"), TextContainer(text: "Copyright (C) <2025> \n该软件通过GNU GENERAL PUBLIC LICENSE (Version 3)协议授权给 \"${AppData().config.user}\",协议内容详见开放源代码许可页面"), @@ -55,12 +56,6 @@ class AboutPage extends StatelessWidget { }, icon: Icon(Icons.balance), label: Text("开放源代码许可"), - ), - ExpansionTile( - title: Text("调试信息"), - children: [ - TextContainer(text: "Storage Type: ${AppData().storage.type ? "SharedPreferences" : "IndexDB"}"), - ], ) ], ), diff --git a/lib/sub_pages_builder/setting_pages/debug_page.dart b/lib/sub_pages_builder/setting_pages/debug_page.dart index fd3b655..8d90a7d 100644 --- a/lib/sub_pages_builder/setting_pages/debug_page.dart +++ b/lib/sub_pages_builder/setting_pages/debug_page.dart @@ -113,6 +113,12 @@ class _DebugPage extends State { ), ) ], + ), + ExpansionTile( + title: Text("调试信息"), + children: [ + TextContainer(text: "Storage Type: ${AppData().storage.type ? "SharedPreferences" : "IndexDB"}"), + ], ) ], ), diff --git a/lib/sub_pages_builder/setting_pages/sync_page.dart b/lib/sub_pages_builder/setting_pages/sync_page.dart index b4f92f3..bea225d 100644 --- a/lib/sub_pages_builder/setting_pages/sync_page.dart +++ b/lib/sub_pages_builder/setting_pages/sync_page.dart @@ -43,7 +43,6 @@ class _DataSyncPage extends State { ), body: ListView( children: [ - TextContainer(text: "该功能还处在预览阶段", style: TextStyle(color: Colors.redAccent)), SettingItem( title: "远程", padding: EdgeInsets.all(8.0), diff --git a/lib/sub_pages_builder/test_pages/local_pk_page.dart b/lib/sub_pages_builder/test_pages/local_pk_page.dart index fa53cd9..a7390c9 100644 --- a/lib/sub_pages_builder/test_pages/local_pk_page.dart +++ b/lib/sub_pages_builder/test_pages/local_pk_page.dart @@ -46,7 +46,8 @@ class _LocalPKSelectPage extends State { appBar: AppBar(title: Text("局域网联机")), body: Column( children: [ - SizedBox(height: mediaQuery.size.height * 0.05), + TextContainer(text: "该功能还处在预览阶段,出现问题请及时提交反馈", style: TextStyle(color: Colors.redAccent)), + SizedBox(height: mediaQuery.size.height * 0.02), ElevatedButton.icon( style: ElevatedButton.styleFrom( fixedSize: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.1), diff --git a/lib/vars/license_storage.dart b/lib/vars/license_storage.dart index 2e32e31..55102a4 100644 --- a/lib/vars/license_storage.dart +++ b/lib/vars/license_storage.dart @@ -1,21 +1,4 @@ class LicenseVars { - static const String noMyDutyAnnouce = """ -由于该软件目前还处在开发阶段,有一些bug是不可避免的。所以在正式使用该软件前你应当阅读并理解以下条款: -1. 该软件仅供学习使用,请勿用于商业用途。 -2. 该软件不会对你的阿拉伯语成绩做出任何担保,若你出现阿拉伯语成绩不理想的情况请先考虑自己的问题 :) -3. 由于软件在不同系统上运行可能存在兼容性问题,软件出错造成的任何损失(包含精神损伤),软件作者和其他贡献者不会担负任何责任 -4. 你知晓并理解如果你错误地使用软件(如使用错误的数据集)造成的任何后果,乃至二次宇宙大爆炸,都需要你自行承担 -5. 其他在GNU AFFERO GENERAL PUBLIC LICENSE (Version 3)开源协议下的条款 -"""; - static const String theWebSpecialAnnouce = ''' -检测到当前是在浏览器中运行,请悉知以下内容: -1. 由于网页端的一些限制,该软件*不一定*能按照预期工作 -2. 软件使用中所有的数据均保存在浏览器缓存中,不会进行任何用户数据上传。清空网站缓存可导致数据永久丢失 -3. 该网页部署于Github Pages,由Github Action自动构建,所以网站会不定期进行热更新,且版本快于发布版 -4. 由于(3)的原因,你可以由此更早地体验到新版功能,但也可能遇到新bug,如果遇到了相关的bug请在Github上提交issue -4. 由于Github Pages服务器地区不可控,我*完全不能*保证你是否能正常链接网站 -5. 网站展示效果不代表实际app发布版效果'''; - static const String theModelLICENSE = '''MIT License Copyright (c) [year] [fullname]