flashcard.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  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. "strings"
  23. "sync"
  24. "time"
  25. "github.com/88250/gulu"
  26. "github.com/88250/lute/ast"
  27. "github.com/88250/lute/parse"
  28. "github.com/open-spaced-repetition/go-fsrs"
  29. "github.com/siyuan-note/logging"
  30. "github.com/siyuan-note/riff"
  31. "github.com/siyuan-note/siyuan/kernel/cache"
  32. "github.com/siyuan-note/siyuan/kernel/sql"
  33. "github.com/siyuan-note/siyuan/kernel/treenode"
  34. "github.com/siyuan-note/siyuan/kernel/util"
  35. )
  36. func GetFlashcardNotebooks() (ret []*Box) {
  37. deck := Decks[builtinDeckID]
  38. if nil == deck {
  39. return
  40. }
  41. deckBlockIDs := deck.GetBlockIDs()
  42. boxes := Conf.GetOpenedBoxes()
  43. for _, box := range boxes {
  44. newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
  45. if 0 < flashcardCount {
  46. box.NewFlashcardCount = newFlashcardCount
  47. box.DueFlashcardCount = dueFlashcardCount
  48. box.FlashcardCount = flashcardCount
  49. ret = append(ret, box)
  50. }
  51. }
  52. return
  53. }
  54. func countTreeFlashcard(rootID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) {
  55. blockIDsMap, blockIDs := getTreeSubTreeChildBlocks(rootID)
  56. for _, deckBlockID := range deckBlockIDs {
  57. if blockIDsMap[deckBlockID] {
  58. flashcardCount++
  59. }
  60. }
  61. if 1 > flashcardCount {
  62. return
  63. }
  64. newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs)
  65. newFlashcardCount = len(newFlashCards)
  66. newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs)
  67. dueFlashcardCount = len(newDueFlashcards)
  68. return
  69. }
  70. func countBoxFlashcard(boxID string, deck *riff.Deck, deckBlockIDs []string) (newFlashcardCount, dueFlashcardCount, flashcardCount int) {
  71. blockIDsMap, blockIDs := getBoxBlocks(boxID)
  72. for _, deckBlockID := range deckBlockIDs {
  73. if blockIDsMap[deckBlockID] {
  74. flashcardCount++
  75. }
  76. }
  77. if 1 > flashcardCount {
  78. return
  79. }
  80. newFlashCards := deck.GetNewCardsByBlockIDs(blockIDs)
  81. newFlashcardCount = len(newFlashCards)
  82. newDueFlashcards := deck.GetDueCardsByBlockIDs(blockIDs)
  83. dueFlashcardCount = len(newDueFlashcards)
  84. return
  85. }
  86. var (
  87. Decks = map[string]*riff.Deck{}
  88. deckLock = sync.Mutex{}
  89. )
  90. func GetNotebookFlashcards(boxID string, page int) (blocks []*Block, total, pageCount int) {
  91. blocks = []*Block{}
  92. entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID))
  93. if nil != err {
  94. logging.LogErrorf("read dir failed: %s", err)
  95. return
  96. }
  97. var rootIDs []string
  98. for _, entry := range entries {
  99. if entry.IsDir() {
  100. continue
  101. }
  102. if !strings.HasSuffix(entry.Name(), ".sy") {
  103. continue
  104. }
  105. rootIDs = append(rootIDs, strings.TrimSuffix(entry.Name(), ".sy"))
  106. }
  107. var treeBlockIDs []string
  108. for _, rootID := range rootIDs {
  109. _, blockIDs := getTreeSubTreeChildBlocks(rootID)
  110. treeBlockIDs = append(treeBlockIDs, blockIDs...)
  111. }
  112. treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
  113. deck := Decks[builtinDeckID]
  114. if nil == deck {
  115. return
  116. }
  117. var allBlockIDs []string
  118. deckBlockIDs := deck.GetBlockIDs()
  119. for _, blockID := range deckBlockIDs {
  120. if gulu.Str.Contains(blockID, treeBlockIDs) {
  121. allBlockIDs = append(allBlockIDs, blockID)
  122. }
  123. }
  124. allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs)
  125. cards := deck.GetCardsByBlockIDs(allBlockIDs)
  126. blocks, total, pageCount = getCardsBlocks(cards, page)
  127. return
  128. }
  129. func GetTreeFlashcards(rootID string, page int) (blocks []*Block, total, pageCount int) {
  130. blocks = []*Block{}
  131. deck := Decks[builtinDeckID]
  132. if nil == deck {
  133. return
  134. }
  135. var allBlockIDs []string
  136. deckBlockIDs := deck.GetBlockIDs()
  137. treeBlockIDsMap, _ := getTreeSubTreeChildBlocks(rootID)
  138. for _, blockID := range deckBlockIDs {
  139. if treeBlockIDsMap[blockID] {
  140. allBlockIDs = append(allBlockIDs, blockID)
  141. }
  142. }
  143. allBlockIDs = gulu.Str.RemoveDuplicatedElem(allBlockIDs)
  144. cards := deck.GetCardsByBlockIDs(allBlockIDs)
  145. blocks, total, pageCount = getCardsBlocks(cards, page)
  146. return
  147. }
  148. func GetFlashcards(deckID string, page int) (blocks []*Block, total, pageCount int) {
  149. blocks = []*Block{}
  150. var cards []riff.Card
  151. if "" == deckID {
  152. for _, deck := range Decks {
  153. blockIDs := deck.GetBlockIDs()
  154. cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...)
  155. }
  156. } else {
  157. deck := Decks[deckID]
  158. if nil == deck {
  159. return
  160. }
  161. blockIDs := deck.GetBlockIDs()
  162. cards = append(cards, deck.GetCardsByBlockIDs(blockIDs)...)
  163. }
  164. blocks, total, pageCount = getCardsBlocks(cards, page)
  165. return
  166. }
  167. func getCardsBlocks(cards []riff.Card, page int) (blocks []*Block, total, pageCount int) {
  168. sort.Slice(cards, func(i, j int) bool { return cards[i].BlockID() < cards[j].BlockID() })
  169. const pageSize = 20
  170. total = len(cards)
  171. pageCount = int(math.Ceil(float64(total) / float64(pageSize)))
  172. start := (page - 1) * pageSize
  173. end := page * pageSize
  174. if start > len(cards) {
  175. start = len(cards)
  176. }
  177. if end > len(cards) {
  178. end = len(cards)
  179. }
  180. cards = cards[start:end]
  181. if 1 > len(cards) {
  182. blocks = []*Block{}
  183. return
  184. }
  185. var blockIDs []string
  186. for _, card := range cards {
  187. blockIDs = append(blockIDs, card.BlockID())
  188. }
  189. sort.Strings(blockIDs)
  190. sqlBlocks := sql.GetBlocks(blockIDs)
  191. blocks = fromSQLBlocks(&sqlBlocks, "", 36)
  192. if 1 > len(blocks) {
  193. blocks = []*Block{}
  194. return
  195. }
  196. for i, b := range blocks {
  197. if nil == b {
  198. blocks[i] = &Block{
  199. ID: blockIDs[i],
  200. Content: Conf.Language(180),
  201. }
  202. continue
  203. }
  204. b.RiffCardID = cards[i].ID()
  205. b.RiffCardReps = cards[i].(*riff.FSRSCard).C.Reps
  206. }
  207. return
  208. }
  209. var (
  210. // reviewCardCache <cardID, card> 用于复习时缓存卡片,以便支持撤销。
  211. reviewCardCache = map[string]riff.Card{}
  212. // skipCardCache <cardID, card> 用于复习时缓存跳过的卡片,以便支持跳过过滤。
  213. skipCardCache = map[string]riff.Card{}
  214. )
  215. func ReviewFlashcard(deckID, cardID string, rating riff.Rating, reviewedCardIDs []string) (err error) {
  216. deckLock.Lock()
  217. defer deckLock.Unlock()
  218. waitForSyncingStorages()
  219. deck := Decks[deckID]
  220. card := deck.GetCard(cardID)
  221. if nil == card {
  222. return
  223. }
  224. if cachedCard := reviewCardCache[cardID]; nil != cachedCard {
  225. // 命中缓存说明这张卡片已经复习过了,这次调用复习是撤销后再次复习
  226. // 将缓存的卡片重新覆盖回卡包中,以恢复最开始复习前的状态
  227. deck.SetCard(cachedCard)
  228. // 从跳过缓存中移除(如果上一次点的是跳过的话),如果不在跳过缓存中,说明上一次点的是复习,这里移除一下也没有副作用
  229. delete(skipCardCache, cardID)
  230. } else {
  231. // 首次复习该卡片,将卡片缓存以便后续支持撤销后再次复习
  232. reviewCardCache[cardID] = card
  233. }
  234. log := deck.Review(cardID, rating)
  235. if err = deck.Save(); nil != err {
  236. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  237. return
  238. }
  239. if err = deck.SaveLog(log); nil != err {
  240. logging.LogErrorf("save review log [%s] failed: %s", deckID, err)
  241. return
  242. }
  243. dueCards, _ := getDueFlashcards(deckID, reviewedCardIDs)
  244. if 1 > len(dueCards) {
  245. // 该卡包中没有待复习的卡片了,说明最后一张卡片已经复习完了,清空撤销缓存和跳过缓存
  246. reviewCardCache = map[string]riff.Card{}
  247. skipCardCache = map[string]riff.Card{}
  248. }
  249. return
  250. }
  251. func SkipReviewFlashcard(deckID, cardID string) (err error) {
  252. deckLock.Lock()
  253. defer deckLock.Unlock()
  254. waitForSyncingStorages()
  255. deck := Decks[deckID]
  256. card := deck.GetCard(cardID)
  257. if nil == card {
  258. return
  259. }
  260. skipCardCache[cardID] = card
  261. return
  262. }
  263. type Flashcard struct {
  264. DeckID string `json:"deckID"`
  265. CardID string `json:"cardID"`
  266. BlockID string `json:"blockID"`
  267. NextDues map[riff.Rating]string `json:"nextDues"`
  268. }
  269. func newFlashcard(card riff.Card, blockID, deckID string, now time.Time) *Flashcard {
  270. nextDues := map[riff.Rating]string{}
  271. for rating, due := range card.NextDues() {
  272. nextDues[rating] = strings.TrimSpace(util.HumanizeDiffTime(due, now, Conf.Lang))
  273. }
  274. return &Flashcard{
  275. DeckID: deckID,
  276. CardID: card.ID(),
  277. BlockID: blockID,
  278. NextDues: nextDues,
  279. }
  280. }
  281. func GetNotebookDueFlashcards(boxID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount int, err error) {
  282. deckLock.Lock()
  283. defer deckLock.Unlock()
  284. waitForSyncingStorages()
  285. entries, err := os.ReadDir(filepath.Join(util.DataDir, boxID))
  286. if nil != err {
  287. logging.LogErrorf("read dir failed: %s", err)
  288. return
  289. }
  290. var rootIDs []string
  291. for _, entry := range entries {
  292. if entry.IsDir() {
  293. continue
  294. }
  295. if !strings.HasSuffix(entry.Name(), ".sy") {
  296. continue
  297. }
  298. rootIDs = append(rootIDs, strings.TrimSuffix(entry.Name(), ".sy"))
  299. }
  300. var treeBlockIDs []string
  301. for _, rootID := range rootIDs {
  302. _, blockIDs := getTreeSubTreeChildBlocks(rootID)
  303. treeBlockIDs = append(treeBlockIDs, blockIDs...)
  304. }
  305. treeBlockIDs = gulu.Str.RemoveDuplicatedElem(treeBlockIDs)
  306. deck := Decks[builtinDeckID]
  307. if nil == deck {
  308. logging.LogWarnf("builtin deck not found")
  309. return
  310. }
  311. cards, unreviewedCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs)
  312. now := time.Now()
  313. for _, card := range cards {
  314. blockID := card.BlockID()
  315. ret = append(ret, newFlashcard(card, blockID, builtinDeckID, now))
  316. }
  317. if 1 > len(ret) {
  318. ret = []*Flashcard{}
  319. }
  320. unreviewedCount = unreviewedCnt
  321. return
  322. }
  323. func GetTreeDueFlashcards(rootID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount int, err error) {
  324. deckLock.Lock()
  325. defer deckLock.Unlock()
  326. waitForSyncingStorages()
  327. deck := Decks[builtinDeckID]
  328. if nil == deck {
  329. return
  330. }
  331. _, treeBlockIDs := getTreeSubTreeChildBlocks(rootID)
  332. cards, unreviewedCnt := getDeckDueCards(deck, reviewedCardIDs, treeBlockIDs)
  333. now := time.Now()
  334. for _, card := range cards {
  335. blockID := card.BlockID()
  336. ret = append(ret, newFlashcard(card, blockID, builtinDeckID, now))
  337. }
  338. if 1 > len(ret) {
  339. ret = []*Flashcard{}
  340. }
  341. unreviewedCount = unreviewedCnt
  342. return
  343. }
  344. func getTreeSubTreeChildBlocks(rootID string) (treeBlockIDsMap map[string]bool, treeBlockIDs []string) {
  345. treeBlockIDsMap = map[string]bool{}
  346. root := treenode.GetBlockTree(rootID)
  347. if nil == root {
  348. return
  349. }
  350. bts := treenode.GetBlockTreesByPathPrefix(strings.TrimSuffix(root.Path, ".sy"))
  351. for _, bt := range bts {
  352. treeBlockIDsMap[bt.ID] = true
  353. treeBlockIDs = append(treeBlockIDs, bt.ID)
  354. }
  355. return
  356. }
  357. func getBoxBlocks(boxID string) (blockIDsMap map[string]bool, blockIDs []string) {
  358. blockIDsMap = map[string]bool{}
  359. bts := treenode.GetBlockTreesByBoxID(boxID)
  360. for _, bt := range bts {
  361. blockIDsMap[bt.ID] = true
  362. blockIDs = append(blockIDs, bt.ID)
  363. }
  364. return
  365. }
  366. func GetDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount int, err error) {
  367. deckLock.Lock()
  368. defer deckLock.Unlock()
  369. waitForSyncingStorages()
  370. if "" == deckID {
  371. ret, unreviewedCount = getAllDueFlashcards(reviewedCardIDs)
  372. return
  373. }
  374. ret, unreviewedCount = getDueFlashcards(deckID, reviewedCardIDs)
  375. return
  376. }
  377. func getDueFlashcards(deckID string, reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount int) {
  378. deck := Decks[deckID]
  379. if nil == deck {
  380. logging.LogWarnf("deck not found [%s]", deckID)
  381. return
  382. }
  383. cards, unreviewedCnt := getDeckDueCards(deck, reviewedCardIDs, nil)
  384. now := time.Now()
  385. for _, card := range cards {
  386. blockID := card.BlockID()
  387. if nil == treenode.GetBlockTree(blockID) {
  388. continue
  389. }
  390. ret = append(ret, newFlashcard(card, blockID, deckID, now))
  391. }
  392. if 1 > len(ret) {
  393. ret = []*Flashcard{}
  394. }
  395. unreviewedCount = unreviewedCnt
  396. return
  397. }
  398. func getAllDueFlashcards(reviewedCardIDs []string) (ret []*Flashcard, unreviewedCount int) {
  399. now := time.Now()
  400. for _, deck := range Decks {
  401. cards, unreviewedCnt := getDeckDueCards(deck, reviewedCardIDs, nil)
  402. unreviewedCount += unreviewedCnt
  403. for _, card := range cards {
  404. blockID := card.BlockID()
  405. if nil == treenode.GetBlockTree(blockID) {
  406. continue
  407. }
  408. ret = append(ret, newFlashcard(card, blockID, deck.ID, now))
  409. }
  410. }
  411. if 1 > len(ret) {
  412. ret = []*Flashcard{}
  413. }
  414. return
  415. }
  416. func (tx *Transaction) doRemoveFlashcards(operation *Operation) (ret *TxErr) {
  417. deckLock.Lock()
  418. defer deckLock.Unlock()
  419. if syncingStorages {
  420. ret = &TxErr{code: TxErrCodeDataIsSyncing}
  421. return
  422. }
  423. deckID := operation.DeckID
  424. blockIDs := operation.BlockIDs
  425. if err := tx.removeBlocksDeckAttr(blockIDs, deckID); nil != err {
  426. return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID}
  427. }
  428. if "" == deckID { // 支持在 All 卡包中移除闪卡 https://github.com/siyuan-note/siyuan/issues/7425
  429. for _, deck := range Decks {
  430. removeFlashcardsByBlockIDs(blockIDs, deck)
  431. }
  432. } else {
  433. removeFlashcardsByBlockIDs(blockIDs, Decks[deckID])
  434. }
  435. return
  436. }
  437. func (tx *Transaction) removeBlocksDeckAttr(blockIDs []string, deckID string) (err error) {
  438. var rootIDs []string
  439. blockRoots := map[string]string{}
  440. for _, blockID := range blockIDs {
  441. bt := treenode.GetBlockTree(blockID)
  442. if nil == bt {
  443. continue
  444. }
  445. rootIDs = append(rootIDs, bt.RootID)
  446. blockRoots[blockID] = bt.RootID
  447. }
  448. rootIDs = gulu.Str.RemoveDuplicatedElem(rootIDs)
  449. trees := map[string]*parse.Tree{}
  450. for _, blockID := range blockIDs {
  451. rootID := blockRoots[blockID]
  452. tree := trees[rootID]
  453. if nil == tree {
  454. tree, _ = tx.loadTree(blockID)
  455. }
  456. if nil == tree {
  457. continue
  458. }
  459. trees[rootID] = tree
  460. node := treenode.GetNodeInTree(tree, blockID)
  461. if nil == node {
  462. continue
  463. }
  464. oldAttrs := parse.IAL2Map(node.KramdownIAL)
  465. deckAttrs := node.IALAttr("custom-riff-decks")
  466. var deckIDs []string
  467. if "" != deckID {
  468. availableDeckIDs := getDeckIDs()
  469. for _, dID := range strings.Split(deckAttrs, ",") {
  470. if dID != deckID && gulu.Str.Contains(dID, availableDeckIDs) {
  471. deckIDs = append(deckIDs, dID)
  472. }
  473. }
  474. }
  475. deckIDs = gulu.Str.RemoveDuplicatedElem(deckIDs)
  476. val := strings.Join(deckIDs, ",")
  477. val = strings.TrimPrefix(val, ",")
  478. val = strings.TrimSuffix(val, ",")
  479. if "" == val {
  480. node.RemoveIALAttr("custom-riff-decks")
  481. } else {
  482. node.SetIALAttr("custom-riff-decks", val)
  483. }
  484. if err = tx.writeTree(tree); nil != err {
  485. return
  486. }
  487. cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL))
  488. pushBroadcastAttrTransactions(oldAttrs, node)
  489. }
  490. return
  491. }
  492. func removeFlashcardsByBlockIDs(blockIDs []string, deck *riff.Deck) {
  493. if nil == deck {
  494. logging.LogErrorf("deck is nil")
  495. return
  496. }
  497. cards := deck.GetCardsByBlockIDs(blockIDs)
  498. if 1 > len(cards) {
  499. return
  500. }
  501. for _, card := range cards {
  502. deck.RemoveCard(card.ID())
  503. }
  504. err := deck.Save()
  505. if nil != err {
  506. logging.LogErrorf("save deck [%s] failed: %s", deck.ID, err)
  507. }
  508. }
  509. func (tx *Transaction) doAddFlashcards(operation *Operation) (ret *TxErr) {
  510. deckLock.Lock()
  511. defer deckLock.Unlock()
  512. if syncingStorages {
  513. ret = &TxErr{code: TxErrCodeDataIsSyncing}
  514. return
  515. }
  516. deckID := operation.DeckID
  517. blockIDs := operation.BlockIDs
  518. foundDeck := false
  519. for _, deck := range Decks {
  520. if deckID == deck.ID {
  521. foundDeck = true
  522. break
  523. }
  524. }
  525. if !foundDeck {
  526. deck, createErr := createDeck0("Built-in Deck", builtinDeckID)
  527. if nil == createErr {
  528. Decks[deck.ID] = deck
  529. }
  530. }
  531. blockRoots := map[string]string{}
  532. for _, blockID := range blockIDs {
  533. bt := treenode.GetBlockTree(blockID)
  534. if nil == bt {
  535. continue
  536. }
  537. blockRoots[blockID] = bt.RootID
  538. }
  539. trees := map[string]*parse.Tree{}
  540. for _, blockID := range blockIDs {
  541. rootID := blockRoots[blockID]
  542. tree := trees[rootID]
  543. if nil == tree {
  544. tree, _ = tx.loadTree(blockID)
  545. }
  546. if nil == tree {
  547. continue
  548. }
  549. trees[rootID] = tree
  550. node := treenode.GetNodeInTree(tree, blockID)
  551. if nil == node {
  552. continue
  553. }
  554. oldAttrs := parse.IAL2Map(node.KramdownIAL)
  555. deckAttrs := node.IALAttr("custom-riff-decks")
  556. deckIDs := strings.Split(deckAttrs, ",")
  557. deckIDs = append(deckIDs, deckID)
  558. deckIDs = gulu.Str.RemoveDuplicatedElem(deckIDs)
  559. val := strings.Join(deckIDs, ",")
  560. val = strings.TrimPrefix(val, ",")
  561. val = strings.TrimSuffix(val, ",")
  562. node.SetIALAttr("custom-riff-decks", val)
  563. if err := tx.writeTree(tree); nil != err {
  564. return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: deckID}
  565. }
  566. cache.PutBlockIAL(blockID, parse.IAL2Map(node.KramdownIAL))
  567. pushBroadcastAttrTransactions(oldAttrs, node)
  568. }
  569. deck := Decks[deckID]
  570. if nil == deck {
  571. logging.LogWarnf("deck [%s] not found", deckID)
  572. return
  573. }
  574. for _, blockID := range blockIDs {
  575. cards := deck.GetCardsByBlockID(blockID)
  576. if 0 < len(cards) {
  577. // 一个块只能添加生成一张闪卡 https://github.com/siyuan-note/siyuan/issues/7476
  578. continue
  579. }
  580. cardID := ast.NewNodeID()
  581. deck.AddCard(cardID, blockID)
  582. }
  583. if err := deck.Save(); nil != err {
  584. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  585. return
  586. }
  587. return
  588. }
  589. func LoadFlashcards() {
  590. riffSavePath := getRiffDir()
  591. if err := os.MkdirAll(riffSavePath, 0755); nil != err {
  592. logging.LogErrorf("create riff dir [%s] failed: %s", riffSavePath, err)
  593. return
  594. }
  595. Decks = map[string]*riff.Deck{}
  596. entries, err := os.ReadDir(riffSavePath)
  597. if nil != err {
  598. logging.LogErrorf("read riff dir failed: %s", err)
  599. return
  600. }
  601. for _, entry := range entries {
  602. name := entry.Name()
  603. if strings.HasSuffix(name, ".deck") {
  604. deckID := strings.TrimSuffix(name, ".deck")
  605. deck, loadErr := riff.LoadDeck(riffSavePath, deckID)
  606. if nil != loadErr {
  607. logging.LogErrorf("load deck [%s] failed: %s", name, loadErr)
  608. continue
  609. }
  610. if 0 == deck.Created {
  611. deck.Created = time.Now().Unix()
  612. }
  613. if 0 == deck.Updated {
  614. deck.Updated = deck.Created
  615. }
  616. Decks[deckID] = deck
  617. }
  618. }
  619. }
  620. const builtinDeckID = "20230218211946-2kw8jgx"
  621. func RenameDeck(deckID, name string) (err error) {
  622. deckLock.Lock()
  623. defer deckLock.Unlock()
  624. waitForSyncingStorages()
  625. deck := Decks[deckID]
  626. deck.Name = name
  627. err = deck.Save()
  628. if nil != err {
  629. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  630. return
  631. }
  632. return
  633. }
  634. func RemoveDeck(deckID string) (err error) {
  635. deckLock.Lock()
  636. defer deckLock.Unlock()
  637. waitForSyncingStorages()
  638. riffSavePath := getRiffDir()
  639. deckPath := filepath.Join(riffSavePath, deckID+".deck")
  640. if gulu.File.IsExist(deckPath) {
  641. if err = os.Remove(deckPath); nil != err {
  642. return
  643. }
  644. }
  645. cardsPath := filepath.Join(riffSavePath, deckID+".cards")
  646. if gulu.File.IsExist(cardsPath) {
  647. if err = os.Remove(cardsPath); nil != err {
  648. return
  649. }
  650. }
  651. LoadFlashcards()
  652. return
  653. }
  654. func CreateDeck(name string) (deck *riff.Deck, err error) {
  655. deckLock.Lock()
  656. defer deckLock.Unlock()
  657. return createDeck(name)
  658. }
  659. func createDeck(name string) (deck *riff.Deck, err error) {
  660. waitForSyncingStorages()
  661. deckID := ast.NewNodeID()
  662. deck, err = createDeck0(name, deckID)
  663. return
  664. }
  665. func createDeck0(name string, deckID string) (deck *riff.Deck, err error) {
  666. riffSavePath := getRiffDir()
  667. deck, err = riff.LoadDeck(riffSavePath, deckID)
  668. if nil != err {
  669. logging.LogErrorf("load deck [%s] failed: %s", deckID, err)
  670. return
  671. }
  672. deck.Name = name
  673. Decks[deckID] = deck
  674. err = deck.Save()
  675. if nil != err {
  676. logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
  677. return
  678. }
  679. return
  680. }
  681. func GetDecks() (decks []*riff.Deck) {
  682. deckLock.Lock()
  683. defer deckLock.Unlock()
  684. for _, deck := range Decks {
  685. if deck.ID == builtinDeckID {
  686. continue
  687. }
  688. decks = append(decks, deck)
  689. }
  690. if 1 > len(decks) {
  691. decks = []*riff.Deck{}
  692. }
  693. sort.Slice(decks, func(i, j int) bool {
  694. return decks[i].Updated > decks[j].Updated
  695. })
  696. return
  697. }
  698. func getRiffDir() string {
  699. return filepath.Join(util.DataDir, "storage", "riff")
  700. }
  701. func getDeckIDs() (deckIDs []string) {
  702. for deckID := range Decks {
  703. deckIDs = append(deckIDs, deckID)
  704. }
  705. return
  706. }
  707. func getDeckDueCards(deck *riff.Deck, reviewedCardIDs, blockIDs []string) (ret []riff.Card, unreviewedCount int) {
  708. ret = []riff.Card{}
  709. dues := deck.Dues()
  710. var tmp []riff.Card
  711. for _, c := range dues {
  712. if 0 < len(blockIDs) && !gulu.Str.Contains(c.BlockID(), blockIDs) {
  713. continue
  714. }
  715. tmp = append(tmp, c)
  716. if 0 < len(reviewedCardIDs) {
  717. if !gulu.Str.Contains(c.ID(), reviewedCardIDs) {
  718. unreviewedCount++
  719. }
  720. } else {
  721. unreviewedCount++
  722. }
  723. }
  724. dues = tmp
  725. if 1 > len(reviewedCardIDs) {
  726. // 未传入已复习的卡片 ID,说明是开始新的复习,需要清空缓存
  727. reviewCardCache = map[string]riff.Card{}
  728. skipCardCache = map[string]riff.Card{}
  729. }
  730. newCount := 0
  731. reviewCount := 0
  732. for _, c := range dues {
  733. if nil != skipCardCache[c.ID()] {
  734. continue
  735. }
  736. fsrsCard := c.Impl().(*fsrs.Card)
  737. if fsrs.New == fsrsCard.State {
  738. newCount++
  739. if newCount > Conf.Flashcard.NewCardLimit {
  740. continue
  741. }
  742. } else {
  743. reviewCount++
  744. if reviewCount > Conf.Flashcard.ReviewCardLimit {
  745. continue
  746. }
  747. }
  748. if 0 < len(reviewedCardIDs) && !gulu.Str.Contains(c.ID(), reviewedCardIDs) {
  749. continue
  750. }
  751. ret = append(ret, c)
  752. }
  753. return
  754. }