Skip to content

Commit 13b15f1

Browse files
Merge pull request #67 from JYinherit/main
feat: Bug Fixes & Performance Optimization
2 parents 6ff51a7 + a6d6ae8 commit 13b15f1

6 files changed

Lines changed: 138 additions & 26 deletions

File tree

lib/funcs/fsrs_func.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class FSRS {
7272
}
7373

7474
int getLeastDueCard() {
75+
if (config.cards.isEmpty) return -1;
7576
int leastDueIndex = 0;
7677
for(int i = 1; i < config.cards.length; i++) {
7778
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 {
8889

8990
void addWordCard(int wordId) {
9091
logger.fine("添加复习卡片: Id: $wordId");
92+
if (config.cards.isEmpty) {
93+
config = config.copyWith(cards: [], reviewLogs: []);
94+
}
9195
// os the wordID == cardID
9296
config.cards.add(Card(cardId: wordId, state: State.learning));
9397
config.reviewLogs.add(ReviewLog(cardId: wordId, rating: Rating.good, reviewDateTime: DateTime.now()));

lib/funcs/utili.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,39 @@ extension StringExtensions on String {
188188
res = res.replaceAll(RegExp(r'[،.][^]*$'), ""); // for "متواصل، متواصل"
189189
return res;
190190
}
191+
192+
/// 简单的中文释义交叉计算(字符 Jaccard 相似度)
193+
bool hasSimilarMeaning(String other) {
194+
// 1. 去除中文/英文常见标点符号和空格
195+
String cleanString(String s) {
196+
return s.replaceAll(RegExp(r'[ \(\)\.,/,。、;()\[\]【】]'), '');
197+
}
198+
199+
String c1 = cleanString(this);
200+
String c2 = cleanString(other);
201+
202+
if (c1.isEmpty || c2.isEmpty) return false;
203+
204+
// 如果一个释义完全包含了另一个,直接判定为相似(如:苹果 和 苹果,香蕉)
205+
if(c1.contains(c2) || c2.contains(c1)) return true;
206+
207+
// 2. 将字串拆分为单字集合
208+
Set<String> set1 = c1.split('').toSet();
209+
Set<String> set2 = c2.split('').toSet();
210+
211+
// 3. 计算共有字符
212+
int intersection = set1.intersection(set2).length;
213+
// int union = set1.union(set2).length;
214+
215+
// 如果短词里包含任何相同的核心字,或共有汉字超过短词的 40% (应对同义替换)
216+
int minLength = min(set1.length, set2.length);
217+
218+
// 如果它们很短,只要共享一个字就算(例如:走 / 行走)
219+
if(minLength <= 2 && intersection >= 1) return true;
220+
221+
double similarity = intersection / minLength;
222+
return similarity >= 0.4;
223+
}
191224
}
192225

193226
extension ListExtensions on List {

lib/pages/learning_page.dart

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,13 @@ class LearningPage extends StatelessWidget {
6969
),
7070
),
7171
onPressed: (){
72-
if(context.read<Global>().globalFSRS.getWillDueCount() != 0) {
73-
context.read<Global>().uiLogger.info("跳转: LearningPage => ForeFSRSSettingPage");
74-
Navigator.push(
75-
context,
76-
MaterialPageRoute(
77-
builder: (context) => ForeFSRSSettingPage()
78-
)
79-
);
80-
} else {
81-
ScaffoldMessenger.of(context).showSnackBar(
82-
SnackBar(content: Text("目前没有要复习的单词"), duration: Duration(seconds: 1),),
83-
);
84-
}
72+
context.read<Global>().uiLogger.info("跳转: LearningPage => ForeFSRSSettingPage");
73+
Navigator.push(
74+
context,
75+
MaterialPageRoute(
76+
builder: (context) => ForeFSRSSettingPage()
77+
)
78+
);
8579
},
8680
child: FittedBox(
8781
fit: BoxFit.contain,
@@ -103,15 +97,23 @@ class LearningPage extends StatelessWidget {
10397
shape: RoundedRectangleBorder(borderRadius: StaticsVar.br),
10498
),
10599
onPressed: (){
100+
if(context.read<Global>().wordData.words.isEmpty) {
101+
ScaffoldMessenger.of(context).showSnackBar(
102+
SnackBar(content: Text("词库为空,无法推送!请先导入词库"), duration: Duration(seconds: 1),),
103+
);
104+
return;
105+
}
106106
final DateTime now = DateTime.now();
107107
final int seed = now.year * 10000 + now.month * 100 + now.day;
108-
final List<WordItem> pushWords = [];
108+
final Set<WordItem> pushWords = {};
109109
final Random rnd = Random(seed);
110-
for(int i = 0; i < context.read<Global>().globalFSRS.config.pushAmount; i++){
110+
int tries = 0;
111+
while(pushWords.length < context.read<Global>().globalFSRS.config.pushAmount && tries < context.read<Global>().globalFSRS.config.pushAmount * 10){
111112
int chosen = rnd.nextInt(context.read<Global>().wordData.words.length);
112113
if(!context.read<Global>().globalFSRS.isContained(chosen)) {
113114
pushWords.add(context.read<Global>().wordData.words.elementAt(chosen));
114115
}
116+
tries++;
115117
}
116118
if(pushWords.isEmpty) {
117119
ScaffoldMessenger.of(context).showSnackBar(
@@ -123,7 +125,7 @@ class LearningPage extends StatelessWidget {
123125
Navigator.push(
124126
context,
125127
MaterialPageRoute(
126-
builder: (context) => FSRSLearningPage(fsrs: context.read<Global>().globalFSRS, words: pushWords)
128+
builder: (context) => FSRSLearningPage(fsrs: context.read<Global>().globalFSRS, words: pushWords.toList())
127129
)
128130
);
129131
},

lib/sub_pages_builder/learning_pages/fsrs_pages.dart

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,38 @@ class ForeFSRSSettingPage extends StatelessWidget {
240240
},
241241
icon: Icon(Icons.done),
242242
label: Text("确认"),
243+
),
244+
if (fsrs.config.enabled) Padding(
245+
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
246+
child: ElevatedButton.icon(
247+
style: ElevatedButton.styleFrom(
248+
backgroundColor: Theme.of(context).colorScheme.errorContainer,
249+
fixedSize: Size.fromHeight(80),
250+
shape: RoundedRectangleBorder(borderRadius: StaticsVar.br)
251+
),
252+
onPressed: (){
253+
showDialog(
254+
context: context,
255+
builder: (context) => AlertDialog(
256+
title: Text("重置并停用 FSRS?"),
257+
content: Text("这将永久清除所有规律学习进度及配置,且不可恢复!"),
258+
actions: [
259+
TextButton(onPressed: () => Navigator.pop(context), child: Text("取消")),
260+
TextButton(
261+
onPressed: () {
262+
fsrs.config = const FSRSConfig(enabled: false);
263+
fsrs.save();
264+
Navigator.popUntil(context, (route) => route.isFirst);
265+
},
266+
child: Text("确认清空", style: TextStyle(color: Colors.red))
267+
)
268+
],
269+
)
270+
);
271+
},
272+
icon: Icon(Icons.delete_forever, color: Theme.of(context).colorScheme.error),
273+
label: Text("重置并停用", style: TextStyle(color: Theme.of(context).colorScheme.error)),
274+
),
243275
)
244276
]
245277
);
@@ -292,10 +324,8 @@ class MainFSRSPage extends StatelessWidget {
292324
}
293325
final wordID = fsrs.getLeastDueCard();
294326
if(wordID == -1) {
295-
Future.delayed(
296-
Duration(seconds: 1), (){if(context.mounted) alart(context, "今日复习任务已完成", onConfirmed: () {Navigator.pop(context);});});
297327
return Center(
298-
child: TextContainer(text: "今日复习任务已完成"),
328+
child: TextContainer(text: "今日复习任务已完成\n(或当前无复习内容)\n点击右上角齿轮可修改配置"),
299329
);
300330
}
301331
return FSRSReviewCardPage(wordID: wordID, fsrs: fsrs, rnd: sharedRnd, controller: controller,);

lib/sub_pages_builder/setting_pages/questions_setting_page.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ class _QuestionsSettingPage extends State<QuestionsSettingPage> {
1616
bool floatButtonFlod = true;
1717
static const Map<int, String> castMap = {0: "单词卡片学习", 1: "中译阿 选择题", 2: "阿译中 选择题", 3: "中译阿 拼写题", 4: "听力题"};
1818

19+
void _updateConfig() {
20+
context.read<Global>().globalConfig = context.read<Global>().globalConfig.copyWith(
21+
quiz: context.read<Global>().globalConfig.quiz.copyWith(
22+
zhar: context.read<Global>().globalConfig.quiz.zhar.copyWith(
23+
questionSections: selectedTypes!.cast<int>()
24+
)
25+
)
26+
);
27+
}
1928

2029
@override
2130
Widget build(BuildContext context) {
2231
context.read<Global>().uiLogger.info("构建 QuestionsSettingPage:$selectedTypes");
2332
late final SubQuizConfig section;
2433
section = context.read<Global>().globalConfig.quiz.zhar;
2534
MediaQueryData mediaQuery = MediaQuery.of(context);
26-
selectedTypes ??= section.questionSections;
35+
selectedTypes ??= List.from(section.questionSections);
2736
List<Widget> listTiles = [];
2837
bool isEven = true;
2938
for(int index = 0; index < selectedTypes!.length; index++) {
@@ -48,6 +57,7 @@ class _QuestionsSettingPage extends State<QuestionsSettingPage> {
4857
context.read<Global>().uiLogger.fine("移除题型项目: $index");
4958
setState(() {
5059
selectedTypes!.removeAt(index.toInt());
60+
_updateConfig();
5161
});
5262
}
5363
},
@@ -80,6 +90,7 @@ class _QuestionsSettingPage extends State<QuestionsSettingPage> {
8090
if(oldIndex < newIndex) newIndex--; // 修正索引
8191
int old = selectedTypes!.removeAt(oldIndex);
8292
selectedTypes!.insert(newIndex, old);
93+
_updateConfig();
8394
});
8495
},
8596
children: listTiles,
@@ -194,6 +205,7 @@ class _QuestionsSettingPage extends State<QuestionsSettingPage> {
194205
context.read<Global>().uiLogger.info("添加题型类型: $i");
195206
setState(() {
196207
selectedTypes!.add(i);
208+
_updateConfig();
197209
});
198210
},
199211
icon: Icon(Icons.add),

lib/vars/global.dart

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Global with ChangeNotifier {
5050
await prefs.setString("wordData", jsonEncode({"Words": [], "Classes": {}}));
5151
wordData = DictData(words: [], classes: []);
5252
logger.info("首次启动: 配置表初始化完成");
53+
globalFSRS = FSRS()..init(outerPrefs: prefs);
5354
await postInit();
5455
} else {
5556
await conveySetting();
@@ -230,11 +231,20 @@ class Global with ChangeNotifier {
230231
// }
231232
DictData dataFormater(Map<String, dynamic> data, DictData exData, String sourceName) {
232233
logger.info("开始词汇格式化");
233-
List<String> wordList = [];
234-
for(WordItem x in exData.words) {
235-
wordList.add(x.arabic);
234+
235+
// Use Maps for O(1) lookup speed instead of O(N) List.indexOf
236+
Map<String, int> rawWordMap = {};
237+
Map<String, int> pureWordMap = {};
238+
List<String> chineseList = [];
239+
240+
for(int i = 0; i < exData.words.length; i++) {
241+
WordItem x = exData.words[i];
242+
rawWordMap[x.arabic] = i;
243+
pureWordMap[x.arabic.removeAracicExtensionPart().trim()] = i;
244+
chineseList.add(x.chinese); // Keep list for indexing since it maps 1:1 with word id
236245
}
237-
int counter = wordList.length;
246+
247+
int counter = exData.words.length;
238248

239249
SourceItem? exSource;
240250
// 查找已有数据中是否有同名的源数据组
@@ -249,9 +259,28 @@ class Global with ChangeNotifier {
249259
for(var className in data.keys){
250260
ClassItem exClass = ClassItem(className: className, wordIndexs: []);
251261
for(var word in data[className]){
252-
if(wordList.contains(word["arabic"])){
262+
String newRaw = word["arabic"];
263+
String newPure = newRaw.removeAracicExtensionPart().trim();
264+
int existingIndex = -1;
265+
266+
if (rawWordMap.containsKey(newRaw)) {
267+
existingIndex = rawWordMap[newRaw]!;
268+
} else if (pureWordMap.containsKey(newPure)) {
269+
int potentialIndex = pureWordMap[newPure]!;
270+
// Pure arabic is the same, but different vowels. Are they the same meaning?
271+
if (chineseList[potentialIndex].hasSimilarMeaning(word["chinese"])) {
272+
existingIndex = potentialIndex;
273+
}
274+
}
275+
276+
if (existingIndex != -1) {
277+
// If it already exists globally, just add it to this class
278+
if(!exClass.wordIndexs.contains(existingIndex)) {
279+
exClass.wordIndexs.add(existingIndex);
280+
}
253281
continue;
254282
}
283+
255284
exClass.wordIndexs.add(counter);
256285
exData.words.add(
257286
WordItem(
@@ -262,7 +291,9 @@ class Global with ChangeNotifier {
262291
id: counter
263292
)
264293
);
265-
wordList.add(word["arabic"]);
294+
rawWordMap[newRaw] = counter;
295+
pureWordMap[newPure] = counter;
296+
chineseList.add(word["chinese"]);
266297
counter ++;
267298
}
268299
exSource.subClasses.add(exClass);

0 commit comments

Comments
 (0)