Skip to content

Commit f782263

Browse files
Merge pull request #36 from OctagonalStar:feat/fsrs-re-design
Feat/fsrs-re-design
2 parents ff416f0 + 1bdedef commit f782263

File tree

4 files changed

+287
-300
lines changed

4 files changed

+287
-300
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- 添加了日志捕获 [#23](https://github.com/OctagonalStar/arabic_learning/issues/23)
88
- 添加了调试页面
9+
- 添加了个性化FSRS预设页面 [#26](https://github.com/OctagonalStar/arabic_learning/issues/26)
910

1011
### Improvement
1112

lib/funcs/fsrs_func.dart

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,50 @@
11
import 'dart:convert';
22

3-
import 'package:arabic_learning/vars/global.dart';
4-
import 'package:flutter/material.dart' show BuildContext;
3+
import 'package:arabic_learning/funcs/utili.dart';
54
import 'package:fsrs/fsrs.dart';
65
import 'package:logging/logging.dart';
76

87
import 'package:arabic_learning/package_replacement/storage.dart';
9-
import 'package:provider/provider.dart';
108

119
class FSRS {
1210
List<Card> cards = [];
1311
List<ReviewLog> reviewLogs = [];
1412
late Scheduler scheduler;
15-
late SharedPreferences prefs;
16-
late Rater rater;
13+
late final SharedPreferences prefs;
1714
late Map<String, dynamic> settingData;
1815
late final Logger logger;
1916
// index != cardId; cardId = wordId = the index of word in global.wordData[words]
2017

21-
Future<bool> init({BuildContext? context}) async {
18+
bool init({required SharedPreferences outerPrefs}) {
19+
prefs = outerPrefs;
2220
logger = Logger('FSRS');
2321
logger.fine("构建FSRS模块");
24-
prefs = context==null ? await SharedPreferences.getInstance() : context.read<Global>().prefs;
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+
};
33+
2534
if(!prefs.containsKey("fsrsData")) {
2635
logger.info("未发现FSRS配置,加载默认配置");
27-
settingData = {
28-
'enabled': false,
29-
'scheduler': {},
30-
'cards': [],
31-
'reviewLog': [],
32-
'rater': {'scheme': 0},
33-
};
3436
prefs.setString("fsrsData", jsonEncode(settingData));
3537
return false;
38+
} else {
39+
settingData = deepMerge(settingData, jsonDecode(prefs.getString("fsrsData")!));
3640
}
37-
settingData = jsonDecode(prefs.getString("fsrsData")!) as Map<String, dynamic>;
41+
3842
if(isEnabled()){
3943
scheduler = Scheduler.fromMap(settingData['scheduler']);
4044
for(int i = 0; i < settingData['cards'].length; i++) {
4145
cards.add(Card.fromMap(settingData['cards'][i]));
4246
reviewLogs.add(ReviewLog.fromMap(settingData['reviewLog'][i]));
4347
}
44-
rater = Rater(settingData['rater']['scheme']);
4548
logger.info("FSRS配置加载完成");
4649
return true;
4750
}
@@ -67,15 +70,11 @@ class FSRS {
6770
return settingData['enabled'];
6871
}
6972

70-
Future<void> createScheduler(int scheme, {BuildContext? context}) async {
71-
await init(context: context);
72-
logger.info("初始化scheduler,选择方案 $scheme");
73-
List<double> desiredRetention = [0.85, 0.9, 0.95, 0.95, 0.99];
74-
scheduler = Scheduler(desiredRetention: desiredRetention[scheme]);
75-
settingData['rater']['scheme'] = scheme;
73+
void createScheduler({required SharedPreferences prefs}) {
74+
logger.info("初始化scheduler,选择相关配置 ${settingData["rater"].toString()}");
75+
scheduler = Scheduler(desiredRetention: settingData["rater"]["desiredRetention"]);
7676
settingData['enabled'] = true;
7777
settingData['scheduler'] = scheduler.toMap();
78-
rater = Rater(scheme);
7978
save();
8079
}
8180

@@ -87,7 +86,7 @@ class FSRS {
8786
logger.fine("记录复习卡片: Id: $wordId; duration: $duration; isCorrect: $isCorrect");
8887
int index = cards.indexWhere((Card card) => card.cardId == wordId); // 避免有时候cardId != wordId
8988
logger.fine("定位复习卡片地址: $index, 目前阶段: ${cards[index].step}, 难度: ${cards[index].difficulty}, 稳定: ${cards[index].stability}, 过期时间(+8): ${cards[index].due.toLocal()}");
90-
final (:card, :reviewLog) = scheduler.reviewCard(cards[index], rater.calculate(duration, isCorrect), reviewDateTime: DateTime.now().toUtc(), reviewDuration: duration);
89+
final (:card, :reviewLog) = scheduler.reviewCard(cards[index], calculate(duration, isCorrect), reviewDateTime: DateTime.now().toUtc(), reviewDuration: duration);
9190
cards[index] = card;
9291
reviewLogs[index] = reviewLog;
9392
logger.fine("卡片 $index 复习后: 目前阶段: ${cards[index].step}, 难度: ${cards[index].difficulty}, 稳定: ${cards[index].stability}, 过期时间(+8): ${cards[index].due.toLocal()}");
@@ -128,31 +127,22 @@ class FSRS {
128127
reviewLogs.add(ReviewLog(cardId: wordId, rating: Rating.good, reviewDateTime: DateTime.now()));
129128
save();
130129
}
131-
}
132-
133-
class Rater {
134-
Rating get easy => Rating.easy;
135-
Rating get good => Rating.good;
136-
Rating get hard => Rating.hard;
137-
Rating get forget => Rating.again;
138-
139-
late int scheme;
140-
141-
Rater(this.scheme);
142-
143-
static const List<List<int>> _difficultyScheme = [
144-
[3000, 8000], // Easy
145-
[2000, 6000], // Fine
146-
[1500, 4000], // OK~
147-
[1000, 2000], // Emm...
148-
[1000, 1500], // Impossible
149-
];
150130

151131
Rating calculate(int duration, bool isCorrect) {
152132
// duration in milliseconds
153-
if (!isCorrect) return Rating.again;
154-
if (duration < _difficultyScheme[scheme][0]) return Rating.easy;
155-
if (duration < _difficultyScheme[scheme][1]) return Rating.good;
133+
if (!isCorrect) {
134+
logger.fine("计算得分: again");
135+
return Rating.again;
136+
}
137+
if (duration < settingData['rater']['easyDuration']) {
138+
logger.fine("计算得分: easy");
139+
return Rating.easy;
140+
}
141+
if (duration < settingData['rater']['goodDuration']) {
142+
logger.fine("计算得分: good");
143+
return Rating.good;
144+
}
145+
logger.fine("计算得分: hard");
156146
return Rating.hard;
157147
}
158148
}

lib/pages/home_page.dart

Lines changed: 112 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -16,133 +16,127 @@ class HomePage extends StatelessWidget {
1616
context.read<Global>().uiLogger.fine("构建 HomePage");
1717
final themeColor = Theme.of(context).colorScheme;
1818
final mediaQuery = MediaQuery.of(context);
19-
final FSRS fsrs = FSRS();
20-
21-
return FutureBuilder(
22-
future: fsrs.init(context: context),
23-
builder: (context, asyncSnapshot) {
24-
return Column(
19+
final FSRS fsrs = FSRS()..init(outerPrefs: context.read<Global>().prefs);
20+
return Column(
21+
children: [
22+
DailyWord(),
23+
SizedBox(height: mediaQuery.size.height * 0.01),
24+
Row(
25+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
2526
children: [
26-
DailyWord(),
27-
SizedBox(height: mediaQuery.size.height * 0.01),
28-
Row(
29-
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
30-
children: [
31-
Container(
32-
width: mediaQuery.size.width * 0.30,
33-
height: mediaQuery.size.height * 0.18,
34-
margin: EdgeInsets.all(4.0),
35-
padding: EdgeInsets.all(16.0),
36-
decoration: BoxDecoration(
37-
color: themeColor.secondaryContainer.withAlpha(150),
38-
boxShadow: [
39-
BoxShadow(
40-
color: themeColor.surfaceBright.withAlpha(150),
41-
offset: Offset(2, 4),
42-
blurRadius: 8.0,
43-
),
44-
],
45-
borderRadius: StaticsVar.br,
46-
),
47-
child: Column(
48-
children: [
49-
Row(
50-
crossAxisAlignment: CrossAxisAlignment.center,
51-
mainAxisAlignment: MainAxisAlignment.center,
52-
children: [
53-
Text('连胜天数', style: TextStyle(fontSize: 12.0)),
54-
context.read<Global>().settingData["learning"]["lastDate"] == DateTime.now().difference(DateTime(2025, 11, 1)).inDays
55-
? Icon(Icons.done, size: 15.0, color: Colors.tealAccent)
56-
: Icon(Icons.error_outline, size: 15.0, color: Colors.amber, semanticLabel: "今天还没学习~"),
57-
],
58-
),
59-
SizedBox(height: mediaQuery.size.height * 0.03),
60-
Text(getStrokeDays(context.read<Global>().settingData).toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
61-
],
62-
),
63-
),
64-
Container(
65-
width: mediaQuery.size.width * 0.50,
66-
height: mediaQuery.size.height * 0.18,
67-
margin: EdgeInsets.all(4.0),
68-
padding: EdgeInsets.all(16.0),
69-
decoration: BoxDecoration(
70-
color: themeColor.secondaryContainer.withAlpha(150),
71-
boxShadow: [
72-
BoxShadow(
73-
color: themeColor.surfaceBright.withAlpha(150),
74-
offset: Offset(2, 4),
75-
blurRadius: 8.0,
76-
),
77-
],
78-
borderRadius: BorderRadius.circular(25.0),
27+
Container(
28+
width: mediaQuery.size.width * 0.30,
29+
height: mediaQuery.size.height * 0.18,
30+
margin: EdgeInsets.all(4.0),
31+
padding: EdgeInsets.all(16.0),
32+
decoration: BoxDecoration(
33+
color: themeColor.secondaryContainer.withAlpha(150),
34+
boxShadow: [
35+
BoxShadow(
36+
color: themeColor.surfaceBright.withAlpha(150),
37+
offset: Offset(2, 4),
38+
blurRadius: 8.0,
7939
),
80-
child: Column(
40+
],
41+
borderRadius: StaticsVar.br,
42+
),
43+
child: Column(
44+
children: [
45+
Row(
46+
crossAxisAlignment: CrossAxisAlignment.center,
47+
mainAxisAlignment: MainAxisAlignment.center,
8148
children: [
82-
Text('已学词汇', style: TextStyle(fontSize: 12.0)),
83-
SizedBox(height: mediaQuery.size.height * 0.03),
84-
Text(context.read<Global>().settingData["learning"]["KnownWords"].length.toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
49+
Text('连胜天数', style: TextStyle(fontSize: 12.0)),
50+
context.read<Global>().settingData["learning"]["lastDate"] == DateTime.now().difference(DateTime(2025, 11, 1)).inDays
51+
? Icon(Icons.done, size: 15.0, color: Colors.tealAccent)
52+
: Icon(Icons.error_outline, size: 15.0, color: Colors.amber, semanticLabel: "今天还没学习~"),
8553
],
8654
),
87-
),
88-
],
55+
SizedBox(height: mediaQuery.size.height * 0.03),
56+
Text(getStrokeDays(context.read<Global>().settingData).toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
57+
],
58+
),
8959
),
90-
Row(
91-
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
92-
children: [
93-
Container(
94-
width: mediaQuery.size.width * 0.50,
95-
height: mediaQuery.size.height * 0.18,
96-
margin: EdgeInsets.all(4.0),
97-
padding: EdgeInsets.all(16.0),
98-
decoration: BoxDecoration(
99-
color: themeColor.secondaryContainer.withAlpha(150),
100-
boxShadow: [
101-
BoxShadow(
102-
color: themeColor.surfaceBright.withAlpha(150),
103-
offset: Offset(2, 4),
104-
blurRadius: 8.0,
105-
),
106-
],
107-
borderRadius: BorderRadius.circular(25.0),
108-
),
109-
child: Column(
110-
children: [
111-
Text('规律性学习', style: TextStyle(fontSize: 12.0)),
112-
SizedBox(height: mediaQuery.size.height * 0.03),
113-
asyncSnapshot.hasData ? Text(asyncSnapshot.data??false ? "${fsrs.getWillDueCount().toString()}个待复习" : "未启用", style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)) : CircularProgressIndicator(),
114-
],
60+
Container(
61+
width: mediaQuery.size.width * 0.50,
62+
height: mediaQuery.size.height * 0.18,
63+
margin: EdgeInsets.all(4.0),
64+
padding: EdgeInsets.all(16.0),
65+
decoration: BoxDecoration(
66+
color: themeColor.secondaryContainer.withAlpha(150),
67+
boxShadow: [
68+
BoxShadow(
69+
color: themeColor.surfaceBright.withAlpha(150),
70+
offset: Offset(2, 4),
71+
blurRadius: 8.0,
11572
),
116-
),
117-
Container(
118-
width: mediaQuery.size.width * 0.30,
119-
height: mediaQuery.size.height * 0.18,
120-
margin: EdgeInsets.all(4.0),
121-
padding: EdgeInsets.all(16.0),
122-
decoration: BoxDecoration(
123-
color: themeColor.secondaryContainer.withAlpha(150),
124-
boxShadow: [
125-
BoxShadow(
126-
color: themeColor.surfaceBright.withAlpha(150),
127-
offset: Offset(2, 4),
128-
blurRadius: 8.0,
129-
),
130-
],
131-
borderRadius: StaticsVar.br,
73+
],
74+
borderRadius: BorderRadius.circular(25.0),
75+
),
76+
child: Column(
77+
children: [
78+
Text('已学词汇', style: TextStyle(fontSize: 12.0)),
79+
SizedBox(height: mediaQuery.size.height * 0.03),
80+
Text(context.read<Global>().settingData["learning"]["KnownWords"].length.toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
81+
],
82+
),
83+
),
84+
],
85+
),
86+
Row(
87+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
88+
children: [
89+
Container(
90+
width: mediaQuery.size.width * 0.50,
91+
height: mediaQuery.size.height * 0.18,
92+
margin: EdgeInsets.all(4.0),
93+
padding: EdgeInsets.all(16.0),
94+
decoration: BoxDecoration(
95+
color: themeColor.secondaryContainer.withAlpha(150),
96+
boxShadow: [
97+
BoxShadow(
98+
color: themeColor.surfaceBright.withAlpha(150),
99+
offset: Offset(2, 4),
100+
blurRadius: 8.0,
132101
),
133-
child: Column(
134-
children: [
135-
Text('单词总数', style: TextStyle(fontSize: 12.0)),
136-
SizedBox(height: mediaQuery.size.height * 0.03),
137-
Text(context.read<Global>().wordCount.toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold))
138-
],
102+
],
103+
borderRadius: BorderRadius.circular(25.0),
104+
),
105+
child: Column(
106+
children: [
107+
Text('规律性学习', style: TextStyle(fontSize: 12.0)),
108+
SizedBox(height: mediaQuery.size.height * 0.03),
109+
Text(fsrs.isEnabled() ? "${fsrs.getWillDueCount().toString()}个待复习" : "未启用", style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
110+
],
111+
),
112+
),
113+
Container(
114+
width: mediaQuery.size.width * 0.30,
115+
height: mediaQuery.size.height * 0.18,
116+
margin: EdgeInsets.all(4.0),
117+
padding: EdgeInsets.all(16.0),
118+
decoration: BoxDecoration(
119+
color: themeColor.secondaryContainer.withAlpha(150),
120+
boxShadow: [
121+
BoxShadow(
122+
color: themeColor.surfaceBright.withAlpha(150),
123+
offset: Offset(2, 4),
124+
blurRadius: 8.0,
139125
),
140-
),
141-
]
142-
)
143-
],
144-
);
145-
}
126+
],
127+
borderRadius: StaticsVar.br,
128+
),
129+
child: Column(
130+
children: [
131+
Text('单词总数', style: TextStyle(fontSize: 12.0)),
132+
SizedBox(height: mediaQuery.size.height * 0.03),
133+
Text(context.read<Global>().wordCount.toString(), style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold))
134+
],
135+
),
136+
),
137+
]
138+
)
139+
],
146140
);
147141
}
148142
}

0 commit comments

Comments
 (0)