flashcard.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  1. // SiYuan - Refactor your thinking
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package model
  17. import (
  18. "math"
  19. "os"
  20. "path/filepath"
  21. "sort"
  22. "strconv"
  23. "strings"
  24. "sync"
  25. "time"
  26. "github.com/88250/gulu"
  27. "github.com/88250/lute/ast"
  28. "github.com/88250/lute/parse"
  29. "github.com/siyuan-note/filelock"
  30. "github.com/siyuan-note/logging"
  31. "github.com/siyuan-note/riff"
  32. "github.com/siyuan-note/siyuan/kernel/cache"
  33. "github.com/siyuan-note/siyuan/kernel/sql"
  34. "github.com/siyuan-note/siyuan/kernel/treenode"
  35. "github.com/siyuan-note/siyuan/kernel/util"
  36. )
  37. type SetFlashcardDueTime struct {
  38. ID string `json:"id"` // 卡片 ID
  39. Due string `json:"due"` // 下次复习时间,格式为 YYYYMMDDHHmmss
  40. }
  41. func SetFlashcardsDueTime(cardDues []*SetFlashcardDueTime) (err error) {
  42. // Add internal kernel API `/api/riff/batchSetRiffCardsDueTime` https://github.com/siyuan-note/siyuan/issues/10423
  43. deckLock.Lock()
  44. defer deckLock.Unlock()
  45. waitForSyncingStorages()
  46. deck := Decks[builtinDeckID]
  47. if nil == deck {
  48. return
  49. }
  50. for _, cardDue := range cardDues {
  51. card := deck.GetCard(cardDue.ID)
  52. if nil == card {
  53. continue
  54. }
  55. due, parseErr := time.Parse("20060102150405", cardDue.Due)
  56. if nil != parseErr {
  57. logging.LogErrorf("parse due time [%s] failed: %s", cardDue.Due, err)
  58. err = parseErr
  59. return
  60. }
  61. card.SetDue(due)
  62. }
  63. if err = deck.Save(); nil != err {
  64. logging.LogErrorf("save deck [%s] failed: %s", builtinDeckID, err)
  65. }
  66. return
  67. }
  68. func ResetFlashcards(typ, id, deckID string, blockIDs []string) {
  69. // Support resetting the learning progress of flashcards https://github.com/siyuan-note/siyuan/issues/9564
  70. if 0 < len(blockIDs) {
  71. if "" == deckID {
  72. // 从全局管理进入时不会指定卡包 ID,这时需要遍历所有卡包
  73. for _, deck := range Decks {
  74. allBlockIDs := deck.GetBlockIDs()
  75. for _, blockID := range blockIDs {
  76. if gulu.Str.Contains(blockID, allBlockIDs) {
  77. deckID = deck.ID
  78. break
  79. }
  80. }
  81. if "" == deckID {
  82. logging.LogWarnf("deck not found for blocks [%s]", strings.Join(blockIDs, ","))
  83. continue
  84. }
  85. resetFlashcards(deckID, blockIDs)
  86. }
  87. return
  88. }
  89. resetFlashcards(deckID, blockIDs)
  90. return
  91. }
  92. var blocks []*Block
  93. switch typ {
  94. case "notebook":
  95. for i := 1; ; i++ {
  96. pagedBlocks, _, _ := GetNotebookFlashcards(id, i, 20)
  97. if 1 > len(pagedBlocks) {
  98. break
  99. }
  100. blocks = append(blocks, pagedBlocks...)
  101. }
  102. for _, block := range blocks {
  103. blockIDs = append(blockIDs, block.ID)
  104. }
  105. case "tree":
  106. for i := 1; ; i++ {
  107. pagedBlocks, _, _ := GetTreeFlashcards(id, i, 20)
  108. if 1 > len(pagedBlocks) {
  109. break
  110. }
  111. blocks = append(blocks, pagedBlocks...)
  112. }
  113. for _, block := range blocks {
  114. blockIDs = append(blockIDs, block.ID)
  115. }
  116. case "deck":
  117. for i := 1; ; i++ {
  118. pagedBlocks, _, _ := GetDeckFlashcards(id, i, 20)
  119. if 1 > len(pagedBlocks) {
  120. break
  121. }
  122. blocks = append(blocks, pagedBlocks...)
  123. }
  124. default:
  125. logging.LogErrorf("invalid type [%s]", typ)
  126. }
  127. blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs)
  128. resetFlashcards(deckID, blockIDs)
  129. }
  130. func resetFlashcards(deckID string, blockIDs []string) {
  131. transactions := []*Transaction{
  132. {
  133. DoOperations: []*Operation{
  134. {
  135. Action: "removeFlashcards",
  136. DeckID: deckID,
  137. BlockIDs: blockIDs,
  138. },
  139. },
  140. },
  141. {
  142. DoOperations: []*Operation{
  143. {
  144. Action: "addFlashcards",
  145. DeckID: deckID,
  146. BlockIDs: blockIDs,
  147. },
  148. },
  149. },
  150. }
  151. PerformTransactions(&transactions)
  152. WaitForWritingFiles()
  153. }
  154. func GetFlashcardNotebooks() (ret []*Box) {
  155. deck := Decks[builtinDeckID]
  156. if nil == deck {
  157. return
  158. }
  159. deckBlockIDs := deck.GetBlockIDs()
  160. boxes := Conf.GetOpenedBoxes()
  161. for _, box := range boxes {
  162. newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
  163. if 0 < flashcardCount {
  164. box.NewFlashcardCount = newFlashcardCount
  165. box.DueFlashcardCount = dueFlashcardCount
  166. box.FlashcardCount = flashcardCount
  167. ret = append(ret, box)
  168. }
  169. }
  170. return
  171. }
  172. func countTreeFlashcard(rootID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) {
  173. blockIDsMap, blockIDs := getTreeSubTreeChildBlocks(rootID)
  174. for _, deckBlockID := range deckBlockIDs {
  175. if blockIDsMap[deckBlockID] {
  176. flashcardCount++
  177. }
  178. }
  179. if 1 > flashcardCount {
  180. return
  181. }
  182. newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs)
  183. newFlashcardCount = len(newFlashCards)
  184. newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs)
  185. dueFlashcardCount = len(newDueFlashcards)
  186. return
  187. }
  188. func countBoxFlashcard(boxID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) {
  189. blockIDsMap, blockIDs := getBoxBlocks(boxID)
  190. for _, deckBlockID := range deckBlockIDs {
  191. if blockIDsMap[deckBlockID] {
  192. flashcardCount++
  193. }
  194. }
  195. if 1 > flashcardCount {
  196. return
  197. }
  198. newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs)
  199. newFlashcardCount = len(newFlashCards)
  200. newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs)
  201. dueFlashcardCount = len(newDueFlashcards)
  202. return
  203. }
  204. var (
  205. Decks = map[string]*riff.Deck{}
  206. deckLock = sync.Mutex{}
  207. )
  208. func GetNotebookFlashcards(boxID string, page, pageSize int) (blocks []*Block, total, pageCount int) {
  209. blocks = []*Block{}
  210. entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID))
  211. if nil != err {
  212. logging.LogErrorf("read dir failed: %s", err)
  213. return
  214. }
  215. var rootIDs []string
  216. for _, entry := range entries {
  217. if entry.IsDir() {
  218. continue
  219. }
  220. if !strings.HasSuffix(entry.Name(), ".sy") {
  221. continue
  222. }
  223. rootIDs = append(rootIDs, strings.TrimSuffix(entry.Name(), ".sy"))
  224. }
  225. var treeBlockIDs []string
  226. for _, rootID := range rootIDs {
  227. _, blockIDs := getTreeSubTreeChildBlocks(rootID)
  228. treeBlockIDs = append(treeBlockIDs, blockIDs...)
  229. }
  230. treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
  231. deck := Decks[builtinDeckID]
  232. if nil == deck {
  233. return
  234. }
  235. var allBlockIDs []string
  236. deckBlockIDs := deck.GetBlockIDs()
  237. for _, blockID := range deckBlockIDs {
  238. if gulu.Str.Contains(blockID, treeBlockIDs) {
  239. allBlockIDs = append(allBlockIDs, blockID)
  240. }
  241. }
  242. allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs)
  243. cards := deck.GetCardsByBlockIDs(allBlockIDs)
  244. blocks, total, pageCount = getCardsBlocks(cards, page, pageSize)
  245. return
  246. }
  247. func GetTreeFlashcards(rootID string, page, pageSize int) (blocks []*Block, total, pageCount int) {
  248. blocks = []*Block{}
  249. cards := getTreeSubTreeFlashcards(rootID)
  250. blocks, total, pageCount = getCardsBlocks(cards, page, pageSize)
  251. return
  252. }
  253. func getTreeSubTreeFlashcards(rootID string) (ret []riff.Card) {
  254. deck := Decks[builtinDeckID]
  255. if nil == deck {
  256. return
  257. }
  258. var allBlockIDs []string
  259. deckBlockIDs := deck.GetBlockIDs()
  260. treeBlockIDsMap, _ := getTreeSubTreeChildBlocks(rootID)
  261. for _, blockID := range deckBlockIDs {
  262. if treeBlockIDsMap[blockID] {
  263. allBlockIDs = append(allBlockIDs, blockID)
  264. }
  265. }
  266. allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs)
  267. ret = deck.GetCardsByBlockIDs(allBlockIDs)
  268. return
  269. }
  270. func getTreeFlashcards(rootID string) (ret []riff.Card) {
  271. deck := Decks[builtinDeckID]
  272. if nil == deck {
  273. return
  274. }
  275. var allBlockIDs []string
  276. deckBlockIDs := deck.GetBlockIDs()
  277. treeBlockIDsMap, _ := getTreeBlocks(rootID)
  278. for _, blockID := range deckBlockIDs {
  279. if treeBlockIDsMap[blockID] {
  280. allBlockIDs = append(allBlockIDs, blockID)
  281. }
  282. }
  283. allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs)
  284. ret = deck.GetCardsByBlockIDs(allBlockIDs)
  285. return
  286. }
  287. func GetDeckFlashcards(deckID string, page, pageSize int) (blocks []*Block, total, pageCount int) {
  288. blocks = []*Block{}
  289. var cards []riff.Card
  290. if "" == deckID {
  291. for _, deck := range Decks {
  292. blockIDs := deck.GetBlockIDs()
  293. cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...)
  294. }
  295. } else {
  296. deck := Decks[deckID]
  297. if nil == deck {
  298. return
  299. }
  300. blockIDs := deck.GetBlockIDs()
  301. cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...)
  302. }
  303. blocks, total, pageCount = getCardsBlocks(cards, page, pageSize)
  304. return
  305. }
  306. func getCardsBlocks(cards []riff.Card, page, pageSize int) (blocks []*Block, total, pageCount int) {
  307. // sort by due date asc https://github.com/siyuan-note/siyuan/pull/9673
  308. sort.Slice(cards, func(i, j int) bool {
  309. due1 := cards[i].(*riff.FSRSCard).C.Due
  310. due2 := cards[j].(*riff.FSRSCard).C.Due
  311. return due1.Before(due2)
  312. })
  313. total = len(cards)
  314. pageCount = int(math.Ceil(float64(total) / float64(pageSize)))
  315. start := (page - 1) * pageSize
  316. end := page * pageSize
  317. if start > len(cards) {
  318. start = len(cards)
  319. }
  320. if end > len(cards) {
  321. end = len(cards)
  322. }
  323. cards = cards[start:end]
  324. if 1 > len(cards) {
  325. blocks = []*Block{}
  326. return
  327. }
  328. var blockIDs []string
  329. for _, card := range cards {
  330. blockIDs = append(blockIDs, card.BlockID())
  331. }
  332. sqlBlocks := sql.GetBlocks(blockIDs)
  333. blocks = fromSQLBlocks(&sqlBlocks, "", 36)
  334. if 1 > len(blocks) {
  335. blocks = []*Block{}
  336. return
  337. }
  338. for i, b := range blocks {
  339. if nil == b {
  340. blocks[i] = &Block{
  341. ID: blockIDs[i],
  342. Content: Conf.Language(180),
  343. }
  344. continue
  345. }
  346. b.RiffCardID = cards[i].ID()
  347. b.RiffCard = getRiffCard(cards[i].(*riff.FSRSCard).C)
  348. }
  349. return
  350. }
  351. var (
  352. // reviewCardCache <cardID, card> 用于复习时缓存卡片,以便支持撤销。
  353. reviewCardCache = map[string]riff.Card{}
  354. // skipCardCache <cardID, card> 用于复习时缓存跳过的卡片,以便支持跳过过滤。
  355. skipCardCache = map[string]riff.Card{}
  356. )
  357. func ReviewFlashcard(deckID, cardID string, rating riff.Rating, reviewedCardIDs []string) (err error) {
  358. deckLock.Lock()
  359. defer deckLock.Unlock()
  360. waitForSyncingStorages()
  361. deck := Decks[deckID]
  362. card := deck.GetCard(cardID)
  363. if nil == card {
  364. return
  365. }
  366. if cachedCard := reviewCardCache[cardID]; nil != cachedCard {
  367. // 命中缓存说明这张卡片已经复习过了,这次调用复习是撤销后再次复习
  368. // 将缓存的卡片重新覆盖回卡包中,以恢复最开始复习前的状态
  369. deck.SetCard(cachedCard)
  370. // 从跳过缓存中移除(如果上一次点的是跳过的话),如果不在跳过缓存中,说明上一次点的是复习,这里移除一下也没有副作用
  371. delete(skipCardCache, cardID)
  372. } else {
  373. // 首次复习该卡片,将卡片缓存以便后续支持撤销后再次复习
  374. reviewCardCache[cardID] = card.Clone()
  375. }
  376. log := deck.Review(cardID, rating)
  377. if err = deck.Save(); nil != err {
  378. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  379. return
  380. }
  381. if err = deck.SaveLog(log); nil != err {
  382. logging.LogErrorf("save review log [%s] failed: %s", deckID, err)
  383. return
  384. }
  385. _, unreviewedCount, _, _ := getDueFlashcards(deckID, reviewedCardIDs)
  386. if 1 > unreviewedCount {
  387. // 该卡包中没有待复习的卡片了,说明最后一张卡片已经复习完了,清空撤销缓存和跳过缓存
  388. reviewCardCache = map[string]riff.Card{}
  389. skipCardCache = map[string]riff.Card{}
  390. }
  391. return
  392. }
  393. func SkipReviewFlashcard(deckID, cardID string) (err error) {
  394. deckLock.Lock()
  395. defer deckLock.Unlock()
  396. waitForSyncingStorages()
  397. deck := Decks[deckID]
  398. card := deck.GetCard(cardID)
  399. if nil == card {
  400. return
  401. }
  402. skipCardCache[cardID] = card
  403. return
  404. }
  405. type Flashcard struct {
  406. DeckID string `json:"deckID"`
  407. CardID string `json:"cardID"`
  408. BlockID string `json:"blockID"`
  409. Lapses int `json:"lapses"`
  410. Reps int `json:"reps"`
  411. State riff.State `json:"state"`
  412. LastReview time.Time `json:"lastReview"`
  413. NextDues map[riff.Rating]string `json:"nextDues"`
  414. }
  415. func newFlashcard(card riff.Card, blockID, deckID string, now time.Time) *Flashcard {
  416. nextDues := map[riff.Rating]string{}
  417. for rating, due := range card.NextDues() {
  418. nextDues[rating] = strings.TrimSpace(util.HumanizeDiffTime(due, now, Conf.Lang))
  419. }
  420. return &Flashcard{
  421. DeckID: deckID,
  422. CardID: card.ID(),
  423. BlockID: blockID,
  424. Lapses: card.GetLapses(),
  425. Reps: card.GetReps(),
  426. State: card.GetState(),
  427. LastReview: card.GetLastReview(),
  428. NextDues: nextDues,
  429. }
  430. }
  431. func GetNotebookDueFlashcards(boxID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) {
  432. deckLock.Lock()
  433. defer deckLock.Unlock()
  434. waitForSyncingStorages()
  435. entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID))
  436. if nil != err {
  437. logging.LogErrorf("read dir failed: %s", err)
  438. return
  439. }
  440. var rootIDs []string
  441. for _, entry := range entries {
  442. if entry.IsDir() {
  443. continue
  444. }
  445. if !strings.HasSuffix(entry.Name(), ".sy") {
  446. continue
  447. }
  448. rootIDs = append(rootIDs, strings.TrimSuffix(entry.Name(), ".sy"))
  449. }
  450. var treeBlockIDs []string
  451. for _, rootID := range rootIDs {
  452. _, blockIDs := getTreeSubTreeChildBlocks(rootID)
  453. treeBlockIDs = append(treeBlockIDs, blockIDs...)
  454. }
  455. treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
  456. deck := Decks[builtinDeckID]
  457. if nil == deck {
  458. logging.LogWarnf("builtin deck not found")
  459. return
  460. }
  461. cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode)
  462. now := time.Now()
  463. for _, card := range cards {
  464. ret = append(ret, newFlashcard(card, card.BlockID(), builtinDeckID, now))
  465. }
  466. if 1 > len(ret) {
  467. ret = []*Flashcard{}
  468. }
  469. unreviewedCount = unreviewedCnt
  470. unreviewedNewCardCount = unreviewedNewCardCnt
  471. unreviewedOldCardCount = unreviewedOldCardCnt
  472. return
  473. }
  474. func GetTreeDueFlashcards(rootID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) {
  475. deckLock.Lock()
  476. defer deckLock.Unlock()
  477. waitForSyncingStorages()
  478. deck := Decks[builtinDeckID]
  479. if nil == deck {
  480. return
  481. }
  482. _, treeBlockIDs := getTreeSubTreeChildBlocks(rootID)
  483. newCardLimit := Conf.Flashcard.NewCardLimit
  484. reviewCardLimit := Conf.Flashcard.ReviewCardLimit
  485. // 文档级新卡/复习卡上限控制 Document-level new card/review card limit control https://github.com/siyuan-note/siyuan/issues/9365
  486. ial := GetBlockAttrs(rootID)
  487. if newCardLimitStr := ial["custom-riff-new-card-limit"]; "" != newCardLimitStr {
  488. var convertErr error
  489. newCardLimit, convertErr = strconv.Atoi(newCardLimitStr)
  490. if nil != convertErr {
  491. logging.LogWarnf("invalid new card limit [%s]: %s", newCardLimitStr, convertErr)
  492. }
  493. }
  494. if reviewCardLimitStr := ial["custom-riff-review-card-limit"]; "" != reviewCardLimitStr {
  495. var convertErr error
  496. reviewCardLimit, convertErr = strconv.Atoi(reviewCardLimitStr)
  497. if nil != convertErr {
  498. logging.LogWarnf("invalid review card limit [%s]: %s", reviewCardLimitStr, convertErr)
  499. }
  500. }
  501. cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs, newCardLimit, reviewCardLimit, Conf.Flashcard.ReviewMode)
  502. now := time.Now()
  503. for _, card := range cards {
  504. ret = append(ret, newFlashcard(card, card.BlockID(), builtinDeckID, now))
  505. }
  506. if 1 > len(ret) {
  507. ret = []*Flashcard{}
  508. }
  509. unreviewedCount = unreviewedCnt
  510. unreviewedNewCardCount = unreviewedNewCardCnt
  511. unreviewedOldCardCount = unreviewedOldCardCnt
  512. return
  513. }
  514. func getTreeSubTreeChildBlocks(rootID string) (treeBlockIDsMap map[string]bool, treeBlockIDs []string) {
  515. treeBlockIDsMap = map[string]bool{}
  516. root := treenode.GetBlockTree(rootID)
  517. if nil == root {
  518. return
  519. }
  520. bts := treenode.GetBlockTreesByPathPrefix(strings.TrimSuffix(root.Path, ".sy"))
  521. for _, bt := range bts {
  522. treeBlockIDsMap[bt.ID] = true
  523. treeBlockIDs = append(treeBlockIDs, bt.ID)
  524. }
  525. return
  526. }
  527. func getTreeBlocks(rootID string) (treeBlockIDsMap map[string]bool, treeBlockIDs []string) {
  528. treeBlockIDsMap = map[string]bool{}
  529. bts := treenode.GetBlockTreesByRootID(rootID)
  530. for _, bt := range bts {
  531. treeBlockIDsMap[bt.ID] = true
  532. treeBlockIDs = append(treeBlockIDs, bt.ID)
  533. }
  534. return
  535. }
  536. func getBoxBlocks(boxID string) (blockIDsMap map[string]bool, blockIDs []string) {
  537. blockIDsMap = map[string]bool{}
  538. bts := treenode.GetBlockTreesByBoxID(boxID)
  539. for _, bt := range bts {
  540. blockIDsMap[bt.ID] = true
  541. blockIDs = append(blockIDs, bt.ID)
  542. }
  543. return
  544. }
  545. func GetDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int, err error) {
  546. deckLock.Lock()
  547. defer deckLock.Unlock()
  548. waitForSyncingStorages()
  549. if "" == deckID {
  550. ret, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount = getAllDueFlashcards(reviewedCardIDs)
  551. return
  552. }
  553. ret, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount = getDueFlashcards(deckID, reviewedCardIDs)
  554. return
  555. }
  556. func getDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int) {
  557. deck := Decks[deckID]
  558. if nil == deck {
  559. logging.LogWarnf("deck not found [%s]", deckID)
  560. return
  561. }
  562. cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, nil, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode)
  563. now := time.Now()
  564. for _, card := range cards {
  565. ret = append(ret, newFlashcard(card, card.BlockID(), deckID, now))
  566. }
  567. if 1 > len(ret) {
  568. ret = []*Flashcard{}
  569. }
  570. unreviewedCount = unreviewedCnt
  571. unreviewedNewCardCount = unreviewedNewCardCnt
  572. unreviewedOldCardCount = unreviewedOldCardCnt
  573. return
  574. }
  575. func getAllDueFlashcards(reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount, unreviewedNewCardCount, unreviewedOldCardCount int) {
  576. now := time.Now()
  577. for _, deck := range Decks {
  578. cards, unreviewedCnt, unreviewedNewCardCnt, unreviewedOldCardCnt := getDeckDueCards(deck, reviewedCardIDs, nil, Conf.Flashcard.NewCardLimit, Conf.Flashcard.ReviewCardLimit, Conf.Flashcard.ReviewMode)
  579. unreviewedCount += unreviewedCnt
  580. unreviewedNewCardCount += unreviewedNewCardCnt
  581. unreviewedOldCardCount += unreviewedOldCardCnt
  582. for _, card := range cards {
  583. ret = append(ret, newFlashcard(card, card.BlockID(), deck.ID, now))
  584. }
  585. }
  586. if 1 > len(ret) {
  587. ret = []*Flashcard{}
  588. }
  589. return
  590. }
  591. func (tx *Transaction) doRemoveFlashcards(operation *Operation) (ret *TxErr) {
  592. deckLock.Lock()
  593. defer deckLock.Unlock()
  594. if isSyncingStorages() {
  595. ret = &TxErr{code: TxErrCodeDataIsSyncing}
  596. return
  597. }
  598. deckID := operation.DeckID
  599. blockIDs := operation.BlockIDs
  600. if err := tx.removeBlocksDeckAttr(blockIDs, deckID); nil != err {
  601. return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID}
  602. }
  603. if "" == deckID { // 支持在 All 卡包中移除闪卡 https://github.com/siyuan-note/siyuan/issues/7425
  604. for _, deck := range Decks {
  605. removeFlashcardsByBlockIDs(blockIDs, deck)
  606. }
  607. } else {
  608. removeFlashcardsByBlockIDs(blockIDs, Decks[deckID])
  609. }
  610. return
  611. }
  612. func (tx *Transaction) removeBlocksDeckAttr(blockIDs []string, deckID string) (err error) {
  613. var rootIDs []string
  614. blockRoots := map[string]string{}
  615. for _, blockID := range blockIDs {
  616. bt := treenode.GetBlockTree(blockID)
  617. if nil == bt {
  618. continue
  619. }
  620. rootIDs = append(rootIDs, bt.RootID)
  621. blockRoots[blockID] = bt.RootID
  622. }
  623. rootIDs = gulu.Str.RemoveDuplicatedElem(rootIDs)
  624. trees := map[string]*parse.Tree{}
  625. for _, blockID := range blockIDs {
  626. rootID := blockRoots[blockID]
  627. tree := trees[rootID]
  628. if nil == tree {
  629. tree, _ = tx.loadTree(blockID)
  630. }
  631. if nil == tree {
  632. continue
  633. }
  634. trees[rootID] = tree
  635. node := treenode.GetNodeInTree(tree, blockID)
  636. if nil == node {
  637. continue
  638. }
  639. oldAttrs := parse.IAL2Map(node.KramdownIAL)
  640. deckAttrs := node.IALAttr("custom-riff-decks")
  641. var deckIDs []string
  642. if "" != deckID {
  643. availableDeckIDs := getDeckIDs()
  644. for _, dID := range strings.Split(deckAttrs, ",") {
  645. if dID != deckID && gulu.Str.Contains(dID, availableDeckIDs) {
  646. deckIDs = append(deckIDs, dID)
  647. }
  648. }
  649. }
  650. deckIDs = gulu.Str.RemoveDuplicatedElem(deckIDs)
  651. val := strings.Join(deckIDs, ",")
  652. val = strings.TrimPrefix(val, ",")
  653. val = strings.TrimSuffix(val, ",")
  654. if "" == val {
  655. node.RemoveIALAttr("custom-riff-decks")
  656. } else {
  657. node.SetIALAttr("custom-riff-decks", val)
  658. }
  659. if err = tx.writeTree(tree); nil != err {
  660. return
  661. }
  662. cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL))
  663. pushBroadcastAttrTransactions(oldAttrs, node)
  664. }
  665. return
  666. }
  667. func removeFlashcardsByBlockIDs(blockIDs []string, deck *riff.Deck) {
  668. if nil == deck {
  669. logging.LogErrorf("deck is nil")
  670. return
  671. }
  672. cards := deck.GetCardsByBlockIDs(blockIDs)
  673. if 1 > len(cards) {
  674. return
  675. }
  676. for _, card := range cards {
  677. deck.RemoveCard(card.ID())
  678. }
  679. err := deck.Save()
  680. if nil != err {
  681. logging.LogErrorf("save deck [%s] failed: %s", deck.ID, err)
  682. }
  683. }
  684. func (tx *Transaction) doAddFlashcards(operation *Operation) (ret *TxErr) {
  685. deckLock.Lock()
  686. defer deckLock.Unlock()
  687. if isSyncingStorages() {
  688. ret = &TxErr{code: TxErrCodeDataIsSyncing}
  689. return
  690. }
  691. deckID := operation.DeckID
  692. blockIDs := operation.BlockIDs
  693. foundDeck := false
  694. for _, deck := range Decks {
  695. if deckID == deck.ID {
  696. foundDeck = true
  697. break
  698. }
  699. }
  700. if !foundDeck {
  701. deck, createErr := createDeck0("Built-in Deck", builtinDeckID)
  702. if nil == createErr {
  703. Decks[deck.ID] = deck
  704. }
  705. }
  706. blockRoots := map[string]string{}
  707. for _, blockID := range blockIDs {
  708. bt := treenode.GetBlockTree(blockID)
  709. if nil == bt {
  710. continue
  711. }
  712. blockRoots[blockID] = bt.RootID
  713. }
  714. trees := map[string]*parse.Tree{}
  715. for _, blockID := range blockIDs {
  716. rootID := blockRoots[blockID]
  717. tree := trees[rootID]
  718. if nil == tree {
  719. tree, _ = tx.loadTree(blockID)
  720. }
  721. if nil == tree {
  722. continue
  723. }
  724. trees[rootID] = tree
  725. node := treenode.GetNodeInTree(tree, blockID)
  726. if nil == node {
  727. continue
  728. }
  729. oldAttrs := parse.IAL2Map(node.KramdownIAL)
  730. deckAttrs := node.IALAttr("custom-riff-decks")
  731. deckIDs := strings.Split(deckAttrs, ",")
  732. deckIDs = append(deckIDs, deckID)
  733. deckIDs = gulu.Str.RemoveDuplicatedElem(deckIDs)
  734. val := strings.Join(deckIDs, ",")
  735. val = strings.TrimPrefix(val, ",")
  736. val = strings.TrimSuffix(val, ",")
  737. node.SetIALAttr("custom-riff-decks", val)
  738. if err := tx.writeTree(tree); nil != err {
  739. return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID}
  740. }
  741. cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL))
  742. pushBroadcastAttrTransactions(oldAttrs, node)
  743. }
  744. deck := Decks[deckID]
  745. if nil == deck {
  746. logging.LogWarnf("deck [%s] not found", deckID)
  747. return
  748. }
  749. for _, blockID := range blockIDs {
  750. cards := deck.GetCardsByBlockID(blockID)
  751. if 0 < len(cards) {
  752. // 一个块只能添加生成一张闪卡 https://github.com/siyuan-note/siyuan/issues/7476
  753. continue
  754. }
  755. cardID := ast.NewNodeID()
  756. deck.AddCard(cardID, blockID)
  757. }
  758. if err := deck.Save(); nil != err {
  759. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  760. return
  761. }
  762. return
  763. }
  764. func LoadFlashcards() {
  765. riffSavePath := getRiffDir()
  766. if err := os.MkdirAll(riffSavePath, 0755); nil != err {
  767. logging.LogErrorf("create riff dir [%s] failed: %s", riffSavePath, err)
  768. return
  769. }
  770. Decks = map[string]*riff.Deck{}
  771. entries, err := os.ReadDir(riffSavePath)
  772. if nil != err {
  773. logging.LogErrorf("read riff dir failed: %s", err)
  774. return
  775. }
  776. for _, entry := range entries {
  777. name := entry.Name()
  778. if strings.HasSuffix(name, ".deck") {
  779. deckID := strings.TrimSuffix(name, ".deck")
  780. deck, loadErr := riff.LoadDeck(riffSavePath, deckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights)
  781. if nil != loadErr {
  782. logging.LogErrorf("load deck [%s] failed: %s", name, loadErr)
  783. continue
  784. }
  785. if 0 == deck.Created {
  786. deck.Created = time.Now().Unix()
  787. }
  788. if 0 == deck.Updated {
  789. deck.Updated = deck.Created
  790. }
  791. Decks[deckID] = deck
  792. }
  793. }
  794. }
  795. const builtinDeckID = "20230218211946-2kw8jgx"
  796. func RenameDeck(deckID, name string) (err error) {
  797. deckLock.Lock()
  798. defer deckLock.Unlock()
  799. waitForSyncingStorages()
  800. deck := Decks[deckID]
  801. deck.Name = name
  802. err = deck.Save()
  803. if nil != err {
  804. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  805. return
  806. }
  807. return
  808. }
  809. func RemoveDeck(deckID string) (err error) {
  810. deckLock.Lock()
  811. defer deckLock.Unlock()
  812. waitForSyncingStorages()
  813. riffSavePath := getRiffDir()
  814. deckPath := filepath.Join(riffSavePath, deckID+".deck")
  815. if filelock.IsExist(deckPath) {
  816. if err = filelock.Remove(deckPath); nil != err {
  817. return
  818. }
  819. }
  820. cardsPath := filepath.Join(riffSavePath, deckID+".cards")
  821. if filelock.IsExist(cardsPath) {
  822. if err = filelock.Remove(cardsPath); nil != err {
  823. return
  824. }
  825. }
  826. LoadFlashcards()
  827. return
  828. }
  829. func CreateDeck(name string) (deck *riff.Deck, err error) {
  830. deckLock.Lock()
  831. defer deckLock.Unlock()
  832. return createDeck(name)
  833. }
  834. func createDeck(name string) (deck *riff.Deck, err error) {
  835. waitForSyncingStorages()
  836. deckID := ast.NewNodeID()
  837. deck, err = createDeck0(name, deckID)
  838. return
  839. }
  840. func createDeck0(name string, deckID string) (deck *riff.Deck, err error) {
  841. riffSavePath := getRiffDir()
  842. deck, err = riff.LoadDeck(riffSavePath, deckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights)
  843. if nil != err {
  844. logging.LogErrorf("load deck [%s] failed: %s", deckID, err)
  845. return
  846. }
  847. deck.Name = name
  848. Decks[deckID] = deck
  849. err = deck.Save()
  850. if nil != err {
  851. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  852. return
  853. }
  854. return
  855. }
  856. func GetDecks() (decks []*riff.Deck) {
  857. deckLock.Lock()
  858. defer deckLock.Unlock()
  859. for _, deck := range Decks {
  860. if deck.ID == builtinDeckID {
  861. continue
  862. }
  863. decks = append(decks, deck)
  864. }
  865. if 1 > len(decks) {
  866. decks = []*riff.Deck{}
  867. }
  868. sort.Slice(decks, func(i, j int) bool {
  869. return decks[i].Updated > decks[j].Updated
  870. })
  871. return
  872. }
  873. func getRiffDir() string {
  874. return filepath.Join(util.DataDir, "storage", "riff")
  875. }
  876. func getDeckIDs() (deckIDs []string) {
  877. for deckID := range Decks {
  878. deckIDs = append(deckIDs, deckID)
  879. }
  880. return
  881. }
  882. func getDeckDueCards(deck *riff.Deck, reviewedCardIDs, blockIDs []string, newCardLimit, reviewCardLimit, reviewMode int) (ret []riff.Card, unreviewedCount, unreviewedNewCardCountInRound, unreviewedOldCardCountInRound int) {
  883. ret = []riff.Card{}
  884. var retNew, retOld []riff.Card
  885. dues := deck.Dues()
  886. var tmp []riff.Card
  887. for _, c := range dues {
  888. if 0 < len(blockIDs) && !gulu.Str.Contains(c.BlockID(), blockIDs) {
  889. continue
  890. }
  891. if nil == treenode.GetBlockTree(c.BlockID()) {
  892. continue
  893. }
  894. tmp = append(tmp, c)
  895. }
  896. dues = tmp
  897. reviewedCardCount := len(reviewedCardIDs)
  898. if 1 > reviewedCardCount {
  899. // 未传入已复习的卡片 ID,说明是开始新的复习,需要清空缓存
  900. reviewCardCache = map[string]riff.Card{}
  901. skipCardCache = map[string]riff.Card{}
  902. }
  903. newCount := 0
  904. reviewCount := 0
  905. for _, reviewedCard := range reviewCardCache {
  906. if riff.New == reviewedCard.GetState() {
  907. newCount++
  908. } else {
  909. reviewCount++
  910. }
  911. }
  912. for _, c := range dues {
  913. if nil != skipCardCache[c.ID()] {
  914. continue
  915. }
  916. if 0 < len(reviewedCardIDs) {
  917. if !gulu.Str.Contains(c.ID(), reviewedCardIDs) {
  918. unreviewedCount++
  919. if riff.New == c.GetState() {
  920. if newCount < newCardLimit {
  921. unreviewedNewCardCountInRound++
  922. }
  923. } else {
  924. if reviewCount < reviewCardLimit {
  925. unreviewedOldCardCountInRound++
  926. }
  927. }
  928. }
  929. } else {
  930. unreviewedCount++
  931. if riff.New == c.GetState() {
  932. if newCount < newCardLimit {
  933. unreviewedNewCardCountInRound++
  934. }
  935. } else {
  936. if reviewCount < reviewCardLimit {
  937. unreviewedOldCardCountInRound++
  938. }
  939. }
  940. }
  941. if riff.New == c.GetState() {
  942. if newCount >= newCardLimit {
  943. continue
  944. }
  945. newCount++
  946. retNew = append(retNew, c)
  947. } else {
  948. if reviewCount >= reviewCardLimit {
  949. continue
  950. }
  951. reviewCount++
  952. retOld = append(retOld, c)
  953. }
  954. ret = append(ret, c)
  955. }
  956. switch reviewMode {
  957. case 1: // 优先复习新卡
  958. ret = nil
  959. ret = append(ret, retNew...)
  960. ret = append(ret, retOld...)
  961. case 2: // 优先复习旧卡
  962. ret = nil
  963. ret = append(ret, retOld...)
  964. ret = append(ret, retNew...)
  965. }
  966. return
  967. }