From e644cc4891eadd9ff51c6fa040020430f406443b Mon Sep 17 00:00:00 2001 From: liufree Date: Tue, 14 Oct 2025 00:31:28 +0800 Subject: [PATCH 01/10] feat: support bases --- src/main.tsx | 8 ++ src/pages/bases/DashMemoryCardView.tsx | 178 +++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 src/pages/bases/DashMemoryCardView.tsx diff --git a/src/main.tsx b/src/main.tsx index 270d827..0090690 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,7 @@ import {Plugin} from 'obsidian'; import ReactComponent from "./pages/ReactComponent"; import {Root} from 'react-dom/client'; import {DashTableView, TableViewType} from "./pages/bases/DashTableView"; +import {DashMemoryCardView, MemoryCardViewType} from "./pages/bases/DashMemoryCardView"; export default class QueryDashPlugin extends Plugin { @@ -23,6 +24,13 @@ export default class QueryDashPlugin extends Plugin { new DashTableView(controller, containerEl) }); + this.registerBasesView(MemoryCardViewType, { + name: 'DashMemoryCardView', + icon: 'lucide-graduation-cap', + factory: (controller, containerEl) => + new DashMemoryCardView(controller, containerEl) + }); + } onunload() { diff --git a/src/pages/bases/DashMemoryCardView.tsx b/src/pages/bases/DashMemoryCardView.tsx new file mode 100644 index 0000000..0a16cfc --- /dev/null +++ b/src/pages/bases/DashMemoryCardView.tsx @@ -0,0 +1,178 @@ +import {BasesView, MarkdownRenderer, QueryController, TFile} from 'obsidian'; +import React, {useEffect, useRef, useState} from 'react'; +import {createRoot} from 'react-dom/client'; +import {Button, Space, Card} from 'antd'; +import dayjs from 'dayjs'; + +export const MemoryCardViewType = 'dash-memory-card-view'; + +export class DashMemoryCardView extends BasesView { + readonly type = MemoryCardViewType; + private containerEl: HTMLElement; + + constructor(controller: QueryController, parentEl: HTMLElement) { + super(controller); + this.containerEl = parentEl.createDiv('memory-card-view-container'); + } + + public async onDataUpdated(): Promise { + this.containerEl.empty(); + const cardData = this.data.data[0]; + const filePath = cardData?.getValue('file.path'); + const filePathStr = typeof filePath === 'string' ? filePath : filePath?.toString() || ''; + let markdownStr = ''; + let title = ''; + let frontmatter: Record | undefined = undefined; + if (filePathStr) { + const file = this.app.vault.getAbstractFileByPath(filePathStr); + if (file instanceof TFile) { + markdownStr = await this.app.vault.read(file); + title = file.name.replace(/\.md$/, ''); + const cache = this.app.metadataCache.getFileCache(file); + if (cache?.frontmatter) { + frontmatter = cache.frontmatter; + } + } + } + const container = this.containerEl.createDiv(); + const root = createRoot(container); + root.render(); + } +} + +interface MemoryCardProps { + markdown: string; + app: any; + filePath: string; + title: string; + frontmatter?: Record; +} + +const updateSRFrontmatter = async ( + app: any, + filePath: string, + frontmatter: Record | undefined, + rating: 1 | 3 | 5, + refresh: () => void +) => { + if (!filePath) return; + const file = app.vault.getAbstractFileByPath(filePath); + if (!(file instanceof TFile)) return; + let repetitions = Number(frontmatter?.['sr-repetitions']) || 0; + let interval = Number(frontmatter?.['sr-interval']) || 1; + let ease = Number(frontmatter?.['sr-ease']) || 2.5; + let lapses = Number(frontmatter?.['sr-lapses']) || 0; + + if (rating < 3) { + repetitions = 0; + lapses += 1; + interval = 1; + ease = Math.max(1.3, Number((ease - 0.2).toFixed(2))); + } else { + repetitions += 1; + if (repetitions === 1) { + interval = 1; + } else if (repetitions === 2) { + interval = 6; + } else { + interval = Math.round(interval * ease); + } + ease = ease + 0.1 - (5 - rating) * (0.08 + (5 - rating) * 0.02); + ease = Math.max(1.3, Number(ease.toFixed(2))); + } + const due = dayjs().add(interval, 'day').format('YYYY-MM-DD'); + await app.fileManager.processFrontMatter(file, (fm: Record) => { + fm['sr-repetitions'] = repetitions; + fm['sr-interval'] = interval; + fm['sr-ease'] = ease; + fm['sr-due'] = due; + fm['sr-lapses'] = lapses; + }); + refresh(); +}; + +const MemoryCard: React.FC = ({markdown, app, filePath, title, frontmatter}) => { + const mdRef = useRef(null); + const [rendered, setRendered] = useState(false); + const [fm, setFM] = useState(frontmatter); + const [md, setMD] = useState(markdown); + + useEffect(() => { + setFM(frontmatter); + setMD(markdown); + }, [frontmatter, markdown]); + + useEffect(() => { + if (mdRef.current && !rendered) { + MarkdownRenderer.render( + app, + md, + mdRef.current, + filePath, + null + ); + setRendered(true); + } + }, [md, app, filePath, rendered]); + + const refresh = async () => { + const file = app.vault.getAbstractFileByPath(filePath); + if (file instanceof TFile) { + const newMD = await app.vault.read(file); + setMD(newMD); + const cache = app.metadataCache.getFileCache(file); + setFM(cache?.frontmatter); + setRendered(false); + } + }; + + const handleDifficulty = async (action: 'hard' | 'medium' | 'easy') => { + let rating: 1 | 3 | 5 = 3; + if (action === 'hard') rating = 1; + else if (action === 'medium') rating = 3; + else if (action === 'easy') rating = 5; + await updateSRFrontmatter(app, filePath, fm, rating, refresh); + }; + + return ( + <> + +

{title}

+ {fm && ( +
+ + + {Object.entries(fm).map(([key, value]) => ( + + + + + ))} + +
{key}{String(value)}
+
+ )} +
+ +
+ + + + + +
+ + ); +}; From cd48a9a5b12734fabd74597f4b540652701cce24 Mon Sep 17 00:00:00 2001 From: liufree Date: Tue, 14 Oct 2025 00:52:55 +0800 Subject: [PATCH 02/10] feat: support bases --- src/pages/QueryDashView.tsx | 7 ++++--- src/pages/bases/DashMemoryCardView.tsx | 11 ++++++++--- src/pages/kanbanview/KanbanView.tsx | 2 -- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/QueryDashView.tsx b/src/pages/QueryDashView.tsx index 0457c34..5914edf 100644 --- a/src/pages/QueryDashView.tsx +++ b/src/pages/QueryDashView.tsx @@ -14,7 +14,7 @@ interface QueryDashViewDashs { const QueryDashView: React.FC = ({app, source}) => { const [sourceType, setSourceType] = useState("table"); - const [isDarkTheme, setIsDarkTheme] = useState( + const [isDarkTheme, setIsDarkTheme] = useState( document.body.classList.contains("theme-dark") ); @@ -50,9 +50,10 @@ const QueryDashView: React.FC = ({app, source}) => { return ; } else if (sourceType === "timeline") { return ; - } else if (sourceType === "task") { - return ; } + /*else if (sourceType === "task") { + return ; + }*/ }; return ( diff --git a/src/pages/bases/DashMemoryCardView.tsx b/src/pages/bases/DashMemoryCardView.tsx index 0a16cfc..99a98ab 100644 --- a/src/pages/bases/DashMemoryCardView.tsx +++ b/src/pages/bases/DashMemoryCardView.tsx @@ -1,7 +1,7 @@ import {BasesView, MarkdownRenderer, QueryController, TFile} from 'obsidian'; import React, {useEffect, useRef, useState} from 'react'; import {createRoot} from 'react-dom/client'; -import {Button, Space, Card} from 'antd'; +import {Button, Space, Card,Modal} from 'antd'; import dayjs from 'dayjs'; export const MemoryCardViewType = 'dash-memory-card-view'; @@ -81,12 +81,17 @@ const updateSRFrontmatter = async ( ease = ease + 0.1 - (5 - rating) * (0.08 + (5 - rating) * 0.02); ease = Math.max(1.3, Number(ease.toFixed(2))); } - const due = dayjs().add(interval, 'day').format('YYYY-MM-DD'); + const due = dayjs().add(interval, 'day'); + const now = dayjs(); + if (due.diff(now, 'day') >= 365) { + Modal.success('恭喜已经掌握该卡片'); + return; + } await app.fileManager.processFrontMatter(file, (fm: Record) => { fm['sr-repetitions'] = repetitions; fm['sr-interval'] = interval; fm['sr-ease'] = ease; - fm['sr-due'] = due; + fm['sr-due'] = due.format('YYYY-MM-DD'); fm['sr-lapses'] = lapses; }); refresh(); diff --git a/src/pages/kanbanview/KanbanView.tsx b/src/pages/kanbanview/KanbanView.tsx index 7538846..36bc50d 100644 --- a/src/pages/kanbanview/KanbanView.tsx +++ b/src/pages/kanbanview/KanbanView.tsx @@ -26,8 +26,6 @@ const KanbanView: React.FC = ({app, source}) => { const executeTaskQuery = (dvApi: any, source: any, key: any) => { dvApi.query(source).then((result: any) => { - console.log("queryResult", result); - console.log("key", key); if (result.successful) { const data = parseTaskData(result.value); let filteredDataList = [...data] From 79baca43cfbc75605a8807d9ca6440da03410f94 Mon Sep 17 00:00:00 2001 From: liufree Date: Tue, 14 Oct 2025 01:11:49 +0800 Subject: [PATCH 03/10] feat: support bases --- src/pages/bases/DashMemoryCardView.tsx | 31 ++++++++++----- src/pages/kanbanview/KanbanView.tsx | 54 +++++++++++++------------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/src/pages/bases/DashMemoryCardView.tsx b/src/pages/bases/DashMemoryCardView.tsx index 99a98ab..0ef0ecd 100644 --- a/src/pages/bases/DashMemoryCardView.tsx +++ b/src/pages/bases/DashMemoryCardView.tsx @@ -1,7 +1,7 @@ import {BasesView, MarkdownRenderer, QueryController, TFile} from 'obsidian'; import React, {useEffect, useRef, useState} from 'react'; import {createRoot} from 'react-dom/client'; -import {Button, Space, Card,Modal} from 'antd'; +import {Button, Space, Card, Modal, notification} from 'antd'; import dayjs from 'dayjs'; export const MemoryCardViewType = 'dash-memory-card-view'; @@ -56,9 +56,9 @@ const updateSRFrontmatter = async ( rating: 1 | 3 | 5, refresh: () => void ) => { - if (!filePath) return; + if (!filePath) return false; const file = app.vault.getAbstractFileByPath(filePath); - if (!(file instanceof TFile)) return; + if (!(file instanceof TFile)) return false; let repetitions = Number(frontmatter?.['sr-repetitions']) || 0; let interval = Number(frontmatter?.['sr-interval']) || 1; let ease = Number(frontmatter?.['sr-ease']) || 2.5; @@ -84,8 +84,7 @@ const updateSRFrontmatter = async ( const due = dayjs().add(interval, 'day'); const now = dayjs(); if (due.diff(now, 'day') >= 365) { - Modal.success('恭喜已经掌握该卡片'); - return; + return true; } await app.fileManager.processFrontMatter(file, (fm: Record) => { fm['sr-repetitions'] = repetitions; @@ -95,6 +94,7 @@ const updateSRFrontmatter = async ( fm['sr-lapses'] = lapses; }); refresh(); + return false; }; const MemoryCard: React.FC = ({markdown, app, filePath, title, frontmatter}) => { @@ -102,6 +102,7 @@ const MemoryCard: React.FC = ({markdown, app, filePath, title, const [rendered, setRendered] = useState(false); const [fm, setFM] = useState(frontmatter); const [md, setMD] = useState(markdown); + const [showModal, setShowModal] = useState(false); useEffect(() => { setFM(frontmatter); @@ -137,7 +138,8 @@ const MemoryCard: React.FC = ({markdown, app, filePath, title, if (action === 'hard') rating = 1; else if (action === 'medium') rating = 3; else if (action === 'easy') rating = 5; - await updateSRFrontmatter(app, filePath, fm, rating, refresh); + const isDueOverYear = await updateSRFrontmatter(app, filePath, fm, rating, refresh); + if (isDueOverYear) setShowModal(true); }; return ( @@ -173,11 +175,22 @@ const MemoryCard: React.FC = ({markdown, app, filePath, title, justifyContent: 'center', }}> - - - + + +
+ setShowModal(false)} + footer={null} + centered + > +
+ Congratulations! You have memorized this note. +
+
); }; diff --git a/src/pages/kanbanview/KanbanView.tsx b/src/pages/kanbanview/KanbanView.tsx index 36bc50d..5deb824 100644 --- a/src/pages/kanbanview/KanbanView.tsx +++ b/src/pages/kanbanview/KanbanView.tsx @@ -118,34 +118,34 @@ const KanbanView: React.FC = ({app, source}) => { useEffect(() => { - const onModify = (file: TFile) => { - setTimeout(() => { - setRefresh((prev) => !prev); // 触发刷新 - }, 300); // 延迟 500 毫秒,可根据实际情况调整 - console.log("文件已修改:", file.path); - }; + // const onModify = (file: TFile) => { + // setTimeout(() => { + // setRefresh((prev) => !prev); // 触发刷新 + // }, 300); // 延迟 500 毫秒,可根据实际情况调整 + // console.log("文件已修改:", file.path); + // }; + // + // const onCreate = (file: TFile) => { + // console.log("文件已创建:", file.path); + // setRefresh(!refresh); // 触发刷新 + // }; + // + // const onDelete = (file: TFile) => { + // console.log("文件已删除:", file.path); + // setRefresh(!refresh); // 触发刷新 + // }; - const onCreate = (file: TFile) => { - console.log("文件已创建:", file.path); - setRefresh(!refresh); // 触发刷新 - }; - - const onDelete = (file: TFile) => { - console.log("文件已删除:", file.path); - setRefresh(!refresh); // 触发刷新 - }; - - // 监听文件事件 - app.vault.on("modify", onModify); - app.vault.on("create", onCreate); - app.vault.on("delete", onDelete); - - // 清理事件监听器 - return () => { - app.vault.off("modify", onModify); - app.vault.off("create", onCreate); - app.vault.off("delete", onDelete); - }; + // // 监听文件事件 + // app.vault.on("modify", onModify); + // app.vault.on("create", onCreate); + // app.vault.on("delete", onDelete); + // + // // 清理事件监听器 + // return () => { + // app.vault.off("modify", onModify); + // app.vault.off("create", onCreate); + // app.vault.off("delete", onDelete); + // }; }, [app]); const cardDetails = (dataList: any) => { From 268d7369be9ccb403f800aa521f0f413d9989335 Mon Sep 17 00:00:00 2001 From: liufree Date: Tue, 14 Oct 2025 01:26:45 +0800 Subject: [PATCH 04/10] feat: support bases --- src/pages/bases/DashMemoryCardView.tsx | 42 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/pages/bases/DashMemoryCardView.tsx b/src/pages/bases/DashMemoryCardView.tsx index 0ef0ecd..5286ca8 100644 --- a/src/pages/bases/DashMemoryCardView.tsx +++ b/src/pages/bases/DashMemoryCardView.tsx @@ -145,22 +145,34 @@ const MemoryCard: React.FC = ({markdown, app, filePath, title, return ( <> -

{title}

- {fm && ( -
- - - {Object.entries(fm).map(([key, value]) => ( - - - - - ))} - -
{key}{String(value)}
+
+
+

{title}

+ {fm && ( +
+ + + {Object.entries(fm).map(([key, value]) => ( + + + + + ))} + +
{key}{String(value)}
+
+ )} +
- )} -
+ {/* 右侧箭头按钮 */} +
Date: Tue, 14 Oct 2025 23:11:59 +0800 Subject: [PATCH 05/10] feat: support bases --- src/pages/bases/DashMemoryCardView.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pages/bases/DashMemoryCardView.tsx b/src/pages/bases/DashMemoryCardView.tsx index 5286ca8..458de80 100644 --- a/src/pages/bases/DashMemoryCardView.tsx +++ b/src/pages/bases/DashMemoryCardView.tsx @@ -116,7 +116,7 @@ const MemoryCard: React.FC = ({markdown, app, filePath, title, md, mdRef.current, filePath, - null + this ); setRendered(true); } @@ -145,8 +145,17 @@ const MemoryCard: React.FC = ({markdown, app, filePath, title, return ( <> -
-
+
+ {/* 左侧箭头按钮 */} +
From 8bf299323cff3b1207259bacdc4d8f1533687f13 Mon Sep 17 00:00:00 2001 From: liufree Date: Tue, 14 Oct 2025 23:16:43 +0800 Subject: [PATCH 06/10] feat: support bases --- src/pages/bases/DashMemoryCardView.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/bases/DashMemoryCardView.tsx b/src/pages/bases/DashMemoryCardView.tsx index 458de80..d4bf549 100644 --- a/src/pages/bases/DashMemoryCardView.tsx +++ b/src/pages/bases/DashMemoryCardView.tsx @@ -17,7 +17,17 @@ export class DashMemoryCardView extends BasesView { public async onDataUpdated(): Promise { this.containerEl.empty(); - const cardData = this.data.data[0]; + const allCards = this.data.data; + let cardData = allCards[0]; + if (allCards.length > 1) { + const sorted = allCards + .map(card => ({card, due: card.getValue('properties.sr-due')})) + .filter(item => item.due) + .sort((a, b) => dayjs(a.due).valueOf() - dayjs(b.due).valueOf()); + if (sorted.length > 0) { + cardData = sorted[0].card; + } + } const filePath = cardData?.getValue('file.path'); const filePathStr = typeof filePath === 'string' ? filePath : filePath?.toString() || ''; let markdownStr = ''; From 21fe55510e4ab515ffb71cd684bee0431855d63b Mon Sep 17 00:00:00 2001 From: liufree Date: Tue, 14 Oct 2025 23:21:14 +0800 Subject: [PATCH 07/10] feat: support mermorycard view --- src/pages/bases/DashMemoryCardView.tsx | 90 ++++++++++++++++++++------ 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/src/pages/bases/DashMemoryCardView.tsx b/src/pages/bases/DashMemoryCardView.tsx index d4bf549..86c9107 100644 --- a/src/pages/bases/DashMemoryCardView.tsx +++ b/src/pages/bases/DashMemoryCardView.tsx @@ -3,12 +3,16 @@ import React, {useEffect, useRef, useState} from 'react'; import {createRoot} from 'react-dom/client'; import {Button, Space, Card, Modal, notification} from 'antd'; import dayjs from 'dayjs'; +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; +dayjs.extend(isSameOrBefore); export const MemoryCardViewType = 'dash-memory-card-view'; export class DashMemoryCardView extends BasesView { readonly type = MemoryCardViewType; private containerEl: HTMLElement; + private sortedCards: any[] = []; + private currentIndex: number = 0; constructor(controller: QueryController, parentEl: HTMLElement) { super(controller); @@ -18,16 +22,33 @@ export class DashMemoryCardView extends BasesView { public async onDataUpdated(): Promise { this.containerEl.empty(); const allCards = this.data.data; - let cardData = allCards[0]; - if (allCards.length > 1) { - const sorted = allCards - .map(card => ({card, due: card.getValue('properties.sr-due')})) - .filter(item => item.due) - .sort((a, b) => dayjs(a.due).valueOf() - dayjs(b.due).valueOf()); - if (sorted.length > 0) { - cardData = sorted[0].card; + // 按 sr-due 升序排序 + this.sortedCards = [...allCards] + .map(card => ({card, due: card.getValue('properties.sr-due')})) + .sort((a, b) => { + if (!a.due && !b.due) return 0; + if (!a.due) return 1; + if (!b.due) return -1; + return dayjs(a.due).valueOf() - dayjs(b.due).valueOf(); + }) + .map(item => item.card); + // 找到 sr-due 最近的卡片 + let idx = 0; + const now = dayjs(); + for (let i = 0; i < this.sortedCards.length; i++) { + const due = this.sortedCards[i].getValue('properties.sr-due'); + if (due && dayjs(due).isSameOrBefore(now, 'day')) { + idx = i; + break; } } + this.currentIndex = idx; + this.renderMemoryCard(); + } + + private async renderMemoryCard() { + const cardData = this.sortedCards[this.currentIndex]; + if (!cardData) return; const filePath = cardData?.getValue('file.path'); const filePathStr = typeof filePath === 'string' ? filePath : filePath?.toString() || ''; let markdownStr = ''; @@ -46,9 +67,36 @@ export class DashMemoryCardView extends BasesView { } const container = this.containerEl.createDiv(); const root = createRoot(container); - root.render(); + root.render( + + ); } + + private handlePrev = () => { + if (this.currentIndex > 0) { + this.currentIndex--; + this.containerEl.empty(); + this.renderMemoryCard(); + } + }; + + private handleNext = () => { + if (this.currentIndex < this.sortedCards.length - 1) { + this.currentIndex++; + this.containerEl.empty(); + this.renderMemoryCard(); + } + }; } interface MemoryCardProps { @@ -57,6 +105,10 @@ interface MemoryCardProps { filePath: string; title: string; frontmatter?: Record; + onPrev: () => void; + onNext: () => void; + isPrevDisabled: boolean; + isNextDisabled: boolean; } const updateSRFrontmatter = async ( @@ -107,7 +159,7 @@ const updateSRFrontmatter = async ( return false; }; -const MemoryCard: React.FC = ({markdown, app, filePath, title, frontmatter}) => { +const MemoryCard: React.FC = ({markdown, app, filePath, title, frontmatter, onPrev, onNext, isPrevDisabled, isNextDisabled}) => { const mdRef = useRef(null); const [rendered, setRendered] = useState(false); const [fm, setFM] = useState(frontmatter); @@ -156,22 +208,20 @@ const MemoryCard: React.FC = ({markdown, app, filePath, title, <>
- {/* 左侧箭头按钮 */}