diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index 938c9aa..9019885 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -72,6 +72,7 @@ class FSRS { } int getLeastDueCard() { + if (config.cards.isEmpty) return -1; int leastDueIndex = 0; for(int i = 1; i < config.cards.length; i++) { if(config.cards[i].due.toLocal().isBefore(config.cards[leastDueIndex].due.toLocal()) && config.cards[i].due.toLocal().difference(DateTime.now()) < Duration(days: 1)) { @@ -88,6 +89,9 @@ class FSRS { void addWordCard(int wordId) { logger.fine("添加复习卡片: Id: $wordId"); + if (config.cards.isEmpty) { + config = config.copyWith(cards: [], reviewLogs: []); + } // os the wordID == cardID config.cards.add(Card(cardId: wordId, state: State.learning)); config.reviewLogs.add(ReviewLog(cardId: wordId, rating: Rating.good, reviewDateTime: DateTime.now())); diff --git a/lib/funcs/utili.dart b/lib/funcs/utili.dart index f222142..82117a1 100644 --- a/lib/funcs/utili.dart +++ b/lib/funcs/utili.dart @@ -188,6 +188,39 @@ extension StringExtensions on String { res = res.replaceAll(RegExp(r'[،.][^]*$'), ""); // for "متواصل، متواصل" return res; } + + /// 简单的中文释义交叉计算(字符 Jaccard 相似度) + bool hasSimilarMeaning(String other) { + // 1. 去除中文/英文常见标点符号和空格 + String cleanString(String s) { + return s.replaceAll(RegExp(r'[ \(\)\.,/,。、;()\[\]【】]'), ''); + } + + String c1 = cleanString(this); + String c2 = cleanString(other); + + if (c1.isEmpty || c2.isEmpty) return false; + + // 如果一个释义完全包含了另一个,直接判定为相似(如:苹果 和 苹果,香蕉) + if(c1.contains(c2) || c2.contains(c1)) return true; + + // 2. 将字串拆分为单字集合 + Set set1 = c1.split('').toSet(); + Set set2 = c2.split('').toSet(); + + // 3. 计算共有字符 + int intersection = set1.intersection(set2).length; + // int union = set1.union(set2).length; + + // 如果短词里包含任何相同的核心字,或共有汉字超过短词的 40% (应对同义替换) + int minLength = min(set1.length, set2.length); + + // 如果它们很短,只要共享一个字就算(例如:走 / 行走) + if(minLength <= 2 && intersection >= 1) return true; + + double similarity = intersection / minLength; + return similarity >= 0.4; + } } extension ListExtensions on List { diff --git a/lib/pages/learning_page.dart b/lib/pages/learning_page.dart index f1f9b23..8153463 100644 --- a/lib/pages/learning_page.dart +++ b/lib/pages/learning_page.dart @@ -69,19 +69,13 @@ class LearningPage extends StatelessWidget { ), ), onPressed: (){ - if(context.read().globalFSRS.getWillDueCount() != 0) { - context.read().uiLogger.info("跳转: LearningPage => ForeFSRSSettingPage"); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ForeFSRSSettingPage() - ) - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("目前没有要复习的单词"), duration: Duration(seconds: 1),), - ); - } + context.read().uiLogger.info("跳转: LearningPage => ForeFSRSSettingPage"); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ForeFSRSSettingPage() + ) + ); }, child: FittedBox( fit: BoxFit.contain, @@ -103,15 +97,23 @@ class LearningPage extends StatelessWidget { shape: RoundedRectangleBorder(borderRadius: StaticsVar.br), ), onPressed: (){ + if(context.read().wordData.words.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("词库为空,无法推送!请先导入词库"), duration: Duration(seconds: 1),), + ); + return; + } final DateTime now = DateTime.now(); final int seed = now.year * 10000 + now.month * 100 + now.day; - final List pushWords = []; + final Set pushWords = {}; final Random rnd = Random(seed); - for(int i = 0; i < context.read().globalFSRS.config.pushAmount; i++){ + 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)); } + tries++; } if(pushWords.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -123,7 +125,7 @@ class LearningPage extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => FSRSLearningPage(fsrs: context.read().globalFSRS, words: pushWords) + builder: (context) => FSRSLearningPage(fsrs: context.read().globalFSRS, words: pushWords.toList()) ) ); }, diff --git a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart index 4d7dfb4..257153b 100644 --- a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart +++ b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart @@ -240,6 +240,38 @@ class ForeFSRSSettingPage extends StatelessWidget { }, icon: Icon(Icons.done), label: Text("确认"), + ), + if (fsrs.config.enabled) Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.errorContainer, + fixedSize: Size.fromHeight(80), + shape: RoundedRectangleBorder(borderRadius: StaticsVar.br) + ), + onPressed: (){ + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("重置并停用 FSRS?"), + content: Text("这将永久清除所有规律学习进度及配置,且不可恢复!"), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text("取消")), + TextButton( + onPressed: () { + fsrs.config = const FSRSConfig(enabled: false); + fsrs.save(); + Navigator.popUntil(context, (route) => route.isFirst); + }, + child: Text("确认清空", style: TextStyle(color: Colors.red)) + ) + ], + ) + ); + }, + icon: Icon(Icons.delete_forever, color: Theme.of(context).colorScheme.error), + label: Text("重置并停用", style: TextStyle(color: Theme.of(context).colorScheme.error)), + ), ) ] ); @@ -292,10 +324,8 @@ class MainFSRSPage extends StatelessWidget { } final wordID = fsrs.getLeastDueCard(); if(wordID == -1) { - Future.delayed( - Duration(seconds: 1), (){if(context.mounted) alart(context, "今日复习任务已完成", onConfirmed: () {Navigator.pop(context);});}); return Center( - child: TextContainer(text: "今日复习任务已完成"), + child: TextContainer(text: "今日复习任务已完成\n(或当前无复习内容)\n点击右上角齿轮可修改配置"), ); } return FSRSReviewCardPage(wordID: wordID, fsrs: fsrs, rnd: sharedRnd, controller: controller,); 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 ca9551f..f8954e2 100644 --- a/lib/sub_pages_builder/setting_pages/questions_setting_page.dart +++ b/lib/sub_pages_builder/setting_pages/questions_setting_page.dart @@ -16,6 +16,15 @@ class _QuestionsSettingPage extends State { bool floatButtonFlod = true; 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( + questionSections: selectedTypes!.cast() + ) + ) + ); + } @override Widget build(BuildContext context) { @@ -23,7 +32,7 @@ class _QuestionsSettingPage extends State { late final SubQuizConfig section; section = context.read().globalConfig.quiz.zhar; MediaQueryData mediaQuery = MediaQuery.of(context); - selectedTypes ??= section.questionSections; + selectedTypes ??= List.from(section.questionSections); List listTiles = []; bool isEven = true; for(int index = 0; index < selectedTypes!.length; index++) { @@ -48,6 +57,7 @@ class _QuestionsSettingPage extends State { context.read().uiLogger.fine("移除题型项目: $index"); setState(() { selectedTypes!.removeAt(index.toInt()); + _updateConfig(); }); } }, @@ -80,6 +90,7 @@ class _QuestionsSettingPage extends State { if(oldIndex < newIndex) newIndex--; // 修正索引 int old = selectedTypes!.removeAt(oldIndex); selectedTypes!.insert(newIndex, old); + _updateConfig(); }); }, children: listTiles, @@ -194,6 +205,7 @@ class _QuestionsSettingPage extends State { context.read().uiLogger.info("添加题型类型: $i"); setState(() { selectedTypes!.add(i); + _updateConfig(); }); }, icon: Icon(Icons.add), diff --git a/lib/vars/global.dart b/lib/vars/global.dart index f93894f..341f957 100644 --- a/lib/vars/global.dart +++ b/lib/vars/global.dart @@ -50,6 +50,7 @@ class Global with ChangeNotifier { await prefs.setString("wordData", jsonEncode({"Words": [], "Classes": {}})); wordData = DictData(words: [], classes: []); logger.info("首次启动: 配置表初始化完成"); + globalFSRS = FSRS()..init(outerPrefs: prefs); await postInit(); } else { await conveySetting(); @@ -230,11 +231,20 @@ class Global with ChangeNotifier { // } DictData dataFormater(Map data, DictData exData, String sourceName) { logger.info("开始词汇格式化"); - List wordList = []; - for(WordItem x in exData.words) { - wordList.add(x.arabic); + + // Use Maps for O(1) lookup speed instead of O(N) List.indexOf + Map rawWordMap = {}; + Map pureWordMap = {}; + List chineseList = []; + + for(int i = 0; i < exData.words.length; i++) { + WordItem x = exData.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 = wordList.length; + + int counter = exData.words.length; SourceItem? exSource; // 查找已有数据中是否有同名的源数据组 @@ -249,9 +259,28 @@ class Global with ChangeNotifier { for(var className in data.keys){ ClassItem exClass = ClassItem(className: className, wordIndexs: []); for(var word in data[className]){ - if(wordList.contains(word["arabic"])){ + String newRaw = word["arabic"]; + String newPure = newRaw.removeAracicExtensionPart().trim(); + int existingIndex = -1; + + if (rawWordMap.containsKey(newRaw)) { + existingIndex = rawWordMap[newRaw]!; + } else if (pureWordMap.containsKey(newPure)) { + int potentialIndex = pureWordMap[newPure]!; + // Pure arabic is the same, but different vowels. Are they the same meaning? + if (chineseList[potentialIndex].hasSimilarMeaning(word["chinese"])) { + existingIndex = potentialIndex; + } + } + + if (existingIndex != -1) { + // If it already exists globally, just add it to this class + if(!exClass.wordIndexs.contains(existingIndex)) { + exClass.wordIndexs.add(existingIndex); + } continue; } + exClass.wordIndexs.add(counter); exData.words.add( WordItem( @@ -262,7 +291,9 @@ class Global with ChangeNotifier { id: counter ) ); - wordList.add(word["arabic"]); + rawWordMap[newRaw] = counter; + pureWordMap[newPure] = counter; + chineseList.add(word["chinese"]); counter ++; } exSource.subClasses.add(exClass);