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