Skip to content

Commit 9e8181a

Browse files
committed
refactor(fsrs): refactor fsrs config structure
Signed-off-by: OctagonalStar <[email protected]>
1 parent 7db9882 commit 9e8181a

File tree

3 files changed

+121
-73
lines changed

3 files changed

+121
-73
lines changed

lib/funcs/fsrs_func.dart

Lines changed: 104 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,69 @@
11
import 'dart:convert';
22

3-
import 'package:arabic_learning/funcs/utili.dart';
3+
import 'package:flutter/foundation.dart';
44
import 'package:fsrs/fsrs.dart';
55
import 'package:logging/logging.dart';
66

77
import 'package:arabic_learning/package_replacement/storage.dart';
88

99
class 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
}

lib/pages/home_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class HomePage extends StatelessWidget {
107107
children: [
108108
Text('规律性学习', style: TextStyle(fontSize: 12.0)),
109109
SizedBox(height: mediaQuery.size.height * 0.03),
110-
Text(fsrs.isEnabled() ? "${fsrs.getWillDueCount().toString()}个待复习" : "未启用", style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
110+
Text(fsrs.config.enabled ? "${fsrs.getWillDueCount().toString()}个待复习" : "未启用", style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
111111
],
112112
),
113113
),

lib/sub_pages_builder/learning_pages/fsrs_pages.dart

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ForeFSRSSettingPage extends StatelessWidget {
1919
context.read<Global>().uiLogger.info("构建 ForeFSRSSettingPage");
2020
MediaQueryData mediaQuery = MediaQuery.of(context);
2121
FSRS fsrs = FSRS()..init(outerPrefs: context.read<Global>().prefs);
22-
if(fsrs.isEnabled() && !forceChoosing) {
22+
if(fsrs.config.enabled && !forceChoosing) {
2323
return MainFSRSPage(fsrs: fsrs);
2424
}
2525
return Scaffold(
@@ -47,17 +47,19 @@ class ForeFSRSSettingPage extends StatelessWidget {
4747
children: [
4848
Expanded(child: Text("期望提取率", style: Theme.of(context).textTheme.bodyLarge)),
4949
Slider(
50-
value: fsrs.settingData['rater']["desiredRetention"],
50+
value: fsrs.config.desiredRetention,
5151
max: 0.99,
5252
min: 0.75,
5353
divisions: 24,
5454
onChanged: (value){
5555
setState(() {
56-
fsrs.settingData['rater']["desiredRetention"] = ((value*100).floorToDouble()/100);
56+
fsrs.config = fsrs.config.copyWith(
57+
desiredRetention: (value*100).floorToDouble()/100
58+
);
5759
});
5860
}
5961
),
60-
Text((fsrs.settingData['rater']["desiredRetention"] as num).toStringAsFixed(2))
62+
Text((fsrs.config.desiredRetention).toStringAsFixed(2))
6163
],
6264
),
6365
Text("期望提取率 是指期望你有多大概率能回忆起某个单词。通常设置值越大,要求的学习间隔越短。"),
@@ -79,17 +81,19 @@ class ForeFSRSSettingPage extends StatelessWidget {
7981
children: [
8082
Expanded(child: Text("优秀评分限时", style: Theme.of(context).textTheme.bodyLarge)),
8183
Slider(
82-
value: (fsrs.settingData['rater']["easyDuration"] as int).toDouble(),
84+
value: fsrs.config.easyDuration.toDouble(),
8385
max: 5000.0,
8486
min: 1000.0,
8587
divisions: 40,
8688
onChanged: (value){
8789
setState(() {
88-
fsrs.settingData['rater']["easyDuration"] = value.toInt();
90+
fsrs.config = fsrs.config.copyWith(
91+
easyDuration: value.toInt()
92+
);
8993
});
9094
}
9195
),
92-
Text(fsrs.settingData['rater']["easyDuration"].toString())
96+
Text(fsrs.config.easyDuration.toString())
9397
],
9498
),
9599
Text("优秀评分限时 是指在你回答问题时,回答正确耗时小于多少时评分为优秀(Easy),单位为毫秒"),
@@ -111,17 +115,19 @@ class ForeFSRSSettingPage extends StatelessWidget {
111115
children: [
112116
Expanded(child: Text("良好评分限时", style: Theme.of(context).textTheme.bodyLarge)),
113117
Slider(
114-
value: (fsrs.settingData['rater']["goodDuration"] as int).toDouble(),
118+
value: fsrs.config.goodDuration.toDouble(),
115119
max: 10000.0,
116120
min: 2000.0,
117121
divisions: 80,
118122
onChanged: (value){
119123
setState(() {
120-
fsrs.settingData['rater']["goodDuration"] = value.toInt();
124+
fsrs.config = fsrs.config.copyWith(
125+
goodDuration: value.toInt()
126+
);
121127
});
122128
}
123129
),
124-
Text(fsrs.settingData['rater']["goodDuration"].toString())
130+
Text(fsrs.config.goodDuration.toString())
125131
],
126132
),
127133
Text("良好评分限时 是指在你回答问题时,回答正确耗时小于多少时评分为良好(Good),单位为毫秒"),

0 commit comments

Comments
 (0)