Something went wrong. Try again.
A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
Something went wrong. Try again.
Go
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202// SiYuan - Refactor your thinking// Copyright (c) 2020-present, b3log.org//// This program is free software: you can redistribute it and/or modify// it under the terms of the GNU Affero General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// This program is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU Affero General Public License for more details.//// You should have received a copy of the GNU Affero General Public License// along with this program. If not, see <https://www.gnu.org/licenses/>.
package model
import ( "math" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time"
"github.com/88250/gulu" "github.com/88250/lute/ast" "github.com/88250/lute/parse" "github.com/open-spaced-repetition/go-fsrs/v3" "github.com/siyuan-note/filelock" "github.com/siyuan-note/logging" "github.com/siyuan-note/riff" "github.com/siyuan-note/siyuan/kernel/cache" "github.com/siyuan-note/siyuan/kernel/sql" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util")
func GetFlashcardsByBlockIDs(blockIDs []string) (ret []*Block) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
ret = []*Block{} deck := Decks[builtinDeckID] if nil == deck { return }
cards := deck.GetCardsByBlockIDs(blockIDs) blocks, _, _ := getCardsBlocks(cards, 1, math.MaxInt)
for _, blockID := range blockIDs { found := false for _, block := range blocks { if blockID == block.ID { found = true ret = append(ret, block) break } } if !found { ret = append(ret, &Block{ ID: blockID, Content: Conf.Language(180), }) } } return}
type SetFlashcardDueTime struct { ID string `json:"id"` // 卡片 ID Due string `json:"due"` // 下次复习时间,格式为 YYYYMMDDHHmmss}
func SetFlashcardsDueTime(cardDues []*SetFlashcardDueTime) (err error) { // Add internal kernel API `/api/riff/batchSetRiffCardsDueTime` https://github.com/siyuan-note/siyuan/issues/10423
deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
deck := Decks[builtinDeckID] if nil == deck { return }
for _, cardDue := range cardDues { card := deck.GetCard(cardDue.ID) if nil == card { continue }
due, parseErr := time.ParseInLocation("20060102150405", cardDue.Due, time.Local) if nil != parseErr { logging.LogErrorf("parse due time [%s] failed: %s", cardDue.Due, err) err = parseErr return }
card.SetDue(due) }
if err = deck.Save(); err != nil { logging.LogErrorf("save deck [%s] failed: %s", builtinDeckID, err) } return}
func ResetFlashcards(typ, id, deckID string, blockIDs []string) { // Support resetting the learning progress of flashcards https://github.com/siyuan-note/siyuan/issues/9564
if 0 < len(blockIDs) { if "" == deckID { // 从全局管理进入时不会指定卡包 ID,这时需要遍历所有卡包 for _, deck := range Decks { allBlockIDs := deck.GetBlockIDs() for _, blockID := range blockIDs { if gulu.Str.Contains(blockID, allBlockIDs) { deckID = deck.ID break } } if "" == deckID { logging.LogWarnf("deck not found for blocks [%s]", strings.Join(blockIDs, ",")) continue } resetFlashcards(deckID, blockIDs) } return }
resetFlashcards(deckID, blockIDs) return }
var blocks []*Block switch typ { case "notebook": for i := 1; ; i++ { pagedBlocks, _, _ := GetNotebookFlashcards(id, i, 20) if 1 > len(pagedBlocks) { break } blocks = append(blocks, pagedBlocks...) } for _, block := range blocks { blockIDs = append(blockIDs, block.ID) } case "tree": for i := 1; ; i++ { pagedBlocks, _, _ := GetTreeFlashcards(id, i, 20) if 1 > len(pagedBlocks) { break } blocks = append(blocks, pagedBlocks...) } for _, block := range blocks { blockIDs = append(blockIDs, block.ID) } case "deck": for i := 1; ; i++ { pagedBlocks, _, _ := GetDeckFlashcards(id, i, 20) if 1 > len(pagedBlocks) { break } blocks = append(blocks, pagedBlocks...) } default: logging.LogErrorf("invalid type [%s]", typ) }
blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs) resetFlashcards(deckID, blockIDs)}
func resetFlashcards(deckID string, blockIDs []string) { transactions := []*Transaction{ { DoOperations: []*Operation{ { Action: "removeFlashcards", DeckID: deckID, BlockIDs: blockIDs, }, }, }, { DoOperations: []*Operation{ { Action: "addFlashcards", DeckID: deckID, BlockIDs: blockIDs, }, }, }, }
PerformTransactions(&transactions) FlushTxQueue()}
func GetFlashcardNotebooks() (ret []*Box) { deck := Decks[builtinDeckID] if nil == deck { return }
deckBlockIDs := deck.GetBlockIDs() boxes := Conf.GetOpenedBoxes() for _, box := range boxes { newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs) if 0 < flashcardCount { box.NewFlashcardCount = newFlashcardCount box.DueFlashcardCount = dueFlashcardCount box.FlashcardCount = flashcardCount ret = append(ret, box) } } return}
func countTreeFlashcard(rootID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) { blockIDsMap, blockIDs := getTreeSubTreeChildBlocks(rootID) for _, deckBlockID := range deckBlockIDs { if blockIDsMap[deckBlockID] { flashcardCount++ } } if 1 > flashcardCount { return }
newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs) newFlashcardCount = len(newFlashCards) newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs) dueFlashcardCount = len(newDueFlashcards) return}
func countBoxFlashcard(boxID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) { blockIDsMap, blockIDs := getBoxBlocks(boxID) for _, deckBlockID := range deckBlockIDs { if blockIDsMap[deckBlockID] { flashcardCount++ } } if 1 > flashcardCount { return }
newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs) newFlashcardCount = len(newFlashCards) newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs) dueFlashcardCount = len(newDueFlashcards) return}
var ( Decks = map[string]*riff.Deck{} deckLock = sync.Mutex{})
func GetNotebookFlashcards(boxID string, page, pageSize int) (blocks []*Block, total, pageCount int) { blocks = []*Block{}
entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID)) if err != nil { logging.LogErrorf("read dir failed: %s", err) return }
var rootIDs []string for _, entry := range entries { if entry.IsDir() { continue }
if !strings.HasSuffix(entry.Name(), ".sy") { continue }
rootIDs = append(rootIDs, strings.TrimSuffix(entry.Name(), ".sy")) }
var treeBlockIDs []string for _, rootID := range rootIDs { _, blockIDs := getTreeSubTreeChildBlocks(rootID) treeBlockIDs = append(treeBlockIDs, blockIDs...) } treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
deck := Decks[builtinDeckID] if nil == deck { return }
var allBlockIDs []string deckBlockIDs := deck.GetBlockIDs() for _, blockID := range deckBlockIDs { if gulu.Str.Contains(blockID, treeBlockIDs) { allBlockIDs = append(allBlockIDs, blockID) } } allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs) cards := deck.GetCardsByBlockIDs(allBlockIDs)
blocks, total, pageCount = getCardsBlocks(cards, page, pageSize) return}
func GetTreeFlashcards(rootID string, page, pageSize int) (blocks []*Block, total, pageCount int) { blocks = []*Block{} cards := getTreeSubTreeFlashcards(rootID) blocks, total, pageCount = getCardsBlocks(cards, page, pageSize) return}
func getTreeSubTreeFlashcards(rootID string) (ret []riff.Card) { deck := Decks[builtinDeckID] if nil == deck { return }
var allBlockIDs []string deckBlockIDs := deck.GetBlockIDs() treeBlockIDsMap, _ := getTreeSubTreeChildBlocks(rootID) for _, blockID := range deckBlockIDs { if treeBlockIDsMap[blockID] { allBlockIDs = append(allBlockIDs, blockID) } } allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs) ret = deck.GetCardsByBlockIDs(allBlockIDs) return}
func getTreeFlashcards(rootID string) (ret []riff.Card) { deck := Decks[builtinDeckID] if nil == deck { return }
var allBlockIDs []string deckBlockIDs := deck.GetBlockIDs() treeBlockIDsMap, _ := getTreeBlocks(rootID) for _, blockID := range deckBlockIDs { if treeBlockIDsMap[blockID] { allBlockIDs = append(allBlockIDs, blockID) } } allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs) ret = deck.GetCardsByBlockIDs(allBlockIDs) return}
func GetDeckFlashcards(deckID string, page, pageSize int) (blocks []*Block, total, pageCount int) { blocks = []*Block{} var cards []riff.Card if "" == deckID { for _, deck := range Decks { blockIDs := deck.GetBlockIDs() cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...) } } else { deck := Decks[deckID] if nil == deck { return }
blockIDs := deck.GetBlockIDs() cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...) }
blocks, total, pageCount = getCardsBlocks(cards, page, pageSize) return}
func getCardsBlocks(cards []riff.Card, page, pageSize int) (blocks []*Block, total, pageCount int) { // sort by due date asc https://github.com/siyuan-note/siyuan/pull/9673 sort.Slice(cards, func(i, j int) bool { due1 := cards[i].(*riff.FSRSCard).C.Due due2 := cards[j].(*riff.FSRSCard).C.Due if due1.IsZero() || due2.IsZero() { // Improve flashcard management sorting https://github.com/siyuan-note/siyuan/issues/14686 cid1 := cards[i].ID() cid2 := cards[j].ID() return cid1 < cid2 } return due1.Before(due2) })
total = len(cards) pageCount = int(math.Ceil(float64(total) / float64(pageSize))) start := (page - 1) * pageSize end := page * pageSize if start > len(cards) { start = len(cards) } if end > len(cards) { end = len(cards) }
cards = cards[start:end] if 1 > len(cards) { blocks = []*Block{} return }
var blockIDs []string for _, card := range cards { blockIDs = append(blockIDs, card.BlockID()) }
sqlBlocks := sql.GetBlocks(blockIDs) blocks = fromSQLBlocks(&sqlBlocks, "", 36) if 1 > len(blocks) { blocks = []*Block{} return }
for i, b := range blocks { if nil == b { blocks[i] = &Block{ ID: blockIDs[i], Content: Conf.Language(180), }
continue }
b.RiffCardID = cards[i].ID() b.RiffCard = getRiffCard(cards[i].(*riff.FSRSCard).C) } return}
func getRiffCard(card *fsrs.Card) *RiffCard { due := card.Due if due.IsZero() { due = time.Now() }
return &RiffCard{ Due: due, Reps: card.Reps, Lapses: card.Lapses, State: card.State, LastReview: card.LastReview, }}
var ( // reviewCardCache <cardID, card> 用于复习时缓存卡片,以便支持撤销。 reviewCardCache = map[string]riff.Card{}
// skipCardCache <cardID, card> 用于复习时缓存跳过的卡片,以便支持跳过过滤。 skipCardCache = map[string]riff.Card{})
func ReviewFlashcard(deckID, cardID string, rating riff.Rating, reviewedCardIDs []string) (err error) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
deck := Decks[deckID] card := deck.GetCard(cardID) if nil == card { return }
if cachedCard := reviewCardCache[cardID]; nil != cachedCard { // 命中缓存说明这张卡片已经复习过了,这次调用复习是撤销后再次复习 // 将缓存的卡片重新覆盖回卡包中,以恢复最开始复习前的状态 deck.SetCard(cachedCard)
// 从跳过缓存中移除(如果上一次点的是跳过的话),如果不在跳过缓存中,说明上一次点的是复习,这里移除一下也没有副作用 delete(skipCardCache, cardID) } else { // 首次复习该卡片,将卡片缓存以便后续支持撤销后再次复习 reviewCardCache[cardID] = card.Clone() }
log := deck.Review(cardID, rating) if err = deck.Save(); err != nil { logging.LogErrorf("save deck [%s] failed: %s", deckID, err) return }
if err = deck.SaveLog(log); err != nil { logging.LogErrorf("save review log [%s] failed: %s", deckID, err) return }
_, unreviewedCount, _, _ := getDueFlashcards(deckID, reviewedCardIDs) if 1 > unreviewedCount { // 该卡包中没有待复习的卡片了,说明最后一张卡片已经复习完了,清空撤销缓存和跳过缓存 reviewCardCache = map[string]riff.Card{} skipCardCache = map[string]riff.Card{} } return}
func SkipReviewFlashcard(deckID, cardID string) (err error) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
deck := Decks[deckID] card := deck.GetCard(cardID) if nil == card { return }
skipCardCache[cardID] = card return}
type Flashcard struct { DeckID string `json:"deckID"` CardID string `json:"cardID"` BlockID string `json:"blockID"` Lapses int `json:"lapses"` Reps int `json:"reps"` State riff.State `json:"state"` LastReview int64 `json:"lastReview"` NextDues map[riff.Rating]string `json:"nextDues"`}
func newFlashcard(card riff.Card, deckID string, now time.Time) *Flashcard { nextDues := map[riff.Rating]string{} for rating, due := range card.NextDues() { nextDues[rating] = strings.TrimSpace(util.HumanizeDiffTime(due, now, Conf.Lang)) }
return &Flashcard{ DeckID: deckID, CardID: card.ID(), BlockID: card.BlockID(), Lapses: card.GetLapses(), Reps: card.GetReps(), State: card.GetState(), LastReview: card.GetLastReview().UnixMilli(), NextDues: nextDues, }}
func GetNotebookDueFlashcards(boxID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID)) if err != nil { logging.LogErrorf("read dir failed: %s", err) return }
var rootIDs []string for _, entry := range entries { if entry.IsDir() { continue }
if !strings.HasSuffix(entry.Name(), ".sy") { continue }
rootIDs = append(rootIDs, strings.TrimSuffix(entry.Name(), ".sy")) }
var treeBlockIDs []string for _, rootID := range rootIDs { _, blockIDs := getTreeSubTreeChildBlocks(rootID) treeBlockIDs = append(treeBlockIDs, blockIDs...) } treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
deck := Decks[builtinDeckID] if nil == deck { logging.LogWarnf("builtin deck not found") return }
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode) now := time.Now() for _, card := range cards { ret = append(ret, newFlashcard(card, builtinDeckID, now)) } if 1 > len(ret) { ret = []*Flashcard{} } unreviewedCount = unreviewedCnt unreviewedNewCardCount = unreviewedNewCardCnt unreviewedOldCardCount = unreviewedOldCardCnt return}
func GetTreeDueFlashcards(rootID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
deck := Decks[builtinDeckID] if nil == deck { return }
_, treeBlockIDs := getTreeSubTreeChildBlocks(rootID) newCardLimit := Conf.Flashcard.NewCardLimit reviewCardLimit := Conf.Flashcard.ReviewCardLimit // 文档级新卡/复习卡上限控制 Document-level new card/review card limit control https://github.com/siyuan-note/siyuan/issues/9365 ial := sql.GetBlockAttrs(rootID) if newCardLimitStr := ial["custom-riff-new-card-limit"]; "" != newCardLimitStr { var convertErr error newCardLimit, convertErr = strconv.Atoi(newCardLimitStr) if nil != convertErr { logging.LogWarnf("invalid new card limit [%s]: %s", newCardLimitStr, convertErr) } } if reviewCardLimitStr := ial["custom-riff-review-card-limit"]; "" != reviewCardLimitStr { var convertErr error reviewCardLimit, convertErr = strconv.Atoi(reviewCardLimitStr) if nil != convertErr { logging.LogWarnf("invalid review card limit [%s]: %s", reviewCardLimitStr, convertErr) } }
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs, newCardLimit, reviewCardLimit, Conf.Flashcard.ReviewMode) now := time.Now() for _, card := range cards { ret = append(ret, newFlashcard(card, builtinDeckID, now)) } if 1 > len(ret) { ret = []*Flashcard{} } unreviewedCount = unreviewedCnt unreviewedNewCardCount = unreviewedNewCardCnt unreviewedOldCardCount = unreviewedOldCardCnt return}
func getTreeSubTreeChildBlocks(rootID string) (treeBlockIDsMap map[string]bool, treeBlockIDs []string) { treeBlockIDsMap = map[string]bool{} root := treenode.GetBlockTree(rootID) if nil == root { return }
bts := treenode.GetBlockTreesByPathPrefix(strings.TrimSuffix(root.Path, ".sy")) for _, bt := range bts { treeBlockIDsMap[bt.ID] = true treeBlockIDs = append(treeBlockIDs, bt.ID) } return}
func getTreeBlocks(rootID string) (treeBlockIDsMap map[string]bool, treeBlockIDs []string) { treeBlockIDsMap = map[string]bool{} bts := treenode.GetBlockTreesByRootID(rootID) for _, bt := range bts { treeBlockIDsMap[bt.ID] = true treeBlockIDs = append(treeBlockIDs, bt.ID) } return}
func getBoxBlocks(boxID string) (blockIDsMap map[string]bool, blockIDs []string) { blockIDsMap = map[string]bool{} bts := treenode.GetBlockTreesByBoxID(boxID) for _, bt := range bts { blockIDsMap[bt.ID] = true blockIDs = append(blockIDs, bt.ID) } return}
func GetDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
if "" == deckID { ret, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount = getAllDueFlashcards(reviewedCardIDs) return }
ret, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount = getDueFlashcards(deckID, reviewedCardIDs) return}
func getDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int) { deck := Decks[deckID] if nil == deck { logging.LogWarnf("deck not found [%s]", deckID) return }
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, nil, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode) now := time.Now() for _, card := range cards { ret = append(ret, newFlashcard(card, deckID, now)) } if 1 > len(ret) { ret = []*Flashcard{} } unreviewedCount = unreviewedCnt unreviewedNewCardCount = unreviewedNewCardCnt unreviewedOldCardCount = unreviewedOldCardCnt return}
func getAllDueFlashcards(reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int) { now := time.Now() for _, deck := range Decks { if deck.ID != builtinDeckID { // Alt+0 闪卡复习入口不再返回卡包闪卡 // Alt+0 flashcard review entry no longer returns to card deck flashcards https://github.com/siyuan-note/siyuan/issues/10635 continue }
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, nil, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode) unreviewedCount += unreviewedCnt unreviewedNewCardCount += unreviewedNewCardCnt unreviewedOldCardCount += unreviewedOldCardCnt for _, card := range cards { ret = append(ret, newFlashcard(card, deck.ID, now)) } } if 1 > len(ret) { ret = []*Flashcard{} } return}
func (tx *Transaction) doRemoveFlashcards(operation *Operation) (ret *TxErr) { deckLock.Lock() defer deckLock.Unlock()
if isSyncingStorages() { ret = &TxErr{code: TxErrCodeDataIsSyncing} return }
deckID := operation.DeckID blockIDs := operation.BlockIDs
if err := tx.removeBlocksDeckAttr(blockIDs, deckID); err != nil { return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID} }
if "" == deckID { // 支持在 All 卡包中移除闪卡 https://github.com/siyuan-note/siyuan/issues/7425 for _, deck := range Decks { removeFlashcardsByBlockIDs(blockIDs, deck) } } else { removeFlashcardsByBlockIDs(blockIDs, Decks[deckID]) } return}
func (tx *Transaction) removeBlocksDeckAttr(blockIDs []string, deckID string) (err error) { var rootIDs []string blockRoots := map[string]string{} for _, blockID := range blockIDs { bt := treenode.GetBlockTree(blockID) if nil == bt { continue }
rootIDs = append(rootIDs, bt.RootID) blockRoots[blockID] = bt.RootID } rootIDs = gulu.Str.RemoveDuplicatedElem(rootIDs)
trees := map[string]*parse.Tree{} for _, blockID := range blockIDs { rootID := blockRoots[blockID]
tree := trees[rootID] if nil == tree { tree, _ = tx.loadTree(blockID) } if nil == tree { continue } trees[rootID] = tree
node := treenode.GetNodeInTree(tree, blockID) if nil == node { continue }
oldAttrs := parse.IAL2Map(node.KramdownIAL)
deckAttrs := node.IALAttr(NodeAttrRiffDecks) var deckIDs []string if "" != deckID { availableDeckIDs := getDeckIDs() for _, dID := range strings.Split(deckAttrs, ",") { if dID != deckID && gulu.Str.Contains(dID, availableDeckIDs) { deckIDs = append(deckIDs, dID) } } }
deckIDs = gulu.Str.RemoveDuplicatedElem(deckIDs) val := strings.Join(deckIDs, ",") val = strings.TrimPrefix(val, ",") val = strings.TrimSuffix(val, ",") if "" == val { node.RemoveIALAttr(NodeAttrRiffDecks) } else { node.SetIALAttr(NodeAttrRiffDecks, val) }
tx.writeTree(tree)
cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL)) pushBroadcastAttrTransactions(oldAttrs, node) }
return}
func removeFlashcardsByBlockIDs(blockIDs []string, deck *riff.Deck) { if nil == deck { logging.LogErrorf("deck is nil") return }
cards := deck.GetCardsByBlockIDs(blockIDs) if 1 > len(cards) { return }
for _, card := range cards { deck.RemoveCard(card.ID()) } err := deck.Save() if err != nil { logging.LogErrorf("save deck [%s] failed: %s", deck.ID, err) }}
func (tx *Transaction) doAddFlashcards(operation *Operation) (ret *TxErr) { deckLock.Lock() defer deckLock.Unlock()
if isSyncingStorages() { ret = &TxErr{code: TxErrCodeDataIsSyncing} return }
deckID := operation.DeckID blockIDs := operation.BlockIDs
foundDeck := false for _, deck := range Decks { if deckID == deck.ID { foundDeck = true break } } if !foundDeck { deck, createErr := createDeck0("Built-in Deck", builtinDeckID) if nil == createErr { Decks[deck.ID] = deck } }
blockRoots := map[string]string{} for _, blockID := range blockIDs { bt := treenode.GetBlockTree(blockID) if nil == bt { continue }
blockRoots[blockID] = bt.RootID }
trees := map[string]*parse.Tree{} for _, blockID := range blockIDs { rootID := blockRoots[blockID]
tree := trees[rootID] if nil == tree { tree, _ = tx.loadTree(blockID) } if nil == tree { continue } trees[rootID] = tree
node := treenode.GetNodeInTree(tree, blockID) if nil == node { continue }
oldAttrs := parse.IAL2Map(node.KramdownIAL)
deckAttrs := node.IALAttr(NodeAttrRiffDecks) deckIDs := strings.Split(deckAttrs, ",") deckIDs = append(deckIDs, deckID) deckIDs = gulu.Str.RemoveDuplicatedElem(deckIDs) val := strings.Join(deckIDs, ",") val = strings.TrimPrefix(val, ",") val = strings.TrimSuffix(val, ",") node.SetIALAttr(NodeAttrRiffDecks, val)
tx.writeTree(tree)
cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL)) pushBroadcastAttrTransactions(oldAttrs, node) }
deck := Decks[deckID] if nil == deck { logging.LogWarnf("deck [%s] not found", deckID) return }
for _, blockID := range blockIDs { cards := deck.GetCardsByBlockID(blockID) if 0 < len(cards) { // 一个块只能添加生成一张闪卡 https://github.com/siyuan-note/siyuan/issues/7476 continue }
deck.AddCard(ast.NewNodeID(), blockID) }
if err := deck.Save(); err != nil { logging.LogErrorf("save deck [%s] failed: %s", deckID, err) return } return}
func LoadFlashcards() { riffSavePath := getRiffDir() if err := os.MkdirAll(riffSavePath, 0755); err != nil { logging.LogErrorf("create riff dir [%s] failed: %s", riffSavePath, err) return }
Decks = map[string]*riff.Deck{}
entries, err := os.ReadDir(riffSavePath) if err != nil { logging.LogErrorf("read riff dir failed: %s", err) return } for _, entry := range entries { name := entry.Name() if strings.HasSuffix(name, ".deck") { deckID := strings.TrimSuffix(name, ".deck") deck, loadErr := riff.LoadDeck(riffSavePath, deckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights) if nil != loadErr { logging.LogErrorf("load deck [%s] failed: %s", name, loadErr) continue }
if 0 == deck.Created { deck.Created = time.Now().Unix() } if 0 == deck.Updated { deck.Updated = deck.Created }
Decks[deckID] = deck } }}
const builtinDeckID = "20230218211946-2kw8jgx"
func RenameDeck(deckID, name string) (err error) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
deck := Decks[deckID] deck.Name = name err = deck.Save() if err != nil { logging.LogErrorf("save deck [%s] failed: %s", deckID, err) return } return}
func RemoveDeck(deckID string) (err error) { deckLock.Lock() defer deckLock.Unlock()
waitForSyncingStorages()
riffSavePath := getRiffDir() deckPath := filepath.Join(riffSavePath, deckID+".deck") if filelock.IsExist(deckPath) { if err = filelock.Remove(deckPath); err != nil { return } }
cardsPath := filepath.Join(riffSavePath, deckID+".cards") if filelock.IsExist(cardsPath) { if err = filelock.Remove(cardsPath); err != nil { return } }
LoadFlashcards() return}
func CreateDeck(name string) (deck *riff.Deck, err error) { deckLock.Lock() defer deckLock.Unlock() return createDeck(name)}
func createDeck(name string) (deck *riff.Deck, err error) { waitForSyncingStorages()
deckID := ast.NewNodeID() deck, err = createDeck0(name, deckID) return}
func createDeck0(name string, deckID string) (deck *riff.Deck, err error) { riffSavePath := getRiffDir() deck, err = riff.LoadDeck(riffSavePath, deckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights) if err != nil { logging.LogErrorf("load deck [%s] failed: %s", deckID, err) return } deck.Name = name Decks[deckID] = deck err = deck.Save() if err != nil { logging.LogErrorf("save deck [%s] failed: %s", deckID, err) return } return}
func GetDecks() (decks []*riff.Deck) { deckLock.Lock() defer deckLock.Unlock()
for _, deck := range Decks { if deck.ID == builtinDeckID { continue }
decks = append(decks, deck) } if 1 > len(decks) { decks = []*riff.Deck{} }
sort.Slice(decks, func(i, j int) bool { return decks[i].Updated > decks[j].Updated }) return}
func getRiffDir() string { return filepath.Join(util.DataDir, "storage", "riff")}
func getDeckIDs() (deckIDs []string) { for deckID := range Decks { deckIDs = append(deckIDs, deckID) } return}
func getDeckDueCards(deck *riff.Deck, reviewedCardIDs, blockIDs []string, newCardLimit, reviewCardLimit, reviewMode int) (ret []riff.Card, unreviewedCount, unreviewedNewCardCountInRound, unreviewedOldCardCountInRound int) { ret = []riff.Card{} var retNew, retOld []riff.Card
dues := deck.Dues() toChecks := map[string]riff.Card{} for _, c := range dues { if 0 < len(blockIDs) && !gulu.Str.Contains(c.BlockID(), blockIDs) { continue }
toChecks[c.BlockID()] = c } var toCheckBlockIDs []string var tmp []riff.Card for bID, _ := range toChecks { toCheckBlockIDs = append(toCheckBlockIDs, bID) } checkResult := treenode.ExistBlockTrees(toCheckBlockIDs) for bID, exists := range checkResult { if exists { tmp = append(tmp, toChecks[bID]) } } dues = tmp
reviewedCardCount := len(reviewedCardIDs) if 1 > reviewedCardCount { // 未传入已复习的卡片 ID,说明是开始新的复习,需要清空缓存 reviewCardCache = map[string]riff.Card{} skipCardCache = map[string]riff.Card{} }
newCount := 0 reviewCount := 0 for _, reviewedCard := range reviewCardCache { if riff.New == reviewedCard.GetState() { newCount++ } else { reviewCount++ } }
for _, c := range dues { if nil != skipCardCache[c.ID()] { continue }
if 0 < len(reviewedCardIDs) { if !gulu.Str.Contains(c.ID(), reviewedCardIDs) { unreviewedCount++ if riff.New == c.GetState() { if newCount < newCardLimit { unreviewedNewCardCountInRound++ } } else { if reviewCount < reviewCardLimit { unreviewedOldCardCountInRound++ } } } } else { unreviewedCount++ if riff.New == c.GetState() { if newCount < newCardLimit { unreviewedNewCardCountInRound++ } } else { if reviewCount < reviewCardLimit { unreviewedOldCardCountInRound++ } } }
if riff.New == c.GetState() { if newCount >= newCardLimit { continue }
newCount++ retNew = append(retNew, c) } else { if reviewCount >= reviewCardLimit { continue }
reviewCount++ retOld = append(retOld, c) }
ret = append(ret, c) }
switch reviewMode { case 1: // 优先复习新卡 ret = nil ret = append(ret, retNew...) ret = append(ret, retOld...) case 2: // 优先复习旧卡 ret = nil ret = append(ret, retOld...) ret = append(ret, retNew...) } return}
const ( NodeAttrRiffDecks = "custom-riff-decks")