2023-06-24 20:39:55 +08:00
|
|
|
|
// SiYuan - Refactor your thinking
|
2022-12-21 14:45:43 +08:00
|
|
|
|
// 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 (
|
2022-12-29 21:54:10 +08:00
|
|
|
|
"math"
|
2022-12-21 14:45:43 +08:00
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
2022-12-29 21:46:35 +08:00
|
|
|
|
"sort"
|
2023-10-08 20:10:17 +08:00
|
|
|
|
"strconv"
|
2022-12-21 14:45:43 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
2022-12-23 22:58:46 +08:00
|
|
|
|
"time"
|
2022-12-21 14:46:27 +08:00
|
|
|
|
|
2022-12-22 19:53:00 +08:00
|
|
|
|
"github.com/88250/gulu"
|
2022-12-21 14:46:27 +08:00
|
|
|
|
"github.com/88250/lute/ast"
|
2022-12-22 19:53:00 +08:00
|
|
|
|
"github.com/88250/lute/parse"
|
2025-03-16 20:04:08 +08:00
|
|
|
|
"github.com/open-spaced-repetition/go-fsrs/v3"
|
2023-11-06 22:13:04 +08:00
|
|
|
|
"github.com/siyuan-note/filelock"
|
2022-12-21 14:46:27 +08:00
|
|
|
|
"github.com/siyuan-note/logging"
|
|
|
|
|
|
"github.com/siyuan-note/riff"
|
2022-12-22 19:53:00 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/cache"
|
2023-02-19 09:38:09 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/sql"
|
2022-12-22 19:53:00 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/treenode"
|
2022-12-21 14:46:27 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
2022-12-21 14:45:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2024-03-07 20:51:14 +08:00
|
|
|
|
func GetFlashcardsByBlockIDs(blockIDs []string) (ret []*Block) {
|
2024-03-07 20:36:47 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
waitForSyncingStorages()
|
|
|
|
|
|
|
2024-03-07 20:51:14 +08:00
|
|
|
|
ret = []*Block{}
|
2024-03-07 20:36:47 +08:00
|
|
|
|
deck := Decks[builtinDeckID]
|
|
|
|
|
|
if nil == deck {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cards := deck.GetCardsByBlockIDs(blockIDs)
|
2024-03-07 20:51:14 +08:00
|
|
|
|
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),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-03-07 20:36:47 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-24 22:04:36 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-18 23:26:59 +08:00
|
|
|
|
due, parseErr := time.ParseInLocation("20060102150405", cardDue.Due, time.Local)
|
2024-02-24 22:04:36 +08:00
|
|
|
|
if nil != parseErr {
|
|
|
|
|
|
logging.LogErrorf("parse due time [%s] failed: %s", cardDue.Due, err)
|
|
|
|
|
|
err = parseErr
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
card.SetDue(due)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = deck.Save(); err != nil {
|
2024-02-24 22:04:36 +08:00
|
|
|
|
logging.LogErrorf("save deck [%s] failed: %s", builtinDeckID, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-11-16 22:54:41 +08:00
|
|
|
|
func ResetFlashcards(typ, id, deckID string, blockIDs []string) {
|
2023-11-16 11:37:56 +08:00
|
|
|
|
// Support resetting the learning progress of flashcards https://github.com/siyuan-note/siyuan/issues/9564
|
|
|
|
|
|
|
|
|
|
|
|
if 0 < len(blockIDs) {
|
2023-11-16 23:52:20 +08:00
|
|
|
|
if "" == deckID {
|
2023-11-17 00:05:30 +08:00
|
|
|
|
// 从全局管理进入时不会指定卡包 ID,这时需要遍历所有卡包
|
2023-11-16 23:59:46 +08:00
|
|
|
|
for _, deck := range Decks {
|
|
|
|
|
|
allBlockIDs := deck.GetBlockIDs()
|
|
|
|
|
|
for _, blockID := range blockIDs {
|
|
|
|
|
|
if gulu.Str.Contains(blockID, allBlockIDs) {
|
|
|
|
|
|
deckID = deck.ID
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-11-17 00:04:15 +08:00
|
|
|
|
if "" == deckID {
|
2023-11-17 00:05:30 +08:00
|
|
|
|
logging.LogWarnf("deck not found for blocks [%s]", strings.Join(blockIDs, ","))
|
2023-11-17 00:04:15 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
2023-11-16 23:59:46 +08:00
|
|
|
|
resetFlashcards(deckID, blockIDs)
|
|
|
|
|
|
}
|
2023-11-17 00:01:52 +08:00
|
|
|
|
return
|
2023-11-16 23:52:20 +08:00
|
|
|
|
}
|
2023-11-17 00:01:52 +08:00
|
|
|
|
|
|
|
|
|
|
resetFlashcards(deckID, blockIDs)
|
2023-11-16 11:37:56 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var blocks []*Block
|
|
|
|
|
|
switch typ {
|
|
|
|
|
|
case "notebook":
|
|
|
|
|
|
for i := 1; ; i++ {
|
2024-02-26 21:00:09 +08:00
|
|
|
|
pagedBlocks, _, _ := GetNotebookFlashcards(id, i, 20)
|
2023-11-16 23:45:50 +08:00
|
|
|
|
if 1 > len(pagedBlocks) {
|
2023-11-16 11:37:56 +08:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
blocks = append(blocks, pagedBlocks...)
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, block := range blocks {
|
|
|
|
|
|
blockIDs = append(blockIDs, block.ID)
|
|
|
|
|
|
}
|
|
|
|
|
|
case "tree":
|
|
|
|
|
|
for i := 1; ; i++ {
|
2024-02-26 21:00:09 +08:00
|
|
|
|
pagedBlocks, _, _ := GetTreeFlashcards(id, i, 20)
|
2023-11-16 23:45:50 +08:00
|
|
|
|
if 1 > len(pagedBlocks) {
|
2023-11-16 11:37:56 +08:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
blocks = append(blocks, pagedBlocks...)
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, block := range blocks {
|
|
|
|
|
|
blockIDs = append(blockIDs, block.ID)
|
|
|
|
|
|
}
|
|
|
|
|
|
case "deck":
|
|
|
|
|
|
for i := 1; ; i++ {
|
2024-02-26 21:00:09 +08:00
|
|
|
|
pagedBlocks, _, _ := GetDeckFlashcards(id, i, 20)
|
2023-11-16 23:45:50 +08:00
|
|
|
|
if 1 > len(pagedBlocks) {
|
2023-11-16 11:37:56 +08:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
blocks = append(blocks, pagedBlocks...)
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
logging.LogErrorf("invalid type [%s]", typ)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs)
|
2023-11-16 22:54:41 +08:00
|
|
|
|
resetFlashcards(deckID, blockIDs)
|
2023-11-16 11:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2024-10-22 19:20:44 +08:00
|
|
|
|
FlushTxQueue()
|
2023-11-16 11:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-13 15:47:25 +08:00
|
|
|
|
func GetFlashcardNotebooks() (ret []*Box) {
|
|
|
|
|
|
deck := Decks[builtinDeckID]
|
|
|
|
|
|
if nil == deck {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-09 10:08:16 +08:00
|
|
|
|
deckBlockIDs := deck.GetBlockIDs()
|
2023-04-13 15:47:25 +08:00
|
|
|
|
boxes := Conf.GetOpenedBoxes()
|
|
|
|
|
|
for _, box := range boxes {
|
2023-05-09 10:08:16 +08:00
|
|
|
|
newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
|
2023-05-08 22:17:52 +08:00
|
|
|
|
if 0 < flashcardCount {
|
2023-05-08 21:54:51 +08:00
|
|
|
|
box.NewFlashcardCount = newFlashcardCount
|
|
|
|
|
|
box.DueFlashcardCount = dueFlashcardCount
|
2023-05-08 22:17:52 +08:00
|
|
|
|
box.FlashcardCount = flashcardCount
|
2023-04-13 15:47:25 +08:00
|
|
|
|
ret = append(ret, box)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-09 10:08:16 +08:00
|
|
|
|
func countTreeFlashcard(rootID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
blockIDsMap, blockIDs := getTreeSubTreeChildBlocks(rootID)
|
2023-05-09 10:28:00 +08:00
|
|
|
|
for _, deckBlockID := range deckBlockIDs {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
if blockIDsMap[deckBlockID] {
|
2023-05-08 22:17:52 +08:00
|
|
|
|
flashcardCount++
|
2023-04-13 19:07:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-05-08 22:17:52 +08:00
|
|
|
|
if 1 > flashcardCount {
|
2023-05-08 21:54:51 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs)
|
|
|
|
|
|
newFlashcardCount = len(newFlashCards)
|
|
|
|
|
|
newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs)
|
|
|
|
|
|
dueFlashcardCount = len(newDueFlashcards)
|
2023-04-13 19:07:09 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-09 10:08:16 +08:00
|
|
|
|
func countBoxFlashcard(boxID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
blockIDsMap, blockIDs := getBoxBlocks(boxID)
|
2023-05-09 10:28:00 +08:00
|
|
|
|
for _, deckBlockID := range deckBlockIDs {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
if blockIDsMap[deckBlockID] {
|
2023-05-09 10:19:13 +08:00
|
|
|
|
flashcardCount++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if 1 > flashcardCount {
|
2023-04-13 15:47:25 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-09 10:19:13 +08:00
|
|
|
|
newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs)
|
|
|
|
|
|
newFlashcardCount = len(newFlashCards)
|
|
|
|
|
|
newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs)
|
|
|
|
|
|
dueFlashcardCount = len(newDueFlashcards)
|
2023-04-13 15:47:25 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
Decks = map[string]*riff.Deck{}
|
|
|
|
|
|
deckLock = sync.Mutex{}
|
|
|
|
|
|
)
|
2022-12-21 14:45:43 +08:00
|
|
|
|
|
2024-02-26 21:00:09 +08:00
|
|
|
|
func GetNotebookFlashcards(boxID string, page, pageSize int) (blocks []*Block, total, pageCount int) {
|
2023-02-27 15:52:45 +08:00
|
|
|
|
blocks = []*Block{}
|
|
|
|
|
|
|
|
|
|
|
|
entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID))
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-02-27 15:52:45 +08:00
|
|
|
|
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"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-24 10:56:38 +08:00
|
|
|
|
var treeBlockIDs []string
|
2023-02-27 15:52:45 +08:00
|
|
|
|
for _, rootID := range rootIDs {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
_, blockIDs := getTreeSubTreeChildBlocks(rootID)
|
2023-04-13 15:47:25 +08:00
|
|
|
|
treeBlockIDs = append(treeBlockIDs, blockIDs...)
|
2023-02-27 15:52:45 +08:00
|
|
|
|
}
|
2023-03-24 10:56:38 +08:00
|
|
|
|
treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
|
2023-02-27 15:52:45 +08:00
|
|
|
|
|
|
|
|
|
|
deck := Decks[builtinDeckID]
|
|
|
|
|
|
if nil == deck {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var allBlockIDs []string
|
|
|
|
|
|
deckBlockIDs := deck.GetBlockIDs()
|
|
|
|
|
|
for _, blockID := range deckBlockIDs {
|
2023-03-24 10:56:38 +08:00
|
|
|
|
if gulu.Str.Contains(blockID, treeBlockIDs) {
|
2023-02-27 15:52:45 +08:00
|
|
|
|
allBlockIDs = append(allBlockIDs, blockID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs)
|
|
|
|
|
|
cards := deck.GetCardsByBlockIDs(allBlockIDs)
|
|
|
|
|
|
|
2024-02-26 21:00:09 +08:00
|
|
|
|
blocks, total, pageCount = getCardsBlocks(cards, page, pageSize)
|
2023-02-27 15:52:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-26 21:00:09 +08:00
|
|
|
|
func GetTreeFlashcards(rootID string, page, pageSize int) (blocks []*Block, total, pageCount int) {
|
2023-02-18 23:53:28 +08:00
|
|
|
|
blocks = []*Block{}
|
2023-10-08 16:35:06 +08:00
|
|
|
|
cards := getTreeSubTreeFlashcards(rootID)
|
2024-02-26 21:00:09 +08:00
|
|
|
|
blocks, total, pageCount = getCardsBlocks(cards, page, pageSize)
|
2023-10-08 16:35:06 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getTreeSubTreeFlashcards(rootID string) (ret []riff.Card) {
|
2023-02-18 23:53:28 +08:00
|
|
|
|
deck := Decks[builtinDeckID]
|
|
|
|
|
|
if nil == deck {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-24 22:53:14 +08:00
|
|
|
|
var allBlockIDs []string
|
|
|
|
|
|
deckBlockIDs := deck.GetBlockIDs()
|
2023-05-09 10:38:12 +08:00
|
|
|
|
treeBlockIDsMap, _ := getTreeSubTreeChildBlocks(rootID)
|
2023-02-24 22:53:14 +08:00
|
|
|
|
for _, blockID := range deckBlockIDs {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
if treeBlockIDsMap[blockID] {
|
2023-02-24 22:53:14 +08:00
|
|
|
|
allBlockIDs = append(allBlockIDs, blockID)
|
2023-02-18 23:53:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs)
|
2023-10-08 16:35:06 +08:00
|
|
|
|
ret = deck.GetCardsByBlockIDs(allBlockIDs)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2023-02-18 23:53:28 +08:00
|
|
|
|
|
2023-10-08 16:35:06 +08:00
|
|
|
|
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)
|
2023-02-18 23:53:28 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-26 21:00:09 +08:00
|
|
|
|
func GetDeckFlashcards(deckID string, page, pageSize int) (blocks []*Block, total, pageCount int) {
|
2022-12-29 23:02:53 +08:00
|
|
|
|
blocks = []*Block{}
|
2023-02-24 23:24:40 +08:00
|
|
|
|
var cards []riff.Card
|
2023-02-15 09:54:17 +08:00
|
|
|
|
if "" == deckID {
|
|
|
|
|
|
for _, deck := range Decks {
|
2023-02-24 23:24:40 +08:00
|
|
|
|
blockIDs := deck.GetBlockIDs()
|
|
|
|
|
|
cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...)
|
2023-02-15 09:54:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
deck := Decks[deckID]
|
|
|
|
|
|
if nil == deck {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-24 23:24:40 +08:00
|
|
|
|
blockIDs := deck.GetBlockIDs()
|
|
|
|
|
|
cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...)
|
2022-12-29 21:46:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-26 21:00:09 +08:00
|
|
|
|
blocks, total, pageCount = getCardsBlocks(cards, page, pageSize)
|
2023-02-24 23:24:40 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2023-02-18 23:53:28 +08:00
|
|
|
|
|
2024-02-26 21:00:09 +08:00
|
|
|
|
func getCardsBlocks(cards []riff.Card, page, pageSize int) (blocks []*Block, total, pageCount int) {
|
2023-11-23 11:37:49 +08:00
|
|
|
|
// 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
|
2025-04-23 17:17:14 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2023-11-23 11:37:49 +08:00
|
|
|
|
return due1.Before(due2)
|
|
|
|
|
|
})
|
2023-03-24 11:26:25 +08:00
|
|
|
|
|
2023-03-13 11:41:37 +08:00
|
|
|
|
total = len(cards)
|
|
|
|
|
|
pageCount = int(math.Ceil(float64(total) / float64(pageSize)))
|
2022-12-29 21:54:10 +08:00
|
|
|
|
start := (page - 1) * pageSize
|
|
|
|
|
|
end := page * pageSize
|
2023-02-24 23:24:40 +08:00
|
|
|
|
if start > len(cards) {
|
|
|
|
|
|
start = len(cards)
|
2022-12-29 21:46:35 +08:00
|
|
|
|
}
|
2023-02-24 23:24:40 +08:00
|
|
|
|
if end > len(cards) {
|
|
|
|
|
|
end = len(cards)
|
2022-12-29 21:46:35 +08:00
|
|
|
|
}
|
2023-03-13 11:41:37 +08:00
|
|
|
|
|
|
|
|
|
|
cards = cards[start:end]
|
|
|
|
|
|
if 1 > len(cards) {
|
2022-12-29 23:02:53 +08:00
|
|
|
|
blocks = []*Block{}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-24 23:24:40 +08:00
|
|
|
|
var blockIDs []string
|
|
|
|
|
|
for _, card := range cards {
|
|
|
|
|
|
blockIDs = append(blockIDs, card.BlockID())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-29 23:02:53 +08:00
|
|
|
|
sqlBlocks := sql.GetBlocks(blockIDs)
|
|
|
|
|
|
blocks = fromSQLBlocks(&sqlBlocks, "", 36)
|
|
|
|
|
|
if 1 > len(blocks) {
|
|
|
|
|
|
blocks = []*Block{}
|
|
|
|
|
|
return
|
2022-12-29 22:00:07 +08:00
|
|
|
|
}
|
2022-12-29 23:15:08 +08:00
|
|
|
|
|
|
|
|
|
|
for i, b := range blocks {
|
|
|
|
|
|
if nil == b {
|
|
|
|
|
|
blocks[i] = &Block{
|
|
|
|
|
|
ID: blockIDs[i],
|
|
|
|
|
|
Content: Conf.Language(180),
|
|
|
|
|
|
}
|
2023-02-24 23:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
continue
|
2022-12-29 23:15:08 +08:00
|
|
|
|
}
|
2023-02-24 23:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
b.RiffCardID = cards[i].ID()
|
2023-11-16 22:49:59 +08:00
|
|
|
|
b.RiffCard = getRiffCard(cards[i].(*riff.FSRSCard).C)
|
2022-12-29 23:15:08 +08:00
|
|
|
|
}
|
2022-12-22 11:09:28 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-16 20:04:08 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-18 17:10:14 +08:00
|
|
|
|
var (
|
|
|
|
|
|
// reviewCardCache <cardID, card> 用于复习时缓存卡片,以便支持撤销。
|
|
|
|
|
|
reviewCardCache = map[string]riff.Card{}
|
|
|
|
|
|
|
|
|
|
|
|
// skipCardCache <cardID, card> 用于复习时缓存跳过的卡片,以便支持跳过过滤。
|
|
|
|
|
|
skipCardCache = map[string]riff.Card{}
|
|
|
|
|
|
)
|
2023-02-24 14:45:28 +08:00
|
|
|
|
|
2023-03-22 14:44:34 +08:00
|
|
|
|
func ReviewFlashcard(deckID, cardID string, rating riff.Rating, reviewedCardIDs []string) (err error) {
|
2022-12-21 15:58:29 +08:00
|
|
|
|
deckLock.Lock()
|
2022-12-23 17:02:25 +08:00
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2022-12-21 15:58:29 +08:00
|
|
|
|
|
2022-12-23 17:02:25 +08:00
|
|
|
|
deck := Decks[deckID]
|
2023-02-24 15:44:14 +08:00
|
|
|
|
card := deck.GetCard(cardID)
|
2023-02-24 14:45:28 +08:00
|
|
|
|
if nil == card {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-25 13:41:51 +08:00
|
|
|
|
if cachedCard := reviewCardCache[cardID]; nil != cachedCard {
|
2023-02-24 15:10:59 +08:00
|
|
|
|
// 命中缓存说明这张卡片已经复习过了,这次调用复习是撤销后再次复习
|
|
|
|
|
|
// 将缓存的卡片重新覆盖回卡包中,以恢复最开始复习前的状态
|
2023-02-24 15:09:39 +08:00
|
|
|
|
deck.SetCard(cachedCard)
|
2023-03-18 17:10:14 +08:00
|
|
|
|
|
|
|
|
|
|
// 从跳过缓存中移除(如果上一次点的是跳过的话),如果不在跳过缓存中,说明上一次点的是复习,这里移除一下也没有副作用
|
|
|
|
|
|
delete(skipCardCache, cardID)
|
2023-02-24 15:09:39 +08:00
|
|
|
|
} else {
|
2023-02-24 15:10:59 +08:00
|
|
|
|
// 首次复习该卡片,将卡片缓存以便后续支持撤销后再次复习
|
2023-12-26 23:23:26 +08:00
|
|
|
|
reviewCardCache[cardID] = card.Clone()
|
2023-02-24 15:09:39 +08:00
|
|
|
|
}
|
2023-02-24 14:45:28 +08:00
|
|
|
|
|
2023-07-27 00:57:10 +08:00
|
|
|
|
log := deck.Review(cardID, rating)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = deck.Save(); err != nil {
|
2022-12-21 21:23:05 +08:00
|
|
|
|
logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
|
2022-12-21 15:58:29 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2023-02-24 14:45:28 +08:00
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = deck.SaveLog(log); err != nil {
|
2023-07-27 00:57:10 +08:00
|
|
|
|
logging.LogErrorf("save review log [%s] failed: %s", deckID, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-26 23:23:26 +08:00
|
|
|
|
_, unreviewedCount, _, _ := getDueFlashcards(deckID, reviewedCardIDs)
|
|
|
|
|
|
if 1 > unreviewedCount {
|
2023-03-18 17:10:14 +08:00
|
|
|
|
// 该卡包中没有待复习的卡片了,说明最后一张卡片已经复习完了,清空撤销缓存和跳过缓存
|
2023-02-24 14:45:28 +08:00
|
|
|
|
reviewCardCache = map[string]riff.Card{}
|
2023-03-18 17:10:14 +08:00
|
|
|
|
skipCardCache = map[string]riff.Card{}
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func SkipReviewFlashcard(deckID, cardID string) (err error) {
|
|
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2023-03-18 17:10:14 +08:00
|
|
|
|
|
|
|
|
|
|
deck := Decks[deckID]
|
|
|
|
|
|
card := deck.GetCard(cardID)
|
|
|
|
|
|
if nil == card {
|
|
|
|
|
|
return
|
2023-02-24 14:45:28 +08:00
|
|
|
|
}
|
2023-03-18 17:10:14 +08:00
|
|
|
|
|
|
|
|
|
|
skipCardCache[cardID] = card
|
2022-12-21 15:58:29 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-22 15:50:20 +08:00
|
|
|
|
type Flashcard struct {
|
2024-03-05 09:39:06 +08:00
|
|
|
|
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"`
|
2024-03-14 22:14:12 +08:00
|
|
|
|
LastReview int64 `json:"lastReview"`
|
2024-03-05 09:39:06 +08:00
|
|
|
|
NextDues map[riff.Rating]string `json:"nextDues"`
|
2022-12-22 15:50:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-09 10:58:14 +08:00
|
|
|
|
func newFlashcard(card riff.Card, deckID string, now time.Time) *Flashcard {
|
2023-02-27 09:46:39 +08:00
|
|
|
|
nextDues := map[riff.Rating]string{}
|
|
|
|
|
|
for rating, due := range card.NextDues() {
|
2023-03-15 18:27:35 +08:00
|
|
|
|
nextDues[rating] = strings.TrimSpace(util.HumanizeDiffTime(due, now, Conf.Lang))
|
2023-02-27 09:46:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &Flashcard{
|
2024-03-05 09:39:06 +08:00
|
|
|
|
DeckID: deckID,
|
|
|
|
|
|
CardID: card.ID(),
|
2024-03-09 10:58:14 +08:00
|
|
|
|
BlockID: card.BlockID(),
|
2024-03-05 09:39:06 +08:00
|
|
|
|
Lapses: card.GetLapses(),
|
|
|
|
|
|
Reps: card.GetReps(),
|
|
|
|
|
|
State: card.GetState(),
|
2024-03-14 22:14:12 +08:00
|
|
|
|
LastReview: card.GetLastReview().UnixMilli(),
|
2024-03-05 09:39:06 +08:00
|
|
|
|
NextDues: nextDues,
|
2023-02-27 09:46:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-21 12:16:12 +08:00
|
|
|
|
func GetNotebookDueFlashcards(boxID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) {
|
2023-02-18 22:32:34 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2023-02-18 22:32:34 +08:00
|
|
|
|
|
2023-02-27 09:46:39 +08:00
|
|
|
|
entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID))
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-02-27 09:46:39 +08:00
|
|
|
|
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"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-24 10:56:38 +08:00
|
|
|
|
var treeBlockIDs []string
|
2023-02-27 09:46:39 +08:00
|
|
|
|
for _, rootID := range rootIDs {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
_, blockIDs := getTreeSubTreeChildBlocks(rootID)
|
2023-04-13 15:47:25 +08:00
|
|
|
|
treeBlockIDs = append(treeBlockIDs, blockIDs...)
|
2023-02-27 09:46:39 +08:00
|
|
|
|
}
|
2023-03-24 10:56:38 +08:00
|
|
|
|
treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
|
2023-02-27 09:46:39 +08:00
|
|
|
|
|
2023-02-18 22:32:34 +08:00
|
|
|
|
deck := Decks[builtinDeckID]
|
|
|
|
|
|
if nil == deck {
|
|
|
|
|
|
logging.LogWarnf("builtin deck not found")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-24 10:33:00 +08:00
|
|
|
|
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode)
|
2023-02-18 22:32:34 +08:00
|
|
|
|
now := time.Now()
|
|
|
|
|
|
for _, card := range cards {
|
2024-03-09 10:58:14 +08:00
|
|
|
|
ret = append(ret, newFlashcard(card, builtinDeckID, now))
|
2023-02-27 09:46:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
|
ret = []*Flashcard{}
|
|
|
|
|
|
}
|
2023-04-12 09:47:48 +08:00
|
|
|
|
unreviewedCount = unreviewedCnt
|
2023-12-21 12:16:12 +08:00
|
|
|
|
unreviewedNewCardCount = unreviewedNewCardCnt
|
|
|
|
|
|
unreviewedOldCardCount = unreviewedOldCardCnt
|
2023-02-27 09:46:39 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-21 12:16:12 +08:00
|
|
|
|
func GetTreeDueFlashcards(rootID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) {
|
2023-02-27 09:46:39 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2023-02-27 09:46:39 +08:00
|
|
|
|
|
|
|
|
|
|
deck := Decks[builtinDeckID]
|
|
|
|
|
|
if nil == deck {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-09 10:38:12 +08:00
|
|
|
|
_, treeBlockIDs := getTreeSubTreeChildBlocks(rootID)
|
2023-10-08 20:10:17 +08:00
|
|
|
|
newCardLimit := Conf.Flashcard.NewCardLimit
|
|
|
|
|
|
reviewCardLimit := Conf.Flashcard.ReviewCardLimit
|
|
|
|
|
|
// 文档级新卡/复习卡上限控制 Document-level new card/review card limit control https://github.com/siyuan-note/siyuan/issues/9365
|
2024-10-17 23:44:55 +08:00
|
|
|
|
ial := sql.GetBlockAttrs(rootID)
|
2023-10-08 20:10:17 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-24 10:33:00 +08:00
|
|
|
|
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs, newCardLimit, reviewCardLimit, Conf.Flashcard.ReviewMode)
|
2023-02-27 09:46:39 +08:00
|
|
|
|
now := time.Now()
|
|
|
|
|
|
for _, card := range cards {
|
2024-03-09 10:58:14 +08:00
|
|
|
|
ret = append(ret, newFlashcard(card, builtinDeckID, now))
|
2023-02-18 22:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
|
ret = []*Flashcard{}
|
|
|
|
|
|
}
|
2023-04-11 18:46:36 +08:00
|
|
|
|
unreviewedCount = unreviewedCnt
|
2023-12-21 12:16:12 +08:00
|
|
|
|
unreviewedNewCardCount = unreviewedNewCardCnt
|
|
|
|
|
|
unreviewedOldCardCount = unreviewedOldCardCnt
|
2023-02-18 22:32:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-09 10:38:12 +08:00
|
|
|
|
func getTreeSubTreeChildBlocks(rootID string) (treeBlockIDsMap map[string]bool, treeBlockIDs []string) {
|
2023-05-09 10:39:01 +08:00
|
|
|
|
treeBlockIDsMap = map[string]bool{}
|
2023-05-08 20:47:57 +08:00
|
|
|
|
root := treenode.GetBlockTree(rootID)
|
|
|
|
|
|
if nil == root {
|
2023-02-19 09:47:03 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-08 20:47:57 +08:00
|
|
|
|
bts := treenode.GetBlockTreesByPathPrefix(strings.TrimSuffix(root.Path, ".sy"))
|
2023-04-12 19:41:19 +08:00
|
|
|
|
for _, bt := range bts {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
treeBlockIDsMap[bt.ID] = true
|
2023-04-12 19:41:19 +08:00
|
|
|
|
treeBlockIDs = append(treeBlockIDs, bt.ID)
|
2023-02-19 09:47:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
return
|
2023-05-09 10:19:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-08 16:35:06 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-09 10:38:12 +08:00
|
|
|
|
func getBoxBlocks(boxID string) (blockIDsMap map[string]bool, blockIDs []string) {
|
|
|
|
|
|
blockIDsMap = map[string]bool{}
|
2023-05-09 10:19:13 +08:00
|
|
|
|
bts := treenode.GetBlockTreesByBoxID(boxID)
|
|
|
|
|
|
for _, bt := range bts {
|
2023-05-09 10:38:12 +08:00
|
|
|
|
blockIDsMap[bt.ID] = true
|
2023-05-09 10:19:13 +08:00
|
|
|
|
blockIDs = append(blockIDs, bt.ID)
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
2023-02-19 09:47:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-21 12:16:12 +08:00
|
|
|
|
func GetDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) {
|
2022-12-23 17:02:25 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2022-12-23 17:02:25 +08:00
|
|
|
|
|
2022-12-22 10:59:33 +08:00
|
|
|
|
if "" == deckID {
|
2023-12-21 12:16:12 +08:00
|
|
|
|
ret, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount = getAllDueFlashcards(reviewedCardIDs)
|
2023-02-24 14:45:28 +08:00
|
|
|
|
return
|
2022-12-22 10:59:33 +08:00
|
|
|
|
}
|
2022-12-21 15:58:29 +08:00
|
|
|
|
|
2023-12-21 12:16:12 +08:00
|
|
|
|
ret, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount = getDueFlashcards(deckID, reviewedCardIDs)
|
2023-02-24 14:45:28 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-21 12:16:12 +08:00
|
|
|
|
func getDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int) {
|
2022-12-21 21:23:05 +08:00
|
|
|
|
deck := Decks[deckID]
|
2023-02-24 14:45:28 +08:00
|
|
|
|
if nil == deck {
|
|
|
|
|
|
logging.LogWarnf("deck not found [%s]", deckID)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-24 10:33:00 +08:00
|
|
|
|
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, nil, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode)
|
2022-12-28 11:14:27 +08:00
|
|
|
|
now := time.Now()
|
2022-12-21 15:58:29 +08:00
|
|
|
|
for _, card := range cards {
|
2024-03-09 10:58:14 +08:00
|
|
|
|
ret = append(ret, newFlashcard(card, deckID, now))
|
2022-12-22 10:59:33 +08:00
|
|
|
|
}
|
2022-12-22 14:48:20 +08:00
|
|
|
|
if 1 > len(ret) {
|
2022-12-22 15:50:20 +08:00
|
|
|
|
ret = []*Flashcard{}
|
2022-12-22 14:48:20 +08:00
|
|
|
|
}
|
2023-04-11 18:46:36 +08:00
|
|
|
|
unreviewedCount = unreviewedCnt
|
2023-12-21 12:16:12 +08:00
|
|
|
|
unreviewedNewCardCount = unreviewedNewCardCnt
|
|
|
|
|
|
unreviewedOldCardCount = unreviewedOldCardCnt
|
2022-12-22 10:59:33 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-21 12:16:12 +08:00
|
|
|
|
func getAllDueFlashcards(reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int) {
|
2022-12-28 11:14:27 +08:00
|
|
|
|
now := time.Now()
|
2022-12-22 10:59:33 +08:00
|
|
|
|
for _, deck := range Decks {
|
2024-03-17 22:45:32 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-24 10:33:00 +08:00
|
|
|
|
cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, nil, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode)
|
2023-04-12 09:47:48 +08:00
|
|
|
|
unreviewedCount += unreviewedCnt
|
2023-12-22 10:14:59 +08:00
|
|
|
|
unreviewedNewCardCount += unreviewedNewCardCnt
|
|
|
|
|
|
unreviewedOldCardCount += unreviewedOldCardCnt
|
2022-12-22 10:59:33 +08:00
|
|
|
|
for _, card := range cards {
|
2024-03-09 10:58:14 +08:00
|
|
|
|
ret = append(ret, newFlashcard(card, deck.ID, now))
|
2022-12-22 10:59:33 +08:00
|
|
|
|
}
|
2022-12-21 15:58:29 +08:00
|
|
|
|
}
|
2022-12-22 14:48:20 +08:00
|
|
|
|
if 1 > len(ret) {
|
2022-12-22 15:50:20 +08:00
|
|
|
|
ret = []*Flashcard{}
|
2022-12-22 14:48:20 +08:00
|
|
|
|
}
|
2022-12-21 15:58:29 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-09 23:00:09 +08:00
|
|
|
|
func (tx *Transaction) doRemoveFlashcards(operation *Operation) (ret *TxErr) {
|
2023-02-24 22:53:14 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2024-01-13 22:30:57 +08:00
|
|
|
|
if isSyncingStorages() {
|
2023-04-09 23:00:09 +08:00
|
|
|
|
ret = &TxErr{code: TxErrCodeDataIsSyncing}
|
2023-02-24 22:53:14 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-12-21 15:18:29 +08:00
|
|
|
|
|
2023-04-09 23:00:09 +08:00
|
|
|
|
deckID := operation.DeckID
|
|
|
|
|
|
blockIDs := operation.BlockIDs
|
2022-12-23 17:02:25 +08:00
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err := tx.removeBlocksDeckAttr(blockIDs, deckID); err != nil {
|
2023-04-09 23:00:09 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID}
|
2023-02-24 22:53:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if "" == deckID { // 支持在 All 卡包中移除闪卡 https://github.com/siyuan-note/siyuan/issues/7425
|
|
|
|
|
|
for _, deck := range Decks {
|
|
|
|
|
|
removeFlashcardsByBlockIDs(blockIDs, deck)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
removeFlashcardsByBlockIDs(blockIDs, Decks[deckID])
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-09 23:00:09 +08:00
|
|
|
|
func (tx *Transaction) removeBlocksDeckAttr(blockIDs []string, deckID string) (err error) {
|
2022-12-22 21:09:05 +08:00
|
|
|
|
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 {
|
2023-04-09 23:00:09 +08:00
|
|
|
|
tree, _ = tx.loadTree(blockID)
|
2022-12-22 21:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
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
|
2023-02-21 10:15:57 +08:00
|
|
|
|
if "" != deckID {
|
2023-02-24 22:53:14 +08:00
|
|
|
|
availableDeckIDs := getDeckIDs()
|
2023-02-21 10:15:57 +08:00
|
|
|
|
for _, dID := range strings.Split(deckAttrs, ",") {
|
|
|
|
|
|
if dID != deckID && gulu.Str.Contains(dID, availableDeckIDs) {
|
|
|
|
|
|
deckIDs = append(deckIDs, dID)
|
|
|
|
|
|
}
|
2022-12-22 21:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(tree); err != nil {
|
2022-12-22 21:09:05 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL))
|
2023-03-03 15:01:42 +08:00
|
|
|
|
pushBroadcastAttrTransactions(oldAttrs, node)
|
2022-12-22 21:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-21 15:18:29 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-24 22:53:14 +08:00
|
|
|
|
func removeFlashcardsByBlockIDs(blockIDs []string, deck *riff.Deck) {
|
2023-02-21 10:15:57 +08:00
|
|
|
|
if nil == deck {
|
|
|
|
|
|
logging.LogErrorf("deck is nil")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-24 22:53:14 +08:00
|
|
|
|
cards := deck.GetCardsByBlockIDs(blockIDs)
|
2023-03-19 15:35:59 +08:00
|
|
|
|
if 1 > len(cards) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-24 22:53:14 +08:00
|
|
|
|
for _, card := range cards {
|
|
|
|
|
|
deck.RemoveCard(card.ID())
|
2023-02-21 10:15:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
err := deck.Save()
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-02-21 10:15:57 +08:00
|
|
|
|
logging.LogErrorf("save deck [%s] failed: %s", deck.ID, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-09 23:00:09 +08:00
|
|
|
|
func (tx *Transaction) doAddFlashcards(operation *Operation) (ret *TxErr) {
|
2022-12-21 14:45:43 +08:00
|
|
|
|
deckLock.Lock()
|
2022-12-23 17:02:25 +08:00
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2024-01-13 22:30:57 +08:00
|
|
|
|
if isSyncingStorages() {
|
2023-04-09 23:00:09 +08:00
|
|
|
|
ret = &TxErr{code: TxErrCodeDataIsSyncing}
|
2022-12-23 17:02:25 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-12-21 14:45:43 +08:00
|
|
|
|
|
2023-04-09 23:00:09 +08:00
|
|
|
|
deckID := operation.DeckID
|
|
|
|
|
|
blockIDs := operation.BlockIDs
|
|
|
|
|
|
|
2023-04-18 13:36:01 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-22 19:53:00 +08:00
|
|
|
|
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 {
|
2023-04-09 23:00:09 +08:00
|
|
|
|
tree, _ = tx.loadTree(blockID)
|
2022-12-22 19:53:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err := tx.writeTree(tree); err != nil {
|
2023-04-09 23:00:09 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID}
|
2022-12-22 19:53:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL))
|
2023-03-03 15:01:42 +08:00
|
|
|
|
pushBroadcastAttrTransactions(oldAttrs, node)
|
2022-12-22 19:53:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-24 10:35:02 +08:00
|
|
|
|
deck := Decks[deckID]
|
2023-02-18 22:32:34 +08:00
|
|
|
|
if nil == deck {
|
|
|
|
|
|
logging.LogWarnf("deck [%s] not found", deckID)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, blockID := range blockIDs {
|
2023-02-25 11:51:07 +08:00
|
|
|
|
cards := deck.GetCardsByBlockID(blockID)
|
|
|
|
|
|
if 0 < len(cards) {
|
|
|
|
|
|
// 一个块只能添加生成一张闪卡 https://github.com/siyuan-note/siyuan/issues/7476
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-20 20:40:43 +08:00
|
|
|
|
deck.AddCard(ast.NewNodeID(), blockID)
|
2023-02-18 22:32:34 +08:00
|
|
|
|
}
|
2023-04-09 23:00:09 +08:00
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err := deck.Save(); err != nil {
|
2023-02-18 22:32:34 +08:00
|
|
|
|
logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
|
|
|
|
|
|
return
|
2022-12-21 14:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-01-18 16:20:57 +08:00
|
|
|
|
func LoadFlashcards() {
|
2022-12-21 14:51:30 +08:00
|
|
|
|
riffSavePath := getRiffDir()
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err := os.MkdirAll(riffSavePath, 0755); err != nil {
|
2022-12-22 10:38:15 +08:00
|
|
|
|
logging.LogErrorf("create riff dir [%s] failed: %s", riffSavePath, err)
|
2022-12-21 14:46:27 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-23 16:19:36 +08:00
|
|
|
|
Decks = map[string]*riff.Deck{}
|
|
|
|
|
|
|
2022-12-21 14:45:43 +08:00
|
|
|
|
entries, err := os.ReadDir(riffSavePath)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2022-12-21 14:45:43 +08:00
|
|
|
|
logging.LogErrorf("read riff dir failed: %s", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
|
|
name := entry.Name()
|
2022-12-21 21:23:05 +08:00
|
|
|
|
if strings.HasSuffix(name, ".deck") {
|
|
|
|
|
|
deckID := strings.TrimSuffix(name, ".deck")
|
2023-09-28 22:39:12 +08:00
|
|
|
|
deck, loadErr := riff.LoadDeck(riffSavePath, deckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights)
|
2022-12-21 14:45:43 +08:00
|
|
|
|
if nil != loadErr {
|
|
|
|
|
|
logging.LogErrorf("load deck [%s] failed: %s", name, loadErr)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-23 22:58:46 +08:00
|
|
|
|
if 0 == deck.Created {
|
|
|
|
|
|
deck.Created = time.Now().Unix()
|
|
|
|
|
|
}
|
|
|
|
|
|
if 0 == deck.Updated {
|
2022-12-23 22:59:32 +08:00
|
|
|
|
deck.Updated = deck.Created
|
2022-12-23 22:58:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-21 21:23:05 +08:00
|
|
|
|
Decks[deckID] = deck
|
2022-12-21 14:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-12-22 10:38:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-18 22:32:34 +08:00
|
|
|
|
const builtinDeckID = "20230218211946-2kw8jgx"
|
|
|
|
|
|
|
2022-12-23 16:19:36 +08:00
|
|
|
|
func RenameDeck(deckID, name string) (err error) {
|
2022-12-22 10:38:15 +08:00
|
|
|
|
deckLock.Lock()
|
2022-12-23 17:02:25 +08:00
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2022-12-22 10:38:15 +08:00
|
|
|
|
|
2022-12-23 17:02:25 +08:00
|
|
|
|
deck := Decks[deckID]
|
2022-12-22 10:38:15 +08:00
|
|
|
|
deck.Name = name
|
|
|
|
|
|
err = deck.Save()
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2022-12-22 10:38:15 +08:00
|
|
|
|
logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
2022-12-21 14:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-23 16:19:36 +08:00
|
|
|
|
func RemoveDeck(deckID string) (err error) {
|
2022-12-23 17:02:25 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2022-12-23 17:02:25 +08:00
|
|
|
|
|
2022-12-23 16:19:36 +08:00
|
|
|
|
riffSavePath := getRiffDir()
|
|
|
|
|
|
deckPath := filepath.Join(riffSavePath, deckID+".deck")
|
2023-11-06 22:13:04 +08:00
|
|
|
|
if filelock.IsExist(deckPath) {
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = filelock.Remove(deckPath); err != nil {
|
2022-12-29 20:57:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-12-23 16:19:36 +08:00
|
|
|
|
}
|
2022-12-29 20:57:16 +08:00
|
|
|
|
|
2022-12-23 16:19:36 +08:00
|
|
|
|
cardsPath := filepath.Join(riffSavePath, deckID+".cards")
|
2023-11-06 22:13:04 +08:00
|
|
|
|
if filelock.IsExist(cardsPath) {
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = filelock.Remove(cardsPath); err != nil {
|
2022-12-29 20:57:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-12-23 16:19:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-01-18 16:20:57 +08:00
|
|
|
|
LoadFlashcards()
|
2022-12-23 16:19:36 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-21 22:29:05 +08:00
|
|
|
|
func CreateDeck(name string) (deck *riff.Deck, err error) {
|
2022-12-23 17:02:25 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
2022-12-29 20:57:16 +08:00
|
|
|
|
return createDeck(name)
|
|
|
|
|
|
}
|
2022-12-23 17:02:25 +08:00
|
|
|
|
|
2022-12-29 20:57:16 +08:00
|
|
|
|
func createDeck(name string) (deck *riff.Deck, err error) {
|
2023-04-28 15:35:30 +08:00
|
|
|
|
waitForSyncingStorages()
|
2022-12-23 17:02:25 +08:00
|
|
|
|
|
2022-12-21 21:23:05 +08:00
|
|
|
|
deckID := ast.NewNodeID()
|
2023-02-18 22:32:34 +08:00
|
|
|
|
deck, err = createDeck0(name, deckID)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func createDeck0(name string, deckID string) (deck *riff.Deck, err error) {
|
|
|
|
|
|
riffSavePath := getRiffDir()
|
2023-09-28 22:39:12 +08:00
|
|
|
|
deck, err = riff.LoadDeck(riffSavePath, deckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2022-12-21 21:23:05 +08:00
|
|
|
|
logging.LogErrorf("load deck [%s] failed: %s", deckID, err)
|
2022-12-21 14:45:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-12-21 21:23:05 +08:00
|
|
|
|
deck.Name = name
|
|
|
|
|
|
Decks[deckID] = deck
|
2023-01-03 18:31:43 +08:00
|
|
|
|
err = deck.Save()
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-03 18:31:43 +08:00
|
|
|
|
logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-12-21 14:45:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-21 21:23:05 +08:00
|
|
|
|
func GetDecks() (decks []*riff.Deck) {
|
2022-12-21 20:11:12 +08:00
|
|
|
|
deckLock.Lock()
|
|
|
|
|
|
defer deckLock.Unlock()
|
|
|
|
|
|
|
2022-12-21 21:23:05 +08:00
|
|
|
|
for _, deck := range Decks {
|
2023-02-18 22:32:34 +08:00
|
|
|
|
if deck.ID == builtinDeckID {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-21 21:23:05 +08:00
|
|
|
|
decks = append(decks, deck)
|
2022-12-21 20:11:12 +08:00
|
|
|
|
}
|
2022-12-21 21:23:05 +08:00
|
|
|
|
if 1 > len(decks) {
|
|
|
|
|
|
decks = []*riff.Deck{}
|
2022-12-21 14:45:43 +08:00
|
|
|
|
}
|
2023-02-23 08:39:37 +08:00
|
|
|
|
|
|
|
|
|
|
sort.Slice(decks, func(i, j int) bool {
|
|
|
|
|
|
return decks[i].Updated > decks[j].Updated
|
|
|
|
|
|
})
|
2022-12-21 14:45:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-12-21 14:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
func getRiffDir() string {
|
|
|
|
|
|
return filepath.Join(util.DataDir, "storage", "riff")
|
|
|
|
|
|
}
|
2022-12-24 10:50:47 +08:00
|
|
|
|
|
|
|
|
|
|
func getDeckIDs() (deckIDs []string) {
|
|
|
|
|
|
for deckID := range Decks {
|
|
|
|
|
|
deckIDs = append(deckIDs, deckID)
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2023-03-18 17:10:14 +08:00
|
|
|
|
|
2024-02-24 10:33:00 +08:00
|
|
|
|
func getDeckDueCards(deck *riff.Deck, reviewedCardIDs, blockIDs []string, newCardLimit, reviewCardLimit, reviewMode int) (ret []riff.Card, unreviewedCount, unreviewedNewCardCountInRound, unreviewedOldCardCountInRound int) {
|
2023-03-18 17:10:14 +08:00
|
|
|
|
ret = []riff.Card{}
|
2024-02-24 10:33:00 +08:00
|
|
|
|
var retNew, retOld []riff.Card
|
|
|
|
|
|
|
2023-03-18 17:10:14 +08:00
|
|
|
|
dues := deck.Dues()
|
2024-07-09 23:25:28 +08:00
|
|
|
|
toChecks := map[string]riff.Card{}
|
2023-12-20 10:53:32 +08:00
|
|
|
|
for _, c := range dues {
|
2023-12-22 10:14:59 +08:00
|
|
|
|
if 0 < len(blockIDs) && !gulu.Str.Contains(c.BlockID(), blockIDs) {
|
2023-12-20 10:53:32 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-09 23:25:28 +08:00
|
|
|
|
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])
|
2023-03-18 17:10:14 +08:00
|
|
|
|
}
|
2023-04-11 18:46:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
dues = tmp
|
|
|
|
|
|
|
2023-12-26 23:23:26 +08:00
|
|
|
|
reviewedCardCount := len(reviewedCardIDs)
|
|
|
|
|
|
if 1 > reviewedCardCount {
|
2023-04-13 10:09:50 +08:00
|
|
|
|
// 未传入已复习的卡片 ID,说明是开始新的复习,需要清空缓存
|
|
|
|
|
|
reviewCardCache = map[string]riff.Card{}
|
|
|
|
|
|
skipCardCache = map[string]riff.Card{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-11 18:46:36 +08:00
|
|
|
|
newCount := 0
|
|
|
|
|
|
reviewCount := 0
|
2023-12-26 23:23:26 +08:00
|
|
|
|
for _, reviewedCard := range reviewCardCache {
|
|
|
|
|
|
if riff.New == reviewedCard.GetState() {
|
2023-12-22 11:16:28 +08:00
|
|
|
|
newCount++
|
2023-03-19 15:35:59 +08:00
|
|
|
|
} else {
|
2023-12-22 11:16:28 +08:00
|
|
|
|
reviewCount++
|
2023-03-19 15:35:59 +08:00
|
|
|
|
}
|
2023-12-26 23:23:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, c := range dues {
|
|
|
|
|
|
if nil != skipCardCache[c.ID()] {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2023-03-19 15:35:59 +08:00
|
|
|
|
|
2023-12-22 10:14:59 +08:00
|
|
|
|
if 0 < len(reviewedCardIDs) {
|
|
|
|
|
|
if !gulu.Str.Contains(c.ID(), reviewedCardIDs) {
|
|
|
|
|
|
unreviewedCount++
|
|
|
|
|
|
if riff.New == c.GetState() {
|
2023-12-26 23:23:26 +08:00
|
|
|
|
if newCount < newCardLimit {
|
|
|
|
|
|
unreviewedNewCardCountInRound++
|
|
|
|
|
|
}
|
2023-12-22 10:14:59 +08:00
|
|
|
|
} else {
|
2023-12-26 23:23:26 +08:00
|
|
|
|
if reviewCount < reviewCardLimit {
|
|
|
|
|
|
unreviewedOldCardCountInRound++
|
|
|
|
|
|
}
|
2023-12-22 10:14:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
unreviewedCount++
|
|
|
|
|
|
if riff.New == c.GetState() {
|
2023-12-26 23:23:26 +08:00
|
|
|
|
if newCount < newCardLimit {
|
|
|
|
|
|
unreviewedNewCardCountInRound++
|
|
|
|
|
|
}
|
2023-12-22 10:14:59 +08:00
|
|
|
|
} else {
|
2023-12-26 23:23:26 +08:00
|
|
|
|
if reviewCount < reviewCardLimit {
|
|
|
|
|
|
unreviewedOldCardCountInRound++
|
|
|
|
|
|
}
|
2023-12-22 10:14:59 +08:00
|
|
|
|
}
|
2023-03-22 14:44:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-26 23:23:26 +08:00
|
|
|
|
if riff.New == c.GetState() {
|
|
|
|
|
|
if newCount >= newCardLimit {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newCount++
|
2024-02-24 10:33:00 +08:00
|
|
|
|
retNew = append(retNew, c)
|
2023-12-26 23:23:26 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if reviewCount >= reviewCardLimit {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reviewCount++
|
2024-02-24 10:33:00 +08:00
|
|
|
|
retOld = append(retOld, c)
|
2023-12-26 23:23:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-18 17:10:14 +08:00
|
|
|
|
ret = append(ret, c)
|
|
|
|
|
|
}
|
2024-02-24 10:33:00 +08:00
|
|
|
|
|
|
|
|
|
|
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...)
|
|
|
|
|
|
}
|
2023-03-18 17:10:14 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|