flashcard.go 29 KB

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