Date: Sun, 30 Mar 2025 22:37:46 +0800
Subject: [PATCH 10/22] avoid undefined
---
src/popup/util/fsrs.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/popup/util/fsrs.js b/src/popup/util/fsrs.js
index b58d87f..8da0da4 100644
--- a/src/popup/util/fsrs.js
+++ b/src/popup/util/fsrs.js
@@ -94,7 +94,7 @@ export const updateProblemWithFSRS = (problem, feedback) => {
card_id: problem.index, // 使用问题索引作为卡片ID
review_time: now, // 复习时间(毫秒时间戳)
review_rating: qualityToRating(feedback.quality), // 复习评分 (1-4)
- review_state: TypeConvert.state(problem.fsrsState ? problem.fsrsState?.state : 'New') // 复习状态 (0-3)
+ review_state: TypeConvert.state(problem.fsrsState ? problem.fsrsState?.state ?? State.New : 'New') // 复习状态 (0-3)
};
// 将复习日志存储到单独的 localStorage 键中
From d92d70b6dc809492f034aa94afd8e977a882df26 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Tue, 1 Apr 2025 00:53:36 +0800
Subject: [PATCH 11/22] Fix: incorrect state judgment && error lastreview time
---
src/popup/util/fsrs.js | 51 +++++++++++++++++++++++++++++++-----------
1 file changed, 38 insertions(+), 13 deletions(-)
diff --git a/src/popup/util/fsrs.js b/src/popup/util/fsrs.js
index 8da0da4..4bc57e8 100644
--- a/src/popup/util/fsrs.js
+++ b/src/popup/util/fsrs.js
@@ -26,9 +26,21 @@ const qualityToRating = (quality) => {
export const calculateNextReview = (problem, feedback) => {
try {
const now = new Date();
- const lastReview = problem.fsrsState && problem.fsrsState.lastReview
- ? new Date(problem.fsrsState.lastReview)
- : new Date(problem.submissionTime || now.getTime());
+
+ // 确保有一个有效的 lastReview 日期
+ let lastReview;
+ if (problem.fsrsState && problem.fsrsState.lastReview) {
+ lastReview = new Date(problem.fsrsState.lastReview);
+ } else if (problem.submissionTime) {
+ lastReview = new Date(problem.submissionTime);
+ } else {
+ lastReview = new Date(now.getTime()); // 默认为昨天
+ }
+
+ // 检查日期是否有效
+ if (isNaN(lastReview.getTime())) {
+ lastReview = new Date(now.getTime()); // 如果无效,使用昨天
+ }
// 如果没有 fsrsState,创建一个默认的
if (!problem.fsrsState) {
@@ -38,38 +50,51 @@ export const calculateNextReview = (problem, feedback) => {
stability: card.stability,
difficulty: card.difficulty,
state: card.state,
- reps: card.reps,
- lapses: card.lapses
+ reviewCount: card.reps,
+ lapses: card.lapses,
+ lastReview: +lastReview // 存储为时间戳
}
});
}
- let card = problem.fsrsState
+ let card = problem.fsrsState;
+
+
+
+ // 确保 nextReview 有效
+ if (!card.nextReview || isNaN(card.nextReview)) {
+ card.nextReview = +lastReview; // 默认为一天后
+ }
const rating = qualityToRating(feedback.quality);
+
+ // 确保所有参数都有有效值
+ const scheduledDays = Math.max(0, Math.floor((card.nextReview - card.lastReview) / (1000 * 60 * 60 * 24)));
+ const elapsedDays = Math.max(0, (now.getTime() - lastReview.getTime()) / (1000 * 60 * 60 * 24));
+
const result = fsrs.next({
due: card.nextReview,
stability: card.stability,
difficulty: card.difficulty,
- elapsed_days: (now.getTime() - lastReview.getTime()) / (1000 * 60 * 60 * 24),
- scheduled_days: Math.floor((card.nextReview - card.lastReview) / (1000 * 60 * 60 * 24)),
+ elapsed_days: elapsedDays,
+ scheduled_days: scheduledDays,
reps: card.reviewCount,
lapse_count: card.lapses,
state: card.state,
- last_review: card.lastReview || undefined,
+ last_review: lastReview, // 使用已经转换好的 Date 对象
}, now, rating);
-
return {
/**长期调度模式,ivl一定大于1d */
nextReview: +result.card.due,
stability: result.card.stability,
difficulty: result.card.difficulty,
state: result.card.state,
- reps: result.card.reps,
+ reviewCount: result.card.reps,
lapses: result.card.lapses
};
} catch (error) {
console.error('Error in calculateNextReview:', error);
+ const now = new Date(); // 在 catch 块中定义 now 变量
return {
nextReview: now.getTime() + (24 * 60 * 60 * 1000),
stability: problem.fsrsState.stability || S_MIN,
@@ -77,7 +102,7 @@ export const calculateNextReview = (problem, feedback) => {
difficulty: problem.fsrsState.difficulty || params.w[4],
/** 长期调度下状态一定是New或Review */
state: problem.fsrsState.state || State.Review,
- reps: (problem.fsrsState.reviewCount || 0) + 1,
+ reviewCount: (problem.fsrsState.reviewCount || 0) + 1,
lapses: problem.fsrsState.lapses || 0
};
}
@@ -106,7 +131,7 @@ export const updateProblemWithFSRS = (problem, feedback) => {
difficulty: fsrsResult.difficulty,
stability: fsrsResult.stability,
state: fsrsResult.state,
- lastReview: fsrsResult.lastReview,
+ lastReview: now,
nextReview: fsrsResult.nextReview,
reviewCount: fsrsResult.reps,
lapses: fsrsResult.lapses,
From 3aed63df9beedc1bbf07d4fcbd3d91b175a612e0 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Tue, 1 Apr 2025 01:04:39 +0800
Subject: [PATCH 12/22] Doc: update title
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 3d33a9a..dba2aae 100644
--- a/README.md
+++ b/README.md
@@ -21,9 +21,9 @@
- Smart Prioritization, Flexible Review, Code Smarter!
+ Train Memory Curves, Smart Prioritization, Flexible Review, Code Smarter!
- 智能评估优先级,灵活复习,更聪明地刷题!
+ 训练记忆曲线,智能评估优先级,灵活复习,更聪明地刷题!

From afce976dabcd28382b5462fae75f2ae1f7d5f5a8 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Tue, 1 Apr 2025 02:16:16 +0800
Subject: [PATCH 13/22] Release: v0.1.2
---
changelog.md | 17 +++++++++++++++++
manifest.base.json | 2 +-
package-lock.json | 2 +-
package.json | 2 +-
4 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/changelog.md b/changelog.md
index b8d59a9..0433e5d 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,23 @@
# Changelog
# Changelog
+## [0.1.2] - 2025-04-01
+### Fixed
+- incorrect state judgment && error lastreview time (#27)
+ 错误的状态判断以及错误的上次复习时间 (#27)
+
+
+
+
+
+
+
+
+
+
+
+
+
## [0.1.1] - 2025-03-24
### Fixed
- Fix the issue where the same question could be rated multiple times within one day on the web version (#XX)
diff --git a/manifest.base.json b/manifest.base.json
index 5a9797e..f1f1696 100644
--- a/manifest.base.json
+++ b/manifest.base.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Leetcode Mastery Scheduler",
- "version": "0.1.1",
+ "version": "0.1.2",
"author": "Hacode",
"description": "Leetcode-Mastery-Scheduler tracks your LeetCode progress and prompt you to review based FSRS",
"homepage_url": "https://github.com/xiaohajiayou/Leetcode-Mastery-Scheduler",
diff --git a/package-lock.json b/package-lock.json
index 13b7cf7..3e582d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "Leetcode-Mastery-Scheduler",
- "version": "0.1.1",
+ "version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
diff --git a/package.json b/package.json
index 2f0ebf3..1593256 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Leetcode-Mastery-Scheduler",
- "version": "0.1.1",
+ "version": "0.1.2",
"description": "\r \r \r P ractice M akes C ode A ccepted\r \r ",
"main": "src/popup.js",
"directories": {
From 9e351163494ea998e395d9cd38a828f134ac68b6 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Wed, 2 Apr 2025 01:20:09 +0800
Subject: [PATCH 14/22] Feat: basic support write notes and export markdown
file
---
popup.html | 17 ++
src/popup/handler/handlerRegister.js | 2 +
src/popup/handler/noteHandler.js | 315 +++++++++++++++++++++++++++
src/popup/handler/pageJumpHandler.js | 15 +-
src/popup/view/view.js | 183 ++++++++++++++--
5 files changed, 512 insertions(+), 20 deletions(-)
create mode 100644 src/popup/handler/noteHandler.js
diff --git a/popup.html b/popup.html
index 9fda98e..2128e10 100644
--- a/popup.html
+++ b/popup.html
@@ -304,6 +304,23 @@ Add Review Card
>
+
+
+
+
+
+
+
diff --git a/src/popup/handler/handlerRegister.js b/src/popup/handler/handlerRegister.js
index 0ca0616..dbd1ab0 100644
--- a/src/popup/handler/handlerRegister.js
+++ b/src/popup/handler/handlerRegister.js
@@ -3,6 +3,7 @@ import { setModeSwitchHandlers } from "./modeSwitchHandler";
import { setPageJumpHandlers } from "./pageJumpHandler"
import { setPopupUnloadHandler } from "./popupUnloadHandler";
import { setRecordOperationHandlers } from "./recordOperationHandler";
+import { setNoteHandlers } from "./noteHandler";
export const registerAllHandlers = () => {
setPageJumpHandlers();
@@ -10,4 +11,5 @@ export const registerAllHandlers = () => {
setRecordOperationHandlers();
setConfigJumpHandlers();
setPopupUnloadHandler();
+ setNoteHandlers();
}
\ No newline at end of file
diff --git a/src/popup/handler/noteHandler.js b/src/popup/handler/noteHandler.js
new file mode 100644
index 0000000..73e3d94
--- /dev/null
+++ b/src/popup/handler/noteHandler.js
@@ -0,0 +1,315 @@
+import { getLocalStorageData, setLocalStorageData } from "../delegate/localStorageDelegate";
+import { getAllProblems } from "../service/problemService";
+import { renderScheduledTableContent } from "../view/view";
+import { store } from "../store";
+
+// 获取所有笔记
+const getAllNotes = async () => {
+ try {
+ const notes = await getLocalStorageData("notes");
+ return notes || {};
+ } catch (e) {
+ console.error("获取笔记数据失败", e);
+ return {}; // 返回空对象而不是抛出错误
+ }
+};
+
+// 同步笔记到存储
+const syncNotes = async (notes) => {
+ if (!notes) {
+ notes = await getAllNotes();
+ }
+ await setLocalStorageData("notes", notes);
+ return notes;
+};
+
+// 注册笔记相关事件处理
+export const setNoteHandlers = () => {
+ console.log("注册笔记处理程序");
+
+ // 使用事件委托来处理笔记按钮点击
+ document.removeEventListener('click', handleNoteButtonClick); // 先移除之前的监听器,避免重复
+ document.addEventListener('click', handleNoteButtonClick);
+
+ // 注册保存笔记按钮事件
+ const saveNoteBtn = document.getElementById('saveNoteBtn');
+ if (saveNoteBtn) {
+ console.log("找到保存按钮");
+ saveNoteBtn.addEventListener('click', saveNote);
+ } else {
+ console.error("找不到保存按钮");
+ }
+
+ // 注册取消按钮事件
+ const cancelBtns = document.querySelectorAll('[data-bs-dismiss="modal"]');
+ if (cancelBtns.length > 0) {
+ console.log("找到取消按钮");
+ cancelBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ // 关闭模态框
+ const noteModal = document.getElementById('noteModal');
+ if (noteModal) {
+ noteModal.style.display = 'none';
+ noteModal.classList.remove('show');
+ }
+ });
+ });
+ } else {
+ console.error("找不到取消按钮");
+ }
+
+ // 注册导出笔记按钮事件
+ const exportNotesBtn = document.getElementById('exportNotesBtn');
+ if (exportNotesBtn) {
+ console.log("找到导出按钮");
+ exportNotesBtn.addEventListener('click', exportAllNotes);
+ } else {
+ console.error("找不到导出按钮");
+ }
+
+ // 初始化工具提示
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
+ tooltipTriggerList.map(function (tooltipTriggerEl) {
+ return new bootstrap.Tooltip(tooltipTriggerEl);
+ });
+}
+
+// 单独定义处理函数,便于移除
+const handleNoteButtonClick = (e) => {
+ const noteButton = e.target.closest('.note-btn-mark');
+ if (noteButton) {
+ console.log("点击了笔记按钮", noteButton);
+ console.log("按钮元素:", noteButton);
+ console.log("data-id属性:", noteButton.getAttribute('data-id'));
+
+ const problemIndex = noteButton.getAttribute('data-id');
+ if (problemIndex) {
+ openNoteModal(problemIndex);
+ } else {
+ console.error("笔记按钮没有 data-id 属性");
+ }
+ }
+};
+
+// 打开笔记模态框
+const openNoteModal = async (problemIndex) => {
+ try {
+ console.log("打开笔记模态框,问题索引:", problemIndex);
+
+ // 使用 getAllProblems 获取问题数据
+ const problems = await getAllProblems();
+ const problem = problems[problemIndex];
+
+ // 如果没有找到问题数据
+ if (!problem) {
+ console.error("找不到问题数据:", problemIndex);
+ return;
+ }
+
+ // 获取笔记数据
+ const notes = await getAllNotes();
+ const noteData = notes[problemIndex];
+
+ console.log("问题数据:", problem);
+
+ // 使用自定义方式打开模态框
+ const noteModal = document.getElementById('noteModal');
+ if (!noteModal) {
+ console.error("找不到模态框元素");
+ return;
+ }
+
+ // 显示模态框
+ noteModal.style.display = 'block';
+ noteModal.classList.add('show');
+
+ // 设置问题索引到隐藏字段
+ const problemIndexInput = document.getElementById('problemIndex');
+ if (problemIndexInput) {
+ problemIndexInput.value = problemIndex;
+ } else {
+ console.error("找不到问题索引输入框");
+ }
+
+ // 设置问题名称 - 使用 innerHTML 直接设置
+ const problemNameContainer = document.querySelector('.modal-body .mb-3:first-of-type');
+ if (problemNameContainer) {
+ // 如果有自定义名称,优先使用自定义名称
+ const customName = noteData && typeof noteData === 'object' ? noteData.customName : undefined;
+ const problemName = customName || problem.name || "未知问题";
+
+ problemNameContainer.innerHTML = `
+ 问题名称 (Problem Name)
+
+ `;
+ console.log("重新创建了问题名称输入框,值为:", problemName);
+ } else {
+ console.error("找不到问题名称容器");
+ }
+
+ // 设置笔记内容
+ const noteContentTextarea = document.getElementById('noteContent');
+ if (noteContentTextarea) {
+ noteContentTextarea.value = noteData ? (typeof noteData === 'object' ? noteData.content : noteData) : '';
+ } else {
+ console.error("找不到笔记内容文本框");
+ }
+
+ // 设置焦点到文本区域
+ setTimeout(() => {
+ if (document.getElementById('noteContent')) {
+ document.getElementById('noteContent').focus();
+ }
+ }, 100);
+ } catch (e) {
+ console.error("打开笔记模态框失败", e);
+ alert("打开笔记失败,请查看控制台获取详细错误信息");
+ }
+}
+
+// 保存笔记
+const saveNote = async () => {
+ try {
+ const problemIndex = document.getElementById('problemIndex').value;
+ const problemNameInput = document.getElementById('noteProblemName');
+ const noteContent = document.getElementById('noteContent').value;
+
+ // 获取用户输入的问题名称,如果输入框为空则使用占位符
+ let problemName = "";
+ if (problemNameInput) {
+ problemName = problemNameInput.value.trim() || problemNameInput.getAttribute('placeholder') || "";
+ }
+
+ console.log("保存笔记,问题索引:", problemIndex);
+ console.log("保存笔记,问题名称:", problemName);
+
+ const notes = await getAllNotes();
+
+ // 使用 getAllProblems 获取问题数据
+ const problems = await getAllProblems();
+ const problem = problems[problemIndex];
+
+ if (!problem) {
+ console.error("找不到问题数据:", problemIndex);
+ return;
+ }
+
+ console.log("原问题名称:", problem.name);
+
+ // 如果笔记为空,则删除该条目
+ if (noteContent.trim() === '') {
+ delete notes[problemIndex];
+ } else {
+ // 保存笔记内容和用户输入的问题名称
+ notes[problemIndex] = {
+ content: noteContent,
+ customName: problemName !== problem.name ? problemName : undefined
+ };
+ }
+
+ // 保存到本地存储
+ await syncNotes(notes);
+
+ // 清除焦点
+ document.activeElement?.blur();
+
+ // 关闭模态框
+ const noteModal = document.getElementById('noteModal');
+ noteModal.style.display = 'none';
+ noteModal.classList.remove('show');
+
+ // 获取最新的问题数据
+ const allProblems = await getAllProblems();
+
+ // 先销毁所有现有的工具提示
+ const existingTooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
+ existingTooltips.forEach(el => {
+ const tooltip = bootstrap.Tooltip.getInstance(el);
+ if (tooltip) {
+ tooltip.dispose();
+ }
+ });
+
+ // 刷新表格以更新笔记图标和问题名称
+ await renderScheduledTableContent(store.reviewScheduledProblems, store.scheduledPage);
+
+ // 重新初始化工具提示
+ setTimeout(() => {
+ // 确保先销毁所有可能存在的工具提示实例
+ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
+ tooltipTriggerList.forEach(el => {
+ // 创建新的工具提示实例
+ new bootstrap.Tooltip(el, {
+ trigger: 'hover', // 只在悬停时显示
+ container: 'body', // 将工具提示附加到 body
+ boundary: 'window' // 确保工具提示不会超出窗口边界
+ });
+ });
+
+ // 重新注册事件监听器
+ setNoteHandlers();
+ }, 200); // 增加延迟时间确保 DOM 完全更新
+
+ console.log("笔记已保存");
+ } catch (e) {
+ console.error("保存笔记失败", e);
+ alert("保存笔记失败,请查看控制台获取详细错误信息");
+ }
+}
+
+// 导出所有笔记
+const exportAllNotes = async () => {
+ try {
+ // 使用 getAllProblems 获取问题数据
+ const problems = await getAllProblems();
+ const notes = await getAllNotes();
+ let notesContent = "# LeetCode Mastery Scheduler notes\n\n";
+ notesContent += "开源仓库链接/repo url: https://github.com/xiaohajiayou/Leetcode-Mastery-Scheduler" + "\n\n";
+
+ // 筛选有笔记的问题
+ const problemIndicesWithNotes = Object.keys(notes).filter(index =>
+ problems[index] && !problems[index].isDeleted &&
+ (typeof notes[index] === 'string' ? notes[index].trim().length > 0 :
+ (notes[index].content && notes[index].content.trim().length > 0))
+ );
+
+ if (problemIndicesWithNotes.length === 0) {
+ alert("没有找到任何笔记!");
+ return;
+ }
+
+ // 按问题名称排序
+ problemIndicesWithNotes.sort((a, b) =>
+ (problems[a].name || "").localeCompare(problems[b].name || "")
+ );
+
+ // 生成markdown格式的笔记内容
+ problemIndicesWithNotes.forEach(index => {
+ const problem = problems[index];
+ const noteData = notes[index];
+ const noteContent = typeof noteData === 'string' ? noteData : noteData.content;
+ const problemName = (typeof noteData === 'object' && noteData.customName) || problem.name || "未命名问题";
+
+ notesContent += `## ${problemName}\n\n`;
+ notesContent += `- 难度: ${problem.level || '未知'}\n`;
+ notesContent += `- 链接: ${problem.url || '#'}\n\n`;
+ notesContent += `### 笔记\n\n${noteContent}\n\n---\n\n`;
+ });
+
+ // 创建下载链接
+ const blob = new Blob([notesContent], { type: 'text/markdown' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `leetcode_notes_${new Date().toISOString().slice(0, 10)}.md`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ console.log("笔记已导出");
+ } catch (e) {
+ console.error("导出笔记失败", e);
+ alert("导出笔记失败,请查看控制台获取详细错误信息");
+ }
+}
\ No newline at end of file
diff --git a/src/popup/handler/pageJumpHandler.js b/src/popup/handler/pageJumpHandler.js
index dd10b77..d01f82a 100644
--- a/src/popup/handler/pageJumpHandler.js
+++ b/src/popup/handler/pageJumpHandler.js
@@ -2,33 +2,40 @@ import { input0DOM, input1DOM, input2DOM, nextButton0DOM, nextButton1DOM, nextBu
import { renderCompletedTableContent, renderReviewTableContent, renderScheduledTableContent } from "../view/view";
import { store } from "../store";
import { setRecordOperationHandlers } from "./recordOperationHandler";
+import { setNoteHandlers } from "./noteHandler";
const goToPrevReviewPage = () => {
renderReviewTableContent(store.needReviewProblems, store.toReviewPage - 1);
setRecordOperationHandlers();
+ setNoteHandlers();
}
const goToNextReviewPage = () => {
renderReviewTableContent(store.needReviewProblems, store.toReviewPage + 1);
setRecordOperationHandlers();
+ setNoteHandlers();
}
-const goToPrevSchedulePage = () => {
- renderScheduledTableContent(store.reviewScheduledProblems, store.scheduledPage - 1);
+const goToPrevSchedulePage = async () => {
+ await renderScheduledTableContent(store.reviewScheduledProblems, store.scheduledPage - 1);
setRecordOperationHandlers();
+ setNoteHandlers();
}
-const goToNextSchedulePage = () => {
- renderScheduledTableContent(store.reviewScheduledProblems, store.scheduledPage + 1);
+const goToNextSchedulePage = async () => {
+ await renderScheduledTableContent(store.reviewScheduledProblems, store.scheduledPage + 1);
setRecordOperationHandlers();
+ setNoteHandlers();
}
const goToPrevCompletedPage = () => {
renderCompletedTableContent(store.completedProblems, store.completedPage - 1);
setRecordOperationHandlers();
+ setNoteHandlers();
}
const goToNextCompletedPage = () => {
renderCompletedTableContent(store.completedProblems, store.completedPage + 1);
setRecordOperationHandlers();
+ setNoteHandlers();
}
const jumpToReviewPage = (event) => {
diff --git a/src/popup/view/view.js b/src/popup/view/view.js
index 85f13f8..2fb8a58 100644
--- a/src/popup/view/view.js
+++ b/src/popup/view/view.js
@@ -7,21 +7,22 @@ import { getCurrentRetrievability,calculatePageNum, getLevelColor, getDelayedHou
import { registerAllHandlers } from "../handler/handlerRegister";
import { hasOperationHistory } from "../service/operationHistoryService";
import { loadConfigs } from "../service/configService";
+import { getLocalStorageData, setLocalStorageData } from "../../popup/delegate/localStorageDelegate";
/*
Tag for problem records
*/
const getProblemUrlCell = (problem, width) => {
const levelColor = getLevelColor(problem.level);
- return `
+ return ` \
- ${problem.name}
-
+ style="text-decoration: none; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">\
+ ${problem.name} \
+ \
`;
};
@@ -44,7 +45,7 @@ const getRetrievabilityCell = (problem) => {
}
return `\
- \
+ \
`
`;
+const getNoteButtonTag = (problem, notes) => {
+ const hasNote = notes[problem.index] && notes[problem.index].content.trim().length > 0;
+ return `
`;
+}
+
const createReviewProblemRecord = (problem) => {
const htmlTag =
`\
@@ -85,16 +93,26 @@ const createReviewProblemRecord = (problem) => {
;
}
-const createScheduleProblemRecord = (problem) => {
+const createScheduleProblemRecord = async (problem) => {
const nextReviewDate = getNextReviewTime(problem);
+
+ // 获取笔记数据
+ let notes = {};
+ try {
+ notes = await getLocalStorageData("notes") || {};
+ } catch (e) {
+ console.error("获取笔记数据失败", e);
+ }
+
const htmlTag =
`\
\
- ${getProblemUrlCell(problem)}\
- ${formatDateTime(nextReviewDate)} \
+ ${getProblemUrlCell(problem, 45)}\
+ ${formatDateTime(nextReviewDate)} \
${getRetrievabilityCell(problem)}\
- \
+ \
${getDeleteButtonTag(problem)}\
+ ${getNoteButtonTag(problem, notes)}\
\
\
`;
@@ -136,6 +154,95 @@ const createCompletedProblemRecord = (problem) => {
;
}
+// 添加笔记模态框HTML
+const renderNoteModal = () => {
+ // 检查是否已经存在模态框
+ if (document.getElementById('noteModal')) {
+ console.log("笔记模态框已存在,不再创建");
+ return; // 如果已存在,不再创建
+ }
+
+ console.log("开始创建笔记模态框");
+
+ const modalHTML = `
+
+
+
+
+
+
+
+ 问题名称 (Problem Name)
+
+
+
+ 笔记内容 (Note Content)
+
+
+
+
+
+
+
`;
+
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
+
+ // 添加模态框样式
+ const style = document.createElement('style');
+ style.textContent = `
+ .modal.show {
+ display: block !important;
+ background-color: rgba(0, 0, 0, 0.5);
+ }
+ #problemName, #noteContent {
+ color: #000 !important;
+ background-color: #fff !important;
+ }
+ #problemName::placeholder {
+ color: #555 !important;
+ opacity: 1 !important;
+ }
+ `;
+ document.head.appendChild(style);
+
+ console.log("笔记模态框已创建,检查元素:");
+ console.log("问题名称输入框:", document.getElementById('problemName'));
+ console.log("笔记内容文本框:", document.getElementById('noteContent'));
+}
+
+
+
+// 添加一个全局函数用于初始化所有 tooltip
+const initializeTooltips = () => {
+ // 先移除所有现有的 tooltip 元素
+ document.querySelectorAll('.tooltip').forEach(el => {
+ el.remove();
+ });
+
+ // 销毁所有现有的 tooltip 实例
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
+ const tooltip = bootstrap.Tooltip.getInstance(el);
+ if (tooltip) {
+ tooltip.dispose();
+ }
+ });
+
+ // 初始化新的 tooltip
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
+ new bootstrap.Tooltip(el, {
+ trigger: 'hover focus', // 只在悬停或获取焦点时显示
+ container: 'body', // 将 tooltip 附加到 body
+ boundary: 'window' // 确保 tooltip 不超出窗口
+ });
+ });
+};
+
export const renderReviewTableContent = (problems, page) => {
/* validation */
console.log(store.toReviewMaxPage);
@@ -180,7 +287,7 @@ export const renderReviewTableContent = (problems, page) => {
needReviewTableDOM.innerHTML = content_html;
}
-export const renderScheduledTableContent = (problems, page) => {
+export const renderScheduledTableContent = async (problems, page) => {
/* validation */
if (page > store.scheduledMaxPage || page < 1) {
input1DOM.classList.add("is-invalid");
@@ -199,31 +306,49 @@ export const renderScheduledTableContent = (problems, page) => {
if (page === store.scheduledMaxPage) nextButton1DOM.setAttribute("disabled", "disabled");
if (page !== store.scheduledMaxPage) nextButton1DOM.removeAttribute("disabled");
-
let content_html =
'\
\
\
- Problem \
+ Problem \
Review \
- Recall \
- \
+ Recall \
+ Action \
\
\
\
';
+ // if (!Array.isArray(problems)) {
+ // problems = Object.values(problems);
+ // }
+ // problems为store.reviewScheduledProblems,即滤除了delete的题目
problems = problems.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
let keys = Object.keys(problems);
+
+ // 获取笔记数据
+ let notes = {};
+ try {
+ notes = await getLocalStorageData("notes") || {};
+ } catch (e) {
+ console.error("获取笔记数据失败", e);
+ }
for (const i of keys) {
- content_html += createScheduleProblemRecord(problems[i]) + '\n';
+ const problem = problems[i];
+ // 使用 createScheduleProblemRecord 函数创建问题记录
+ content_html += await createScheduleProblemRecord(problem);
}
content_html += ` `
noReviewTableDOM.innerHTML = content_html;
+
+ // 初始化 tooltip
+ setTimeout(() => {
+ initializeTooltips();
+ }, 100);
}
export const renderCompletedTableContent = (problems, page) => {
@@ -267,6 +392,11 @@ export const renderCompletedTableContent = (problems, page) => {
content_html += ``
completedTableDOM.innerHTML = content_html;
+
+ // 初始化 tooltip
+ setTimeout(() => {
+ initializeTooltips();
+ }, 100);
}
export const renderSiteMode = async () => {
@@ -293,6 +423,11 @@ export const renderAll = async () => {
await renderSiteMode();
await syncProblems();
+ // 创建笔记模态框
+
+
+
+
const problems = Object.values(await getAllProblems()).filter(p => p.isDeleted !== true);
console.log('Filtering and sorting problems...');
@@ -334,9 +469,25 @@ export const renderAll = async () => {
console.log('Filtering and sorting completed.');
// renderReviewTableContent(store.needReviewProblems, 1);
- renderScheduledTableContent(store.reviewScheduledProblems, 1);
+ await renderScheduledTableContent(store.reviewScheduledProblems, 1);
// renderCompletedTableContent(store.completedProblems, 1);
await renderUndoButton();
+ renderNoteModal();
registerAllHandlers();
+
+ // 初始化所有 tooltip
+ setTimeout(() => {
+ initializeTooltips();
+ }, 200);
+
+ // 添加全局点击事件监听器,点击页面任何地方时隐藏所有 tooltip
+ document.addEventListener('click', (e) => {
+ // 如果点击的不是 tooltip 触发元素,则隐藏所有 tooltip
+ if (!e.target.hasAttribute('data-bs-toggle') || e.target.getAttribute('data-bs-toggle') !== 'tooltip') {
+ document.querySelectorAll('.tooltip').forEach(el => {
+ el.remove();
+ });
+ }
+ });
}
From 163abd25f27c43631d565963fc6e0babc38d4bf9 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Wed, 2 Apr 2025 23:31:45 +0800
Subject: [PATCH 15/22] Fix: limit leetcode content script just for problems
page
---
manifest.base.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/manifest.base.json b/manifest.base.json
index ff28eb8..2d58894 100644
--- a/manifest.base.json
+++ b/manifest.base.json
@@ -47,7 +47,7 @@
},
{
"matches": [
- "https://leetcode.com/*"
+ "https://leetcode.com/problems/*"
],
"js": [
"dist/leetcode.js"
@@ -56,7 +56,7 @@
},
{
"matches": [
- "https://leetcode.cn/*"
+ "https://leetcode.cn/problems/*"
],
"js": [
"dist/leetcodecn.js"
From 8b255452a0a27d75bd42f0ffdec60ebdfa07ea18 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Wed, 2 Apr 2025 23:52:41 +0800
Subject: [PATCH 16/22] Release: v0.1.3
---
README.md | 29 ++++++++++++++---------------
changelog.md | 15 +++++++++++----
manifest.base.json | 2 +-
package-lock.json | 2 +-
package.json | 2 +-
popup.html | 4 ++--
6 files changed, 30 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index dba2aae..2fbecfd 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@
4. You can complete your review by ticking the box in the plugin's `popup` window, or you can click into the problem page and complete the review via the `rate it` button.
-5. Open the problem list to see all the problems in your current review plan.
+5. Open the question list, view all the questions in the current review plan, write notes, and export notes as Markdown.
6. Happy problem-solving! The key to mastering things quickly is to avoid forgetting!
# 🚀 用法
@@ -46,7 +46,7 @@
2. 插件主页自动评估每道题的可检索性优先级(能够回忆起来的概率),用户可根据时间安排,灵活调整每日的复习量。
3. FSRS算法允许休息和提前突击复习,其算法会随时间流逝,自动推理整体题目的回忆概率,动态调整下一次复习时间,以适应你的学习节奏。
4. 可在插件的`popup`弹窗中打勾完成复习;也可点击进入题目页面,通过`rate`按钮完成复习。
-5. 打开题目列表,查看当前复习规划的所有题目
+5. 打开题目列表,查看当前复习规划的所有题目,撰写笔记,导出笔记为Markdown
6. 刷题快乐,速成的本质在于不要遗忘!

@@ -81,27 +81,26 @@
| 监控提醒 | ✅ 已完成 | bilibili、youtube |
| url添加力扣题目 | ✅ 已完成 | 配合 IDE 刷题,工位摸鱼专用 |
| url添加自定义卡片 | ✅ 已完成 | 用于记录面试手撕题、其他刷题网站用户暂时替代方案 |
+| 提供笔记功能 | ✅ 已完成 | 题目列表中新增笔记按钮,支持导出所有笔记为Markdown |
| 收集Anki fsrs 训练数据 | ✅ 已完成 | 待用于测试fsrs官方端口训练 |
| 接入Anki fsrs官方训练端口 | ❌ 待完成 | 待评估可行性(用户可拟合出最适合自己的记忆曲线) |
| 不同网站题目数据源切换 | ❌ 待完成 | 待完成(目前仅支持力扣国际站和中国站,待兼容洛谷等) |
| 兼容火狐 | ❌ 待完成 | 待完成 |
-| 提供笔记功能 | ❌ 待完成 | 待评估可行性(浏览器存储数据上限可能无法支持) |
-
| 兼容`ctrl + enter` | ❌ 待完成 | 目前优先级较低 |
# 📝 Next Steps
-| Task/Feature | Status | Remarks |
-|-----------------------------|------------|-------------------------------------------------------------------------|
-| Multi-device cloud sync | ✅ Completed | Edge, Chrome |
-| Monitoring reminders | ✅ Completed | Bilibili, YouTube |
-| Add LeetCode URL | ✅ Completed | For use with IDE for problem-solving, perfect for stealth studying at work |
-| Add custom card URL | ✅ Completed | recording interview problems, serves as a temporary solution for other problem-solving websites |
-| Switch data sources for different websites | ❌ Pending | Pending completion (currently only supports LeetCode Global and China, pending compatibility with Luogu, etc.) |
-| Firefox compatibility | ❌ Pending | Pending feasibility assessment |
-| Provide note-taking feature | ❌ Pending | Pending feasibility assessment (browser storage data limits may not support) |
-| Integrate with Anki fsrs official training port | ❌ Pending | Pending feasibility assessment (users can fit their own optimal memory curve) |
-| Compatibility with `Ctrl + Enter` | ❌ Pending | Currently lower priority |
+| Task/Feature | Status | Notes |
+|-----------------------|------------|------------------------------------|
+| Multi-device data cloud sync | ✅ Completed | Edge, Chrome |
+| Monitor reminder | ✅ Completed | bilibili, youtube |
+| URL add LeetCode problems | ✅ Completed | For brushing questions with IDE, dedicated to working position entertainment |
+| URL add custom cards | ✅ Completed | For recording interview hand-torn problems, alternative solution for other question brushing websites |
+| Provide note-taking feature | ✅ Completed | Add note button in question list, support exporting all notes as Markdown |
+| Collect Anki fsrs training data | ✅ Completed | Pending for testing fsrs official port training |
+| Integrate Anki fsrs official training port | ❌ Pending | To be assessed for feasibility (users can fit their own optimal memory curve) |
+| Switch between different website question data sources | ❌ Pending | To be completed (currently only supports LeetCode international and Chinese stations, to be compatible with Luogu, etc.) |
+| Compatibility with Firefox | ❌ Pending | To be completed |
diff --git a/changelog.md b/changelog.md
index 0433e5d..a0549f9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,10 +1,17 @@
# Changelog
-# Changelog
-## [0.1.2] - 2025-04-01
+## [0.1.3] - 2025-04-02
+
+### Added
+- Add note writing and export features to enhance the learning experience and allow users to better organize their knowledge (#5)
+ 新增笔记撰写和导出功能,提升学习体验,帮助用户更好地整理知识(#5)
+
### Fixed
-- incorrect state judgment && error lastreview time (#27)
- 错误的状态判断以及错误的上次复习时间 (#27)
+- Fix the issue where LeetCode URLs were incorrectly matched, leading to errors in question identification (#31)
+ 修复LeetCode URL错误匹配问题,避免题目识别出错(#31)
+
+- Fix the issue where the S value in the FSRS algorithm was not updated due to incorrect matching, ensuring accurate data for training (#27)
+ 修复因错误匹配导致FSRS算法中S值未更新问题,确保训练数据准确(#27)
diff --git a/manifest.base.json b/manifest.base.json
index 7cfc794..4258625 100644
--- a/manifest.base.json
+++ b/manifest.base.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Leetcode Mastery Scheduler",
- "version": "0.1.2",
+ "version": "0.1.3",
"author": "Hacode",
"description": "Leetcode-Mastery-Scheduler tracks your LeetCode progress and prompt you to review based FSRS",
"homepage_url": "https://github.com/xiaohajiayou/Leetcode-Mastery-Scheduler",
diff --git a/package-lock.json b/package-lock.json
index 3e582d8..eae727d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "Leetcode-Mastery-Scheduler",
- "version": "0.1.2",
+ "version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
diff --git a/package.json b/package.json
index 1593256..66fc920 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Leetcode-Mastery-Scheduler",
- "version": "0.1.2",
+ "version": "0.1.3",
"description": "
\r \r \r P ractice M akes C ode A ccepted\r \r ",
"main": "src/popup.js",
"directories": {
diff --git a/popup.html b/popup.html
index 2dc9494..38cbfba 100644
--- a/popup.html
+++ b/popup.html
@@ -357,11 +357,11 @@
Add Review Card
NEW!
- 新增复习记录本地收集,用于优化 FSRS 参数。 / Add local collection of review records for optimizing FSRS parameters
+ 新增笔记撰写和导出功能。 / Add note writing and export features
From 80ff35d6922d4e0c4cf348d8e0c4ea04d61eea1c Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Sat, 5 Apr 2025 23:59:01 +0800
Subject: [PATCH 17/22] Feat: basic infer fsrs func
---
popup.html | 27 ++++++++
src/popup/daily-review.js | 94 +++++++++++++++++++++++++-
src/popup/delegate/fsrsDelegate.js | 102 +++++++++++++++++++++++++++++
src/popup/service/fsrsService.js | 36 ++++++++++
4 files changed, 258 insertions(+), 1 deletion(-)
create mode 100644 src/popup/delegate/fsrsDelegate.js
create mode 100644 src/popup/service/fsrsService.js
diff --git a/popup.html b/popup.html
index 38cbfba..6c3e509 100644
--- a/popup.html
+++ b/popup.html
@@ -518,6 +518,33 @@
Active Reminder
2. Disable to stop all pop-up reminders
+
+
+
+
+
+
+
FSRS参数优化
+
+
+
+ 导出记录
+
+
+ 优化参数
+
+
+
+
+
+
+ 当前复习记录数: 0
+
+
+ 点击按钮优化FSRS算法参数
+
+
+
diff --git a/src/popup/daily-review.js b/src/popup/daily-review.js
index 18279b3..a6cd507 100644
--- a/src/popup/daily-review.js
+++ b/src/popup/daily-review.js
@@ -12,6 +12,7 @@ import {handleAddProblem} from "./script/submission.js"
import Swal from 'sweetalert2';
// 导入 getAllRevlogs 函数
import { getAllRevlogs, exportRevlogsToCSV } from './util/fsrs.js';
+import { getRevlogCount, optimizeParameters } from './service/fsrsService.js';
// 在文件开头添加
const LAST_AVERAGE_KEY = 'lastRetrievabilityAverage';
@@ -783,6 +784,94 @@ function initializeAddProblem() {
}
}
+// 显示弹窗函数
+function showModal(title, content) {
+ Swal.fire({
+ title: title,
+ html: content,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c',
+ width: '600px'
+ });
+}
+
+// 初始化FSRS参数优化卡片
+async function initializeFSRSOptimization() {
+ try {
+ // 获取并显示复习记录数量
+ const count = await getRevlogCount();
+ const revlogCountElement = document.getElementById('revlogCount');
+ if (revlogCountElement) {
+ revlogCountElement.textContent = count;
+ }
+
+ // 添加优化按钮点击事件
+ const optimizeParamsBtn = document.getElementById('optimizeParamsBtn');
+ if (optimizeParamsBtn) {
+ optimizeParamsBtn.addEventListener('click', async () => {
+ // 将originalText变量移到try块之外
+ const originalText = optimizeParamsBtn.textContent || '优化参数';
+
+ try {
+ // 显示加载中提示
+ optimizeParamsBtn.disabled = true;
+ optimizeParamsBtn.innerHTML = ' 优化中...';
+
+ // 创建进度显示元素
+ const progressContainer = document.createElement('div');
+ progressContainer.className = 'progress mt-2';
+ progressContainer.style.height = '5px';
+ progressContainer.innerHTML = `
+
+
+ `;
+ optimizeParamsBtn.parentNode.appendChild(progressContainer);
+
+ // 进度回调函数
+ const onProgress = (progress) => {
+ console.log('Progress update:', progress);
+ const percent = Math.round(progress.percent * 100);
+ const progressBar = progressContainer.querySelector('.progress-bar');
+ if (progressBar) {
+ progressBar.style.width = `${percent}%`;
+ progressBar.setAttribute('aria-valuenow', percent);
+ progressBar.textContent = `${percent}%`;
+ }
+ };
+
+ // 调用优化API
+ const result = await optimizeParameters(onProgress);
+
+ // 移除进度显示元素
+ progressContainer.remove();
+
+ // 显示结果弹窗
+ showModal('FSRS参数优化结果', `
+
+
${JSON.stringify(result, null, 2)}
+
+ `);
+ } catch (error) {
+ console.error('Error optimizing FSRS parameters:', error);
+ showModal('错误', `优化参数时发生错误: ${error.message}`);
+ } finally {
+ // 恢复按钮状态
+ optimizeParamsBtn.disabled = false;
+ optimizeParamsBtn.textContent = originalText;
+ }
+ });
+ }
+ } catch (error) {
+ console.error('Error initializing FSRS optimization:', error);
+ }
+}
+
// 添加设置相关的初始化函数
async function initializeOptions() {
await loadConfigs();
@@ -818,7 +907,10 @@ async function initializeOptions() {
if (reminderToggle) {
reminderToggle.checked = store.isReminderEnabled || false;
}
-
+
+ // 初始化FSRS参数优化卡片
+ await initializeFSRSOptimization();
+
// 修改保存成功提示
optionsForm.addEventListener('submit', async e => {
e.preventDefault();
diff --git a/src/popup/delegate/fsrsDelegate.js b/src/popup/delegate/fsrsDelegate.js
new file mode 100644
index 0000000..e120b3b
--- /dev/null
+++ b/src/popup/delegate/fsrsDelegate.js
@@ -0,0 +1,102 @@
+// FSRS参数优化相关的API请求处理
+export const optimizeFSRSParams = async (csvContent, onProgress) => {
+ try {
+ const formData = new FormData();
+ const csvBlob = new Blob([csvContent], { type: 'text/csv' });
+ formData.append('file', csvBlob, 'revlog.csv');
+ formData.append('sse', '1');
+ formData.append('hour_offset', '4');
+ formData.append('enable_short_term', '0');
+ formData.append('timezone', 'Asia/Shanghai');
+
+ const response = await fetch('https://ishiko732-fsrs-online-training.hf.space/api/train', {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ // 手动解析SSE响应
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let result = null;
+ let lastProgress = null;
+ let doneParams = null;
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const chunk = decoder.decode(value, { stream: true });
+ const lines = chunk.split('\n');
+
+ // 处理SSE响应
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // 处理事件类型
+ if (line.startsWith('event: ')) {
+ const eventType = line.substring(7);
+ console.log('事件类型:', eventType);
+
+ // 查找下一个data行
+ let dataLine = '';
+ for (let j = i + 1; j < lines.length; j++) {
+ if (lines[j].startsWith('data: ')) {
+ dataLine = lines[j];
+ break;
+ }
+ }
+
+ if (dataLine) {
+ try {
+ const data = JSON.parse(dataLine.substring(6));
+
+ // 处理进度信息
+ if (eventType === 'progress') {
+ lastProgress = data;
+ // 如果提供了进度回调函数,则调用它
+ if (onProgress) {
+ onProgress(data);
+ }
+ }
+
+ // 处理完成事件
+ if (eventType === 'done') {
+ doneParams = data;
+ console.log('捕获到done事件中的参数:', doneParams);
+ }
+
+ // 处理训练结果
+ if (eventType === 'info' && data.type === 'Train') {
+ result = data;
+ }
+ } catch (e) {
+ console.warn('Error parsing SSE data:', e, dataLine);
+ }
+ }
+ }
+ }
+ }
+
+ // 优先返回done标签中的参数
+ if (doneParams) {
+ return doneParams;
+ }
+
+ // 如果没有获取到done参数,但有进度信息,则返回进度信息
+ if (!result && lastProgress) {
+ result = {
+ type: 'Progress',
+ progress: lastProgress
+ };
+ }
+
+ return result || { type: 'Error', message: 'No result received' };
+ } catch (error) {
+ console.error('Error optimizing FSRS parameters:', error);
+ throw error;
+ }
+};
\ No newline at end of file
diff --git a/src/popup/service/fsrsService.js b/src/popup/service/fsrsService.js
new file mode 100644
index 0000000..b44ae60
--- /dev/null
+++ b/src/popup/service/fsrsService.js
@@ -0,0 +1,36 @@
+import { getAllRevlogs, exportRevlogsToCSV } from '../util/fsrs.js';
+import { optimizeFSRSParams } from '../delegate/fsrsDelegate.js';
+
+// 获取复习记录数量
+export const getRevlogCount = async () => {
+ try {
+ const allRevlogs = await getAllRevlogs();
+ let totalCount = 0;
+
+ // 计算所有卡片的复习记录总数
+ Object.values(allRevlogs).forEach(cardRevlogs => {
+ totalCount += cardRevlogs.length;
+ });
+
+ return totalCount;
+ } catch (error) {
+ console.error('Error getting revlog count:', error);
+ return 0;
+ }
+};
+
+// 优化FSRS参数
+export const optimizeParameters = async (onProgress) => {
+ try {
+ // 获取并导出CSV格式的复习记录
+ const csvContent = await exportRevlogsToCSV();
+
+ // 调用API进行参数优化
+ const result = await optimizeFSRSParams(csvContent, onProgress);
+
+ return result;
+ } catch (error) {
+ console.error('Error optimizing parameters:', error);
+ throw error;
+ }
+};
\ No newline at end of file
From 1f56cdb8921b246fcadd526d859ba7d5516d26f8 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Sun, 6 Apr 2025 23:01:59 +0800
Subject: [PATCH 18/22] Feat: local fsrs infer complete without clound version
---
popup.html | 23 +-
src/popup/daily-review.js | 2500 ++++++++++++++++--------------
src/popup/popup.css | 64 +
src/popup/script/submission.js | 4 +-
src/popup/service/fsrsService.js | 219 ++-
src/popup/util/fsrs.js | 208 +--
src/popup/util/utils.js | 68 +
src/popup/view/view.js | 2 +
8 files changed, 1807 insertions(+), 1281 deletions(-)
diff --git a/popup.html b/popup.html
index 6c3e509..e3577b1 100644
--- a/popup.html
+++ b/popup.html
@@ -524,24 +524,29 @@ Active Reminder
-
FSRS参数优化
+ Fsrs Param Optim
-
- 导出记录
+
+
-
- 优化参数
+
+
-
- 当前复习记录数: 0
+
+
+ Current review count: 0
+
+
+
+ Click to optimize FSRS parameters
-
- 点击按钮优化FSRS算法参数
+
+ (Fit best parameters when you have enough data.)
diff --git a/src/popup/daily-review.js b/src/popup/daily-review.js
index a6cd507..c42b161 100644
--- a/src/popup/daily-review.js
+++ b/src/popup/daily-review.js
@@ -1,1142 +1,1358 @@
-import { renderAll } from './view/view.js';
-import { getAllProblems, syncProblems } from "./service/problemService.js";
-import { getLevelColor,getCurrentRetrievability } from './util/utils.js';
-import { handleFeedbackSubmission, handleAddBlankProblem } from './script/submission.js';
-import './popup.css';
-import { isCloudSyncEnabled, loadConfigs, setCloudSyncEnabled, setProblemSorter,setDefaultCardLimit,setReminderEnabled } from "./service/configService";
-import { store,daily_store } from './store';
-import { optionPageFeedbackMsgDOM } from './util/doms';
-import { descriptionOf, idOf, problemSorterArr } from "./util/sort";
-import {handleAddProblem} from "./script/submission.js"
-// 在文件顶部导入 SweetAlert2
-import Swal from 'sweetalert2';
-// 导入 getAllRevlogs 函数
-import { getAllRevlogs, exportRevlogsToCSV } from './util/fsrs.js';
-import { getRevlogCount, optimizeParameters } from './service/fsrsService.js';
-
-// 在文件开头添加
-const LAST_AVERAGE_KEY = 'lastRetrievabilityAverage';
-const LAST_UPDATE_TIME_KEY = 'lastUpdateTime';
-let yesterdayRetrievabilityAverage = 0.00;
-
-
-
-async function loadProblemList() {
- await renderAll();
-}
-
-
-// 获取上次存储的平均值和时间
-function loadLastAverageData() {
- const lastData = {
- average: parseFloat(localStorage.getItem(LAST_AVERAGE_KEY)) || 0.00,
- timestamp: parseInt(localStorage.getItem(LAST_UPDATE_TIME_KEY)) || 0
- };
- return lastData;
-}
-
-async function loadDailyReviewData() {
- const problems = Object.values(await getAllProblems()).filter(p => p.isDeleted !== true);
- daily_store.reviewScheduledProblems = problems
- .sort((a, b) => {
- const retrievabilityA = getCurrentRetrievability(a);
- const retrievabilityB = getCurrentRetrievability(b);
- return retrievabilityA - retrievabilityB; // 升序排序,最小值在前
- });
-
- // 获取今天已复习和待复习的题目
- daily_store.dailyReviewProblems = daily_store.reviewScheduledProblems
- .filter(problem => isReviewedToday(problem) || isReviewDueToday(problem))
- .sort((a, b) => {
- // 首先按照是否已复习排序(已复习的排在前面)
- const aReviewed = isReviewedToday(a);
- const bReviewed = isReviewedToday(b);
- if (aReviewed !== bReviewed) {
- return bReviewed ? 1 : -1;
- }
- // 如果复习状态相同,则按可检索性排序
- const retrievabilityA = getCurrentRetrievability(a);
- const retrievabilityB = getCurrentRetrievability(b);
- return retrievabilityA - retrievabilityB;
- });
-
-
- console.log('总题目数:', problems.length);
- console.log('今日待复习题目数:', daily_store.dailyReviewProblems.length);
-
- // 添加调试日志
- daily_store.dailyReviewProblems.forEach(problem => {
- const isReviewed = isReviewedToday(problem);
- const isDue = isReviewDueToday(problem);
- console.log('题目状态:', {
- name: problem.name,
- lastReview: problem.fsrsState?.lastReview,
- nextReview: problem.fsrsState?.nextReview,
- isReviewedToday: isReviewed,
- isDueToday: isDue,
- retrievability: getCurrentRetrievability(problem)
- });
- });
-}
-
-// 存储当前的平均值和时间
-function saveCurrentAverageData(average) {
- localStorage.setItem(LAST_AVERAGE_KEY, average.toString());
- localStorage.setItem(LAST_UPDATE_TIME_KEY, Date.now().toString());
-}
-
-// 设置当前日期
-function setCurrentDate() {
- const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
- const today = new Date().toLocaleDateString('en-US', options);
- document.getElementById('currentDate').textContent = today;
-}
-
-
-// 判断是否是今天需要复习的题目
-function isReviewDueToday(problem) {
- if (!problem.fsrsState?.nextReview) {
- console.log('题目没有下次复习时间:', problem.name);
- return false;
- }
-
- const today = new Date();
- today.setHours(0, 0, 0, 0);
-
- const nextReview = new Date(problem.fsrsState.nextReview);
- nextReview.setHours(0, 0, 0, 0);
-
- const isDue = nextReview <= today;
-
- console.log('复习时间检查:', {
- problemName: problem.name,
- nextReview: nextReview.toISOString(),
- today: today.toISOString(),
- isDue: isDue
- });
-
- return isDue;
-}
-
-function isReviewedToday(problem) {
- if (!problem.fsrsState?.lastReview) return false;
-
- const today = new Date();
- today.setHours(0, 0, 0, 0);
-
- const lastReview = new Date(problem.fsrsState.lastReview);
- lastReview.setHours(0, 0, 0, 0);
-
- return lastReview.getTime() === today.getTime();
-}
-
-
-
-
-// 计算可检索性均值
-function calculateRetrievabilityAverage() {
- const problems = daily_store.reviewScheduledProblems;
- if (!problems || problems.length === 0) return 0;
-
- const totalRetrievability = problems.reduce((sum, problem) => {
- const retrievability = getCurrentRetrievability(problem);
- return sum + retrievability;
- }, 0);
-
- return Number((totalRetrievability / problems.length).toFixed(2));
-}
-
-
-// 更新顶部统计信息
-function updateStats() {
- console.log('更新统计信息');
- // 设置默认值
- let completedCount = 0;
- let totalProblems = 0;
- // 添加空值检查
- if (!daily_store || !daily_store.dailyReviewProblems) {
- console.log('daily_store 或 dailyReviewProblems 为空:', {
- daily_store: daily_store,
- problems: daily_store?.dailyReviewProblems
- });
-
-
- // 更新显示
- document.getElementById('completedCount').textContent = completedCount;
- document.getElementById('totalCount').textContent = totalProblems;
- document.getElementById('completionRate').textContent = '0%';
- updateProgressCircle(0);
- return;
- }
-
-
-
- // 获取当前显示的卡片数量
- let cardLimit = parseInt(document.getElementById('cardLimit').value, 10)|| store.defaultCardLimit || 1;
- console.log('当前卡片限制值:', {
- rawValue: document.getElementById('cardLimit').value,
- parsedCardLimit: cardLimit,
- element: document.getElementById('cardLimit')
- });
-
-
-
- // 计算今日已复习的题目数量
- completedCount = daily_store.dailyReviewProblems.filter(problem =>
- isReviewedToday(problem)
- ).length;
-
-
- totalProblems = daily_store.dailyReviewProblems?.length || 0;
- if (cardLimit > totalProblems) {
- cardLimit = totalProblems;
- }
-
- // 添加空状态提示
- const addProblemWrapper = document.querySelector('.add-problem-wrapper');
- // 先移除可能存在的空状态提示
- const existingEmptyState = document.querySelector('.empty-state');
- if (existingEmptyState) {
- existingEmptyState.remove();
- }
-
- if (totalProblems === 0 || cardLimit === 0) {
- const emptyState = document.createElement('div');
- emptyState.className = 'empty-state';
- emptyState.innerHTML = `
-
-
- Time to learn something new!
- `;
- addProblemWrapper.insertAdjacentElement('beforebegin', emptyState);
- }
-
-
- // 更新显示的已复习数量
- document.getElementById('completedCount').textContent = completedCount;
- document.getElementById('totalCount').textContent = cardLimit; // 使用当前的卡片数量
-
- // 更新进度条
- const completionRate = cardLimit > 0 ? Math.round((completedCount / cardLimit) * 100) : 0;
- updateProgressCircle(completionRate);
- document.getElementById('completionRate').textContent = `${completionRate}%`;
- // document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
- // 计算当前的可检索性均值,并确保是数字类型
- const currentRetrievabilityAverage = parseFloat(calculateRetrievabilityAverage()) || 0;
- console.log('当前可检索性均值:', {
- raw: calculateRetrievabilityAverage(),
- parsed: currentRetrievabilityAverage,
- type: typeof currentRetrievabilityAverage
- });
- const retrievabilityElement = document.getElementById('retrievabilityAverage');
- retrievabilityElement.textContent = currentRetrievabilityAverage;
-
-
- // 获取上次存储的数据
- const lastData = loadLastAverageData();
- const hoursSinceLastUpdate = (Date.now() - lastData.timestamp) / (1000 * 60 * 60);
-
- // 如果超过24小时,更新存储的数据
- if (hoursSinceLastUpdate >= 24) {
- console.log('距离上次更新已超过24小时:', {
- hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
- lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
- lastAverage: lastData.average.toFixed(2),
- currentAverage: currentRetrievabilityAverage.toFixed(2)
- });
-
- yesterdayRetrievabilityAverage = lastData.average;
- saveCurrentAverageData(currentRetrievabilityAverage);
-
- console.log('已更新存储数据:', {
- newYesterdayAverage: yesterdayRetrievabilityAverage.toFixed(2),
- savedCurrentAverage: currentRetrievabilityAverage.toFixed(2),
- saveTime: new Date().toLocaleString()
- });
- } else {
- console.log('距离上次更新未超过24小时:', {
- hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
- lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
- usingLastAverage: lastData.average.toFixed(2)
- });
- yesterdayRetrievabilityAverage = lastData.average;
- }
-
- // 更新趋势图标
- const trendIcon = document.getElementById('trendIcon');
- if (currentRetrievabilityAverage > yesterdayRetrievabilityAverage) {
- trendIcon.className = 'fas fa-arrow-up trend-icon trend-up';
- } else if (currentRetrievabilityAverage < yesterdayRetrievabilityAverage) {
- trendIcon.className = 'fas fa-arrow-down trend-icon trend-down';
- } else {
- trendIcon.className = '';
- }
-
- // 根据可检索性均值调整颜色和背景提示
- const lowMemoryWarning = document.getElementById('lowMemoryWarning');
- if (currentRetrievabilityAverage < 0.90) {
- retrievabilityElement.classList.add('low');
- lowMemoryWarning.classList.add('active');
- } else {
- retrievabilityElement.classList.remove('low');
- lowMemoryWarning.classList.remove('active');
- }
- updateCardLimitDisplay(); // 这里也添加一次调用
-}
-
-function updateProgressCircle(completionRate) {
- const progressCircle = document.querySelector('.completion-circle');
- const radius = 54; // 圆的半径
- const circumference = 2 * Math.PI * radius; // 圆的周长
-
- // 计算偏移量
- const offset = circumference - (completionRate / 100) * circumference;
- progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;
- progressCircle.style.strokeDashoffset = offset;
-
- // 更新显示的百分比
- // document.getElementById('completionRate').textContent = `${completionRate}%`;
- document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
-
- // 添加动画效果
- const innerCircle = document.querySelector('.inner-circle');
- innerCircle.style.transform = `scale(1.1)`; // 放大内圈
- setTimeout(() => {
- innerCircle.style.transform = `scale(1)`; // 恢复原状
- }, 500); // 动画持续时间
-}
-
-
-
-
-// 更新卡片限制和显示
-export function updateCardLimitDisplay() {
- const input = document.getElementById('cardLimit');
- const totalDisplay = document.querySelector('.total-problems');
- const totalProblems = daily_store.dailyReviewProblems?.length || 0;
-
- // 更新最大值和总数显示
- input.max = Math.max(totalProblems, 1);
- totalDisplay.textContent = `/ ${totalProblems}`;
-
- // 使用保存的默认值或回退到3
- let currentValue = store.defaultCardLimit || 1;
- if (currentValue > totalProblems && totalProblems > 0) {
- currentValue = totalProblems;
- // store.defaultCardLimit = totalProblems;
- // setDefaultCardLimit(totalProblems);
- }
- input.value = currentValue;
-
- // 禁用条件
- if (totalProblems === 0) {
- input.value = 0;
- input.disabled = true;
- totalDisplay.textContent = "/ 0";
- } else {
- input.disabled = false;
- }
-
- console.log('更新卡片限制显示:', {
- currentValue: input.value,
- max: input.max,
- totalProblems
- });
-}
-
-// 更新卡片显示
-export function updateCardDisplay() {
- console.log('更新卡片显示');
-
- updateStats(); // 更新统计信息,传递当前显示的卡片数量
-
-
- createReviewCards(); // 创建新的卡片
-}
-
-
-
-
-// 改变卡片数量
-// 所有功能函数
-export async function changeCardLimit(delta) {
- console.log('执行 changeCardLimit, delta:', delta);
- const input = document.getElementById('cardLimit');
- const currentValue = parseInt(input.value, 10);
- const newValue = currentValue + delta;
- const maxValue = daily_store.dailyReviewProblems?.length || 0;
-
- if (newValue >= 1 && newValue <= maxValue) {
- input.value = newValue;
- await setDefaultCardLimit(newValue);
- store.defaultCardLimit = newValue;
- updateCardDisplay();
- }
-}
-
-
-
-
-// 标记题目为已复习
-async function markAsReviewed(button, problem) {
- console.log('执行 markAsReviewed', button, problem);
-
- const card = button.closest('.review-card');
- if (!card) {
- console.log('未找到对应的卡片');
- return;
- }
-
- console.log('找到卡片,开始更新状态');
-
- // 更换图标并更改样式
- const icon = button.querySelector('i');
- icon.classList.remove('fa-check-circle');
- icon.classList.add('fa-circle-check');
- icon.style.color = '#0D6E6E';
-
- // 禁用按钮
- button.disabled = true;
- card.style.opacity = '0.4';
-
-
-
- // 更新统计信息
- updateCardDisplay();
- console.log('更新完成');
-}
-
-
-// 创建题目卡片时的事件绑定
-function createReviewCards() {
- console.log('开始创建卡片');
- const reviewList = document.getElementById('reviewList');
- const template = document.getElementById('reviewCardTemplate');
- const cardLimit = parseInt(document.getElementById('cardLimit').value, 10);
-
- reviewList.innerHTML = '';
-
- const problems = daily_store.dailyReviewProblems || [];
- problems.slice(0, cardLimit).forEach((problem, index) => {
- const cardNode = template.content.cloneNode(true);
- const card = cardNode.querySelector('.review-card');
-
- // 安全地访问 fsrsState
- const fsrsState = problem.fsrsState || {};
-
-
- // 设置题目信息
- const problemName = card.querySelector('.problem-name');
- problemName.textContent = problem.name || 'unknown';
-
- // 设置难度和复习信息
- const difficultySpan = card.querySelector('.difficulty');
- const level = problem.level || 'Unknown';
- difficultySpan.textContent = level;
- // 使用现有的 CSS 类
- difficultySpan.classList.add(`difficulty-${level}`);
-
- // 设置可检索性
- const retrievability = getCurrentRetrievability(problem);
- const retrievabilitySpan = card.querySelector('.retrievability');
- retrievabilitySpan.textContent = `${retrievability.toFixed(1)}`;
- retrievabilitySpan.classList.add(retrievability < 0.9 ? 'text-danger' : 'text-success');
-
-
- // 设置下次复习时间
- const nextReviewTips = fsrsState.nextReview
- ? (() => {
- const nextReviewDate = new Date(fsrsState.nextReview);
- const now = new Date();
-
- // 获取当前日期和下次复习日期(不含时间)
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
- const reviewDay = new Date(nextReviewDate.getFullYear(), nextReviewDate.getMonth(), nextReviewDate.getDate());
-
- // 计算日期差(天数)
- const diffTime = reviewDay.getTime() - today.getTime();
- const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
-
- if (diffDays < 0) {
- // 已经过了计划复习日期
- const daysOverdue = Math.abs(diffDays);
- return `Delay by ${daysOverdue} day${daysOverdue > 1 ? 's' : ''}`;
- } else if (diffDays === 0) {
- // 今天需要复习
- return 'Review today';
- } else if (diffDays === 1) {
- // 明天需要复习
- return 'Review tomorrow';
- } else {
- // x天后复习
- return `Review in ${diffDays} days`;
- }
- })()
- : 'Not scheduled';
- card.querySelector('.next-review').textContent = nextReviewTips;
-
- // 格式化上次复习时间
- const lastReviewText = fsrsState.lastReview
- ? new Date(fsrsState.lastReview).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })
- : 'Never reviewed';
-
- // 格式化上次复习时间
- const nextReviewText = fsrsState.nextReview
- ? new Date(fsrsState.nextReview).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })
- : 'Never reviewed';
-
-
-
- // 设置hover提示
- const tooltipContent = [
- `Last Review: ${lastReviewText}`,
- `Next Review: ${nextReviewText}`,
- problem.url ? 'Click to open problem' : ''
- ].filter(Boolean).join('\n');
-
- card.title = tooltipContent;
-
- // 检查今日是否已复习
- const isReviewedToday = fsrsState.lastReview &&
- new Date(fsrsState.lastReview).toDateString() === new Date().toDateString();
-
- // 设置按钮状态
- const reviewButton = card.querySelector('.btn-review');
- if (reviewButton) {
- if (isReviewedToday) {
- const icon = reviewButton.querySelector('i');
- icon.classList.remove('fa-check-circle');
- icon.classList.add('fa-circle-check');
- icon.style.color = '#0D6E6E';
- reviewButton.disabled = true;
- card.style.opacity = '0.4';
- }
-
- reviewButton.onclick = async function(e) {
- e.preventDefault();
- e.stopPropagation();
- console.log('复习按钮被点击');
-
- const updatedProblem = await handleFeedbackSubmission(problem);
- if (updatedProblem) {
- markAsReviewed(this, updatedProblem);
- }
- // markAsReviewed(this, problem); // 修改这里,传入按钮元素和问题对象
- };
- }
-
- // 添加题目链接功能
- if (problem.url) {
- card.style.cursor = 'pointer';
- card.onclick = function(e) {
- if (!e.target.closest('.btn-review')) {
- window.open(problem.url, '_blank');
- }
- };
- }
-
- reviewList.appendChild(cardNode);
- });
-}
-
-
-
-
-
-
-
-
-
-
-
-
-// 显示/隐藏添加题目弹窗
-function toggleAddProblemDialog(show) {
- const dialog = document.getElementById('addProblemDialog');
- if (!dialog) return;
-
- if (show) {
- dialog.style.display = 'block';
- } else {
- dialog.style.display = 'none';
-
- // 清除所有输入字段
- const problemUrl = document.getElementById('problemUrl');
- const problemName = document.getElementById('problemName');
- const customUrl = document.getElementById('customUrl');
-
- if (problemUrl) problemUrl.value = '';
- if (problemName) problemName.value = '';
- if (customUrl) customUrl.value = '';
-
- // 重置选项卡到默认状态
- const urlTabButton = document.getElementById('urlTabButton');
- const manualTabButton = document.getElementById('manualTabButton');
- const urlTab = document.getElementById('urlTab');
- const manualTab = document.getElementById('manualTab');
-
- if (urlTabButton && manualTabButton && urlTab && manualTab) {
- urlTabButton.classList.add('active');
- manualTabButton.classList.remove('active');
- urlTab.classList.add('active');
- manualTab.classList.remove('active');
- }
- }
-}
-
-
-
-// 初始化添加题目功能
-function initializeAddProblem() {
- const addButton = document.querySelector('.gear-button.add-problem');
- if (!addButton) return;
-
- // 添加选项卡切换样式
- const style = document.createElement('style');
- style.textContent = `
- .tab-container {
- margin-bottom: 15px;
- }
-
- .tab-buttons {
- display: flex;
- border-bottom: 1px solid #3a4a5c;
- margin-bottom: 15px;
- }
-
- .tab-button {
- background: none;
- border: none;
- padding: 8px 15px;
- color: #a0aec0;
- cursor: pointer;
- transition: all 0.3s;
- border-bottom: 2px solid transparent;
- }
-
- .tab-button.active {
- color: #4a9d9c;
- border-bottom: 2px solid #4a9d9c;
- }
-
- .tab-content {
- display: none;
- }
-
- .tab-content.active {
- display: block;
- }
-
- /* 修复弹窗背景色 - 使用更强的选择器 */
- #addProblemDialog .modal-content {
- background-color: #1d2e3d !important;
- color: #ffffff !important;
- }
-
- #addProblemDialog .tab-content,
- #addProblemDialog .form-group {
- background-color: #1d2e3d !important;
- color: #ffffff !important;
- }
-
- #addProblemDialog input.form-control,
- #addProblemDialog select.form-control {
- background-color: #2d3e4d !important;
- color: #ffffff !important;
- border: 1px solid #3a4a5c !important;
- }
-
- #addProblemDialog input.form-control::placeholder {
- color: #8096a8 !important;
- }
-
- #addProblemDialog label {
- color: #a0aec0 !important;
- }
- `;
- document.head.appendChild(style);
-
- // 点击添加按钮显示弹窗
- addButton.addEventListener('click', () => {
- toggleAddProblemDialog(true);
- });
-
- // 选项卡切换功能
- const urlTabButton = document.getElementById('urlTabButton');
- const manualTabButton = document.getElementById('manualTabButton');
- const urlTab = document.getElementById('urlTab');
- const manualTab = document.getElementById('manualTab');
-
- if (urlTabButton && manualTabButton) {
- urlTabButton.addEventListener('click', () => {
- urlTabButton.classList.add('active');
- manualTabButton.classList.remove('active');
- urlTab.classList.add('active');
- manualTab.classList.remove('active');
- });
-
- manualTabButton.addEventListener('click', () => {
- manualTabButton.classList.add('active');
- urlTabButton.classList.remove('active');
- manualTab.classList.add('active');
- urlTab.classList.remove('active');
- });
- }
-
- // 取消按钮
- const cancelButton = document.getElementById('cancelAdd');
- if (cancelButton) {
- cancelButton.addEventListener('click', () => {
- toggleAddProblemDialog(false);
- });
- }
-
- // 确认添加按钮
- const confirmButton = document.getElementById('confirmAdd');
- if (confirmButton) {
- confirmButton.addEventListener('click', async () => {
- try {
- let result;
-
- // 判断当前激活的是哪个选项卡
- if (urlTab.classList.contains('active')) {
- // 从URL添加
- const url = document.getElementById('problemUrl').value.trim();
- if (!url) {
- throw new Error('Please enter a valid problem URL.');
- }
- result = await handleAddProblem(url);
- } else {
- // 创建空白卡片
- const name = document.getElementById('problemName').value.trim();
- const level = document.getElementById('problemLevel').value;
- const customUrl = document.getElementById('customUrl').value.trim();
-
- if (!name) {
- throw new Error('Please enter the problem name.');
- }
-
- if (!level) {
- throw new Error('Please select a difficulty level.');
- }
-
- // 如果提供了URL,检查其格式是否有效
- if (customUrl && !customUrl.match(/^https?:\/\/.+/)) {
- throw new Error('Please enter a valid URL starting with http:// or https://');
- }
-
- result = await handleAddBlankProblem(name, level, customUrl);
- }
-
- toggleAddProblemDialog(false);
- await loadDailyReviewData();
- updateCardDisplay();
-
- // 显示成功提示
- Swal.fire({
- icon: 'success',
- title: 'SUCCESS',
- text: 'Problem added to review list.',
- showConfirmButton: false,
- timer: 1500,
- background: '#1d2e3d',
- color: '#ffffff',
- toast: true,
- position: 'center-end',
- customClass: {
- popup: 'colored-toast'
- }
- });
- } catch (error) {
- // 显示错误提示
- Swal.fire({
- icon: 'error',
- title: 'ADD FAIL',
- text: error.message,
- background: '#1d2e3d',
- color: '#ffffff',
- confirmButtonColor: '#4a9d9c'
- });
- }
- });
- }
-
- // 点击弹窗外部关闭弹窗
- const dialog = document.getElementById('addProblemDialog');
- if (dialog) {
- dialog.addEventListener('click', (e) => {
- if (e.target === dialog) {
- toggleAddProblemDialog(false);
- }
- });
- }
-}
-
-// 显示弹窗函数
-function showModal(title, content) {
- Swal.fire({
- title: title,
- html: content,
- background: '#1d2e3d',
- color: '#ffffff',
- confirmButtonColor: '#4a9d9c',
- width: '600px'
- });
-}
-
-// 初始化FSRS参数优化卡片
-async function initializeFSRSOptimization() {
- try {
- // 获取并显示复习记录数量
- const count = await getRevlogCount();
- const revlogCountElement = document.getElementById('revlogCount');
- if (revlogCountElement) {
- revlogCountElement.textContent = count;
- }
-
- // 添加优化按钮点击事件
- const optimizeParamsBtn = document.getElementById('optimizeParamsBtn');
- if (optimizeParamsBtn) {
- optimizeParamsBtn.addEventListener('click', async () => {
- // 将originalText变量移到try块之外
- const originalText = optimizeParamsBtn.textContent || '优化参数';
-
- try {
- // 显示加载中提示
- optimizeParamsBtn.disabled = true;
- optimizeParamsBtn.innerHTML = ' 优化中...';
-
- // 创建进度显示元素
- const progressContainer = document.createElement('div');
- progressContainer.className = 'progress mt-2';
- progressContainer.style.height = '5px';
- progressContainer.innerHTML = `
-
-
- `;
- optimizeParamsBtn.parentNode.appendChild(progressContainer);
-
- // 进度回调函数
- const onProgress = (progress) => {
- console.log('Progress update:', progress);
- const percent = Math.round(progress.percent * 100);
- const progressBar = progressContainer.querySelector('.progress-bar');
- if (progressBar) {
- progressBar.style.width = `${percent}%`;
- progressBar.setAttribute('aria-valuenow', percent);
- progressBar.textContent = `${percent}%`;
- }
- };
-
- // 调用优化API
- const result = await optimizeParameters(onProgress);
-
- // 移除进度显示元素
- progressContainer.remove();
-
- // 显示结果弹窗
- showModal('FSRS参数优化结果', `
-
-
${JSON.stringify(result, null, 2)}
-
- `);
- } catch (error) {
- console.error('Error optimizing FSRS parameters:', error);
- showModal('错误', `优化参数时发生错误: ${error.message}`);
- } finally {
- // 恢复按钮状态
- optimizeParamsBtn.disabled = false;
- optimizeParamsBtn.textContent = originalText;
- }
- });
- }
- } catch (error) {
- console.error('Error initializing FSRS optimization:', error);
- }
-}
-
-// 添加设置相关的初始化函数
-async function initializeOptions() {
- await loadConfigs();
-
- const optionsForm = document.getElementById('optionsForm');
- if (!optionsForm) return; // 如果找不到表单元素,直接返回
-
- // 初始化题目排序选择器
- const problemSorterSelect = document.getElementById('problemSorterSelect');
- if (problemSorterSelect) {
- const problemSorterMetaArr = problemSorterArr.map(sorter => ({
- id: idOf(sorter),
- text: descriptionOf(sorter)
- }));
-
- problemSorterMetaArr.forEach(sorterMeta => {
- const optionElement = document.createElement('option');
- optionElement.value = sorterMeta.id;
- optionElement.textContent = sorterMeta.text;
- problemSorterSelect.append(optionElement);
- });
- }
-
- // 初始化云同步开关
- const syncToggle = document.getElementById('syncToggle');
- if (syncToggle) {
- syncToggle.checked = store.isCloudSyncEnabled || false;
- }
-
-
- // 初始化提醒开关
- const reminderToggle = document.getElementById('reminderToggle');
- if (reminderToggle) {
- reminderToggle.checked = store.isReminderEnabled || false;
- }
-
- // 初始化FSRS参数优化卡片
- await initializeFSRSOptimization();
-
- // 修改保存成功提示
- optionsForm.addEventListener('submit', async e => {
- e.preventDefault();
- const selectedSorterId = problemSorterSelect.value;
- const isCloudSyncEnabled = syncToggle.checked;
- const isReminderEnabled = reminderToggle.checked;
-
- await setProblemSorter(Number(selectedSorterId));
- await setCloudSyncEnabled(isCloudSyncEnabled);
- await setReminderEnabled(isReminderEnabled);
-
- // 使用 SweetAlert2 显示保存成功提示
- Swal.fire({
- icon: 'success',
- title: '设置已保存',
- text: '您的设置已成功更新',
- showConfirmButton: false,
- timer: 1500,
- background: '#1d2e3d',
- color: '#ffffff',
- toast: true,
- position: 'center-end',
- customClass: {
- popup: 'colored-toast'
- }
- });
- });
-}
-
-
-
-// 初始化函数
-export async function initializeReviewPage() {
- console.log('初始化复习页面');
- // 首先加载配置
- await loadConfigs();
- console.log('加载的默认卡片数量:', store.defaultCardLimit);
- await loadDailyReviewData(); // 加载真实数据
- const gearButtons = document.querySelectorAll('.gear-button');
- gearButtons.forEach(button => {
- button.replaceWith(button.cloneNode(true));
- });
-
-
- // 绑定齿轮按钮事件
- document.querySelectorAll('.gear-button').forEach(button => {
- button.addEventListener('click', function() {
- console.log('齿轮按钮被点击');
- const delta = this.classList.contains('left') ? -1 : 1;
- changeCardLimit(delta);
- });
- });
-
- // 绑定卡片数量输入框变化事件
- const cardLimitInput = document.getElementById('cardLimit');
- cardLimitInput.addEventListener('change', function() {
- console.log('卡片数量改变');
- updateCardDisplay();
- });
-
- // 初始化显示
- setCurrentDate();
- updateStats();
- // updateCardLimitDisplay();
- createReviewCards();
- initializeAddProblem();
-}
-
-export function initializeFeedbackButton() {
- const button = document.querySelector('.feedback-btn'); // 使用新的类名
- if (!button) return;
-
- button.addEventListener('mouseenter', function() {
- this.style.background = '#1a3244';
- this.style.borderColor = '#61dafb';
- this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(2px)';
- this.querySelector('i').style.color = '#61dafb';
- });
-
- button.addEventListener('mouseleave', function() {
- this.style.background = '#1d2e3d';
- this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
- this.style.boxShadow = 'none';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(0)';
- this.querySelector('i').style.color = '#61dafb';
- });
- const buttonReview = document.querySelector('.feedback-btn-review'); // 使用新的类名
- if (!buttonReview) return;
-
- buttonReview.addEventListener('mouseenter', function() {
- this.style.background = '#1a3244';
- this.style.borderColor = '#61dafb';
- this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(2px)';
- this.querySelector('i').style.color = '#61dafb';
- });
-
- buttonReview.addEventListener('mouseleave', function() {
- this.style.background = '#1d2e3d';
- this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
- this.style.boxShadow = 'none';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(0)';
- this.querySelector('i').style.color = '#61dafb';
- });
-}
-
-
-
-// 页面切换功能
-document.addEventListener('DOMContentLoaded', async function() {
- console.log('DOM加载完成,开始初始化复习页面和切换绑定');
- await initializeReviewPage();
- // 添加设置初始化
- initializeFeedbackButton();
-
-
- // 检查是否找到导航按钮
- const navButtons = document.querySelectorAll('.nav-btn');
- console.log('找到导航按钮数量:', navButtons.length);
-
- // 检查是否找到视图
- const views = document.querySelectorAll('.view');
- console.log('找到视图数量:', views.length);
-
- // 打印所有视图的ID
- views.forEach(view => console.log('视图ID:', view.id));
-
- navButtons.forEach((button, index) => {
- console.log(`为第 ${index + 1} 个按钮绑定点击事件:`, button.textContent);
-
- button.addEventListener('click', async function(e) {
- e.preventDefault(); // 阻止默认行为
- e.stopPropagation(); // 阻止事件冒泡
-
- console.log('按钮被点击:', this.textContent);
-
- // 移除所有按钮的激活状态
- navButtons.forEach(btn => btn.classList.remove('active'));
- // 添加当前按钮的激活状态
- this.classList.add('active');
-
- // 获取目标视图
- const targetView = this.textContent.trim();
- console.log('目标视图:', targetView);
-
- let viewId;
- switch(targetView) {
- case 'Review':
- viewId = 'reviewView';
- await initializeReviewPage();
- break;
- case 'Problems':
- viewId = 'problemListView';
- await loadProblemList(); // 加载题目列表
- initializeFeedbackButton();
- // renderAll();
- break;
- case 'Settings':
- viewId = 'moreView';
- await initializeOptions();
- break;
- }
-
- console.log('切换到视图ID:', viewId);
-
- // 切换视图
- views.forEach(view => {
- console.log('检查视图:', view.id);
- if(view.id === viewId) {
- view.classList.add('active');
- view.style.display = 'block';
- console.log('激活视图:', view.id);
- } else {
- view.classList.remove('active');
- view.style.display = 'none';
- console.log('隐藏视图:', view.id);
- }
- });
- });
- });
-
- // 调试 revlogs
- try {
- console.log('===== 开始调试 revlogs =====');
- const allRevlogs = await getAllRevlogs();
- console.log('所有复习日志:', allRevlogs);
-
- // 计算总复习次数
- let totalReviews = 0;
- Object.keys(allRevlogs).forEach(cardId => {
- totalReviews += allRevlogs[cardId]?.length || 0;
- });
- console.log(`总复习次数: ${totalReviews}`);
-
- // 导出 CSV 并打印
- const csvContent = await exportRevlogsToCSV();
- console.log('CSV 格式的复习日志:');
- console.log(csvContent);
- console.log('===== 结束调试 revlogs =====');
- } catch (error) {
- console.error('调试 revlogs 时出错:', error);
- }
-});
-
-
-
-
-
-
-
-
-// 以防万一,也添加 window.onload
-window.onload = function() {
- console.log('页面完全加载完成');
- if (!document.querySelector('.review-card')) {
- console.log('卡片未创建,重新初始化');
- setCurrentDate();
- updateStats();
- updateCardLimitDisplay();
- createReviewCards();
- }
-
-
-};
+import { renderAll } from './view/view.js';
+import { getAllProblems, syncProblems } from "./service/problemService.js";
+import { getLevelColor,getCurrentRetrievability } from './util/utils.js';
+import { handleFeedbackSubmission, handleAddBlankProblem } from './script/submission.js';
+import './popup.css';
+import { isCloudSyncEnabled, loadConfigs, setCloudSyncEnabled, setProblemSorter,setDefaultCardLimit,setReminderEnabled } from "./service/configService";
+import { store,daily_store } from './store';
+import { optionPageFeedbackMsgDOM } from './util/doms';
+import { descriptionOf, idOf, problemSorterArr } from "./util/sort";
+import {handleAddProblem} from "./script/submission.js"
+// 在文件顶部导入 SweetAlert2
+import Swal from 'sweetalert2';
+// 导入 getAllRevlogs 函数
+import { getAllRevlogs, exportRevlogsToCSV,saveFSRSParams } from './util/fsrs.js';
+import { getRevlogCount, optimizeParameters,updateFSRSInstance } from './service/fsrsService.js';
+
+// 在文件开头添加
+const LAST_AVERAGE_KEY = 'lastRetrievabilityAverage';
+const LAST_UPDATE_TIME_KEY = 'lastUpdateTime';
+let yesterdayRetrievabilityAverage = 0.00;
+
+
+
+async function loadProblemList() {
+ await renderAll();
+}
+
+
+// 获取上次存储的平均值和时间
+function loadLastAverageData() {
+ const lastData = {
+ average: parseFloat(localStorage.getItem(LAST_AVERAGE_KEY)) || 0.00,
+ timestamp: parseInt(localStorage.getItem(LAST_UPDATE_TIME_KEY)) || 0
+ };
+ return lastData;
+}
+
+async function loadDailyReviewData() {
+ const problems = Object.values(await getAllProblems()).filter(p => p.isDeleted !== true);
+ daily_store.reviewScheduledProblems = problems
+ .sort((a, b) => {
+ const retrievabilityA = getCurrentRetrievability(a);
+ const retrievabilityB = getCurrentRetrievability(b);
+ return retrievabilityA - retrievabilityB; // 升序排序,最小值在前
+ });
+
+ // 获取今天已复习和待复习的题目
+ daily_store.dailyReviewProblems = daily_store.reviewScheduledProblems
+ .filter(problem => isReviewedToday(problem) || isReviewDueToday(problem))
+ .sort((a, b) => {
+ // 首先按照是否已复习排序(已复习的排在前面)
+ const aReviewed = isReviewedToday(a);
+ const bReviewed = isReviewedToday(b);
+ if (aReviewed !== bReviewed) {
+ return bReviewed ? 1 : -1;
+ }
+ // 如果复习状态相同,则按可检索性排序
+ const retrievabilityA = getCurrentRetrievability(a);
+ const retrievabilityB = getCurrentRetrievability(b);
+ return retrievabilityA - retrievabilityB;
+ });
+
+
+ console.log('总题目数:', problems.length);
+ console.log('今日待复习题目数:', daily_store.dailyReviewProblems.length);
+
+ // 添加调试日志
+ daily_store.dailyReviewProblems.forEach(problem => {
+ const isReviewed = isReviewedToday(problem);
+ const isDue = isReviewDueToday(problem);
+ console.log('题目状态:', {
+ name: problem.name,
+ lastReview: problem.fsrsState?.lastReview,
+ nextReview: problem.fsrsState?.nextReview,
+ isReviewedToday: isReviewed,
+ isDueToday: isDue,
+ retrievability: getCurrentRetrievability(problem)
+ });
+ });
+}
+
+// 存储当前的平均值和时间
+function saveCurrentAverageData(average) {
+ localStorage.setItem(LAST_AVERAGE_KEY, average.toString());
+ localStorage.setItem(LAST_UPDATE_TIME_KEY, Date.now().toString());
+}
+
+// 设置当前日期
+function setCurrentDate() {
+ const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
+ const today = new Date().toLocaleDateString('en-US', options);
+ document.getElementById('currentDate').textContent = today;
+}
+
+
+// 判断是否是今天需要复习的题目
+function isReviewDueToday(problem) {
+ if (!problem.fsrsState?.nextReview) {
+ console.log('题目没有下次复习时间:', problem.name);
+ return false;
+ }
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ const nextReview = new Date(problem.fsrsState.nextReview);
+ nextReview.setHours(0, 0, 0, 0);
+
+ const isDue = nextReview <= today;
+
+ console.log('复习时间检查:', {
+ problemName: problem.name,
+ nextReview: nextReview.toISOString(),
+ today: today.toISOString(),
+ isDue: isDue
+ });
+
+ return isDue;
+}
+
+function isReviewedToday(problem) {
+ if (!problem.fsrsState?.lastReview) return false;
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ const lastReview = new Date(problem.fsrsState.lastReview);
+ lastReview.setHours(0, 0, 0, 0);
+
+ return lastReview.getTime() === today.getTime();
+}
+
+
+
+
+// 计算可检索性均值
+function calculateRetrievabilityAverage() {
+ const problems = daily_store.reviewScheduledProblems;
+ if (!problems || problems.length === 0) return 0;
+
+ const totalRetrievability = problems.reduce((sum, problem) => {
+ const retrievability = getCurrentRetrievability(problem);
+ return sum + retrievability;
+ }, 0);
+
+ return Number((totalRetrievability / problems.length).toFixed(2));
+}
+
+
+// 更新顶部统计信息
+function updateStats() {
+ console.log('更新统计信息');
+ // 设置默认值
+ let completedCount = 0;
+ let totalProblems = 0;
+ // 添加空值检查
+ if (!daily_store || !daily_store.dailyReviewProblems) {
+ console.log('daily_store 或 dailyReviewProblems 为空:', {
+ daily_store: daily_store,
+ problems: daily_store?.dailyReviewProblems
+ });
+
+
+ // 更新显示
+ document.getElementById('completedCount').textContent = completedCount;
+ document.getElementById('totalCount').textContent = totalProblems;
+ document.getElementById('completionRate').textContent = '0%';
+ updateProgressCircle(0);
+ return;
+ }
+
+
+
+ // 获取当前显示的卡片数量
+ let cardLimit = parseInt(document.getElementById('cardLimit').value, 10)|| store.defaultCardLimit || 1;
+ console.log('当前卡片限制值:', {
+ rawValue: document.getElementById('cardLimit').value,
+ parsedCardLimit: cardLimit,
+ element: document.getElementById('cardLimit')
+ });
+
+
+
+ // 计算今日已复习的题目数量
+ completedCount = daily_store.dailyReviewProblems.filter(problem =>
+ isReviewedToday(problem)
+ ).length;
+
+
+ totalProblems = daily_store.dailyReviewProblems?.length || 0;
+ if (cardLimit > totalProblems) {
+ cardLimit = totalProblems;
+ }
+
+ // 添加空状态提示
+ const addProblemWrapper = document.querySelector('.add-problem-wrapper');
+ // 先移除可能存在的空状态提示
+ const existingEmptyState = document.querySelector('.empty-state');
+ if (existingEmptyState) {
+ existingEmptyState.remove();
+ }
+
+ if (totalProblems === 0 || cardLimit === 0) {
+ const emptyState = document.createElement('div');
+ emptyState.className = 'empty-state';
+ emptyState.innerHTML = `
+
+
+ Time to learn something new!
+ `;
+ addProblemWrapper.insertAdjacentElement('beforebegin', emptyState);
+ }
+
+
+ // 更新显示的已复习数量
+ document.getElementById('completedCount').textContent = completedCount;
+ document.getElementById('totalCount').textContent = cardLimit; // 使用当前的卡片数量
+
+ // 更新进度条
+ const completionRate = cardLimit > 0 ? Math.round((completedCount / cardLimit) * 100) : 0;
+ updateProgressCircle(completionRate);
+ document.getElementById('completionRate').textContent = `${completionRate}%`;
+ // document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
+ // 计算当前的可检索性均值,并确保是数字类型
+ const currentRetrievabilityAverage = parseFloat(calculateRetrievabilityAverage()) || 0;
+ console.log('当前可检索性均值:', {
+ raw: calculateRetrievabilityAverage(),
+ parsed: currentRetrievabilityAverage,
+ type: typeof currentRetrievabilityAverage
+ });
+ const retrievabilityElement = document.getElementById('retrievabilityAverage');
+ retrievabilityElement.textContent = currentRetrievabilityAverage;
+
+
+ // 获取上次存储的数据
+ const lastData = loadLastAverageData();
+ const hoursSinceLastUpdate = (Date.now() - lastData.timestamp) / (1000 * 60 * 60);
+
+ // 如果超过24小时,更新存储的数据
+ if (hoursSinceLastUpdate >= 24) {
+ console.log('距离上次更新已超过24小时:', {
+ hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
+ lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
+ lastAverage: lastData.average.toFixed(2),
+ currentAverage: currentRetrievabilityAverage.toFixed(2)
+ });
+
+ yesterdayRetrievabilityAverage = lastData.average;
+ saveCurrentAverageData(currentRetrievabilityAverage);
+
+ console.log('已更新存储数据:', {
+ newYesterdayAverage: yesterdayRetrievabilityAverage.toFixed(2),
+ savedCurrentAverage: currentRetrievabilityAverage.toFixed(2),
+ saveTime: new Date().toLocaleString()
+ });
+ } else {
+ console.log('距离上次更新未超过24小时:', {
+ hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
+ lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
+ usingLastAverage: lastData.average.toFixed(2)
+ });
+ yesterdayRetrievabilityAverage = lastData.average;
+ }
+
+ // 更新趋势图标
+ const trendIcon = document.getElementById('trendIcon');
+ if (currentRetrievabilityAverage > yesterdayRetrievabilityAverage) {
+ trendIcon.className = 'fas fa-arrow-up trend-icon trend-up';
+ } else if (currentRetrievabilityAverage < yesterdayRetrievabilityAverage) {
+ trendIcon.className = 'fas fa-arrow-down trend-icon trend-down';
+ } else {
+ trendIcon.className = '';
+ }
+
+ // 根据可检索性均值调整颜色和背景提示
+ const lowMemoryWarning = document.getElementById('lowMemoryWarning');
+ if (currentRetrievabilityAverage < 0.90) {
+ retrievabilityElement.classList.add('low');
+ lowMemoryWarning.classList.add('active');
+ } else {
+ retrievabilityElement.classList.remove('low');
+ lowMemoryWarning.classList.remove('active');
+ }
+ updateCardLimitDisplay(); // 这里也添加一次调用
+}
+
+function updateProgressCircle(completionRate) {
+ const progressCircle = document.querySelector('.completion-circle');
+ const radius = 54; // 圆的半径
+ const circumference = 2 * Math.PI * radius; // 圆的周长
+
+ // 计算偏移量
+ const offset = circumference - (completionRate / 100) * circumference;
+ progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;
+ progressCircle.style.strokeDashoffset = offset;
+
+ // 更新显示的百分比
+ // document.getElementById('completionRate').textContent = `${completionRate}%`;
+ document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
+
+ // 添加动画效果
+ const innerCircle = document.querySelector('.inner-circle');
+ innerCircle.style.transform = `scale(1.1)`; // 放大内圈
+ setTimeout(() => {
+ innerCircle.style.transform = `scale(1)`; // 恢复原状
+ }, 500); // 动画持续时间
+}
+
+
+
+
+// 更新卡片限制和显示
+export function updateCardLimitDisplay() {
+ const input = document.getElementById('cardLimit');
+ const totalDisplay = document.querySelector('.total-problems');
+ const totalProblems = daily_store.dailyReviewProblems?.length || 0;
+
+ // 更新最大值和总数显示
+ input.max = Math.max(totalProblems, 1);
+ totalDisplay.textContent = `/ ${totalProblems}`;
+
+ // 使用保存的默认值或回退到3
+ let currentValue = store.defaultCardLimit || 1;
+ if (currentValue > totalProblems && totalProblems > 0) {
+ currentValue = totalProblems;
+ // store.defaultCardLimit = totalProblems;
+ // setDefaultCardLimit(totalProblems);
+ }
+ input.value = currentValue;
+
+ // 禁用条件
+ if (totalProblems === 0) {
+ input.value = 0;
+ input.disabled = true;
+ totalDisplay.textContent = "/ 0";
+ } else {
+ input.disabled = false;
+ }
+
+ console.log('更新卡片限制显示:', {
+ currentValue: input.value,
+ max: input.max,
+ totalProblems
+ });
+}
+
+// 更新卡片显示
+export function updateCardDisplay() {
+ console.log('更新卡片显示');
+
+ updateStats(); // 更新统计信息,传递当前显示的卡片数量
+
+
+ createReviewCards(); // 创建新的卡片
+}
+
+
+
+
+// 改变卡片数量
+// 所有功能函数
+export async function changeCardLimit(delta) {
+ console.log('执行 changeCardLimit, delta:', delta);
+ const input = document.getElementById('cardLimit');
+ const currentValue = parseInt(input.value, 10);
+ const newValue = currentValue + delta;
+ const maxValue = daily_store.dailyReviewProblems?.length || 0;
+
+ if (newValue >= 1 && newValue <= maxValue) {
+ input.value = newValue;
+ await setDefaultCardLimit(newValue);
+ store.defaultCardLimit = newValue;
+ updateCardDisplay();
+ }
+}
+
+
+
+
+// 标记题目为已复习
+async function markAsReviewed(button, problem) {
+ console.log('执行 markAsReviewed', button, problem);
+
+ const card = button.closest('.review-card');
+ if (!card) {
+ console.log('未找到对应的卡片');
+ return;
+ }
+
+ console.log('找到卡片,开始更新状态');
+
+ // 更换图标并更改样式
+ const icon = button.querySelector('i');
+ icon.classList.remove('fa-check-circle');
+ icon.classList.add('fa-circle-check');
+ icon.style.color = '#0D6E6E';
+
+ // 禁用按钮
+ button.disabled = true;
+ card.style.opacity = '0.4';
+
+
+
+ // 更新统计信息
+ updateCardDisplay();
+ console.log('更新完成');
+}
+
+
+// 创建题目卡片时的事件绑定
+function createReviewCards() {
+ console.log('开始创建卡片');
+ const reviewList = document.getElementById('reviewList');
+ const template = document.getElementById('reviewCardTemplate');
+ const cardLimit = parseInt(document.getElementById('cardLimit').value, 10);
+
+ reviewList.innerHTML = '';
+
+ const problems = daily_store.dailyReviewProblems || [];
+ problems.slice(0, cardLimit).forEach((problem, index) => {
+ const cardNode = template.content.cloneNode(true);
+ const card = cardNode.querySelector('.review-card');
+
+ // 安全地访问 fsrsState
+ const fsrsState = problem.fsrsState || {};
+
+
+ // 设置题目信息
+ const problemName = card.querySelector('.problem-name');
+ problemName.textContent = problem.name || 'unknown';
+
+ // 设置难度和复习信息
+ const difficultySpan = card.querySelector('.difficulty');
+ const level = problem.level || 'Unknown';
+ difficultySpan.textContent = level;
+ // 使用现有的 CSS 类
+ difficultySpan.classList.add(`difficulty-${level}`);
+
+ // 设置可检索性
+ const retrievability = getCurrentRetrievability(problem);
+ const retrievabilitySpan = card.querySelector('.retrievability');
+ retrievabilitySpan.textContent = `${retrievability.toFixed(1)}`;
+ retrievabilitySpan.classList.add(retrievability < 0.9 ? 'text-danger' : 'text-success');
+
+
+ // 设置下次复习时间
+ const nextReviewTips = fsrsState.nextReview
+ ? (() => {
+ const nextReviewDate = new Date(fsrsState.nextReview);
+ const now = new Date();
+
+ // 获取当前日期和下次复习日期(不含时间)
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const reviewDay = new Date(nextReviewDate.getFullYear(), nextReviewDate.getMonth(), nextReviewDate.getDate());
+
+ // 计算日期差(天数)
+ const diffTime = reviewDay.getTime() - today.getTime();
+ const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays < 0) {
+ // 已经过了计划复习日期
+ const daysOverdue = Math.abs(diffDays);
+ return `Delay by ${daysOverdue} day${daysOverdue > 1 ? 's' : ''}`;
+ } else if (diffDays === 0) {
+ // 今天需要复习
+ return 'Review today';
+ } else if (diffDays === 1) {
+ // 明天需要复习
+ return 'Review tomorrow';
+ } else {
+ // x天后复习
+ return `Review in ${diffDays} days`;
+ }
+ })()
+ : 'Not scheduled';
+ card.querySelector('.next-review').textContent = nextReviewTips;
+
+ // 格式化上次复习时间
+ const lastReviewText = fsrsState.lastReview
+ ? new Date(fsrsState.lastReview).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ : 'Never reviewed';
+
+ // 格式化上次复习时间
+ const nextReviewText = fsrsState.nextReview
+ ? new Date(fsrsState.nextReview).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ : 'Never reviewed';
+
+
+
+ // 设置hover提示
+ const tooltipContent = [
+ `Last Review: ${lastReviewText}`,
+ `Next Review: ${nextReviewText}`,
+ problem.url ? 'Click to open problem' : ''
+ ].filter(Boolean).join('\n');
+
+ card.title = tooltipContent;
+
+ // 检查今日是否已复习
+ const isReviewedToday = fsrsState.lastReview &&
+ new Date(fsrsState.lastReview).toDateString() === new Date().toDateString();
+
+ // 设置按钮状态
+ const reviewButton = card.querySelector('.btn-review');
+ if (reviewButton) {
+ if (isReviewedToday) {
+ const icon = reviewButton.querySelector('i');
+ icon.classList.remove('fa-check-circle');
+ icon.classList.add('fa-circle-check');
+ icon.style.color = '#0D6E6E';
+ reviewButton.disabled = true;
+ card.style.opacity = '0.4';
+ }
+
+ reviewButton.onclick = async function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('复习按钮被点击');
+
+ const updatedProblem = await handleFeedbackSubmission(problem);
+ if (updatedProblem) {
+ markAsReviewed(this, updatedProblem);
+ }
+ // markAsReviewed(this, problem); // 修改这里,传入按钮元素和问题对象
+ };
+ }
+
+ // 添加题目链接功能
+ if (problem.url) {
+ card.style.cursor = 'pointer';
+ card.onclick = function(e) {
+ if (!e.target.closest('.btn-review')) {
+ window.open(problem.url, '_blank');
+ }
+ };
+ }
+
+ reviewList.appendChild(cardNode);
+ });
+}
+
+
+
+
+
+
+
+
+
+
+
+
+// 显示/隐藏添加题目弹窗
+function toggleAddProblemDialog(show) {
+ const dialog = document.getElementById('addProblemDialog');
+ if (!dialog) return;
+
+ if (show) {
+ dialog.style.display = 'block';
+ } else {
+ dialog.style.display = 'none';
+
+ // 清除所有输入字段
+ const problemUrl = document.getElementById('problemUrl');
+ const problemName = document.getElementById('problemName');
+ const customUrl = document.getElementById('customUrl');
+
+ if (problemUrl) problemUrl.value = '';
+ if (problemName) problemName.value = '';
+ if (customUrl) customUrl.value = '';
+
+ // 重置选项卡到默认状态
+ const urlTabButton = document.getElementById('urlTabButton');
+ const manualTabButton = document.getElementById('manualTabButton');
+ const urlTab = document.getElementById('urlTab');
+ const manualTab = document.getElementById('manualTab');
+
+ if (urlTabButton && manualTabButton && urlTab && manualTab) {
+ urlTabButton.classList.add('active');
+ manualTabButton.classList.remove('active');
+ urlTab.classList.add('active');
+ manualTab.classList.remove('active');
+ }
+ }
+}
+
+
+
+// 初始化添加题目功能
+function initializeAddProblem() {
+ const addButton = document.querySelector('.gear-button.add-problem');
+ if (!addButton) return;
+
+ // 添加选项卡切换样式
+ const style = document.createElement('style');
+ style.textContent = `
+ .tab-container {
+ margin-bottom: 15px;
+ }
+
+ .tab-buttons {
+ display: flex;
+ border-bottom: 1px solid #3a4a5c;
+ margin-bottom: 15px;
+ }
+
+ .tab-button {
+ background: none;
+ border: none;
+ padding: 8px 15px;
+ color: #a0aec0;
+ cursor: pointer;
+ transition: all 0.3s;
+ border-bottom: 2px solid transparent;
+ }
+
+ .tab-button.active {
+ color: #4a9d9c;
+ border-bottom: 2px solid #4a9d9c;
+ }
+
+ .tab-content {
+ display: none;
+ }
+
+ .tab-content.active {
+ display: block;
+ }
+
+ /* 修复弹窗背景色 - 使用更强的选择器 */
+ #addProblemDialog .modal-content {
+ background-color: #1d2e3d !important;
+ color: #ffffff !important;
+ }
+
+ #addProblemDialog .tab-content,
+ #addProblemDialog .form-group {
+ background-color: #1d2e3d !important;
+ color: #ffffff !important;
+ }
+
+ #addProblemDialog input.form-control,
+ #addProblemDialog select.form-control {
+ background-color: #2d3e4d !important;
+ color: #ffffff !important;
+ border: 1px solid #3a4a5c !important;
+ }
+
+ #addProblemDialog input.form-control::placeholder {
+ color: #8096a8 !important;
+ }
+
+ #addProblemDialog label {
+ color: #a0aec0 !important;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // 点击添加按钮显示弹窗
+ addButton.addEventListener('click', () => {
+ toggleAddProblemDialog(true);
+ });
+
+ // 选项卡切换功能
+ const urlTabButton = document.getElementById('urlTabButton');
+ const manualTabButton = document.getElementById('manualTabButton');
+ const urlTab = document.getElementById('urlTab');
+ const manualTab = document.getElementById('manualTab');
+
+ if (urlTabButton && manualTabButton) {
+ urlTabButton.addEventListener('click', () => {
+ urlTabButton.classList.add('active');
+ manualTabButton.classList.remove('active');
+ urlTab.classList.add('active');
+ manualTab.classList.remove('active');
+ });
+
+ manualTabButton.addEventListener('click', () => {
+ manualTabButton.classList.add('active');
+ urlTabButton.classList.remove('active');
+ manualTab.classList.add('active');
+ urlTab.classList.remove('active');
+ });
+ }
+
+ // 取消按钮
+ const cancelButton = document.getElementById('cancelAdd');
+ if (cancelButton) {
+ cancelButton.addEventListener('click', () => {
+ toggleAddProblemDialog(false);
+ });
+ }
+
+ // 确认添加按钮
+ const confirmButton = document.getElementById('confirmAdd');
+ if (confirmButton) {
+ confirmButton.addEventListener('click', async () => {
+ try {
+ let result;
+
+ // 判断当前激活的是哪个选项卡
+ if (urlTab.classList.contains('active')) {
+ // 从URL添加
+ const url = document.getElementById('problemUrl').value.trim();
+ if (!url) {
+ throw new Error('Please enter a valid problem URL.');
+ }
+ result = await handleAddProblem(url);
+ } else {
+ // 创建空白卡片
+ const name = document.getElementById('problemName').value.trim();
+ const level = document.getElementById('problemLevel').value;
+ const customUrl = document.getElementById('customUrl').value.trim();
+
+ if (!name) {
+ throw new Error('Please enter the problem name.');
+ }
+
+ if (!level) {
+ throw new Error('Please select a difficulty level.');
+ }
+
+ // 如果提供了URL,检查其格式是否有效
+ if (customUrl && !customUrl.match(/^https?:\/\/.+/)) {
+ throw new Error('Please enter a valid URL starting with http:// or https://');
+ }
+
+ result = await handleAddBlankProblem(name, level, customUrl);
+ }
+
+ toggleAddProblemDialog(false);
+ await loadDailyReviewData();
+ updateCardDisplay();
+
+ // 显示成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'SUCCESS',
+ text: 'Problem added to review list.',
+ showConfirmButton: false,
+ timer: 1500,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ }
+ });
+ } catch (error) {
+ // 显示错误提示
+ Swal.fire({
+ icon: 'error',
+ title: 'ADD FAIL',
+ text: error.message,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c'
+ });
+ }
+ });
+ }
+
+ // 点击弹窗外部关闭弹窗
+ const dialog = document.getElementById('addProblemDialog');
+ if (dialog) {
+ dialog.addEventListener('click', (e) => {
+ if (e.target === dialog) {
+ toggleAddProblemDialog(false);
+ }
+ });
+ }
+}
+
+// 显示弹窗函数
+function showModal(title, content, buttons = null) {
+ const modalOptions = {
+ title: title,
+ html: content,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c',
+ width: '600px'
+ };
+
+ // 如果有自定义按钮,则使用自定义按钮
+ if (buttons && Array.isArray(buttons)) {
+ modalOptions.showConfirmButton = false;
+ modalOptions.showCloseButton = true;
+ modalOptions.html += `
+
+ ${buttons.map(btn => `
+
+ ${btn.text}
+
+ `).join('')}
+
+ `;
+
+ // 使用SweetAlert2显示模态框
+ Swal.fire(modalOptions);
+
+ // 为每个按钮添加点击事件 - 移到这里,在Swal.fire之后立即绑定
+ setTimeout(() => {
+ buttons.forEach(btn => {
+ const btnElement = document.getElementById(`modal-btn-${btn.text}`);
+ if (btnElement && btn.onClick) {
+ btnElement.addEventListener('click', async (e) => {
+ e.preventDefault();
+ try {
+ // 执行按钮点击事件处理程序
+ await btn.onClick();
+ // 关闭弹窗
+ Swal.close();
+ } catch (error) {
+ console.error('按钮点击事件处理程序执行失败:', error);
+ }
+ });
+ }
+ });
+ }, 100); // 添加一个小延迟确保DOM已更新
+ } else {
+ // 如果没有自定义按钮,则使用默认按钮
+ modalOptions.showConfirmButton = true;
+ modalOptions.confirmButtonText = '确定';
+
+ // 使用SweetAlert2显示模态框
+ Swal.fire(modalOptions);
+ }
+}
+
+// 初始化FSRS参数优化卡片
+async function initializeFSRSOptimization() {
+ try {
+ // 获取并显示复习记录数量
+ const count = await getRevlogCount();
+ const revlogCountElement = document.getElementById('revlogCount');
+ const revlogCountEnElement = document.getElementById('revlogCount_en');
+ if (revlogCountElement) {
+ revlogCountElement.textContent = count;
+ }
+ if (revlogCountEnElement) {
+ revlogCountEnElement.textContent = count;
+ }
+
+ // 添加导出按钮点击事件
+ const exportRevlogsBtn = document.getElementById('exportRevlogsBtn');
+ if (exportRevlogsBtn) {
+ exportRevlogsBtn.addEventListener('click', async () => {
+ // 保存原始按钮内容
+ const originalContent = exportRevlogsBtn.innerHTML;
+
+ try {
+ // 显示加载中提示
+ exportRevlogsBtn.disabled = true;
+ exportRevlogsBtn.innerHTML = ' ';
+
+ // 导出CSV
+ const csvContent = await exportRevlogsToCSV();
+
+ // 创建下载链接
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.setAttribute('href', url);
+ link.setAttribute('download', `fsrs_revlogs_${new Date().toISOString().slice(0, 10)}.csv`);
+ link.style.display = 'none';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 显示成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'Export Success',
+ html: `
+
+ 已成功导出 ${count} 条复习记录
+
+
+ Successfully exported ${count} review records to CSV file
+
+
+ `,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ },
+ confirmButtonColor: '#4a9d9c',
+ confirmButtonText: 'OK'
+ });
+ } catch (error) {
+ console.error('Error exporting revlogs:', error);
+ Swal.fire({
+ icon: 'error',
+ title: 'Export Failed',
+ html: `
+
+ 导出复习记录时发生错误
+
+
+ Error occurred while exporting review records:
+
+
+
+ ${error.message}
+
+
+ `,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c',
+ confirmButtonText: 'OK'
+ });
+ } finally {
+ // 恢复按钮状态
+ exportRevlogsBtn.disabled = false;
+ exportRevlogsBtn.innerHTML = originalContent;
+ }
+ });
+ }
+
+ // 添加优化按钮点击事件
+ const optimizeParamsBtn = document.getElementById('optimizeParamsBtn');
+ if (optimizeParamsBtn) {
+ optimizeParamsBtn.addEventListener('click', async () => {
+ // 保存原始按钮内容
+ const originalContent = optimizeParamsBtn.innerHTML;
+
+ // 创建进度显示元素
+ const progressContainer = document.createElement('div');
+ progressContainer.className = 'progress optimize-progress';
+ progressContainer.innerHTML = `
+
+
+ `;
+ optimizeParamsBtn.parentNode.appendChild(progressContainer);
+
+ // 更改按钮状态
+ optimizeParamsBtn.disabled = true;
+ optimizeParamsBtn.innerHTML = ' ';
+
+ try {
+ // 进度回调函数
+ const onProgress = (progress) => {
+ console.log('Progress update:', progress);
+ const percent = Math.round(progress.percent * 100);
+ const progressBar = progressContainer.querySelector('.progress-bar');
+ if (progressBar) {
+ progressBar.style.width = `${percent}%`;
+ progressBar.setAttribute('aria-valuenow', percent);
+ progressBar.textContent = `${percent}%`;
+ }
+ };
+
+ // 调用优化API
+ const result = await optimizeParameters(onProgress);
+
+ // 显示结果弹窗
+ if (result && result.type === 'Success' && result.params) {
+ // 生成唯一ID
+ const detailId = `paramsDetail_${Date.now()}`;
+
+ // 显示优化后的参数,并添加保存按钮
+ const modalResult = await Swal.fire({
+ title: 'SUCCESS',
+ html: `
+
+
+ 参数优化完成!点击确认将自动保存并应用新参数。
+
+
+ Optimization done! Click OK to save and use the new settings.
+
+
+
+
+ 查看详细参数/View all parameters
+
+
+
+
${JSON.stringify(result.params, null, 2)}
+
+
+
+
+ `,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c',
+ confirmButtonText: 'OK',
+ showCloseButton: true,
+ closeButtonHtml: ' ',
+ didRender: () => {
+ // 在弹窗渲染后绑定事件
+ const toggleBtn = document.getElementById(`toggleDetail_${detailId}`);
+ const detailDiv = document.getElementById(detailId);
+ if (toggleBtn && detailDiv) {
+ toggleBtn.addEventListener('click', () => {
+ detailDiv.classList.toggle('d-none');
+ const icon = toggleBtn.querySelector('i');
+ if (icon) {
+ icon.classList.toggle('fa-chevron-right');
+ icon.classList.toggle('fa-chevron-down');
+ }
+ });
+ }
+ }
+ });
+
+ if (modalResult.isConfirmed) {
+ try {
+ // 保存参数到本地存储
+ await saveFSRSParams(result.params);
+ // 更新FSRS实例
+ await updateFSRSInstance(result.params);
+ // 显示成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'Save Success',
+ text: '参数已成功应用 /New settings applied.',
+ background: '#1d2e3d',
+ showConfirmButton: false,
+ timer: 3000,
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ }
+ });
+ } catch (error) {
+ console.error('Error saving FSRS parameters:', error);
+ Swal.fire({
+ icon: 'error',
+ title: 'Save Failed',
+ text: `Error saving parameters: ${error.message}`,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c'
+ });
+ }
+ }
+ } else {
+ // 显示其他类型的结果
+ showModal('FSRS参数优化结果', `
+
+
${JSON.stringify(result, null, 2)}
+
+ `);
+ }
+ } catch (error) {
+ console.error('Error optimizing FSRS parameters:', error);
+ showModal('Error', `Error optimizing parameters: ${error.message}`);
+ } finally {
+ // 恢复按钮状态
+ optimizeParamsBtn.disabled = false;
+ optimizeParamsBtn.innerHTML = originalContent;
+ // 移除进度条
+ progressContainer.remove();
+ }
+ });
+ }
+ } catch (error) {
+ console.error('Error initializing FSRS optimization:', error);
+ }
+}
+
+// 添加设置相关的初始化函数
+async function initializeOptions() {
+ await loadConfigs();
+
+ const optionsForm = document.getElementById('optionsForm');
+ if (!optionsForm) return; // 如果找不到表单元素,直接返回
+
+ // 初始化题目排序选择器
+ const problemSorterSelect = document.getElementById('problemSorterSelect');
+ if (problemSorterSelect) {
+ const problemSorterMetaArr = problemSorterArr.map(sorter => ({
+ id: idOf(sorter),
+ text: descriptionOf(sorter)
+ }));
+
+ problemSorterMetaArr.forEach(sorterMeta => {
+ const optionElement = document.createElement('option');
+ optionElement.value = sorterMeta.id;
+ optionElement.textContent = sorterMeta.text;
+ problemSorterSelect.append(optionElement);
+ });
+ }
+
+ // 初始化云同步开关
+ const syncToggle = document.getElementById('syncToggle');
+ if (syncToggle) {
+ syncToggle.checked = store.isCloudSyncEnabled || false;
+ }
+
+
+ // 初始化提醒开关
+ const reminderToggle = document.getElementById('reminderToggle');
+ if (reminderToggle) {
+ reminderToggle.checked = store.isReminderEnabled || false;
+ }
+
+ // 初始化FSRS参数优化卡片
+ await initializeFSRSOptimization();
+
+ // 修改保存成功提示
+ optionsForm.addEventListener('submit', async e => {
+ e.preventDefault();
+ const selectedSorterId = problemSorterSelect.value;
+ const isCloudSyncEnabled = syncToggle.checked;
+ const isReminderEnabled = reminderToggle.checked;
+
+ await setProblemSorter(Number(selectedSorterId));
+ await setCloudSyncEnabled(isCloudSyncEnabled);
+ await setReminderEnabled(isReminderEnabled);
+
+ // 使用 SweetAlert2 显示保存成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'Settings Saved',
+ text: 'Your settings have been successfully updated',
+ showConfirmButton: false,
+ timer: 1500,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ }
+ });
+ });
+}
+
+
+
+// 初始化函数
+export async function initializeReviewPage() {
+ console.log('初始化复习页面');
+ // 首先加载配置
+ await loadConfigs();
+ console.log('加载的默认卡片数量:', store.defaultCardLimit);
+ await loadDailyReviewData(); // 加载真实数据
+ const gearButtons = document.querySelectorAll('.gear-button');
+ gearButtons.forEach(button => {
+ button.replaceWith(button.cloneNode(true));
+ });
+
+
+ // 绑定齿轮按钮事件
+ document.querySelectorAll('.gear-button').forEach(button => {
+ button.addEventListener('click', function() {
+ console.log('齿轮按钮被点击');
+ const delta = this.classList.contains('left') ? -1 : 1;
+ changeCardLimit(delta);
+ });
+ });
+
+ // 绑定卡片数量输入框变化事件
+ const cardLimitInput = document.getElementById('cardLimit');
+ cardLimitInput.addEventListener('change', function() {
+ console.log('卡片数量改变');
+ updateCardDisplay();
+ });
+
+ // 初始化显示
+ setCurrentDate();
+ updateStats();
+ // updateCardLimitDisplay();
+ createReviewCards();
+ initializeAddProblem();
+}
+
+export function initializeFeedbackButton() {
+ const button = document.querySelector('.feedback-btn'); // 使用新的类名
+ if (!button) return;
+
+ button.addEventListener('mouseenter', function() {
+ this.style.background = '#1a3244';
+ this.style.borderColor = '#61dafb';
+ this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(2px)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+
+ button.addEventListener('mouseleave', function() {
+ this.style.background = '#1d2e3d';
+ this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
+ this.style.boxShadow = 'none';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(0)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+ const buttonReview = document.querySelector('.feedback-btn-review'); // 使用新的类名
+ if (!buttonReview) return;
+
+ buttonReview.addEventListener('mouseenter', function() {
+ this.style.background = '#1a3244';
+ this.style.borderColor = '#61dafb';
+ this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(2px)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+
+ buttonReview.addEventListener('mouseleave', function() {
+ this.style.background = '#1d2e3d';
+ this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
+ this.style.boxShadow = 'none';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(0)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+}
+
+
+
+// 页面切换功能
+document.addEventListener('DOMContentLoaded', async function() {
+ console.log('DOM加载完成,开始初始化复习页面和切换绑定');
+ await initializeReviewPage();
+ // 添加设置初始化
+ initializeFeedbackButton();
+
+
+ // 检查是否找到导航按钮
+ const navButtons = document.querySelectorAll('.nav-btn');
+ console.log('找到导航按钮数量:', navButtons.length);
+
+ // 检查是否找到视图
+ const views = document.querySelectorAll('.view');
+ console.log('找到视图数量:', views.length);
+
+ // 打印所有视图的ID
+ views.forEach(view => console.log('视图ID:', view.id));
+
+ navButtons.forEach((button, index) => {
+ console.log(`为第 ${index + 1} 个按钮绑定点击事件:`, button.textContent);
+
+ button.addEventListener('click', async function(e) {
+ e.preventDefault(); // 阻止默认行为
+ e.stopPropagation(); // 阻止事件冒泡
+
+ console.log('按钮被点击:', this.textContent);
+
+ // 移除所有按钮的激活状态
+ navButtons.forEach(btn => btn.classList.remove('active'));
+ // 添加当前按钮的激活状态
+ this.classList.add('active');
+
+ // 获取目标视图
+ const targetView = this.textContent.trim();
+ console.log('目标视图:', targetView);
+
+ let viewId;
+ switch(targetView) {
+ case 'Review':
+ viewId = 'reviewView';
+ await initializeReviewPage();
+ break;
+ case 'Problems':
+ viewId = 'problemListView';
+ await loadProblemList(); // 加载题目列表
+ initializeFeedbackButton();
+ // renderAll();
+ break;
+ case 'Settings':
+ viewId = 'moreView';
+ await initializeOptions();
+ break;
+ }
+
+ console.log('切换到视图ID:', viewId);
+
+ // 切换视图
+ views.forEach(view => {
+ console.log('检查视图:', view.id);
+ if(view.id === viewId) {
+ view.classList.add('active');
+ view.style.display = 'block';
+ console.log('激活视图:', view.id);
+ } else {
+ view.classList.remove('active');
+ view.style.display = 'none';
+ console.log('隐藏视图:', view.id);
+ }
+ });
+ });
+ });
+
+ // 调试 revlogs
+ try {
+ console.log('===== 开始调试 revlogs =====');
+ const allRevlogs = await getAllRevlogs();
+ console.log('所有复习日志:', allRevlogs);
+
+ // 计算总复习次数
+ let totalReviews = 0;
+ Object.keys(allRevlogs).forEach(cardId => {
+ totalReviews += allRevlogs[cardId]?.length || 0;
+ });
+ console.log(`总复习次数: ${totalReviews}`);
+
+ // 导出 CSV 并打印
+ const csvContent = await exportRevlogsToCSV();
+ console.log('CSV 格式的复习日志:');
+ console.log(csvContent);
+ console.log('===== 结束调试 revlogs =====');
+ } catch (error) {
+ console.error('调试 revlogs 时出错:', error);
+ }
+});
+
+
+
+
+
+
+
+
+// 以防万一,也添加 window.onload
+window.onload = function() {
+ console.log('页面完全加载完成');
+ if (!document.querySelector('.review-card')) {
+ console.log('卡片未创建,重新初始化');
+ setCurrentDate();
+ updateStats();
+ updateCardLimitDisplay();
+ createReviewCards();
+ }
+
+
+};
diff --git a/src/popup/popup.css b/src/popup/popup.css
index c77c743..bfb54d3 100644
--- a/src/popup/popup.css
+++ b/src/popup/popup.css
@@ -1109,4 +1109,68 @@ td, th {
.update-summary a:hover {
text-decoration: underline;
color: #afffff;
+}
+
+/* 图标按钮样式 */
+.btn-icon {
+ background: none;
+ border: none;
+ color: #4a9d9c;
+ font-size: 1em;
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-color: transparent;
+}
+
+.btn-icon:hover {
+ background-color: rgba(74, 157, 156, 0.1);
+ color: #61dafb;
+ transform: translateY(-1px);
+}
+
+.btn-icon:active {
+ transform: translateY(0);
+}
+
+.btn-icon-sm {
+ width: 24px;
+ height: 24px;
+}
+
+/* 优化参数进度条样式 */
+.optimize-progress {
+ height: 3px !important;
+ background-color: rgba(74, 157, 156, 0.1) !important;
+ border-radius: 4px !important;
+ margin-top: 12px !important;
+ overflow: hidden !important;
+}
+
+.optimize-progress .progress-bar {
+ background: linear-gradient(90deg, #4a9d9c, #61dafb) !important;
+ transition: width 0.3s ease !important;
+}
+
+.optimize-progress .progress-bar-animated {
+ animation: progress-bar-stripes 1s linear infinite !important;
+}
+
+.optimize-progress .progress-bar-striped {
+ background-image: linear-gradient(
+ 45deg,
+ rgba(255, 255, 255, 0.15) 25%,
+ transparent 25%,
+ transparent 50%,
+ rgba(255, 255, 255, 0.15) 50%,
+ rgba(255, 255, 255, 0.15) 75%,
+ transparent 75%,
+ transparent
+ ) !important;
+ background-size: 1rem 1rem !important;
}
\ No newline at end of file
diff --git a/src/popup/script/submission.js b/src/popup/script/submission.js
index 7c33bbd..354dd51 100644
--- a/src/popup/script/submission.js
+++ b/src/popup/script/submission.js
@@ -1,7 +1,7 @@
import { getDifficultyBasedSteps, getSubmissionResult, isSubmissionSuccess, isSubmitButton, needReview, updateProblemUponSuccessSubmission } from "../util/utils";
import { getAllProblems, createOrUpdateProblem, getCurrentProblemInfoFromLeetCodeByHref,getCurrentProblemInfoFromLeetCodeByUrl, syncProblems } from "../service/problemService";
import { Problem } from "../entity/problem";
-import { updateProblemWithFSRS } from "../util/fsrs";
+import { updateProblemWithFSRS } from "../service/fsrsService";
@@ -226,7 +226,7 @@ export async function handleFeedbackSubmission(problem = null) {
}
}
- problem = updateProblemWithFSRS(problem, feedback);
+ problem = await updateProblemWithFSRS(problem, feedback);
await createOrUpdateProblem(problem);
// 只有在页面提交时才显示成功提示
diff --git a/src/popup/service/fsrsService.js b/src/popup/service/fsrsService.js
index b44ae60..7ed8128 100644
--- a/src/popup/service/fsrsService.js
+++ b/src/popup/service/fsrsService.js
@@ -1,5 +1,160 @@
-import { getAllRevlogs, exportRevlogsToCSV } from '../util/fsrs.js';
+import { FSRS, Rating, S_MIN, State, TypeConvert, createEmptyCard } from 'ts-fsrs';
+import { defaultParams, qualityToRating, getFSRSParams, saveFSRSParams, saveRevlog, getAllRevlogs, exportRevlogsToCSV } from '../util/fsrs.js';
import { optimizeFSRSParams } from '../delegate/fsrsDelegate.js';
+import { syncLocalAndCloudStorage } from '../util/utils.js';
+import localStorageDelegate from '../delegate/localStorageDelegate.js';
+import { store } from "../store";
+import { mergeFSRSParams, mergeRevlogs } from '../util/utils';
+
+
+
+// 创建FSRS实例
+let fsrsInstance = null;
+
+// 获取FSRS实例
+export const getFSRSInstance = async () => {
+ if (fsrsInstance) {
+ return fsrsInstance;
+ }
+
+ // 获取本地参数
+ const localParams = await getFSRSParams();
+
+ // 创建新的FSRS实例
+ fsrsInstance = new FSRS(localParams);
+ console.log('创建新的FSRS实例,参数:', localParams);
+
+ return fsrsInstance;
+};
+
+// 更新FSRS实例
+export const updateFSRSInstance = async (newParams) => {
+ // 创建新的FSRS实例
+ fsrsInstance = new FSRS(newParams);
+ console.log('更新FSRS实例,新参数:', newParams);
+
+ return fsrsInstance;
+};
+
+// 计算下次复习时间
+export const calculateNextReview = async (problem, feedback) => {
+ try {
+ const now = new Date();
+
+ // 确保有一个有效的 lastReview 日期
+ let lastReview;
+ if (problem.fsrsState && problem.fsrsState.lastReview) {
+ lastReview = new Date(problem.fsrsState.lastReview);
+ } else if (problem.submissionTime) {
+ lastReview = new Date(problem.submissionTime);
+ } else {
+ lastReview = new Date(now.getTime()); // 默认为昨天
+ }
+
+ // 检查日期是否有效
+ if (isNaN(lastReview.getTime())) {
+ lastReview = new Date(now.getTime()); // 如果无效,使用昨天
+ }
+
+ // 如果没有 fsrsState,创建一个默认的
+ if (!problem.fsrsState) {
+ problem.fsrsState = createEmptyCard(lastReview, (card) => {
+ return {
+ nextReview: +card.due,
+ stability: card.stability,
+ difficulty: card.difficulty,
+ state: card.state,
+ reviewCount: card.reps,
+ lapses: card.lapses,
+ lastReview: +lastReview // 存储为时间戳
+ }
+ });
+ }
+ let card = problem.fsrsState;
+
+ // 确保 nextReview 有效
+ if (!card.nextReview || isNaN(card.nextReview)) {
+ card.nextReview = +lastReview; // 默认为一天后
+ }
+
+ const rating = qualityToRating(feedback.quality);
+
+ // 确保所有参数都有有效值
+ const scheduledDays = Math.max(0, Math.floor((card.nextReview - card.lastReview) / (1000 * 60 * 60 * 24)));
+ const elapsedDays = Math.max(0, (now.getTime() - lastReview.getTime()) / (1000 * 60 * 60 * 24));
+
+ // 获取FSRS实例
+ const fsrs = await getFSRSInstance();
+
+ const result = fsrs.next({
+ due: card.nextReview,
+ stability: card.stability,
+ difficulty: card.difficulty,
+ elapsed_days: elapsedDays,
+ scheduled_days: scheduledDays,
+ reps: card.reviewCount,
+ lapse_count: card.lapses,
+ state: card.state,
+ last_review: lastReview, // 使用已经转换好的 Date 对象
+ }, now, rating);
+
+ return {
+ /**长期调度模式,ivl一定大于1d */
+ nextReview: +result.card.due,
+ stability: result.card.stability,
+ difficulty: result.card.difficulty,
+ state: result.card.state,
+ reviewCount: result.card.reps,
+ lapses: result.card.lapses
+ };
+ } catch (error) {
+ console.error('Error in calculateNextReview:', error);
+ const now = new Date(); // 在 catch 块中定义 now 变量
+ return {
+ nextReview: now.getTime() + (24 * 60 * 60 * 1000),
+ stability: problem.fsrsState.stability || S_MIN,
+ /** ref: https://github.com/open-spaced-repetition/ts-fsrs/blob/5eabd189d4740027ce1018cc968e67ca46c048a3/src/fsrs/default.ts#L20-L40 */
+ difficulty: problem.fsrsState.difficulty || defaultParams.w[4],
+ /** 长期调度下状态一定是New或Review */
+ state: problem.fsrsState.state || State.Review,
+ reviewCount: (problem.fsrsState.reviewCount || 0) + 1,
+ lapses: problem.fsrsState.lapses || 0
+ };
+ }
+};
+
+// 更新问题状态
+export const updateProblemWithFSRS = async (problem, feedback) => {
+ const now = Date.now();
+ const fsrsResult = await calculateNextReview(problem, feedback);
+
+ // 创建新的复习日志条目,只包含必要字段
+ const newRevlog = {
+ card_id: problem.index, // 使用问题索引作为卡片ID
+ review_time: now, // 复习时间(毫秒时间戳)
+ review_rating: qualityToRating(feedback.quality), // 复习评分 (1-4)
+ review_state: TypeConvert.state(problem.fsrsState ? problem.fsrsState?.state ?? State.New : 'New') // 复习状态 (0-3)
+ };
+
+ // 将复习日志存储到单独的 localStorage 键中
+ await saveRevlog(problem.index, newRevlog);
+
+ // 更新问题状态(不修改原有结构)
+ problem.fsrsState = {
+ ...problem.fsrsState,
+ difficulty: fsrsResult.difficulty,
+ stability: fsrsResult.stability,
+ state: fsrsResult.state,
+ lastReview: now,
+ nextReview: fsrsResult.nextReview,
+ reviewCount: fsrsResult.reps,
+ lapses: fsrsResult.lapses,
+ quality: feedback.quality
+ };
+
+ problem.modificationTime = now;
+ return problem;
+};
// 获取复习记录数量
export const getRevlogCount = async () => {
@@ -28,9 +183,69 @@ export const optimizeParameters = async (onProgress) => {
// 调用API进行参数优化
const result = await optimizeFSRSParams(csvContent, onProgress);
+ // 检查结果是否包含params字段(来自done标签)
+ if (result && result.params) {
+ console.log('获取到优化后的FSRS参数:', result.params);
+
+ // 不再自动保存参数,而是返回结果供用户确认
+ return {
+ type: 'Success',
+ params: result.params,
+ metrics: result.metrics || {}
+ };
+ }
+
+ // 如果是进度信息
+ if (result && result.type === 'Progress') {
+ return result;
+ }
+
+ // 如果是训练结果
+ if (result && result.type === 'Train') {
+ return {
+ type: 'Train',
+ message: '训练完成,但未获取到完整参数'
+ };
+ }
+
+ // 其他情况
return result;
} catch (error) {
console.error('Error optimizing parameters:', error);
throw error;
}
-};
\ No newline at end of file
+};
+
+// 同步FSRS历史记录
+export const syncFSRSHistory = async () => {
+ try {
+ // 检查是否启用了云同步
+ if (!store.isCloudSyncEnabled) {
+ console.log('云同步未启用,跳过FSRS历史记录同步');
+ return;
+ }
+
+ // 同步FSRS参数和复习日志
+ await syncFSRSParams();
+ await syncRevlogs();
+
+ // 更新FSRS实例
+ const updatedParams = await getFSRSParams();
+ await updateFSRSInstance(updatedParams);
+
+ console.log('FSRS历史记录同步完成');
+ } catch (error) {
+ console.error('同步FSRS历史记录失败:', error);
+ }
+};
+
+
+export const syncFSRSParams = async () => {
+ if (!store.isCloudSyncEnabled) return;
+ await syncLocalAndCloudStorage('fsrs_params', mergeFSRSParams);
+}
+
+export const syncRevlogs = async () => {
+ if (!store.isCloudSyncEnabled) return;
+ await syncLocalAndCloudStorage('fsrs_revlogs', mergeRevlogs);
+}
\ No newline at end of file
diff --git a/src/popup/util/fsrs.js b/src/popup/util/fsrs.js
index 4bc57e8..f7801c3 100644
--- a/src/popup/util/fsrs.js
+++ b/src/popup/util/fsrs.js
@@ -1,18 +1,18 @@
import { FSRS, Rating, S_MIN, State, TypeConvert, createEmptyCard, dateDiffInDays, generatorParameters } from 'ts-fsrs';
+import localStorageDelegate from '../delegate/localStorageDelegate.js';
+import cloudStorageDelegate from '../delegate/cloudStorageDelegate.js';
+import { store } from '../store';
// 1. 创建自定义参数
-const params = generatorParameters({
+export const defaultParams = generatorParameters({
request_retention: 0.9, // 期望记忆保持率 90%
maximum_interval: 365, // 最大间隔天数
enable_fuzz: false, // 禁用时间模糊化
enable_short_term: false // 启用短期记忆影响
});
-// 2. 创建 FSRS 实例
-const fsrs = new FSRS(params);
-
-// 3. 评分映射(4个等级)
-const qualityToRating = (quality) => {
+// 2. 评分映射(4个等级)
+export const qualityToRating = (quality) => {
switch(quality) {
case 1: return Rating.Again; // 完全不会
case 2: return Rating.Hard; // 有点难
@@ -22,127 +22,63 @@ const qualityToRating = (quality) => {
}
};
-// 4. 计算下次复习时间
-export const calculateNextReview = (problem, feedback) => {
+// 3. 获取本地FSRS参数
+export const getFSRSParams = async () => {
try {
- const now = new Date();
-
- // 确保有一个有效的 lastReview 日期
- let lastReview;
- if (problem.fsrsState && problem.fsrsState.lastReview) {
- lastReview = new Date(problem.fsrsState.lastReview);
- } else if (problem.submissionTime) {
- lastReview = new Date(problem.submissionTime);
- } else {
- lastReview = new Date(now.getTime()); // 默认为昨天
+ const result = await localStorageDelegate.get('fsrs_params');
+ console.log('找到本地FSRS参数:', result);
+ if (!result) {
+ console.log('未找到本地FSRS参数,使用默认参数');
+ return defaultParams;
}
- // 检查日期是否有效
- if (isNaN(lastReview.getTime())) {
- lastReview = new Date(now.getTime()); // 如果无效,使用昨天
+ // 如果结果是字符串,尝试解析它
+ if (typeof result === 'string') {
+ try {
+ const localParams = JSON.parse(result);
+ console.log('获取到本地FSRS参数:', localParams);
+ return localParams;
+ } catch (e) {
+ console.error('解析本地FSRS参数失败:', e);
+ return defaultParams;
+ }
}
+
+ // 如果结果已经是对象,直接返回
+ return result;
+ } catch (error) {
+ console.error('获取本地FSRS参数失败:', error);
+ return defaultParams;
+ }
+};
- // 如果没有 fsrsState,创建一个默认的
- if (!problem.fsrsState) {
- problem.fsrsState = createEmptyCard(lastReview, (card) => {
- return {
- nextReview: +card.due,
- stability: card.stability,
- difficulty: card.difficulty,
- state: card.state,
- reviewCount: card.reps,
- lapses: card.lapses,
- lastReview: +lastReview // 存储为时间戳
- }
- });
- }
- let card = problem.fsrsState;
+// 4. 保存FSRS参数到本地存储
+export const saveFSRSParams = async (newParams) => {
+ try {
+ // 为参数添加时间戳
+ const paramsWithTimestamp = {
+ ...newParams,
+ timestamp: Date.now()
+ };
-
+ // 保存到本地存储(字符串格式)
+ await localStorageDelegate.set('fsrs_params', JSON.stringify(paramsWithTimestamp));
+ console.log('FSRS参数已保存到本地存储');
- // 确保 nextReview 有效
- if (!card.nextReview || isNaN(card.nextReview)) {
- card.nextReview = +lastReview; // 默认为一天后
+ // 保存到云端存储(对象格式)
+ if (store.isCloudSyncEnabled) {
+ await cloudStorageDelegate.set('fsrs_params', paramsWithTimestamp);
+ console.log('FSRS参数已保存到云端存储');
}
-
- const rating = qualityToRating(feedback.quality);
-
- // 确保所有参数都有有效值
- const scheduledDays = Math.max(0, Math.floor((card.nextReview - card.lastReview) / (1000 * 60 * 60 * 24)));
- const elapsedDays = Math.max(0, (now.getTime() - lastReview.getTime()) / (1000 * 60 * 60 * 24));
-
- const result = fsrs.next({
- due: card.nextReview,
- stability: card.stability,
- difficulty: card.difficulty,
- elapsed_days: elapsedDays,
- scheduled_days: scheduledDays,
- reps: card.reviewCount,
- lapse_count: card.lapses,
- state: card.state,
- last_review: lastReview, // 使用已经转换好的 Date 对象
- }, now, rating);
-
- return {
- /**长期调度模式,ivl一定大于1d */
- nextReview: +result.card.due,
- stability: result.card.stability,
- difficulty: result.card.difficulty,
- state: result.card.state,
- reviewCount: result.card.reps,
- lapses: result.card.lapses
- };
+
+ return true;
} catch (error) {
- console.error('Error in calculateNextReview:', error);
- const now = new Date(); // 在 catch 块中定义 now 变量
- return {
- nextReview: now.getTime() + (24 * 60 * 60 * 1000),
- stability: problem.fsrsState.stability || S_MIN,
- /** ref: https://github.com/open-spaced-repetition/ts-fsrs/blob/5eabd189d4740027ce1018cc968e67ca46c048a3/src/fsrs/default.ts#L20-L40 */
- difficulty: problem.fsrsState.difficulty || params.w[4],
- /** 长期调度下状态一定是New或Review */
- state: problem.fsrsState.state || State.Review,
- reviewCount: (problem.fsrsState.reviewCount || 0) + 1,
- lapses: problem.fsrsState.lapses || 0
- };
+ console.error('保存FSRS参数失败:', error);
+ return false;
}
};
-
-// 5. 更新问题状态
-export const updateProblemWithFSRS = (problem, feedback) => {
- const now = Date.now();
- const fsrsResult = calculateNextReview(problem, feedback);
-
- // 创建新的复习日志条目,只包含必要字段
- const newRevlog = {
- card_id: problem.index, // 使用问题索引作为卡片ID
- review_time: now, // 复习时间(毫秒时间戳)
- review_rating: qualityToRating(feedback.quality), // 复习评分 (1-4)
- review_state: TypeConvert.state(problem.fsrsState ? problem.fsrsState?.state ?? State.New : 'New') // 复习状态 (0-3)
- };
-
- // 将复习日志存储到单独的 localStorage 键中
- saveRevlog(problem.index, newRevlog);
-
- // 更新问题状态(不修改原有结构)
- problem.fsrsState = {
- ...problem.fsrsState,
- difficulty: fsrsResult.difficulty,
- stability: fsrsResult.stability,
- state: fsrsResult.state,
- lastReview: now,
- nextReview: fsrsResult.nextReview,
- reviewCount: fsrsResult.reps,
- lapses: fsrsResult.lapses,
- quality: feedback.quality
- };
-
- problem.modificationTime = now;
- return problem;
-};
-
-// 保存单个复习日志
+// 5. 保存单个复习日志
export const saveRevlog = async (cardId, revlog) => {
try {
// 从 localStorage 获取现有的复习日志
@@ -168,12 +104,17 @@ export const saveRevlog = async (cardId, revlog) => {
// 添加新的复习日志
existingRevlogs[cardId].push(revlog);
- // 保存回 localStorage
+ // 保存到本地存储
await new Promise((resolve) => {
chrome.storage.local.set({ 'fsrs_revlogs': JSON.stringify(existingRevlogs) });
resolve();
});
+ // 如果启用了云同步,同时保存到云端
+ if (store.isCloudSyncEnabled) {
+ await cloudStorageDelegate.set('fsrs_revlogs', existingRevlogs);
+ }
+
return true;
} catch (error) {
console.error('Error saving revlog:', error);
@@ -181,31 +122,46 @@ export const saveRevlog = async (cardId, revlog) => {
}
};
-// 获取所有复习日志
+// 6. 获取所有复习日志
export const getAllRevlogs = async () => {
try {
- const revlogsStr = await new Promise((resolve) => {
+ let result;
+
+ // 如果启用了云同步,优先从云端获取
+ if (store.isCloudSyncEnabled) {
+ result = await cloudStorageDelegate.get('fsrs_revlogs');
+ if (result && Object.keys(result).length > 0) {
+ console.log('从云端获取复习日志:', result);
+ return result;
+ }
+ }
+
+ // 如果云端没有数据或未启用云同步,从本地获取
+ result = await new Promise((resolve) => {
chrome.storage.local.get(['fsrs_revlogs'], (result) => {
resolve(result.fsrs_revlogs || '{}');
});
});
- let allRevlogs;
- try {
- allRevlogs = JSON.parse(revlogsStr);
- } catch (e) {
- console.error('Error parsing revlogs:', e);
- return {};
+ // 如果结果是字符串,尝试解析它
+ if (typeof result === 'string') {
+ try {
+ return JSON.parse(result);
+ } catch (e) {
+ console.error('Error parsing revlogs:', e);
+ return {};
+ }
}
- return allRevlogs;
+ // 如果结果已经是对象,直接返回
+ return result || {};
} catch (error) {
console.error('Error getting revlogs:', error);
return {};
}
};
-// 导出复习日志为CSV格式
+// 7. 导出复习日志为CSV格式
export const exportRevlogsToCSV = async () => {
try {
// 获取所有复习日志
diff --git a/src/popup/util/utils.js b/src/popup/util/utils.js
index 31cb824..4273dca 100644
--- a/src/popup/util/utils.js
+++ b/src/popup/util/utils.js
@@ -173,3 +173,71 @@ export const getCurrentRetrievability = (problem) => {
const elapsedDays = dateDiffInDays(new Date(problem.fsrsState.lastReview), new Date());
return forgetting_curve(elapsedDays, problem.fsrsState.stability);
};
+
+export const mergeFSRSParams = (params1, params2) => {
+ if (params2 === undefined || params2 === null) return params1;
+ if (params1 === undefined || params1 === null) return params2;
+
+ // 如果云端数据比本地数据新,使用云端数据
+ const timestamp1 = params1.timestamp || 0;
+ const timestamp2 = params2.timestamp || 0;
+
+ // 返回较新的数据
+ const mergedParams = timestamp1 > timestamp2 ? params1 : params2;
+
+ // 确保返回的数据包含最新的时间戳
+ mergedParams.timestamp = Date.now();
+
+ return mergedParams;
+}
+
+export const mergeRevlogs = (revlogs1, revlogs2) => {
+ if (revlogs2 === undefined || revlogs2 === null) return revlogs1 || {};
+ if (revlogs1 === undefined || revlogs1 === null) return revlogs2 || {};
+
+ // 确保 revlogs1 和 revlogs2 是对象
+ revlogs1 = typeof revlogs1 === 'object' ? revlogs1 : {};
+ revlogs2 = typeof revlogs2 === 'object' ? revlogs2 : {};
+
+ // 合并复习日志
+ const mergedRevlogs = { ...revlogs1 };
+
+ // 遍历第二个复习日志集合
+ Object.keys(revlogs2).forEach(cardId => {
+ if (!mergedRevlogs[cardId]) {
+ // 如果第一个集合没有该卡片的复习日志,直接使用第二个集合的
+ mergedRevlogs[cardId] = Array.isArray(revlogs2[cardId]) ? revlogs2[cardId] : [];
+ } else {
+ // 如果两个集合都有该卡片的复习日志,合并两边的日志
+ const logs2 = Array.isArray(revlogs2[cardId]) ? revlogs2[cardId] : [];
+ const logs1 = Array.isArray(mergedRevlogs[cardId]) ? mergedRevlogs[cardId] : [];
+
+ // 创建一个Map来存储唯一的复习日志
+ const uniqueLogsMap = new Map();
+
+ // 添加第一个集合的日志
+ logs1.forEach(log => {
+ if (log && typeof log === 'object') {
+ const key = `${log.card_id}_${log.review_time}_${log.review_rating}`;
+ uniqueLogsMap.set(key, log);
+ }
+ });
+
+ // 添加第二个集合的日志
+ logs2.forEach(log => {
+ if (log && typeof log === 'object') {
+ const key = `${log.card_id}_${log.review_time}_${log.review_rating}`;
+ uniqueLogsMap.set(key, log);
+ }
+ });
+
+ // 转换回数组并按时间排序
+ mergedRevlogs[cardId] = Array.from(uniqueLogsMap.values())
+ .sort((a, b) => b.review_time - a.review_time);
+ }
+ });
+
+ return mergedRevlogs;
+}
+
+
diff --git a/src/popup/view/view.js b/src/popup/view/view.js
index 2fb8a58..8ba8e86 100644
--- a/src/popup/view/view.js
+++ b/src/popup/view/view.js
@@ -8,6 +8,7 @@ import { registerAllHandlers } from "../handler/handlerRegister";
import { hasOperationHistory } from "../service/operationHistoryService";
import { loadConfigs } from "../service/configService";
import { getLocalStorageData, setLocalStorageData } from "../../popup/delegate/localStorageDelegate";
+import { syncFSRSHistory } from "../service/fsrsService";
/*
Tag for problem records
@@ -422,6 +423,7 @@ export const renderAll = async () => {
await loadConfigs();
await renderSiteMode();
await syncProblems();
+ // await syncFSRSHistory();
// 创建笔记模态框
From f025797d0cc9eb5079bf821c9e7fc77d06432beb Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Mon, 7 Apr 2025 23:55:49 +0800
Subject: [PATCH 19/22] Fix: rate button disappear when resize window
---
src/popup/script/submission.js | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/src/popup/script/submission.js b/src/popup/script/submission.js
index 354dd51..837ee91 100644
--- a/src/popup/script/submission.js
+++ b/src/popup/script/submission.js
@@ -178,6 +178,27 @@ export const addRecordButton = () => {
// 添加到页面
document.body.appendChild(button);
+
+ // 添加窗口大小变化监听器
+ window.addEventListener('resize', () => {
+ const buttonRect = button.getBoundingClientRect();
+ const maxRight = window.innerWidth - button.offsetWidth - 10;
+ const maxBottom = window.innerHeight - button.offsetHeight - 10;
+
+ // 如果按钮超出可视区域,调整位置
+ if (parseInt(button.style.right) > maxRight) {
+ button.style.right = `${maxRight}px`;
+ }
+ if (parseInt(button.style.bottom) > maxBottom) {
+ button.style.bottom = `${maxBottom}px`;
+ }
+
+ // 保存调整后的位置
+ localStorage.setItem('LMS_rateButtonPosition', JSON.stringify({
+ bottom: parseInt(button.style.bottom),
+ right: parseInt(button.style.right)
+ }));
+ });
};
From 5dcb2be2e3b3729a7f1d899125314ec070228ca2 Mon Sep 17 00:00:00 2001
From: xiaohajiayou <923390377@qq.com>
Date: Tue, 8 Apr 2025 00:09:25 +0800
Subject: [PATCH 20/22] Release: v0.1.4
---
README.md | 29 +++++++++++++++++------------
changelog.md | 30 ++++++++++++++++++++++++++++++
manifest.base.json | 2 +-
package-lock.json | 2 +-
package.json | 2 +-
popup.html | 4 ++--
6 files changed, 52 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 2fbecfd..e6b1650 100644
--- a/README.md
+++ b/README.md
@@ -83,24 +83,29 @@
| url添加自定义卡片 | ✅ 已完成 | 用于记录面试手撕题、其他刷题网站用户暂时替代方案 |
| 提供笔记功能 | ✅ 已完成 | 题目列表中新增笔记按钮,支持导出所有笔记为Markdown |
| 收集Anki fsrs 训练数据 | ✅ 已完成 | 待用于测试fsrs官方端口训练 |
-| 接入Anki fsrs官方训练端口 | ❌ 待完成 | 待评估可行性(用户可拟合出最适合自己的记忆曲线) |
+| 接入Anki fsrs官方训练端口 | ✅ 已完成 | 目前仅支持本地复习记录训练(云同步用户可能存在影响) |
+| 扩展webdev云同步服务 | ❌ 待完成 | 待接入坚果云 |
+| 支持语言切换 | ❌ 待完成 | 待完成 |
| 不同网站题目数据源切换 | ❌ 待完成 | 待完成(目前仅支持力扣国际站和中国站,待兼容洛谷等) |
| 兼容火狐 | ❌ 待完成 | 待完成 |
| 兼容`ctrl + enter` | ❌ 待完成 | 目前优先级较低 |
# 📝 Next Steps
-| Task/Feature | Status | Notes |
-|-----------------------|------------|------------------------------------|
-| Multi-device data cloud sync | ✅ Completed | Edge, Chrome |
-| Monitor reminder | ✅ Completed | bilibili, youtube |
-| URL add LeetCode problems | ✅ Completed | For brushing questions with IDE, dedicated to working position entertainment |
-| URL add custom cards | ✅ Completed | For recording interview hand-torn problems, alternative solution for other question brushing websites |
-| Provide note-taking feature | ✅ Completed | Add note button in question list, support exporting all notes as Markdown |
-| Collect Anki fsrs training data | ✅ Completed | Pending for testing fsrs official port training |
-| Integrate Anki fsrs official training port | ❌ Pending | To be assessed for feasibility (users can fit their own optimal memory curve) |
-| Switch between different website question data sources | ❌ Pending | To be completed (currently only supports LeetCode international and Chinese stations, to be compatible with Luogu, etc.) |
-| Compatibility with Firefox | ❌ Pending | To be completed |
+| Task/Feature | Status | Remarks |
+|----------------------------|-----------|----------------------------------------------|
+| Multi-device cloud sync | ✳️ Completed | Edge, Chrome |
+| Monitoring reminders | ✳️ Completed | bilibili, youtube |
+| Add LeetCode URL | ✳️ Completed | For IDE coding practice, perfect for working |
+| Add custom card URL | ✳️ Completed | For recording interview problems, alternative for other coding websites |
+| Provide note-taking feature | ✳️ Completed | Add note button in problem list, support exporting all notes to Markdown |
+| Collect Anki FSRS training data | ✳️ Completed | To be used for testing FSRS official training endpoint |
+| Integrate Anki FSRS official training endpoint | ✳️ Completed | Currently supports training with local review records (may affect cloud sync users) |
+| Expand webdev cloud sync service | ❌ Pending | To be integrated with Nutstore |
+| Support language switching | ❌ Pending | Pending completion |
+| Switch data sources for different websites | ❌ Pending | Pending completion (currently only supports LeetCode international and Chinese sites, to be compatible with Luogu, etc.) |
+| Compatibility with Firefox | ❌ Pending | Pending completion |
+| Compatibility with `ctrl + enter` | ❌ Pending | Lower priority for now |
diff --git a/changelog.md b/changelog.md
index a0549f9..0e15123 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,35 @@
# Changelog
+
+
+## [0.1.4] - 2025-04-08
+----------------------
+
+### Added
+
+* 新增本地 FSRS 算法参数优化,用户可以拟合最适合自己的记忆曲线。(#15)
+
+
+### Fixed
+
+* 修复了在页面缩放时,rate 按钮消失的问题。(#32)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
## [0.1.3] - 2025-04-02
### Added
diff --git a/manifest.base.json b/manifest.base.json
index 4258625..38f881b 100644
--- a/manifest.base.json
+++ b/manifest.base.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Leetcode Mastery Scheduler",
- "version": "0.1.3",
+ "version": "0.1.4",
"author": "Hacode",
"description": "Leetcode-Mastery-Scheduler tracks your LeetCode progress and prompt you to review based FSRS",
"homepage_url": "https://github.com/xiaohajiayou/Leetcode-Mastery-Scheduler",
diff --git a/package-lock.json b/package-lock.json
index eae727d..3913eae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "Leetcode-Mastery-Scheduler",
- "version": "0.1.3",
+ "version": "0.1.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
diff --git a/package.json b/package.json
index 66fc920..da24320 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Leetcode-Mastery-Scheduler",
- "version": "0.1.3",
+ "version": "0.1.4",
"description": "\r \r \r P ractice M akes C ode A ccepted\r \r ",
"main": "src/popup.js",
"directories": {
diff --git a/popup.html b/popup.html
index e3577b1..f042dc0 100644
--- a/popup.html
+++ b/popup.html
@@ -357,11 +357,11 @@ Add Review Card
NEW!
- 新增笔记撰写和导出功能。 / Add note writing and export features
+ 支持通过复习记录优化fsrs算法参数。 / Support Optimize FSRS parameters.
From b505b252f164f2bd588c4abb67ffb627ec3717ad Mon Sep 17 00:00:00 2001
From: Haco <75477391+xiaohajiayou@users.noreply.github.com>
Date: Mon, 14 Apr 2025 01:30:04 +0800
Subject: [PATCH 21/22] Fix/avoid using a hard-code timezone &&Refactor: modify
github star button style(#38)
* Fix/avoid using a hard-code timezone (#36)
* Release: v0.1.4
* Fix/avoid using a hard-code timezone
---------
Co-authored-by: Haco <75477391+xiaohajiayou@users.noreply.github.com>
Co-authored-by: xiaohajiayou <923390377@qq.com>
* Refactor: modify github star button style
* Fix/avoid using a hard-code timezone (#36)
* Release: v0.1.4
* Fix/avoid using a hard-code timezone
---------
Co-authored-by: Haco <75477391+xiaohajiayou@users.noreply.github.com>
Co-authored-by: xiaohajiayou <923390377@qq.com>
* Refactor: modify github star button style
---------
Co-authored-by: ishiko
---
README.md | 4 +--
popup.html | 55 ++++++++++++++++--------------
src/popup/delegate/fsrsDelegate.js | 5 ++-
src/popup/popup.css | 46 ++++++++++++++++++++++---
4 files changed, 78 insertions(+), 32 deletions(-)
diff --git a/README.md b/README.md
index e6b1650..f9a2046 100644
--- a/README.md
+++ b/README.md
@@ -84,7 +84,7 @@
| 提供笔记功能 | ✅ 已完成 | 题目列表中新增笔记按钮,支持导出所有笔记为Markdown |
| 收集Anki fsrs 训练数据 | ✅ 已完成 | 待用于测试fsrs官方端口训练 |
| 接入Anki fsrs官方训练端口 | ✅ 已完成 | 目前仅支持本地复习记录训练(云同步用户可能存在影响) |
-| 扩展webdev云同步服务 | ❌ 待完成 | 待接入坚果云 |
+| 扩展webdav云同步服务 | ❌ 待完成 | 待接入坚果云 |
| 支持语言切换 | ❌ 待完成 | 待完成 |
| 不同网站题目数据源切换 | ❌ 待完成 | 待完成(目前仅支持力扣国际站和中国站,待兼容洛谷等) |
| 兼容火狐 | ❌ 待完成 | 待完成 |
@@ -101,7 +101,7 @@
| Provide note-taking feature | ✳️ Completed | Add note button in problem list, support exporting all notes to Markdown |
| Collect Anki FSRS training data | ✳️ Completed | To be used for testing FSRS official training endpoint |
| Integrate Anki FSRS official training endpoint | ✳️ Completed | Currently supports training with local review records (may affect cloud sync users) |
-| Expand webdev cloud sync service | ❌ Pending | To be integrated with Nutstore |
+| Expand webdav cloud sync service | ❌ Pending | To be integrated with Nutstore |
| Support language switching | ❌ Pending | Pending completion |
| Switch data sources for different websites | ❌ Pending | Pending completion (currently only supports LeetCode international and Chinese sites, to be compatible with Luogu, etc.) |
| Compatibility with Firefox | ❌ Pending | Pending completion |
diff --git a/popup.html b/popup.html
index f042dc0..586772a 100644
--- a/popup.html
+++ b/popup.html
@@ -164,28 +164,28 @@ Add Review Card