diff --git a/README.md b/README.md index aaaf067..f9a2046 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!
- 智能评估优先级,灵活复习,更聪明地刷题! + 训练记忆曲线,智能评估优先级,灵活复习,更聪明地刷题!

![alt text](https://s2.loli.net/2025/02/16/eNEV49CM8ABWdZ7.jpg) @@ -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. 刷题快乐,速成的本质在于不要遗忘! ![alt text](https://s2.loli.net/2025/02/20/CrmZewAQlWUNuc4.gif) @@ -79,27 +79,33 @@ |--------------------|------------|--------------------| | 多设备数据云同步 | ✅ 已完成 | Edge、Chrome | | 监控提醒 | ✅ 已完成 | bilibili、youtube | -| url添加题目 | ✅ 已完成 | 目前仅力扣题目(配合 IDE 刷题,工位摸鱼专用) | -| 添加自定义卡片 | ❌ 待完成 | 待完成(用于记录面试手撕题或原创笔试题) | -| 兼容`ctrl + enter` | ❌ 待完成 | 待评估工作量 | +| url添加力扣题目 | ✅ 已完成 | 配合 IDE 刷题,工位摸鱼专用 | +| url添加自定义卡片 | ✅ 已完成 | 用于记录面试手撕题、其他刷题网站用户暂时替代方案 | +| 提供笔记功能 | ✅ 已完成 | 题目列表中新增笔记按钮,支持导出所有笔记为Markdown | +| 收集Anki fsrs 训练数据 | ✅ 已完成 | 待用于测试fsrs官方端口训练 | +| 接入Anki fsrs官方训练端口 | ✅ 已完成 | 目前仅支持本地复习记录训练(云同步用户可能存在影响) | +| 扩展webdav云同步服务 | ❌ 待完成 | 待接入坚果云 | +| 支持语言切换 | ❌ 待完成 | 待完成 | | 不同网站题目数据源切换 | ❌ 待完成 | 待完成(目前仅支持力扣国际站和中国站,待兼容洛谷等) | -| 兼容火狐 | ❌ 待完成 | 待评估可行性 | -| 提供笔记功能 | ❌ 待完成 | 待评估可行性(浏览器存储数据上限可能无法支持) | -| 接入Anki fsrs官方训练端口 | ❌ 待完成 | 待评估可行性(用户可拟合出最适合自己的记忆曲线) | +| 兼容火狐 | ❌ 待完成 | 待完成 | +| 兼容`ctrl + enter` | ❌ 待完成 | 目前优先级较低 | # 📝 Next Steps -| Task/Feature | Status | Notes | -|-----------------------|------------|--------------------------------------------| -| Multi-device data cloud sync | ✅ Completed | Edge, Chrome | -| Monitoring reminders | ✅ Completed | bilibili, youtube | -| URL-based problem addition | ✅ Completed | Currently only for LeetCode problems (ideal for IDE-based problem-solving and stealthy practice at work) | -| Add custom cards | ❌ Pending | Pending (for recording interview whiteboard problems or original written test questions) | -| Compatibility with `ctrl + enter` | ❌ Pending | Workload to be assessed | -| Switching between different website problem data sources | ❌ Pending | Pending (currently supports only LeetCode international and Chinese sites; future compatibility with Luogu and others to be explored) | -| Compatibility with Firefox | ❌ Pending | Feasibility to be assessed | -| Note-taking feature | ❌ Pending | Feasibility to be assessed (browser storage limits may be a constraint) | -| Integration with Anki FSRS official training port | ❌ Pending | Feasibility to be assessed (users may be able to fit their own optimal memory curve) | +| 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 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 | +| Compatibility with `ctrl + enter` | ❌ Pending | Lower priority for now | diff --git a/changelog.md b/changelog.md index 1dab3c9..ebb8e2a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,101 @@ # Changelog + +### [0.1.5] - 2025-04-14 +---------------------- + +#### Fixed + +- Fix the issue where the timezone was fixed in the FSRS parameter optimization commit (#38) +- 修复fsrs参数优化提交中固定了时区的问题。(#38) + +### [0.1.4] - 2025-04-08 +---------------------- + +#### Added + +- Add local FSRS algorithm parameter optimization, allowing users to fit the memory curve that best suits them (#15) +- 新增本地 FSRS 算法参数优化,用户可以拟合最适合自己的记忆曲线。(#15) + +#### Fixed + +- Fix the issue where the rate button disappeared when the page was zoomed (#32) +- 修复了在页面缩放时,rate 按钮消失的问题。(#32) + + + + + + + + + + + + + + + + + +## [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 +- 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) + + + + + + + + + + + + + +## [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) + 修复网页端同一道题一日内可以多次评分问题(#21) + +- Fix the issue where the review status did not refresh immediately after reviewing a question in the popup (#XX) + 修复popup复习题目后状态没有立即刷新问题(#25) + +### Added +- Add review history logging feature to prepare for the integration with FSRS training in the future (#XX) + 新增复习记录日志收集,为后续接入fsrs训练做准备(#15) + + + + + + +## [0.1.0] - 2024-03-16 +### Fixed +- Integrate official FSRS R calculation interface (#18) + 接入fsrs官方R计算接口 (#18) + +- Optimize popup speed (#11) + 优化popup弹出速度 (#11) + +### Added +- Add blank card feature, allowing users to create external problems (#13、#3) + 新增空白卡片功能,允许用户新建外部题目(其他网站题目或笔试原创题)(#13、#3) + + + + + ## [0.0.10] - 2024-03-10 ### Fixed - Fix issue with page jump functionality (#8) @@ -12,6 +108,10 @@ - Add feature to customize the position of the "Rate It" button 新增功能:支持自定义“rate it”按钮的位置 + + + + ## [0.0.9] - 2025-02-28 ### Initial Release - release Basic functionality of Leetcode-Mastery-Scheduler diff --git a/manifest.base.json b/manifest.base.json index d3269c2..8b848e9 100644 --- a/manifest.base.json +++ b/manifest.base.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Leetcode Mastery Scheduler", - "version": "0.0.10", + "version": "0.1.5", "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", @@ -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" diff --git a/package-lock.json b/package-lock.json index 1b24442..4551fd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Leetcode-Mastery-Scheduler", - "version": "1.0.0", + "version": "0.1.5", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index cdc1bde..e0f9b80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Leetcode-Mastery-Scheduler", - "version": "1.0.0", + "version": "0.1.5", "description": "

\r \r
\r Practice Makes Code Accepted\r
\r

", "main": "src/popup.js", "directories": { diff --git a/popup.html b/popup.html index 1fa2299..20fda3b 100644 --- a/popup.html +++ b/popup.html @@ -164,28 +164,28 @@

Add Review Card

@@ -340,11 +357,11 @@

Add Review Card

NEW! - 新增评分按钮位置自定义 / Adjustable rating button position + 支持通过复习记录优化fsrs算法参数。 / Support Optimize FSRS parameters.
- View full changelog now(V0.0.10) + View full changelog now(V0.1.5)
@@ -385,15 +402,21 @@

Add Review Card

+ > + - GitHub + style="text-decoration: none;" + data-bs-toggle="tooltip" + data-bs-placement="top" + title="🌟 Star us please! / 点个星星吧!求求了~" + > +
@@ -501,6 +524,38 @@

Active Reminder

2. Disable to stop all pop-up reminders + + +
+
+
+ +

Fsrs Param Optim

+
+
+ + +
+
+ +
+ + + Current review count: 0 + + + + Click to optimize FSRS parameters + + + (Fit best parameters when you have enough data.) + +
+
@@ -558,7 +613,6 @@
- - + \ No newline at end of file diff --git a/src/popup/daily-review.js b/src/popup/daily-review.js index 783af50..c42b161 100644 --- a/src/popup/daily-review.js +++ b/src/popup/daily-review.js @@ -1,1032 +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'; - -// 在文件开头添加 -const LAST_AVERAGE_KEY = 'lastRetrievabilityAverage'; -const LAST_UPDATE_TIME_KEY = 'lastUpdateTime'; -let yesterdayRetrievabilityAverage = 0.00; - -// 获取上次存储的平均值和时间 -function loadLastAverageData() { - const lastData = { - average: parseFloat(localStorage.getItem(LAST_AVERAGE_KEY)) || 0.00, - timestamp: parseInt(localStorage.getItem(LAST_UPDATE_TIME_KEY)) || 0 - }; - return lastData; -} - -// 存储当前的平均值和时间 -function saveCurrentAverageData(average) { - localStorage.setItem(LAST_AVERAGE_KEY, average.toString()); - localStorage.setItem(LAST_UPDATE_TIME_KEY, Date.now().toString()); -} - - -// 判断是否是今天需要复习的题目 -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(); -} - -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 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('更新统计信息'); - // 添加空值检查 - if (!daily_store || !daily_store.dailyReviewProblems) { - console.log('daily_store 或 dailyReviewProblems 为空:', { - daily_store: daily_store, - problems: daily_store?.dailyReviewProblems - }); - // 设置默认值 - const completedCount = 0; - const totalProblems = 0; - - // 更新显示 - document.getElementById('completedCount').textContent = completedCount; - document.getElementById('totalCount').textContent = totalProblems; - document.getElementById('completionRate').textContent = '0%'; - updateProgressCircle(0); - return; - } - - // 计算今日已复习的题目数量 - const completedCount = daily_store.dailyReviewProblems.filter(problem => - isReviewedToday(problem) - ).length; - - //背景脚本通信 - if(completedCount>0){ - chrome.runtime.sendMessage({ - action: 'updateReviewStatus', - count: completedCount - }); - } - - // 获取当前显示的卡片数量 - let cardLimit = parseInt(document.getElementById('cardLimit').value, 10)|| store.defaultCardLimit || 1; - console.log('当前卡片限制值:', { - rawValue: document.getElementById('cardLimit').value, - parsedCardLimit: cardLimit, - element: document.getElementById('cardLimit') - }); - - - const 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 updateCardDisplay() { - console.log('更新卡片显示'); - - // 重置已复习的问题数量 - // mockReviewData.completedProblems = 0; // 重置已复习数量 - updateStats(); // 更新统计信息,传递当前显示的卡片数量 - - createReviewCards(); // 创建新的卡片 -} - -// 更新卡片限制和显示 -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 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'; - - // 更新问题状态 - // if (problem && problem.fsrsState) { - // problem.fsrsState.lastReview = Date.now(); - // problem.fsrsState.reviewCount = (problem.fsrsState.reviewCount || 0) + 1; - // 这里可以添加保存到存储的逻辑 - // await updateProblem(problem); - // } - - // 更新统计信息 - updateStats(); - console.log('更新完成'); -} - -// 标记所有题目为已复习 -function markAllAsReviewed() { - console.log('执行 markAllAsReviewed'); - const buttons = document.querySelectorAll('.review-card .btn-review:not(:disabled)'); - console.log('找到未禁用的按钮数量:', buttons.length); - buttons.forEach(button => markAsReviewed(button)); -} - -// 创建题目卡片时的事件绑定 -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 msUntilReview = new Date(fsrsState.nextReview) - new Date(); - const daysUntilReview = Math.ceil(msUntilReview / (1000 * 60 * 60 * 24)); - - if (msUntilReview >= 0 && msUntilReview <= 24 * 60 * 60 * 1000) { - return 'Review today'; // 24小时内需要复习 - } else if (daysUntilReview > 0) { - return `Review in ${daysUntilReview} day${daysUntilReview > 1 ? 's' : ''}`; - } else { - const daysOverdue = Math.abs(daysUntilReview); - return `Delay by ${daysOverdue} day${daysOverdue > 1 ? 's' : ''}`; - } - })() - : '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 setCurrentDate() { - const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; - const today = new Date().toLocaleDateString('en-US', options); - document.getElementById('currentDate').textContent = today; -} - -// 页面切换功能 -document.addEventListener('DOMContentLoaded', function() { - console.log('DOM加载完成,开始初始化页面切换功能'); - - // 检查是否找到导航按钮 - 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); - } - }); - }); - }); -}); - - - -// 创建题目卡片 -function createProblemCard(problem) { - const card = document.createElement('div'); - card.className = 'problem-card'; - card.innerHTML = ` -

${problem.index}. ${problem.name}

-
- ${problem.difficulty} - 上次复习: ${problem.lastReview} -
-
-
- - ${problem.retrievability.toFixed(2)} -
-
- - ${problem.proficiency}/${problem.maxProficiency} -
-
- `; - return card; -} - -async function loadProblemList() { - await renderAll(); -} - - -// 显示/隐藏添加题目弹窗 -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); - } - }); - } -} - -// 添加设置相关的初始化函数 -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; - } - - // 修改保存成功提示 - 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(); -}); - -// 以防万一,也添加 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 => ` + + `).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. + +
+
+ +
+
+
${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/delegate/fsrsDelegate.js b/src/popup/delegate/fsrsDelegate.js new file mode 100644 index 0000000..b2d26a2 --- /dev/null +++ b/src/popup/delegate/fsrsDelegate.js @@ -0,0 +1,105 @@ +// FSRS参数优化相关的API请求处理 +export const optimizeFSRSParams = async (csvContent, onProgress) => { + try { + const formData = new FormData(); + const csvBlob = new Blob([csvContent], { type: 'text/csv' }); + // ref: https://github.com/ishiko732/fsrs-online-training/blob/73b3281e4c972bf965083dcfe61f087383b4a083/components/lib/tz.ts#L3-L4 + // Chrome > 24, Edge > 12, Firefox > 29 + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone + formData.append('file', csvBlob, 'revlog.csv'); + formData.append('sse', '1'); + formData.append('hour_offset', '4'); + formData.append('enable_short_term', '0'); + formData.append('timezone', timeZone); + + 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/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 = ` + + + `; + 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/popup.css b/src/popup/popup.css index c77c743..527be61 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -299,7 +299,6 @@ display: inline-block; /* 确保渐变效果生效 */ .retrievability-icon { margin-right: 10px; color: #4a9d9c; - animation: pulse 2s infinite; } .retrievability-value { @@ -757,15 +756,73 @@ iframe { #github-star-container { display: flex; - align-items: center; /* 垂直居中对齐 */ - height: 30px; /* 固定高度,与按钮一致 */ + align-items: center; + height: 30px; } -/* 确保 GitHub Star 按钮的样式 */ +/* GitHub Star 按钮样式 */ .github-star-btn { + font-size: 0.875rem; + font-family: 'Courier Prime', monospace; + background: #1d2e3d; + border: 1px solid rgba(97, 218, 251, 0.3); + color: #61dafb; + border-radius: 6px; display: flex; align-items: center; - height: 100%; /* 使其填满容器高度 */ + gap: 0.6rem; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + padding: 0.35rem 0.8rem; + animation: starPulse 2s infinite; +} + +@keyframes starPulse { + 0% { + box-shadow: 0 0 0 0 rgba(97, 218, 251, 0.4); + } + 70% { + box-shadow: 0 0 0 10px rgba(97, 218, 251, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(97, 218, 251, 0); + } +} + +.github-star-btn i { + font-size: 0.875rem; + color: #61dafb; + transition: all 0.3s ease; + animation: starTwinkle 2s infinite; +} + +@keyframes starTwinkle { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.2); + opacity: 0.8; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.github-star-btn:hover { + background: #1a3244; + border-color: #61dafb; + box-shadow: 0 0 15px rgba(97, 218, 251, 0.7); + color: #61dafb; + animation: none; /* 悬停时停止脉冲动画 */ +} + +.github-star-btn:hover i { + animation: none; /* 悬停时停止星星闪烁动画 */ + transform: scale(1.2); } .feedback-btn-review { @@ -1109,4 +1166,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/leetcode.js b/src/popup/script/leetcode.js index 73c9905..a5570c5 100644 --- a/src/popup/script/leetcode.js +++ b/src/popup/script/leetcode.js @@ -1,10 +1,10 @@ import { loadConfigs } from "../service/configService"; -import { submissionListener,addRecordButton } from "./submission"; +import { addRecordButton } from "./submission"; console.log(`Hello Leetcode-Mastery-Scheduler!`); await loadConfigs(); -// document.addEventListener('click', submissionListener); + document.addEventListener('DOMContentLoaded', addRecordButton); diff --git a/src/popup/script/leetcodecn.js b/src/popup/script/leetcodecn.js index 286063c..66709a8 100644 --- a/src/popup/script/leetcodecn.js +++ b/src/popup/script/leetcodecn.js @@ -1,10 +1,10 @@ import { loadConfigs } from "../service/configService"; -import { submissionListener,addRecordButton } from "./submission"; +import { addRecordButton } from "./submission"; console.log(`Hello Leetcode-Mastery-Scheduler!`); await loadConfigs(); -// document.addEventListener('click', submissionListener); + document.addEventListener('DOMContentLoaded', addRecordButton); diff --git a/src/popup/script/submission.js b/src/popup/script/submission.js index f922697..837ee91 100644 --- a/src/popup/script/submission.js +++ b/src/popup/script/submission.js @@ -1,73 +1,10 @@ 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"; -/* - monitorSubmissionResult will repeateadly check for the submission result. -*/ -const monitorSubmissionResult = () => { +import { updateProblemWithFSRS } from "../service/fsrsService"; - let submissionResult; - let maxRetry = 10; - const retryInterval = 1000; - const functionId = setInterval(async () => { - if (maxRetry <= 0) { - clearInterval(functionId); - return; - } - - submissionResult = getSubmissionResult(); - - if (submissionResult === undefined || submissionResult.length === 0) { - maxRetry--; - return; - } - - clearInterval(functionId); - let isSuccess = isSubmissionSuccess(submissionResult); - - if (!isSuccess) return; - - const { problemIndex, problemName, problemLevel, problemUrl } = await getCurrentProblemInfoFromLeetCodeByHref(); - await syncProblems(); // prior to fetch local problem data, sync local problem data with cloud - const problems = await getAllProblems(); - let problem = problems[problemIndex]; - - if (problem && problem.isDeleted !== true) { - const reviewNeeded = needReview(problem); - if (reviewNeeded) { - await createOrUpdateProblem(updateProblemUponSuccessSubmission(problem)); - } - } else { - problem = new Problem(problemIndex, problemName, problemLevel, problemUrl, Date.now(), getDifficultyBasedSteps(problemLevel)[0], Date.now()); - await createOrUpdateProblem(problem); - } - await syncProblems(); // after problem updated, sync to cloud - - console.log("Submission successfully tracked!"); - - }, retryInterval) -}; - -export const submissionListener = (event) => { - - const element = event.target; - - const filterConditions = [ - isSubmitButton(element), - element.parentElement && isSubmitButton(element.parentElement), - element.parentElement && element.parentElement.parentElement && isSubmitButton(element.parentElement.parentElement), - ] - - const isSubmission = filterConditions.reduce((prev, curr) => prev || curr); - - if (isSubmission) { - monitorSubmissionResult(); - } - -}; @@ -241,12 +178,36 @@ 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) + })); + }); }; // 抽取成通用的处理函数 export async function handleFeedbackSubmission(problem = null) { try { + // 记录是否为页面提交 + const isPageSubmission = !problem; + // 显示难度反馈弹窗 const feedback = await showDifficultyFeedbackDialog().catch(error => { console.log(error); // "用户取消评分" @@ -258,25 +219,47 @@ export async function handleFeedbackSubmission(problem = null) { return null; } - // 如果没有传入 problem,说明是新提交,需要获取题目信息 + // 如果没有传入 problem,说明是页面提交,需要获取题目信息 if (!problem) { await syncProblems(); // 同步云端数据 const { problemIndex, problemName, problemLevel, problemUrl } = await getCurrentProblemInfoFromLeetCodeByHref(); const problems = await getAllProblems(); problem = problems[problemIndex]; - if (problem && problem.isDeleted !== true) { - problem = updateProblemWithFSRS(problem, feedback); - await createOrUpdateProblem(updateProblemUponSuccessSubmission(problem)); - } else { + if (!problem || problem.isDeleted == true) { problem = new Problem(problemIndex, problemName, problemLevel, problemUrl, Date.now(), getDifficultyBasedSteps(problemLevel)[0], Date.now()); - problem = updateProblemWithFSRS(problem, feedback); - await createOrUpdateProblem(problem); } - } else { - // 如果传入了 problem,说明是复习 - problem = updateProblemWithFSRS(problem, feedback); - await createOrUpdateProblem(problem); + } + + // 检查上次复习时间是否是今天,如果是则不允许再次复习 + if (problem.fsrsState && problem.fsrsState.lastReview) { + const lastReviewDate = new Date(problem.fsrsState.lastReview); + const today = new Date(); + + // 比较年、月、日是否相同(考虑时区影响) + if (lastReviewDate.getFullYear() === today.getFullYear() && + lastReviewDate.getMonth() === today.getMonth() && + lastReviewDate.getDate() === today.getDate()) { + + // 显示双语警告提示 + showToast("今天已经复习过这道题了,请明天再来!\nYou've already reviewed this problem today. Please come back tomorrow!", "warning"); + return null; + } + } + + problem = await updateProblemWithFSRS(problem, feedback); + await createOrUpdateProblem(problem); + + // 只有在页面提交时才显示成功提示 + if (isPageSubmission) { + // 计算下次复习时间与今天的天数差 + const nextReviewDate = new Date(problem.fsrsState.nextReview); + const today = new Date(); + const diffTime = nextReviewDate.getTime() - today.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + // 显示复习成功提示,包含下次复习时间 + showToast(`复习成功!下次复习时间:${nextReviewDate.toLocaleDateString()}(${diffDays}天后)\nReview successful! Next review: ${nextReviewDate.toLocaleDateString()} (in ${diffDays} days)`, "success"); } await syncProblems(); // 同步到云端 @@ -288,7 +271,109 @@ export async function handleFeedbackSubmission(problem = null) { } } - +// 添加一个更醒目的提示框函数,支持不同类型的提示 +function showToast(message, type = "info", duration = 4000) { + // 检查是否已存在toast样式 + if (!document.getElementById('lms-toast-style')) { + const style = document.createElement('style'); + style.id = 'lms-toast-style'; + style.textContent = ` + .lms-toast { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + padding: 12px 24px; + border-radius: 4px; + z-index: 10000; + font-size: 14px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + animation: lms-toast-in 0.3s ease; + max-width: 80%; + text-align: center; + white-space: pre-line; + font-weight: 500; + } + + .lms-toast-info { + background-color: #1890ff; + color: white; + border-left: 4px solid #096dd9; + } + + .lms-toast-success { + background-color: #52c41a; + color: white; + border-left: 4px solid #389e0d; + } + + .lms-toast-warning { + background-color: #ffd666; + color: #874d00; + border-left: 4px solid #faad14; + font-weight: bold; + } + + .lms-toast-error { + background-color: #ff4d4f; + color: white; + border-left: 4px solid #cf1322; + font-weight: bold; + } + + @keyframes lms-toast-in { + from { + opacity: 0; + transform: translate(-50%, -20px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } + } + + .lms-toast-icon { + margin-right: 8px; + font-weight: bold; + } + `; + document.head.appendChild(style); + } + + // 移除可能存在的旧提示 + const existingToast = document.querySelector('.lms-toast'); + if (existingToast) { + existingToast.remove(); + } + + const toast = document.createElement('div'); + toast.className = `lms-toast lms-toast-${type}`; + + // 添加图标 + let icon = ''; + switch(type) { + case 'info': icon = 'ℹ️'; break; + case 'success': icon = '✅'; break; + case 'warning': icon = '⚠️'; break; + case 'error': icon = '❌'; break; + } + + toast.innerHTML = `${icon}${message}`; + document.body.appendChild(toast); + + // 添加点击关闭功能 + toast.addEventListener('click', () => { + toast.style.opacity = '0'; + toast.style.transition = 'opacity 0.3s ease'; + setTimeout(() => toast.remove(), 300); + }); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transition = 'opacity 0.3s ease'; + setTimeout(() => toast.remove(), 300); + }, duration); +} // 6. 显示评分对话框 const showDifficultyFeedbackDialog = () => { diff --git a/src/popup/service/fsrsService.js b/src/popup/service/fsrsService.js new file mode 100644 index 0000000..7ed8128 --- /dev/null +++ b/src/popup/service/fsrsService.js @@ -0,0 +1,251 @@ +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 () => { + 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); + + // 检查结果是否包含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; + } +}; + +// 同步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 42a7a0f..f7801c3 100644 --- a/src/popup/util/fsrs.js +++ b/src/popup/util/fsrs.js @@ -1,18 +1,18 @@ -import { FSRS, Rating, createEmptyCard, generatorParameters } from 'ts-fsrs'; +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,117 +22,168 @@ const qualityToRating = (quality) => { } }; -// 4. 计算下次复习时间 -export const calculateNextReview = (problem, feedback) => { +// 3. 获取本地FSRS参数 +export const getFSRSParams = async () => { try { - const now = new Date(); - - // 如果没有 fsrsState,创建一个默认的 - if (!problem.fsrsState) { - problem.fsrsState = { - difficulty: null, - quality: null, - lastReview: problem.submissionTime || now.getTime(), - nextReview: null, - reviewCount: 0, - stability: 0, - state: 'New', - lapses: 0 - }; - } - - const lastReview = problem.fsrsState.lastReview - ? new Date(problem.fsrsState.lastReview) - : new Date(problem.submissionTime || now.getTime()); - - let card = createEmptyCard(lastReview); - - if (problem.fsrsState.state !== 'New') { - card = { - ...card, - state: problem.fsrsState.state, - stability: problem.fsrsState.stability || 0, - difficulty: problem.fsrsState.difficulty || 0, - reps: problem.fsrsState.reviewCount || 0, - lapses: problem.fsrsState.lapses || 0, - // 添加时间相关字段 - elapsed_days: problem.fsrsState.lastReview - ? (now - new Date(problem.fsrsState.lastReview)) / (24 * 60 * 60 * 1000) - : 0, - scheduled_days: problem.fsrsState.nextReview - ? (new Date(problem.fsrsState.nextReview) - new Date(problem.fsrsState.lastReview)) / (24 * 60 * 60 * 1000) - : 0 - }; + const result = await localStorageDelegate.get('fsrs_params'); + console.log('找到本地FSRS参数:', result); + if (!result) { + console.log('未找到本地FSRS参数,使用默认参数'); + return defaultParams; } - const rating = qualityToRating(feedback.quality); - const scheduling_cards = fsrs.repeat(card, now); - const result = scheduling_cards[rating]; - - if (!result || !result.card) { - console.error('FSRS calculation failed:', result); - // 默认间隔 - const defaultDays = { - [Rating.Again]: 1, - [Rating.Hard]: 3, - [Rating.Good]: 7, - [Rating.Easy]: 14 - }[rating] || 1; - - return { - nextReview: now.getTime() + (defaultDays * 24 * 60 * 60 * 1000), - stability: card.stability, - difficulty: card.difficulty, - state: card.state, - reps: card.reps + 1, - lapses: card.lapses - }; + // 如果结果是字符串,尝试解析它 + 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; + } +}; - // 确保间隔至少为1天 - const nextReviewTime = Math.max( - result.card.due.getTime(), - now.getTime() + (24 * 60 * 60 * 1000) - ); - - return { - nextReview: nextReviewTime, - stability: result.card.stability, - difficulty: result.card.difficulty, - state: result.card.state, - reps: result.card.reps, - lapses: result.card.lapses +// 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参数已保存到本地存储'); + + // 保存到云端存储(对象格式) + if (store.isCloudSyncEnabled) { + await cloudStorageDelegate.set('fsrs_params', paramsWithTimestamp); + console.log('FSRS参数已保存到云端存储'); + } + + return true; } catch (error) { - console.error('Error in calculateNextReview:', error); - return { - nextReview: now.getTime() + (24 * 60 * 60 * 1000), - stability: problem.fsrsState.stability || 0, - difficulty: problem.fsrsState.difficulty || 0, - state: problem.fsrsState.state || 'New', - reps: (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); +// 5. 保存单个复习日志 +export const saveRevlog = async (cardId, revlog) => { + try { + // 从 localStorage 获取现有的复习日志 + const existingRevlogsStr = await new Promise((resolve) => { + chrome.storage.local.get(['fsrs_revlogs'], (result) => { + resolve(result.fsrs_revlogs || '{}'); + }); + }); + + let existingRevlogs; + try { + existingRevlogs = JSON.parse(existingRevlogsStr); + } catch (e) { + console.error('Error parsing revlogs:', e); + existingRevlogs = {}; + } + + // 确保该卡片的日志数组存在 + if (!existingRevlogs[cardId]) { + existingRevlogs[cardId] = []; + } + + // 添加新的复习日志 + existingRevlogs[cardId].push(revlog); + + // 保存到本地存储 + 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); + return false; + } +}; - 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 - }; +// 6. 获取所有复习日志 +export const getAllRevlogs = async () => { + try { + 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 || '{}'); + }); + }); + + // 如果结果是字符串,尝试解析它 + if (typeof result === 'string') { + try { + return JSON.parse(result); + } catch (e) { + console.error('Error parsing revlogs:', e); + return {}; + } + } + + // 如果结果已经是对象,直接返回 + return result || {}; + } catch (error) { + console.error('Error getting revlogs:', error); + return {}; + } +}; - problem.modificationTime = now; - return problem; +// 7. 导出复习日志为CSV格式 +export const exportRevlogsToCSV = async () => { + try { + // 获取所有复习日志 + const allRevlogs = await getAllRevlogs(); + + // CSV 头部 - 只包含必要字段 + const csvHeader = 'card_id,review_time,review_rating,review_state\n'; + + // 收集所有卡片的复习日志 + let csvContent = csvHeader; + + Object.keys(allRevlogs).forEach(cardId => { + const cardRevlogs = allRevlogs[cardId] || []; + cardRevlogs.forEach(log => { + // 只导出必要字段 + csvContent += `${log.card_id},${log.review_time},${log.review_rating},${log.review_state}\n`; + }); + }); + + return csvContent; + } catch (error) { + console.error('Error exporting revlogs to CSV:', error); + return ''; + } }; 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 85f13f8..8ba8e86 100644 --- a/src/popup/view/view.js +++ b/src/popup/view/view.js @@ -7,21 +7,23 @@ 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"; +import { syncFSRSHistory } from "../service/fsrsService"; /* 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 +46,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 +94,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 +155,95 @@ const createCompletedProblemRecord = (problem) => { ; } +// 添加笔记模态框HTML +const renderNoteModal = () => { + // 检查是否已经存在模态框 + if (document.getElementById('noteModal')) { + console.log("笔记模态框已存在,不再创建"); + return; // 如果已存在,不再创建 + } + + console.log("开始创建笔记模态框"); + + const modalHTML = ` + `; + + 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 +288,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 +307,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 +393,11 @@ export const renderCompletedTableContent = (problems, page) => { content_html += `` completedTableDOM.innerHTML = content_html; + + // 初始化 tooltip + setTimeout(() => { + initializeTooltips(); + }, 100); } export const renderSiteMode = async () => { @@ -292,6 +423,12 @@ export const renderAll = async () => { await loadConfigs(); await renderSiteMode(); await syncProblems(); + // await syncFSRSHistory(); + + // 创建笔记模态框 + + + const problems = Object.values(await getAllProblems()).filter(p => p.isDeleted !== true); console.log('Filtering and sorting problems...'); @@ -334,9 +471,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(); + }); + } + }); }