11import 'dart:convert' ;
22
3- import 'package:arabic_learning/funcs/utili .dart' ;
3+ import 'package:flutter/foundation .dart' ;
44import 'package:fsrs/fsrs.dart' ;
55import 'package:logging/logging.dart' ;
66
77import 'package:arabic_learning/package_replacement/storage.dart' ;
88
99class FSRS {
10- List <Card > cards = [];
11- List <ReviewLog > reviewLogs = [];
12- late Scheduler scheduler;
1310 late final SharedPreferences prefs;
14- late Map < String , dynamic > settingData ;
11+ late FSRSConfig config ;
1512 late final Logger logger;
1613 // index != cardId; cardId = wordId = the index of word in global.wordData[words]
1714
1815 bool init ({required SharedPreferences outerPrefs}) {
1916 prefs = outerPrefs;
2017 logger = Logger ('FSRS' );
2118 logger.fine ("构建FSRS模块" );
22- settingData = {
23- 'enabled' : false ,
24- 'scheduler' : {},
25- 'cards' : [],
26- 'reviewLog' : [],
27- 'rater' : {
28- "desiredRetention" : 0.9 ,
29- "easyDuration" : 3000 ,
30- "goodDuration" : 6000
31- },
32- };
3319
3420 if (! prefs.containsKey ("fsrsData" )) {
3521 logger.info ("未发现FSRS配置,加载默认配置" );
36- prefs.setString ("fsrsData" , jsonEncode (settingData));
22+ config = FSRSConfig ();
23+ prefs.setString ("fsrsData" , jsonEncode (config));
3724 return false ;
3825 } else {
39- settingData = deepMerge (settingData, jsonDecode (prefs.getString ("fsrsData" )! ));
40- }
41-
42- if (isEnabled ()){
43- scheduler = Scheduler .fromMap (settingData['scheduler' ]);
44- for (int i = 0 ; i < settingData['cards' ].length; i++ ) {
45- cards.add (Card .fromMap (settingData['cards' ][i]));
46- reviewLogs.add (ReviewLog .fromMap (settingData['reviewLog' ][i]));
47- }
26+ config = FSRSConfig .buildFromMap (jsonDecode (prefs.getString ("fsrsData" )! ));
4827 logger.info ("FSRS配置加载完成" );
49- return true ;
5028 }
29+
30+ if (config.enabled) return true ;
5131 logger.info ("FSRS未启用" );
5232 return false ;
5333 }
5434
5535 void save () async {
5636 logger.info ("正在保存FSRS配置" );
57- settingData['scheduler' ] = scheduler.toMap ();
58- List cardsCache = [];
59- List logCache = [];
60- for (int i = 0 ; i < cards.length; i++ ) {
61- cardsCache.add (cards[i].toMap ());
62- logCache.add (reviewLogs[i].toMap ());
63- }
64- settingData['cards' ] = cardsCache;
65- settingData['reviewLog' ] = logCache;
66- prefs.setString ("fsrsData" , jsonEncode (settingData));
67- }
68-
69- bool isEnabled () {
70- return settingData['enabled' ];
37+ prefs.setString ("fsrsData" , jsonEncode (config.toMap ()));
7138 }
7239
7340 void createScheduler ({required SharedPreferences prefs}) {
74- logger.info ("初始化scheduler,选择相关配置 ${settingData ["rater" ].toString ()}" );
75- scheduler = Scheduler (desiredRetention: settingData["rater" ]["desiredRetention" ]);
76- settingData['enabled' ] = true ;
77- settingData['scheduler' ] = scheduler.toMap ();
41+ logger.info ("初始化scheduler,选择相关配置 ${config .toMap ().toString ()}" );
42+ config = config.copyWith (
43+ enabled: true ,
44+ scheduler: Scheduler (desiredRetention: config.desiredRetention)
45+ );
7846 save ();
7947 }
8048
8149 int willDueIn (int index) {
82- return cards[index].due.toLocal ().difference (DateTime .now ()).inDays;
50+ return config. cards[index].due.toLocal ().difference (DateTime .now ()).inDays;
8351 }
8452
8553 void reviewCard (int wordId, int duration, bool isCorrect) {
8654 logger.fine ("记录复习卡片: Id: $wordId ; duration: $duration ; isCorrect: $isCorrect " );
87- int index = cards.indexWhere ((Card card) => card.cardId == wordId); // 避免有时候cardId != wordId
88- logger.fine ("定位复习卡片地址: $index , 目前阶段: ${cards [index ].step }, 难度: ${cards [index ].difficulty }, 稳定: ${cards [index ].stability }, 过期时间(+8): ${cards [index ].due .toLocal ()}" );
89- final (: card, : reviewLog) = scheduler.reviewCard (cards[index], calculate (duration, isCorrect), reviewDateTime: DateTime .now ().toUtc (), reviewDuration: duration);
90- cards[index] = card;
91- reviewLogs[index] = reviewLog;
92- logger.fine ("卡片 $index 复习后: 目前阶段: ${cards [index ].step }, 难度: ${cards [index ].difficulty }, 稳定: ${cards [index ].stability }, 过期时间(+8): ${cards [index ].due .toLocal ()}" );
55+ int index = config. cards.indexWhere ((Card card) => card.cardId == wordId); // 避免有时候cardId != wordId
56+ logger.fine ("定位复习卡片地址: $index , 目前阶段: ${config . cards [index ].step }, 难度: ${config . cards [index ].difficulty }, 稳定: ${config . cards [index ].stability }, 过期时间(+8): ${config . cards [index ].due .toLocal ()}" );
57+ final (: card, : reviewLog) = config. scheduler! .reviewCard (config. cards[index], calculate (duration, isCorrect), reviewDateTime: DateTime .now ().toUtc (), reviewDuration: duration);
58+ config. cards[index] = card;
59+ config. reviewLogs[index] = reviewLog;
60+ logger.fine ("卡片 $index 复习后: 目前阶段: ${config . cards [index ].step }, 难度: ${config . cards [index ].difficulty }, 稳定: ${config . cards [index ].stability }, 过期时间(+8): ${config . cards [index ].due .toLocal ()}" );
9361 save ();
9462 }
9563
9664 int getWillDueCount () {
9765 int dueCards = 0 ;
98- for (int i = 0 ; i < cards.length; i++ ) {
66+ for (int i = 0 ; i < config. cards.length; i++ ) {
9967 if (willDueIn (i) < 1 ) {
10068 dueCards++ ;
10169 }
@@ -105,26 +73,26 @@ class FSRS {
10573
10674 int getLeastDueCard () {
10775 int leastDueIndex = 0 ;
108- for (int i = 1 ; i < cards.length; i++ ) {
109- if (cards[i].due.toLocal ().isBefore (cards[leastDueIndex].due.toLocal ()) && cards[i].due.toLocal ().difference (DateTime .now ()) < Duration (days: 1 )) {
76+ for (int i = 1 ; i < config. cards.length; i++ ) {
77+ if (config. cards[i].due.toLocal ().isBefore (config. cards[leastDueIndex].due.toLocal ()) && config. cards[i].due.toLocal ().difference (DateTime .now ()) < Duration (days: 1 )) {
11078 leastDueIndex = i;
11179 }
11280 }
113- if (cards[leastDueIndex].due.difference (DateTime .now ()) > Duration (days: 1 )) return - 1 ;
114- return cards[leastDueIndex].cardId;
81+ if (config. cards[leastDueIndex].due.difference (DateTime .now ()) > Duration (days: 1 )) return - 1 ;
82+ return config. cards[leastDueIndex].cardId;
11583 }
11684
11785 bool isContained (int wordId) {
118- for (Card card in cards) {
86+ for (Card card in config. cards) {
11987 if (card.cardId == wordId) return true ;
12088 }
12189 return false ;
12290 }
12391
12492 void addWordCard (int wordId) {
12593 // os the wordID == cardID
126- cards.add (Card (cardId: wordId, state: State .learning));
127- reviewLogs.add (ReviewLog (cardId: wordId, rating: Rating .good, reviewDateTime: DateTime .now ()));
94+ config. cards.add (Card (cardId: wordId, state: State .learning));
95+ config. reviewLogs.add (ReviewLog (cardId: wordId, rating: Rating .good, reviewDateTime: DateTime .now ()));
12896 save ();
12997 }
13098
@@ -134,15 +102,89 @@ class FSRS {
134102 logger.fine ("计算得分: again" );
135103 return Rating .again;
136104 }
137- if (duration < settingData[ 'rater' ][ ' easyDuration' ] ) {
105+ if (duration < config. easyDuration) {
138106 logger.fine ("计算得分: easy" );
139107 return Rating .easy;
140108 }
141- if (duration < settingData[ 'rater' ][ ' goodDuration' ] ) {
109+ if (duration < config. goodDuration) {
142110 logger.fine ("计算得分: good" );
143111 return Rating .good;
144112 }
145113 logger.fine ("计算得分: hard" );
146114 return Rating .hard;
147115 }
116+ }
117+
118+ @immutable
119+ class FSRSConfig {
120+ final bool enabled;
121+ final Scheduler ? scheduler;
122+ final List <Card > cards;
123+ final List <ReviewLog > reviewLogs;
124+ final double desiredRetention;
125+ final int easyDuration;
126+ final int goodDuration;
127+
128+ const FSRSConfig ({
129+ bool ? enabled,
130+ this .scheduler,
131+ List <Card >? cards,
132+ List <ReviewLog >? reviewLogs,
133+ double ? desiredRetention,
134+ int ? easyDuration,
135+ int ? goodDuration
136+ }) :
137+ enabled = enabled?? false ,
138+ cards = cards?? const [],
139+ reviewLogs = reviewLogs?? const [],
140+ desiredRetention = desiredRetention?? 0.9 ,
141+ easyDuration = easyDuration?? 3000 ,
142+ goodDuration = goodDuration?? 6000 ;
143+
144+ Map <String , dynamic > toMap (){
145+ return {
146+ 'enabled' : enabled,
147+ 'scheduler' : scheduler? .toMap () ?? {},
148+ 'cards' : List <Map >.generate (cards.length, (index) => cards[index].toMap (), growable: false ),
149+ 'reviewLog' : List <Map >.generate (reviewLogs.length, (index) => reviewLogs[index].toMap (), growable: false ),
150+ "desiredRetention" : desiredRetention,
151+ "easyDuration" : easyDuration,
152+ "goodDuration" : goodDuration
153+ };
154+ }
155+
156+ FSRSConfig copyWith ({
157+ bool ? enabled,
158+ Scheduler ? scheduler,
159+ List <Card >? cards,
160+ List <ReviewLog >? reviewLogs,
161+ double ? desiredRetention,
162+ int ? easyDuration,
163+ int ? goodDuration
164+ }) {
165+ return FSRSConfig (
166+ enabled: enabled?? this .enabled,
167+ scheduler: scheduler?? this .scheduler,
168+ cards: cards?? this .cards,
169+ reviewLogs: reviewLogs?? this .reviewLogs,
170+ desiredRetention: desiredRetention?? this .desiredRetention,
171+ easyDuration: easyDuration?? this .easyDuration,
172+ goodDuration: goodDuration?? this .goodDuration
173+ );
174+ }
175+
176+ static FSRSConfig buildFromMap (Map <String , dynamic > configData){
177+ if (configData["enabled" ]?? false ) {
178+ return FSRSConfig (
179+ enabled: configData["enabled" ],
180+ scheduler: Scheduler .fromMap (configData["scheduler" ]),
181+ cards: List <Card >.generate (configData["cards" ].length,(index) => Card .fromMap (configData["cards" ][index]), growable: true ),
182+ reviewLogs: List <ReviewLog >.generate (configData["reviewLog" ].length,(index) => ReviewLog .fromMap (configData["reviewLog" ][index]), growable: true ),
183+ desiredRetention: configData["desiredRetention" ],
184+ easyDuration: configData["easyDuration" ],
185+ goodDuration: configData["goodDuration" ]
186+ );
187+ }
188+ return FSRSConfig (enabled: false );
189+ }
148190}
0 commit comments