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)}
+
+ )} +
+ +
+ + + + + +
+ + ); +};