From 51081e7f7cb471ee956d821fc5ef10d5ef39fc14 Mon Sep 17 00:00:00 2001 From: JYinherit Date: Thu, 2 Apr 2026 19:11:37 +0400 Subject: [PATCH 1/7] =?UTF-8?q?FIX:FSRS=E9=87=8D=E5=A4=8D=E5=A4=8D?= =?UTF-8?q?=E4=B9=A0BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: JYinherit --- lib/funcs/fsrs_func.dart | 14 ++- .../learning_pages/fsrs_pages.dart | 114 ++++++++++++++++-- 2 files changed, 109 insertions(+), 19 deletions(-) diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index 1fc94bc..ddeff48 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -68,7 +68,7 @@ class FSRS { int getWillDueCount() { int dueCards = 0; for(int i = 0; i < config.cards.length; i++) { - if(willDueIn(i) < 1) { + if(config.cards[i].due.toLocal().isBefore(DateTime.now())) { dueCards++; } } @@ -77,13 +77,15 @@ 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)) { - leastDueIndex = i; + int leastDueIndex = -1; + for(int i = 0; i < config.cards.length; i++) { + if(config.cards[i].due.toLocal().isBefore(DateTime.now())) { + if(leastDueIndex == -1 || config.cards[i].due.toLocal().isBefore(config.cards[leastDueIndex].due.toLocal())) { + leastDueIndex = i; + } } } - if(config.cards[leastDueIndex].due.difference(DateTime.now()) > Duration(days: 1)) return -1; + if (leastDueIndex == -1) return -1; return config.cards[leastDueIndex].cardId; } diff --git a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart index b4910a3..4aece60 100644 --- a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart +++ b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart @@ -281,25 +281,77 @@ class ForeFSRSSettingPage extends StatelessWidget { } } -class MainFSRSPage extends StatelessWidget { +class MainFSRSPage extends StatefulWidget { final FSRS fsrs; const MainFSRSPage({super.key, required this.fsrs}); - + + @override + State createState() => _MainFSRSPageState(); +} + +class _MainFSRSPageState extends State { + late List dueCardIds; + late final PageController controller; + late final Random sharedRnd; + + @override + void initState() { + super.initState(); + controller = PageController(); + sharedRnd = Random(); + _loadDueCards(); + } + + static const int _repeatCount = 3; + + void _loadDueCards() { + final uniqueIds = widget.fsrs.config.cards + .where((card) => card.due.toLocal().isBefore(DateTime.now())) + .map((card) => card.cardId) + .toList(); + // 交错顺序 [A,B,C, A,B,C, ...] — 每轮过完所有词再重复 + dueCardIds = [ + for (int i = 0; i < _repeatCount; i++) + ...uniqueIds + ]; + } + + void _refresh() { + setState(() { + _loadDueCards(); + // Jump back to first content page + controller.jumpToPage(1); + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - context.read().uiLogger.info("构建 MainFSRSPage"); + context.read().uiLogger.info("构建 MainFSRSPage, 待复习: ${dueCardIds.length}"); MediaQueryData mediaQuery = MediaQuery.of(context); - final PageController controller = PageController(); - Random sharedRnd = Random(); + + // page 0: summary, pages 1..N: review cards, page N+1: done + final int totalPages = dueCardIds.isEmpty ? 1 : dueCardIds.length + 2; + return Scaffold( appBar: AppBar( title: const Text("规律学习"), actions: [ + IconButton( + icon: Icon(Icons.refresh), + tooltip: "刷新复习列表", + onPressed: _refresh, + ), IconButton( icon: Icon(Icons.keyboard_option_key), onPressed: () { showModalBottomSheet( - context: context, + context: context, isScrollControlled: true, enableDrag: false, builder: (context) => ForeFSRSSettingPage(forceChoosing: true) @@ -311,24 +363,60 @@ class MainFSRSPage extends StatelessWidget { body: PageView.builder( scrollDirection: Axis.vertical, controller: controller, + physics: const PageScrollPhysics(), + itemCount: totalPages, itemBuilder: (context, index) { - final wordID = fsrs.getLeastDueCard(); - if(wordID == -1) { + // Page 0: entry summary + if (index == 0) { + if (dueCardIds.isEmpty) { + return Center( + child: TextContainer(text: "当前无需要复习的内容\n点击右上角可修改配置"), + ); + } return Center( - child: TextContainer(text: "当前无需要复习的内容\n点击右上角可修改配置"), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextContainer( + text: "你有${dueCardIds.length}个单词需要复习!\n上滑页面开始复习", + size: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.4), + textAlign: TextAlign.center, + ), + Icon(Icons.arrow_upward, size: 48.0, color: Colors.grey) + ], + ), ); } - if(index == 0) { + + // Last page: completed + if (index == dueCardIds.length + 1) { return Center( child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - TextContainer(text: "你有${fsrs.getWillDueCount().toString()}个单词需要复习!\n上滑页面开始复习",size: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.4),textAlign: TextAlign.center), - Icon(Icons.arrow_upward, size: 48.0, color: Colors.grey) + TextContainer( + text: "本轮复习完成!\n如有新到期单词可点击右上角刷新", + size: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.4), + textAlign: TextAlign.center, + ), + ElevatedButton.icon( + onPressed: _refresh, + icon: Icon(Icons.refresh), + label: Text("刷新"), + ) ], ), ); } - return FSRSReviewCardPage(wordID: wordID, fsrs: fsrs, rnd: sharedRnd, controller: controller,); + + // Review pages 1..N + final wordID = dueCardIds[index - 1]; + return FSRSReviewCardPage( + wordID: wordID, + fsrs: widget.fsrs, + rnd: sharedRnd, + controller: controller, + ); } ) ); From 03c024188ea511306719231f94c1c762fcee9d5b Mon Sep 17 00:00:00 2001 From: JYinherit Date: Sat, 4 Apr 2026 21:35:52 +0400 Subject: [PATCH 2/7] Fix:Repeated data disturbs FSRS function Signed-off-by: JYinherit --- lib/funcs/fsrs_func.dart | 49 +++++-- .../learning_pages/fsrs_pages.dart | 135 ++++++++++++------ pubspec.lock | 16 +-- 3 files changed, 138 insertions(+), 62 deletions(-) diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index ddeff48..8e4f6ba 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -29,6 +29,28 @@ class FSRS { } else { config = FSRSConfig.buildFromMap(jsonDecode(appData.storage.getString("fsrsData")!)); logger.info("FSRS配置加载完成"); + + // 清洗潜在的重复脏数据 (Deduplication) + final Set seenIds = {}; + final List uniqueCards = []; + final List uniqueLogs = []; + + for(int i = 0; i < config.cards.length; i++) { + final currentCardId = config.cards[i].cardId; + if(!seenIds.contains(currentCardId)) { + seenIds.add(currentCardId); + uniqueCards.add(config.cards[i]); + if(i < config.reviewLogs.length) { + uniqueLogs.add(config.reviewLogs[i]); + } + } + } + + if(uniqueCards.length < config.cards.length) { + logger.warning("发现并清理了 ${config.cards.length - uniqueCards.length} 条重复复习记录"); + config = config.copyWith(cards: uniqueCards, reviewLogs: uniqueLogs); + save(); + } } if(config.enabled) return true; @@ -68,7 +90,7 @@ class FSRS { int getWillDueCount() { int dueCards = 0; for(int i = 0; i < config.cards.length; i++) { - if(config.cards[i].due.toLocal().isBefore(DateTime.now())) { + if(config.cards[i].due.toLocal().difference(DateTime.now()).inDays < 1) { dueCards++; } } @@ -79,7 +101,7 @@ class FSRS { if (config.cards.isEmpty) return -1; int leastDueIndex = -1; for(int i = 0; i < config.cards.length; i++) { - if(config.cards[i].due.toLocal().isBefore(DateTime.now())) { + if(config.cards[i].due.toLocal().difference(DateTime.now()).inDays < 1) { if(leastDueIndex == -1 || config.cards[i].due.toLocal().isBefore(config.cards[leastDueIndex].due.toLocal())) { leastDueIndex = i; } @@ -95,6 +117,10 @@ class FSRS { void addWordCard(int wordId) { logger.fine("添加复习卡片: Id: $wordId"); + if(isContained(wordId)){ + logger.fine("卡片 Id: $wordId 已存在,跳过重复添加"); + return; + } if (config.cards.isEmpty) { config = config.copyWith(cards: [], reviewLogs: []); } @@ -135,6 +161,7 @@ class FSRSConfig { final bool preferSimilar; final bool selfEvaluate; final int pushAmount; + final bool reinforceMemory; const FSRSConfig({ bool? enabled, @@ -146,7 +173,8 @@ class FSRSConfig { int? goodDuration, bool? preferSimilar, bool? selfEvaluate, - int? pushAmount + int? pushAmount, + bool? reinforceMemory }) : enabled = enabled??false, cards = cards??const [], @@ -156,7 +184,8 @@ class FSRSConfig { goodDuration = goodDuration??6000, preferSimilar = preferSimilar??false, selfEvaluate = selfEvaluate??false, - pushAmount = pushAmount??0; + pushAmount = pushAmount??0, + reinforceMemory = reinforceMemory??true; Map toMap(){ return { @@ -169,7 +198,8 @@ class FSRSConfig { "goodDuration": goodDuration, "preferSimilar": preferSimilar, "selfEvaluate": selfEvaluate, - "pushAmount": pushAmount + "pushAmount": pushAmount, + "reinforceMemory": reinforceMemory }; } @@ -183,7 +213,8 @@ class FSRSConfig { int? goodDuration, bool? preferSimilar, bool? selfEvaluate, - int? pushAmount + int? pushAmount, + bool? reinforceMemory }) { return FSRSConfig( enabled: enabled??this.enabled, @@ -195,7 +226,8 @@ class FSRSConfig { goodDuration: goodDuration??this.goodDuration, preferSimilar: preferSimilar??this.preferSimilar, selfEvaluate: selfEvaluate??this.selfEvaluate, - pushAmount: pushAmount??this.pushAmount + pushAmount: pushAmount??this.pushAmount, + reinforceMemory: reinforceMemory??this.reinforceMemory ); } @@ -211,7 +243,8 @@ class FSRSConfig { goodDuration: configData["goodDuration"], preferSimilar: configData["preferSimilar"], selfEvaluate: configData["selfEvaluate"], - pushAmount: configData["pushAmount"] + pushAmount: configData["pushAmount"], + reinforceMemory: configData["reinforceMemory"] ); } return FSRSConfig(enabled: false); diff --git a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart index 4aece60..10d6e48 100644 --- a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart +++ b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart @@ -229,6 +229,36 @@ class ForeFSRSSettingPage extends StatelessWidget { ], ), ), + Container( + decoration: BoxDecoration( + borderRadius: StaticsVar.br, + color: Theme.of(context).colorScheme.onPrimary + ), + margin: EdgeInsets.all(8.0), + padding: EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded(child: Text("强化记忆循环", style: Theme.of(context).textTheme.bodyLarge)), + Switch( + value: fsrs.config.reinforceMemory, + onChanged: (value){ + setState(() { + fsrs.config = fsrs.config.copyWith( + reinforceMemory: value + ); + }); + } + ) + ], + ), + Text("开启后同一个到期卡片在队列中将按交错顺序出现多次(3次),加深印象。"), + Text("关闭后每个词汇当次复习只在队列里出现一次。") + ], + ), + ), ElevatedButton.icon( style: ElevatedButton.styleFrom( fixedSize: Size.fromHeight(100), @@ -290,7 +320,7 @@ class MainFSRSPage extends StatefulWidget { } class _MainFSRSPageState extends State { - late List dueCardIds; + late List queue; late final PageController controller; late final Random sharedRnd; @@ -299,29 +329,36 @@ class _MainFSRSPageState extends State { super.initState(); controller = PageController(); sharedRnd = Random(); - _loadDueCards(); + queue = []; + _extendQueue(); } - static const int _repeatCount = 3; - - void _loadDueCards() { + void _extendQueue() { final uniqueIds = widget.fsrs.config.cards - .where((card) => card.due.toLocal().isBefore(DateTime.now())) + .where((card) => card.due.toLocal().difference(DateTime.now()).inDays < 1) .map((card) => card.cardId) .toList(); - // 交错顺序 [A,B,C, A,B,C, ...] — 每轮过完所有词再重复 - dueCardIds = [ - for (int i = 0; i < _repeatCount; i++) - ...uniqueIds - ]; + + if (uniqueIds.isEmpty) return; + + // 按新设定:如果启用强化记忆,重复3次,否则仅1遍 + if (widget.fsrs.config.reinforceMemory) { + queue.addAll([ + for (int i = 0; i < 3; i++) ...uniqueIds + ]); + } else { + queue.addAll(uniqueIds); + } } void _refresh() { setState(() { - _loadDueCards(); - // Jump back to first content page - controller.jumpToPage(1); + queue.clear(); + _extendQueue(); }); + if (controller.hasClients) { + controller.jumpToPage(0); + } } @override @@ -332,19 +369,16 @@ class _MainFSRSPageState extends State { @override Widget build(BuildContext context) { - context.read().uiLogger.info("构建 MainFSRSPage, 待复习: ${dueCardIds.length}"); + context.read().uiLogger.info("构建 MainFSRSPage, 动态队列长度: ${queue.length}"); MediaQueryData mediaQuery = MediaQuery.of(context); - // page 0: summary, pages 1..N: review cards, page N+1: done - final int totalPages = dueCardIds.isEmpty ? 1 : dueCardIds.length + 2; - return Scaffold( appBar: AppBar( title: const Text("规律学习"), actions: [ IconButton( icon: Icon(Icons.refresh), - tooltip: "刷新复习列表", + tooltip: "强制刷新", onPressed: _refresh, ), IconButton( @@ -364,13 +398,12 @@ class _MainFSRSPageState extends State { scrollDirection: Axis.vertical, controller: controller, physics: const PageScrollPhysics(), - itemCount: totalPages, itemBuilder: (context, index) { - // Page 0: entry summary + // Page 0: 引导页 if (index == 0) { - if (dueCardIds.isEmpty) { + if (queue.isEmpty) { return Center( - child: TextContainer(text: "当前无需要复习的内容\n点击右上角可修改配置"), + child: TextContainer(text: "当前无任何待复习的内容\n可以休息会儿或者去[学习推送]里加些新词"), ); } return Center( @@ -378,7 +411,7 @@ class _MainFSRSPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ TextContainer( - text: "你有${dueCardIds.length}个单词需要复习!\n上滑页面开始复习", + text: "已载入待复习队列\n上滑页面开始复习", size: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.4), textAlign: TextAlign.center, ), @@ -388,31 +421,41 @@ class _MainFSRSPageState extends State { ); } - // Last page: completed - if (index == dueCardIds.length + 1) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextContainer( - text: "本轮复习完成!\n如有新到期单词可点击右上角刷新", - size: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.4), - textAlign: TextAlign.center, - ), - ElevatedButton.icon( - onPressed: _refresh, - icon: Icon(Icons.refresh), - label: Text("刷新"), - ) - ], - ), - ); + int arrayIndex = index - 1; + + // 动态扩充:当滑到底时触发延展 + if (arrayIndex >= queue.length) { + _extendQueue(); + } + + // 扩充了还是不够,说明真的穷尽了 + if (arrayIndex >= queue.length) { + if (arrayIndex == queue.length) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextContainer( + text: "太棒了!当前没有任何卡片需要复习!\n若刚复习完请等待下一个间隔", + size: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.4), + textAlign: TextAlign.center, + ), + ElevatedButton.icon( + onPressed: _refresh, + icon: Icon(Icons.refresh), + label: Text("全盘刷新"), + ) + ], + ), + ); + } + // 返回 null 阻断 PageView 继续下滑 + return null; } - // Review pages 1..N - final wordID = dueCardIds[index - 1]; + // 常规复习页 return FSRSReviewCardPage( - wordID: wordID, + wordID: queue[arrayIndex], fsrs: widget.fsrs, rnd: sharedRnd, controller: controller, diff --git a/pubspec.lock b/pubspec.lock index 20274ba..86a5885 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -548,10 +548,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -1073,26 +1073,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.15" timezone: dependency: transitive description: From 52994d82ccc9aa42367f460a61ca24fc06af688f Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:04:31 +0800 Subject: [PATCH 3/7] perf: replace some identify Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/funcs/fsrs_func.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index 8e4f6ba..6aa711f 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -90,7 +90,7 @@ class FSRS { int getWillDueCount() { int dueCards = 0; for(int i = 0; i < config.cards.length; i++) { - if(config.cards[i].due.toLocal().difference(DateTime.now()).inDays < 1) { + if(willDueIn(i) < 1) { dueCards++; } } @@ -98,10 +98,9 @@ class FSRS { } int getLeastDueCard() { - if (config.cards.isEmpty) return -1; int leastDueIndex = -1; for(int i = 0; i < config.cards.length; i++) { - if(config.cards[i].due.toLocal().difference(DateTime.now()).inDays < 1) { + if(willDueIn(i) < 1) { if(leastDueIndex == -1 || config.cards[i].due.toLocal().isBefore(config.cards[leastDueIndex].due.toLocal())) { leastDueIndex = i; } From a7988c28be88f9a37c1059ade2a4d059af55babf Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:20:28 +0800 Subject: [PATCH 4/7] feat: merge the addWordCard and reviewWordCard Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/funcs/fsrs_func.dart | 39 +++++++++---------- .../learning_pages/fsrs_pages.dart | 8 ++-- .../learning_pages/learning_pages_build.dart | 11 ++---- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index 6aa711f..a96ac72 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -76,14 +76,26 @@ class FSRS { return config.cards[index].due.toLocal().difference(DateTime.now()).inDays; } - void reviewCard(int wordId, int duration, bool isCorrect, {Rating? forceRate}) { + void produceCard(int wordId, {int? duration, bool? isCorrect, Rating? forceRate}) { logger.fine("记录复习卡片: Id: $wordId; duration: $duration; isCorrect: $isCorrect"); int index = config.cards.indexWhere((Card card) => card.cardId == wordId); // 避免有时候cardId != wordId - logger.fine("定位复习卡片地址: $index, 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}"); - final (:card, :reviewLog) = config.scheduler!.reviewCard(config.cards[index], forceRate ?? calculate(duration, isCorrect), reviewDateTime: DateTime.now().toUtc(), reviewDuration: duration); - config.cards[index] = card; - config.reviewLogs[index] = reviewLog; - logger.fine("卡片 $index 复习后: 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}"); + if(index == -1) { + // 卡片不存在 进行添加 + logger.fine("添加复习卡片: Id: $wordId"); + config.cards.add(Card(cardId: wordId, state: State.learning)); + config.reviewLogs.add(ReviewLog(cardId: wordId, rating: Rating.good, reviewDateTime: DateTime.now())); + } else { + // 卡片存在 进行复习 + if((duration == null || isCorrect == null) && forceRate == null) { + logger.shout("传入信息缺失: wordId: $wordId; duration: $duration; isCorrect: $isCorrect; forceRate: $forceRate"); + return; // 避免错误信息导入 + } + logger.fine("定位复习卡片地址: $index, 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}"); + final (:card, :reviewLog) = config.scheduler!.reviewCard(config.cards[index], forceRate ?? calculate(duration!, isCorrect!), reviewDateTime: DateTime.now().toUtc(), reviewDuration: duration); + config.cards[index] = card; + config.reviewLogs[index] = reviewLog; + logger.fine("卡片 $index 复习后: 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}"); + } save(); } @@ -114,21 +126,6 @@ class FSRS { return config.cards.any((Card card) => card.cardId == wordId); } - void addWordCard(int wordId) { - logger.fine("添加复习卡片: Id: $wordId"); - if(isContained(wordId)){ - logger.fine("卡片 Id: $wordId 已存在,跳过重复添加"); - return; - } - 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())); - save(); - } - Rating calculate(int duration, bool isCorrect) { // duration in milliseconds if (!isCorrect) { diff --git a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart index 2454da0..8068f36 100644 --- a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart +++ b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart @@ -520,14 +520,14 @@ class _FSRSReviewCardPage extends State { }); context.read().updateLearningStreak(); if(widget.fsrs.config.selfEvaluate) { - widget.fsrs.reviewCard(widget.wordID, end.difference(start).inMilliseconds, true, forceRate: (const [Rating.easy, Rating.good, Rating.hard, Rating.again]).elementAt(value)); + widget.fsrs.produceCard(widget.wordID, forceRate: (const [Rating.easy, Rating.good, Rating.hard, Rating.again]).elementAt(value)); return true; } else { if(correct == value) { - widget.fsrs.reviewCard(widget.wordID, end.difference(start).inMilliseconds, true); + widget.fsrs.produceCard(widget.wordID, duration: end.difference(start).inMilliseconds, isCorrect: true); return true; } else { - widget.fsrs.reviewCard(widget.wordID, end.difference(start).inMilliseconds, false); + widget.fsrs.produceCard(widget.wordID, duration: end.difference(start).inMilliseconds, isCorrect: false); return false; } } @@ -679,7 +679,7 @@ class _FSRSLearningPageState extends State { setState(() { corrected = true; }); - widget.fsrs.addWordCard(widget.words[index].id); + widget.fsrs.produceCard(widget.words[index].id); return true; } else { return false; 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 2d82a83..41c61e4 100644 --- a/lib/sub_pages_builder/learning_pages/learning_pages_build.dart +++ b/lib/sub_pages_builder/learning_pages/learning_pages_build.dart @@ -45,16 +45,11 @@ class _InLearningPageState extends State { if(isCorrect) correctCount++; FSRS fsrs = FSRS(); if(widget.countInReview && fsrs.config.enabled) { - if(fsrs.isContained(targetWord.id)){ - if(isTypingQuestion) { - fsrs.reviewCard(targetWord.id, takentime, isCorrect, forceRate: isCorrect ? Rating.good : Rating.again); - } else { - fsrs.reviewCard(targetWord.id, takentime, isCorrect); - } + if(isTypingQuestion) { + fsrs.produceCard(targetWord.id, forceRate: isCorrect ? Rating.good : Rating.again); } else { - if(isCorrect) fsrs.addWordCard(targetWord.id); + fsrs.produceCard(targetWord.id, duration: takentime, isCorrect: isCorrect); } - } } From 61c52f9ee57557ef88a68753783831a367f1233b Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:17:16 +0800 Subject: [PATCH 5/7] fix: produceCard not able to add wordCard Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- lib/funcs/fsrs_func.dart | 34 +++++++++++-------- .../learning_pages/fsrs_pages.dart | 14 +++----- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index a96ac72..b7e75e8 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -72,16 +72,22 @@ class FSRS { save(); } - int willDueIn(int index) { - return config.cards[index].due.toLocal().difference(DateTime.now()).inDays; + int willDueIn(Card card) { + return card.due.toLocal().difference(DateTime.now()).inDays; } void produceCard(int wordId, {int? duration, bool? isCorrect, Rating? forceRate}) { - logger.fine("记录复习卡片: Id: $wordId; duration: $duration; isCorrect: $isCorrect"); - int index = config.cards.indexWhere((Card card) => card.cardId == wordId); // 避免有时候cardId != wordId + logger.fine("记录复习卡片: Id: $wordId; duration: $duration; isCorrect: $isCorrect; forceRate: $forceRate"); + final int index = config.cards.indexWhere((Card card) => card.cardId == wordId); if(index == -1) { // 卡片不存在 进行添加 logger.fine("添加复习卡片: Id: $wordId"); + if(config.cards.isEmpty) { + config = config.copyWith( + cards: [], + reviewLogs: [] + ); + } config.cards.add(Card(cardId: wordId, state: State.learning)); config.reviewLogs.add(ReviewLog(cardId: wordId, rating: Rating.good, reviewDateTime: DateTime.now())); } else { @@ -101,8 +107,8 @@ class FSRS { int getWillDueCount() { int dueCards = 0; - for(int i = 0; i < config.cards.length; i++) { - if(willDueIn(i) < 1) { + for(Card card in config.cards) { + if(willDueIn(card) < 1) { dueCards++; } } @@ -110,16 +116,16 @@ class FSRS { } int getLeastDueCard() { - int leastDueIndex = -1; - for(int i = 0; i < config.cards.length; i++) { - if(willDueIn(i) < 1) { - if(leastDueIndex == -1 || config.cards[i].due.toLocal().isBefore(config.cards[leastDueIndex].due.toLocal())) { - leastDueIndex = i; + Card? leastDueCard; + for(Card card in config.cards) { + if(willDueIn(card) < 1) { + if(leastDueCard == null || card.due.toLocal().isBefore(leastDueCard.due.toLocal())) { + leastDueCard = card; } } } - if (leastDueIndex == -1) return -1; - return config.cards[leastDueIndex].cardId; + if (leastDueCard == null) return -1; + return leastDueCard.cardId; } bool isContained(int wordId) { @@ -181,7 +187,7 @@ class FSRSConfig { preferSimilar = preferSimilar??false, selfEvaluate = selfEvaluate??false, pushAmount = pushAmount??0, - reinforceMemory = reinforceMemory??true; + reinforceMemory = reinforceMemory??false; Map toMap(){ return { diff --git a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart index 8068f36..ae2c50f 100644 --- a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart +++ b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart @@ -320,28 +320,24 @@ class MainFSRSPage extends StatefulWidget { } class _MainFSRSPageState extends State { - late List queue; - late final PageController controller; - late final Random sharedRnd; + List queue = []; + final PageController controller = PageController(); + final Random sharedRnd = Random(); @override void initState() { super.initState(); - controller = PageController(); - sharedRnd = Random(); - queue = []; _extendQueue(); } void _extendQueue() { - final uniqueIds = widget.fsrs.config.cards - .where((card) => card.due.toLocal().difference(DateTime.now()).inDays < 1) + final List uniqueIds = widget.fsrs.config.cards + .where((card) => widget.fsrs.willDueIn(card) < 1) .map((card) => card.cardId) .toList(); if (uniqueIds.isEmpty) return; - // 按新设定:如果启用强化记忆,重复3次,否则仅1遍 if (widget.fsrs.config.reinforceMemory) { queue.addAll([ for (int i = 0; i < 3; i++) ...uniqueIds From 9ce9798f9cc4107bdb0e7a58a7657c219113d7bb Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Sat, 11 Apr 2026 10:36:08 +0800 Subject: [PATCH 6/7] docs: update changelog Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- CHANGELOG.md | 10 ++++++++++ pubspec.lock | 16 ++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13f4d43..e911f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## DEVELOPING - ????-??-?? - (??????) + +### Improvement + +- 优化了FSRS学习机制 + +### Fix + +- 修复了可能的复习列表重复问题 + ## v1.0.0 - 2026-3-18 - (100000) ### Added diff --git a/pubspec.lock b/pubspec.lock index 86a5885..20274ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -548,10 +548,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -1073,26 +1073,26 @@ packages: dependency: transitive description: name: test - sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.29.0" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.15" + version: "0.6.16" timezone: dependency: transitive description: From ede81345a381df28f1fe7a647cfcff4d263ede90 Mon Sep 17 00:00:00 2001 From: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> Date: Sat, 11 Apr 2026 10:39:04 +0800 Subject: [PATCH 7/7] docs: update changelog added part Signed-off-by: OctagonalStar <76486554+OctagonalStar@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e911f39..61b87b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## DEVELOPING - ????-??-?? - (??????) +### Added + +- 复习模块添加了强化学习选项 + ### Improvement - 优化了FSRS学习机制