flashcard.go 20 KB

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