1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174 |
- // 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/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.Parse("20060102150405", cardDue.Due)
- if nil != parseErr {
- logging.LogErrorf("parse due time [%s] failed: %s", cardDue.Due, err)
- err = parseErr
- return
- }
- card.SetDue(due)
- }
- if err = deck.Save(); nil != err {
- 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)
- WaitForWritingFiles()
- }
- 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 nil != err {
- 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
- 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
- }
- 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(); nil != err {
- logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
- return
- }
- if err = deck.SaveLog(log); nil != err {
- 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 nil != err {
- 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 := 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); nil != err {
- 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("custom-riff-decks")
- 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("custom-riff-decks")
- } else {
- node.SetIALAttr("custom-riff-decks", val)
- }
- if err = tx.writeTree(tree); nil != err {
- return
- }
- 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 nil != err {
- 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("custom-riff-decks")
- 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("custom-riff-decks", val)
- if err := tx.writeTree(tree); nil != err {
- return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID}
- }
- 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
- }
- cardID := ast.NewNodeID()
- deck.AddCard(cardID, blockID)
- }
- if err := deck.Save(); nil != err {
- logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
- return
- }
- return
- }
- func LoadFlashcards() {
- riffSavePath := getRiffDir()
- if err := os.MkdirAll(riffSavePath, 0755); nil != err {
- logging.LogErrorf("create riff dir [%s] failed: %s", riffSavePath, err)
- return
- }
- Decks = map[string]*riff.Deck{}
- entries, err := os.ReadDir(riffSavePath)
- if nil != err {
- 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 nil != err {
- 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); nil != err {
- return
- }
- }
- cardsPath := filepath.Join(riffSavePath, deckID+".cards")
- if filelock.IsExist(cardsPath) {
- if err = filelock.Remove(cardsPath); nil != err {
- 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 nil != err {
- logging.LogErrorf("load deck [%s] failed: %s", deckID, err)
- return
- }
- deck.Name = name
- Decks[deckID] = deck
- err = deck.Save()
- if nil != err {
- 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()
- var tmp []riff.Card
- for _, c := range dues {
- if 0 < len(blockIDs) && !gulu.Str.Contains(c.BlockID(), blockIDs) {
- continue
- }
- if nil == treenode.GetBlockTree(c.BlockID()) {
- continue
- }
- tmp = append(tmp, c)
- }
- 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
- }
|