A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 896 lines 43 kB view raw
1import {Dialog} from "../dialog"; 2import {fetchPost} from "../util/fetch"; 3import {isMobile} from "../util/functions"; 4import {Protyle} from "../protyle"; 5import {Constants} from "../constants"; 6import {onGet} from "../protyle/util/onGet"; 7import {hasClosestByAttribute, hasClosestByClassName} from "../protyle/util/hasClosest"; 8import {hideElements} from "../protyle/ui/hideElements"; 9import {isPaidUser, needSubscribe} from "../util/needSubscribe"; 10import {fullscreen} from "../protyle/breadcrumb/action"; 11import {MenuItem} from "../menus/Menu"; 12import {escapeHtml} from "../util/escape"; 13/// #if !MOBILE 14import {openFile} from "../editor/util"; 15/// #endif 16/// #if !BROWSER 17import {ipcRenderer} from "electron"; 18/// #endif 19import * as dayjs from "dayjs"; 20import {getDisplayName, movePathTo} from "../util/pathName"; 21import {App} from "../index"; 22import {resize} from "../protyle/util/resize"; 23import {setStorageVal} from "../protyle/util/compatibility"; 24import {focusByRange} from "../protyle/util/selection"; 25import {updateCardHV} from "./util"; 26import {showMessage} from "../dialog/message"; 27import {Menu} from "../plugin/Menu"; 28import {transaction} from "../protyle/wysiwyg/transaction"; 29 30const genCardCount = (cardsData: ICardData, allIndex = 0) => { 31 let newIndex = 0; 32 let oldIndex = 0; 33 cardsData.cards.forEach((item, index) => { 34 if (index > allIndex) { 35 return; 36 } 37 if (item.state === 0) { 38 newIndex++; 39 } else { 40 oldIndex++; 41 } 42 }); 43 return `<span class="ariaLabel" aria-label="${window.siyuan.languages.flashcardNewCard}"> 44 <span class="ft__error">${newIndex}</span> / 45 <span class="ariaLabel ft__primary" aria-label="${window.siyuan.languages.flashcardNewCard}">${cardsData.unreviewedNewCardCount}</span> 46</span> 47<span class="fn__space"></span>+<span class="fn__space"></span> 48<span class="ariaLabel" aria-label="${window.siyuan.languages.flashcardReviewCard}"> 49 <span class="ft__error">${oldIndex}</span> / 50 <span class="ft__success">${cardsData.unreviewedOldCardCount}</span> 51</span>`; 52}; 53 54export const genCardHTML = (options: { 55 id: string, 56 cardType: TCardType, 57 cardsData: ICardData, 58 isTab: boolean 59}) => { 60 let iconsHTML: string; 61 /// #if MOBILE 62 iconsHTML = `<div class="toolbar toolbar--border"> 63 <svg class="toolbar__icon"><use xlink:href="#iconRiffCard"></use></svg> 64 <span class="fn__flex-1 fn__flex-center toolbar__text">${window.siyuan.languages.riffCard}</span> 65 <div data-type="count" class="${options.cardsData.cards.length === 0 ? "fn__none" : "fn__flex"}">${genCardCount(options.cardsData)}</span></div> 66 <svg class="toolbar__icon" data-id="${options.id || ""}" data-cardtype="${options.cardType}" data-type="filter"><use xlink:href="#iconFilter"></use></svg> 67 <svg class="toolbar__icon" data-type="more"><use xlink:href="#iconMore"></use></svg> 68 <svg class="toolbar__icon" data-type="close"><use xlink:href="#iconCloseRound"></use></svg> 69</div>`; 70 /// #else 71 iconsHTML = `<div class="block__icons"> 72 ${options.isTab ? '<div class="fn__flex-1"></div>' : `<div class="block__logo"> 73 <svg class="block__logoicon"><use xlink:href="#iconRiffCard"></use></svg>${window.siyuan.languages.riffCard} 74 </div>`} 75 <span class="fn__flex-1 resize__move" style="min-height: 100%"></span> 76 <div data-type="count" class="ft__on-surface ft__smaller fn__flex-center${options.cardsData.cards.length === 0 ? " fn__none" : " fn__flex"}">${genCardCount(options.cardsData)}</span></div> 77 <div class="fn__space"></div> 78 <button data-id="${options.id || ""}" data-cardtype="${options.cardType}" data-type="filter" class="block__icon block__icon--show"> 79 <svg><use xlink:href="#iconFilter"></use></svg> 80 </button> 81 <div class="fn__space"></div> 82 <div data-type="fullscreen" class="b3-tooltips b3-tooltips__sw block__icon block__icon--show" aria-label="${window.siyuan.languages.fullscreen}"> 83 <svg><use xlink:href="#iconFullscreen"></use></svg> 84 </div> 85 <div class="fn__space${options.cardsData.cards.length === 0 ? " fn__none" : ""}"></div> 86 <div data-type="more" class="${options.cardsData.cards.length === 0 ? "fn__none " : ""}b3-tooltips b3-tooltips__sw block__icon block__icon--show" aria-label="${window.siyuan.languages.more}"> 87 <svg><use xlink:href="#iconMore"></use></svg> 88 </div> 89 <div class="fn__space${options.isTab ? " fn__none" : ""}"></div> 90 <div data-type="sticktab" class="b3-tooltips b3-tooltips__sw block__icon block__icon--show${options.isTab ? " fn__none" : ""}" aria-label="${window.siyuan.languages.openBy}"> 91 <svg><use xlink:href="#iconOpen"></use></svg> 92 </div> 93 </div>`; 94 /// #endif 95 return `<div class="card__main"> 96 ${iconsHTML} 97 <div class="card__block fn__flex-1 ${options.cardsData.cards.length === 0 ? "fn__none" : ""}" data-type="render"></div> 98 <div class="card__empty card__empty--space${options.cardsData.cards.length === 0 ? "" : " fn__none"}" data-type="empty"> 99 <div>🔮</div> 100 ${window.siyuan.languages.noDueCard} 101 </div> 102 <div class="fn__flex card__action fn__none"> 103 <button class="b3-button b3-button--cancel" disabled="disabled" data-type="-2" style="width: 25%;min-width: 86px;display: flex"> 104 <svg><use xlink:href="#iconLeft"></use></svg> 105 ${!isMobile() ? "(p / q)" : ""} 106 </button> 107 <span class="fn__space"></span> 108 <button data-type="-1" class="b3-button fn__flex-1">${window.siyuan.languages.cardShowAnswer}${!isMobile() ? " (" + window.siyuan.languages.space + " / " + window.siyuan.languages.enterKey + ")" : ""}</button> 109 </div> 110 <div class="fn__flex card__action fn__none"> 111 <div> 112 <button class="b3-button b3-button--cancel" disabled="disabled" style="display: flex;margin-bottom: 8px;height: 28px;padding: 0;" data-type="-2"><svg><use xlink:href="#iconLeft"></use></svg>${!isMobile() ? "(p / q)" : ""}</button> 113 <button data-type="-3" aria-label="0 / x" class="b3-button b3-button--cancel b3-tooltips__n b3-tooltips"> 114 <div class="card__icon">💤</div> 115 ${window.siyuan.languages.skip}${!isMobile() ? " (0)" : ""} 116 </button> 117 </div> 118 <div> 119 <span></span> 120 <button data-type="1" aria-label="1 / j / a" class="b3-button b3-button--error b3-tooltips__n b3-tooltips"> 121 <div class="card__icon">🙈</div> 122 ${window.siyuan.languages.cardRatingAgain}${!isMobile() ? " (1)" : ""} 123 </button> 124 </div> 125 <div> 126 <span></span> 127 <button data-type="2" aria-label="2 / k / s" class="b3-button b3-button--warning b3-tooltips__n b3-tooltips"> 128 <div class="card__icon">😬</div> 129 ${window.siyuan.languages.cardRatingHard}${!isMobile() ? " (2)" : ""} 130 </button> 131 </div> 132 <div> 133 <span></span> 134 <button data-type="3" aria-label="3 / l / d / ${window.siyuan.languages.space} / ${window.siyuan.languages.enterKey}" class="b3-button b3-button--info b3-tooltips__n b3-tooltips"> 135 <div class="card__icon">😊</div> 136 ${window.siyuan.languages.cardRatingGood}${!isMobile() ? " (3)" : ""} 137 </button> 138 </div> 139 <div> 140 <span></span> 141 <button data-type="4" aria-label="4 / ; / f" class="b3-button b3-button--success b3-tooltips__n b3-tooltips"> 142 <div class="card__icon">🌈</div> 143 ${window.siyuan.languages.cardRatingEasy}${!isMobile() ? " (4)" : ""} 144 </button> 145 </div> 146 </div> 147</div>`; 148}; 149 150const getEditor = (id: string, protyle: IProtyle, element: Element, currentCard: ICard) => { 151 fetchPost("/api/block/getDocInfo", { 152 id, 153 }, (docResponse) => { 154 protyle.wysiwyg.renderCustom(docResponse.data.ial); 155 fetchPost("/api/filetree/getDoc", { 156 id, 157 mode: 0, 158 size: Constants.SIZE_GET_MAX 159 }, (response) => { 160 onGet({ 161 updateReadonly: true, 162 data: response, 163 protyle, 164 action: response.data.rootID === response.data.id ? [] : [Constants.CB_GET_ALL], 165 afterCB: () => { 166 if (protyle.element.classList.contains("fn__none")) { 167 return; 168 } 169 let hasHide = false; 170 if (!window.siyuan.config.flashcard.superBlock && 171 !window.siyuan.config.flashcard.heading && 172 !window.siyuan.config.flashcard.list && 173 !window.siyuan.config.flashcard.mark) { 174 hasHide = false; 175 } else { 176 if (window.siyuan.config.flashcard.superBlock) { 177 if (protyle.wysiwyg.element.querySelector(":scope > .sb")) { 178 hasHide = true; 179 } 180 } 181 if (window.siyuan.config.flashcard.heading) { 182 if (protyle.wysiwyg.element.querySelector(':scope > [data-type="NodeHeading"]')) { 183 hasHide = true; 184 } 185 } 186 if (window.siyuan.config.flashcard.list) { 187 if (protyle.wysiwyg.element.querySelector(".list, .li")) { 188 hasHide = true; 189 } 190 } 191 if (window.siyuan.config.flashcard.mark) { 192 if (protyle.wysiwyg.element.querySelector('span[data-type~="mark"]')) { 193 hasHide = true; 194 } 195 } 196 } 197 const actionElements = element.querySelectorAll(".card__action"); 198 if (!hasHide) { 199 protyle.element.classList.remove("card__block--hidemark", "card__block--hideli", "card__block--hidesb", "card__block--hideh"); 200 actionElements[0].classList.add("fn__none"); 201 actionElements[1].querySelectorAll("button.b3-button").forEach((element, btnIndex) => { 202 if (btnIndex < 2) { 203 return; 204 } 205 element.previousElementSibling.textContent = currentCard.nextDues[btnIndex - 1]; 206 }); 207 actionElements[1].classList.remove("fn__none"); 208 } else { 209 if (window.siyuan.config.flashcard.superBlock) { 210 protyle.element.classList.add("card__block--hidesb"); 211 } 212 if (window.siyuan.config.flashcard.heading) { 213 protyle.element.classList.add("card__block--hideh"); 214 } 215 if (window.siyuan.config.flashcard.list) { 216 protyle.element.classList.add("card__block--hideli"); 217 } 218 if (window.siyuan.config.flashcard.mark) { 219 protyle.element.classList.add("card__block--hidemark"); 220 } 221 actionElements[0].classList.remove("fn__none"); 222 actionElements[1].classList.add("fn__none"); 223 } 224 } 225 }); 226 }); 227 }); 228 229}; 230 231export const bindCardEvent = async (options: { 232 app: App, 233 element: Element, 234 title?: string, 235 cardsData: ICardData 236 cardType: TCardType, 237 id?: string, 238 dialog?: Dialog, 239 index?: number, 240}) => { 241 if (window.siyuan.storage[Constants.LOCAL_FLASHCARD].fullscreen) { 242 fullscreen(options.element.querySelector(".card__main"), 243 options.element.querySelector('[data-type="fullscreen"]')); 244 } 245 let index = 0; 246 if (typeof options.index === "number") { 247 index = options.index; 248 } 249 const editor = new Protyle(options.app, options.element.querySelector("[data-type='render']") as HTMLElement, { 250 blockId: "", 251 action: [Constants.CB_GET_ALL], 252 render: { 253 background: false, 254 gutter: true, 255 breadcrumbDocName: true, 256 title: true, 257 hideTitleOnZoom: true, 258 }, 259 typewriterMode: false 260 }); 261 if (window.siyuan.mobile) { 262 window.siyuan.mobile.popEditor = editor; 263 } 264 if (options.cardsData.cards.length > 0) { 265 getEditor(options.cardsData.cards[index].blockID, editor.protyle, options.element, options.cardsData.cards[index]); 266 } 267 options.element.setAttribute("data-key", Constants.DIALOG_OPENCARD); 268 const actionElements = options.element.querySelectorAll(".card__action"); 269 if (options.index === 0 || typeof options.index === "undefined") { 270 actionElements[0].firstElementChild.setAttribute("disabled", "disabled"); 271 actionElements[1].querySelector(".b3-button").setAttribute("disabled", "disabled"); 272 } else { 273 actionElements[0].firstElementChild.removeAttribute("disabled"); 274 actionElements[1].querySelector(".b3-button").removeAttribute("disabled"); 275 } 276 const countElement = options.element.querySelector('[data-type="count"]'); 277 const filterElement = options.element.querySelector('[data-type="filter"]'); 278 const fetchNewRound = () => { 279 const currentCardType = filterElement.getAttribute("data-cardtype"); 280 const docId = filterElement.getAttribute("data-id"); 281 fetchPost(currentCardType === "all" ? "/api/riff/getRiffDueCards" : 282 (currentCardType === "doc" ? "/api/riff/getTreeRiffDueCards" : "/api/riff/getNotebookRiffDueCards"), { 283 rootID: docId, 284 deckID: docId, 285 notebook: docId, 286 }, async (treeCards) => { 287 index = 0; 288 options.cardsData = treeCards.data; 289 for (let i = 0; i < options.app.plugins.length; i++) { 290 options.cardsData = await options.app.plugins[i].updateCards(options.cardsData); 291 } 292 if (options.cardsData.cards.length > 0) { 293 nextCard({ 294 countElement, 295 editor, 296 actionElements, 297 index, 298 cardsData: options.cardsData 299 }); 300 } else { 301 allDone(countElement, editor, actionElements); 302 } 303 }); 304 }; 305 306 countElement.innerHTML = genCardCount(options.cardsData, index); 307 options.element.firstChild.addEventListener("click", (event: MouseEvent) => { 308 const target = event.target as HTMLElement; 309 let type = ""; 310 const currentCard = options.cardsData.cards[index]; 311 const docId = filterElement.getAttribute("data-id"); 312 if (typeof event.detail === "string") { 313 if (["1", "j", "a"].includes(event.detail)) { 314 type = "1"; 315 } else if (["2", "k", "s"].includes(event.detail)) { 316 type = "2"; 317 } else if (["3", "l", "d"].includes(event.detail)) { 318 type = "3"; 319 } else if (["4", ";", "f"].includes(event.detail)) { 320 type = "4"; 321 } else if ([" ", "enter"].includes(event.detail)) { 322 type = "-1"; 323 } else if (["p", "q"].includes(event.detail)) { 324 type = "-2"; 325 } else if (["0", "x"].includes(event.detail)) { 326 type = "-3"; 327 } 328 } else { 329 const fullscreenElement = hasClosestByAttribute(target, "data-type", "fullscreen"); 330 if (fullscreenElement) { 331 fullscreen(options.element.querySelector(".card__main"), 332 options.element.querySelector('[data-type="fullscreen"]')); 333 resize(editor.protyle); 334 window.siyuan.storage[Constants.LOCAL_FLASHCARD].fullscreen = !window.siyuan.storage[Constants.LOCAL_FLASHCARD].fullscreen; 335 setStorageVal(Constants.LOCAL_FLASHCARD, window.siyuan.storage[Constants.LOCAL_FLASHCARD]); 336 event.stopPropagation(); 337 event.preventDefault(); 338 return; 339 } 340 const moreElement = hasClosestByAttribute(target, "data-type", "more"); 341 if (moreElement && currentCard) { 342 event.stopPropagation(); 343 event.preventDefault(); 344 if (filterElement.getAttribute("data-cardtype") === "all" && filterElement.getAttribute("data-id")) { 345 showMessage(window.siyuan.languages.noSupportTip); 346 return; 347 } 348 const menu = new Menu(); 349 menu.addItem({ 350 id: "setDueTime", 351 icon: "iconClock", 352 label: window.siyuan.languages.setDueTime, 353 click() { 354 const timedialog = new Dialog({ 355 title: window.siyuan.languages.setDueTime, 356 content: `<div class="b3-dialog__content"> 357 <div class="b3-label__text">${window.siyuan.languages.showCardDay}</div> 358 <div class="fn__hr"></div> 359 <input class="b3-text-field fn__block" value="1" type="number" step="1" min="1"> 360</div> 361<div class="b3-dialog__action"> 362 <button class="b3-button b3-button--cancel">${window.siyuan.languages.cancel}</button><div class="fn__space"></div> 363 <button class="b3-button b3-button--text">${window.siyuan.languages.confirm}</button> 364</div>`, 365 width: isMobile() ? "92vw" : "520px", 366 }); 367 const inputElement = timedialog.element.querySelector("input") as HTMLInputElement; 368 const btnsElement = timedialog.element.querySelectorAll(".b3-button"); 369 timedialog.bindInput(inputElement, () => { 370 (btnsElement[1] as HTMLButtonElement).click(); 371 }); 372 inputElement.focus(); 373 inputElement.select(); 374 btnsElement[0].addEventListener("click", () => { 375 timedialog.destroy(); 376 }); 377 btnsElement[1].addEventListener("click", () => { 378 fetchPost("/api/riff/batchSetRiffCardsDueTime", { 379 cardDues: [{ 380 id: currentCard.cardID, 381 due: dayjs().add(parseInt(inputElement.value), "day").format("YYYYMMDDHHmmss") 382 }] 383 }, () => { 384 actionElements[0].classList.add("fn__none"); 385 actionElements[1].classList.remove("fn__none"); 386 if (currentCard.state === 0) { 387 options.cardsData.unreviewedNewCardCount--; 388 } else { 389 options.cardsData.unreviewedOldCardCount--; 390 } 391 options.element.dispatchEvent(new CustomEvent("click", {detail: "0"})); 392 options.cardsData.cards.splice(index, 1); 393 index--; 394 timedialog.destroy(); 395 }); 396 }); 397 } 398 }); 399 if (currentCard.state !== 0) { 400 menu.addItem({ 401 id: "reset", 402 icon: "iconRefresh", 403 label: window.siyuan.languages.reset, 404 click() { 405 fetchPost("/api/riff/resetRiffCards", { 406 type: filterElement.getAttribute("data-cardtype"), 407 id: docId, 408 deckID: Constants.QUICK_DECK_ID, 409 blockIDs: [currentCard.blockID], 410 }, () => { 411 const minLang = window.siyuan.languages._time["1m"].replace("%s", ""); 412 currentCard.lapses = 0; 413 currentCard.lastReview = -62135596800000; 414 currentCard.reps = 0; 415 currentCard.state = 0; 416 currentCard.nextDues = { 417 1: minLang, 418 2: minLang.replace("1", "5"), 419 3: minLang.replace("1", "10"), 420 4: window.siyuan.languages._time["1d"].replace("%s", "").replace("1", "6") 421 }; 422 actionElements[1].querySelectorAll("button.b3-button").forEach((element, btnIndex) => { 423 if (btnIndex < 2) { 424 return; 425 } 426 element.previousElementSibling.textContent = currentCard.nextDues[btnIndex - 1]; 427 }); 428 options.cardsData.unreviewedOldCardCount--; 429 options.cardsData.unreviewedNewCardCount++; 430 countElement.innerHTML = genCardCount(options.cardsData, index); 431 }); 432 } 433 }); 434 } 435 menu.addItem({ 436 id: "removeRiffCard", 437 icon: "iconTrashcan", 438 label: `${window.siyuan.languages.remove} <b>${window.siyuan.languages.riffCard}</b>`, 439 click() { 440 actionElements[0].classList.add("fn__none"); 441 actionElements[1].classList.remove("fn__none"); 442 if (currentCard.state === 0) { 443 options.cardsData.unreviewedNewCardCount--; 444 } else { 445 options.cardsData.unreviewedOldCardCount--; 446 } 447 options.element.dispatchEvent(new CustomEvent("click", {detail: "0"})); 448 transaction(undefined, [{ 449 action: "removeFlashcards", 450 deckID: Constants.QUICK_DECK_ID, 451 blockIDs: [currentCard.blockID] 452 }]); 453 options.cardsData.cards.splice(index, 1); 454 index--; 455 } 456 }); 457 menu.addSeparator(); 458 menu.addItem({ 459 id: "forgetCountAndRevisionCountAndCardStatusAndLastReviewTime", 460 iconHTML: "", 461 type: "readonly", 462 label: `<div class="fn__flex"> 463 <div class="fn__flex-1 ft__breakword">${window.siyuan.languages.forgetCount}</div> 464 <div class="fn__space"></div> 465 <div>${currentCard.lapses}</div> 466</div><div class="fn__flex"> 467 <div class="fn__flex-1 ft__breakword">${window.siyuan.languages.revisionCount}</div> 468 <div class="fn__space"></div> 469 <div>${currentCard.reps}</div> 470</div><div class="fn__flex"> 471 <div class="fn__flex-1 ft__breakword">${window.siyuan.languages.cardStatus}</div> 472 <div class="fn__space"></div> 473 <div class="${currentCard.state === 0 ? "ft__primary" : "ft__success"}">${currentCard.state === 0 ? window.siyuan.languages.flashcardNewCard : window.siyuan.languages.flashcardReviewCard}</div> 474</div><div class="fn__flex${currentCard.lastReview > 0 ? "" : " fn__none"}"> 475 <div class="fn__flex-1 ft__breakword" style="width: 170px;">${window.siyuan.languages.lastReviewTime}</div> 476 <div class="fn__space"></div> 477 <div>${dayjs(currentCard.lastReview).format("YYYY-MM-DD")}</div> 478</div>`, 479 }); 480 /// #if MOBILE 481 menu.fullscreen(); 482 /// #else 483 const rect = moreElement.getBoundingClientRect(); 484 menu.open({ 485 x: rect.left, 486 y: rect.bottom 487 }); 488 /// #endif 489 return; 490 } 491 /// #if !MOBILE 492 const sticktabElement = hasClosestByAttribute(target, "data-type", "sticktab"); 493 if (sticktabElement) { 494 const stickMenu = new Menu(); 495 stickMenu.addItem({ 496 id: "openInNewTab", 497 icon: "iconOpen", 498 label: window.siyuan.languages.openInNewTab, 499 click() { 500 openFile({ 501 app: options.app, 502 custom: { 503 icon: "iconRiffCard", 504 title: window.siyuan.languages.spaceRepetition, 505 data: { 506 cardsData: options.cardsData, 507 index, 508 cardType: filterElement.getAttribute("data-cardtype") as TCardType, 509 id: docId, 510 title: options.title 511 }, 512 id: "siyuan-card" 513 }, 514 }); 515 options.dialog.destroy(); 516 } 517 }); 518 stickMenu.addItem({ 519 id: "insertRight", 520 icon: "iconLayoutRight", 521 label: window.siyuan.languages.insertRight, 522 click() { 523 openFile({ 524 app: options.app, 525 position: "right", 526 custom: { 527 icon: "iconRiffCard", 528 title: window.siyuan.languages.spaceRepetition, 529 data: { 530 cardsData: options.cardsData, 531 index, 532 cardType: filterElement.getAttribute("data-cardtype") as TCardType, 533 id: docId, 534 title: options.title 535 }, 536 id: "siyuan-card" 537 }, 538 }); 539 options.dialog.destroy(); 540 } 541 }); 542 /// #if !BROWSER 543 stickMenu.addItem({ 544 id: "openByNewWindow", 545 icon: "iconOpenWindow", 546 label: window.siyuan.languages.openByNewWindow, 547 click() { 548 const json = [{ 549 "title": window.siyuan.languages.spaceRepetition, 550 "icon": "iconRiffCard", 551 "instance": "Tab", 552 "children": { 553 "instance": "Custom", 554 "customModelType": "siyuan-card", 555 "customModelData": { 556 "cardsData": options.cardsData, 557 "index": index, 558 "cardType": filterElement.getAttribute("data-cardtype"), 559 "id": docId, 560 "title": options.title 561 } 562 } 563 }]; 564 ipcRenderer.send(Constants.SIYUAN_OPEN_WINDOW, { 565 // 需要 encode, 否则 https://github.com/siyuan-note/siyuan/issues/9343 566 url: `${window.location.protocol}//${window.location.host}/stage/build/app/window.html?v=${Constants.SIYUAN_VERSION}&json=${encodeURIComponent(JSON.stringify(json))}` 567 }); 568 options.dialog.destroy(); 569 } 570 }); 571 /// #endif 572 const rect = sticktabElement.getBoundingClientRect(); 573 stickMenu.open({ 574 x: rect.left, 575 y: rect.bottom 576 }); 577 event.stopPropagation(); 578 event.preventDefault(); 579 return; 580 } 581 /// #endif 582 const closeElement = hasClosestByAttribute(target, "data-type", "close"); 583 if (closeElement) { 584 if (options.dialog) { 585 options.dialog.destroy(); 586 } 587 event.stopPropagation(); 588 event.preventDefault(); 589 return; 590 } 591 const filterTempElement = hasClosestByAttribute(target, "data-type", "filter"); 592 if (filterTempElement) { 593 fetchPost("/api/riff/getRiffDecks", {}, (response) => { 594 window.siyuan.menus.menu.remove(); 595 window.siyuan.menus.menu.append(new MenuItem({ 596 id: "all", 597 iconHTML: "", 598 label: window.siyuan.languages.all, 599 click() { 600 filterElement.setAttribute("data-id", ""); 601 filterElement.setAttribute("data-cardtype", "all"); 602 fetchNewRound(); 603 }, 604 }).element); 605 window.siyuan.menus.menu.append(new MenuItem({ 606 id: "fileTree", 607 iconHTML: "", 608 label: window.siyuan.languages.fileTree, 609 click() { 610 movePathTo((toPath, toNotebook) => { 611 filterElement.setAttribute("data-id", toPath[0] === "/" ? toNotebook[0] : getDisplayName(toPath[0], true, true)); 612 filterElement.setAttribute("data-cardtype", toPath[0] === "/" ? "notebook" : "doc"); 613 fetchNewRound(); 614 }, [], undefined, window.siyuan.languages.specifyPath, true); 615 } 616 }).element); 617 if (options.title || response.data.length > 0) { 618 window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element); 619 } 620 if (options.title) { 621 window.siyuan.menus.menu.append(new MenuItem({ 622 iconHTML: "", 623 label: escapeHtml(options.title), 624 click() { 625 filterElement.setAttribute("data-id", options.id); 626 filterElement.setAttribute("data-cardtype", options.cardType); 627 fetchNewRound(); 628 }, 629 }).element); 630 if (response.data.length > 0) { 631 window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element); 632 } 633 } 634 response.data.forEach((deck: { id: string, name: string }) => { 635 window.siyuan.menus.menu.append(new MenuItem({ 636 iconHTML: "", 637 label: escapeHtml(deck.name), 638 click() { 639 filterElement.setAttribute("data-id", deck.id); 640 filterElement.setAttribute("data-cardtype", "all"); 641 fetchNewRound(); 642 }, 643 }).element); 644 }); 645 const filterRect = filterTempElement.getBoundingClientRect(); 646 window.siyuan.menus.menu.popup({x: filterRect.left, y: filterRect.bottom}); 647 }); 648 event.stopPropagation(); 649 event.preventDefault(); 650 return; 651 } 652 653 const newroundElement = hasClosestByAttribute(target, "data-type", "newround"); 654 if (newroundElement) { 655 fetchNewRound(); 656 event.stopPropagation(); 657 event.preventDefault(); 658 return; 659 } 660 } 661 if (!type) { 662 const buttonElement = hasClosestByClassName(target, "b3-button"); 663 if (buttonElement) { 664 type = buttonElement.getAttribute("data-type"); 665 } 666 } 667 if (!type || !currentCard) { 668 return; 669 } 670 event.preventDefault(); 671 event.stopPropagation(); 672 hideElements(["toolbar", "hint", "util", "gutter"], editor.protyle); 673 if (type === "-1") { // 显示答案 674 if (actionElements[0].classList.contains("fn__none")) { 675 type = "3"; 676 } else { 677 editor.protyle.element.classList.remove("card__block--hidemark", "card__block--hideli", "card__block--hidesb", "card__block--hideh"); 678 actionElements[0].classList.add("fn__none"); 679 actionElements[1].querySelectorAll("button.b3-button").forEach((element, btnIndex) => { 680 if (btnIndex < 2) { 681 return; 682 } 683 element.previousElementSibling.textContent = currentCard.nextDues[btnIndex - 1]; 684 }); 685 actionElements[1].classList.remove("fn__none"); 686 emitEvent(options.app, currentCard, type); 687 return; 688 } 689 } else if (type === "-2") { // 上一步 690 if (index > 0) { 691 index--; 692 nextCard({ 693 countElement, 694 editor, 695 actionElements, 696 index, 697 cardsData: options.cardsData 698 }); 699 emitEvent(options.app, options.cardsData.cards[index + 1], type); 700 } 701 return; 702 } 703 if (["1", "2", "3", "4", "-3"].includes(type) && actionElements[0].classList.contains("fn__none")) { 704 fetchPost(type === "-3" ? "/api/riff/skipReviewRiffCard" : "/api/riff/reviewRiffCard", { 705 deckID: currentCard.deckID, 706 cardID: currentCard.cardID, 707 rating: parseInt(type), 708 reviewedCards: options.cardsData.cards 709 }, () => { 710 /// #if MOBILE 711 if (type !== "-3" && 712 ((0 !== window.siyuan.config.sync.provider && isPaidUser()) || 713 (0 === window.siyuan.config.sync.provider && !needSubscribe(""))) && 714 window.siyuan.config.repo.key && window.siyuan.config.sync.enabled) { 715 document.getElementById("toolbarSync").classList.remove("fn__none"); 716 } 717 /// #endif 718 index++; 719 if (index > options.cardsData.cards.length - 1) { 720 const currentCardType = filterElement.getAttribute("data-cardtype"); 721 fetchPost(currentCardType === "all" ? "/api/riff/getRiffDueCards" : 722 (currentCardType === "doc" ? "/api/riff/getTreeRiffDueCards" : "/api/riff/getNotebookRiffDueCards"), { 723 rootID: docId, 724 deckID: docId, 725 notebook: docId, 726 reviewedCards: options.cardsData.cards 727 }, async (result) => { 728 emitEvent(options.app, options.cardsData.cards[index - 1], type); 729 index = 0; 730 options.cardsData = result.data; 731 for (let i = 0; i < options.app.plugins.length; i++) { 732 options.cardsData = await options.app.plugins[i].updateCards(options.cardsData); 733 } 734 if (options.cardsData.cards.length === 0) { 735 if (options.cardsData.unreviewedCount > 0) { 736 newRound(countElement, editor, actionElements, result.data.unreviewedCount); 737 } else { 738 allDone(countElement, editor, actionElements); 739 } 740 } else { 741 nextCard({ 742 countElement, 743 editor, 744 actionElements, 745 index, 746 cardsData: options.cardsData 747 }); 748 } 749 }); 750 return; 751 } 752 nextCard({ 753 countElement, 754 editor, 755 actionElements, 756 index, 757 cardsData: options.cardsData 758 }); 759 emitEvent(options.app, options.cardsData.cards[index - 1], type); 760 }); 761 } 762 }); 763 return editor; 764}; 765 766const emitEvent = (app: App, card: ICard, type: string) => { 767 app.plugins.forEach(item => { 768 item.eventBus.emit("click-flashcard-action", { 769 type, 770 card 771 }); 772 }); 773}; 774 775export const openCard = (app: App) => { 776 if (window.siyuan.config.readonly) { 777 return; 778 } 779 fetchPost("/api/riff/getRiffDueCards", {deckID: ""}, (cardsResponse) => { 780 openCardByData(app, cardsResponse.data, "all"); 781 }); 782}; 783 784export const openCardByData = async (app: App, cardsData: ICardData, cardType: TCardType, id?: string, title?: string) => { 785 const exit = window.siyuan.dialogs.find(item => { 786 if (item.element.getAttribute("data-key") === Constants.DIALOG_OPENCARD) { 787 item.destroy(); 788 return true; 789 } 790 }); 791 if (exit) { 792 return; 793 } 794 let lastRange: Range; 795 if (getSelection().rangeCount > 0) { 796 lastRange = getSelection().getRangeAt(0); 797 } 798 for (let i = 0; i < app.plugins.length; i++) { 799 cardsData = await app.plugins[i].updateCards(cardsData); 800 } 801 const dialog = new Dialog({ 802 positionId: Constants.DIALOG_OPENCARD, 803 content: genCardHTML({id, cardType, cardsData, isTab: false}), 804 width: isMobile() ? "100vw" : "80vw", 805 height: isMobile() ? "100vh" : "70vh", 806 destroyCallback() { 807 if (editor) { 808 editor.destroy(); 809 if (window.siyuan.mobile) { 810 window.siyuan.mobile.popEditor = null; 811 } 812 } 813 if (lastRange) { 814 focusByRange(lastRange); 815 } 816 }, 817 resizeCallback(type: string) { 818 if (type !== "d" && type !== "t" && editor) { 819 editor.resize(); 820 } 821 } 822 }); 823 (dialog.element.querySelector(".b3-dialog__scrim") as HTMLElement).style.backgroundColor = "var(--b3-theme-surface)"; 824 (dialog.element.querySelector(".b3-dialog__container") as HTMLElement).style.maxWidth = "1024px"; 825 const editor = await bindCardEvent({ 826 app, 827 element: dialog.element, 828 cardsData, 829 title, 830 id, 831 cardType, 832 dialog 833 }); 834 editor.resize(); 835 dialog.editors = { 836 card: editor 837 }; 838 /// #if !MOBILE 839 const focusElement = dialog.element.querySelector(".block__icons button.block__icon") as HTMLElement; 840 focusElement.focus(); 841 const range = document.createRange(); 842 range.selectNodeContents(focusElement); 843 range.collapse(); 844 focusByRange(range); 845 /// #endif 846 updateCardHV(); 847}; 848 849const nextCard = (options: { 850 countElement: Element, 851 editor: Protyle, 852 actionElements: NodeListOf<Element>, 853 index: number, 854 cardsData: ICardData 855}) => { 856 options.editor.protyle.element.classList.remove("fn__none"); 857 options.editor.protyle.element.nextElementSibling.classList.add("fn__none"); 858 options.countElement.innerHTML = genCardCount(options.cardsData, options.index); 859 options.countElement.classList.remove("fn__none"); 860 if (options.index === 0) { 861 options.actionElements[0].firstElementChild.setAttribute("disabled", "disabled"); 862 options.actionElements[1].querySelector(".b3-button").setAttribute("disabled", "disabled"); 863 } else { 864 options.actionElements[0].firstElementChild.removeAttribute("disabled"); 865 options.actionElements[1].querySelector(".b3-button").removeAttribute("disabled"); 866 } 867 getEditor(options.cardsData.cards[options.index].blockID, options.editor.protyle, 868 hasClosestByAttribute(options.countElement, "data-key", Constants.DIALOG_OPENCARD) as HTMLElement, 869 options.cardsData.cards[options.index]); 870}; 871 872const allDone = (countElement: Element, editor: Protyle, actionElements: NodeListOf<Element>) => { 873 countElement.classList.add("fn__none"); 874 editor.protyle.element.classList.add("fn__none"); 875 const emptyElement = editor.protyle.element.nextElementSibling; 876 emptyElement.innerHTML = `<div>🔮</div>${window.siyuan.languages.noDueCard}`; 877 emptyElement.classList.remove("fn__none"); 878 actionElements[0].classList.add("fn__none"); 879 actionElements[1].classList.add("fn__none"); 880 const moreElement = countElement.parentElement.querySelector('[data-type="more"]'); 881 moreElement.classList.add("fn__none"); 882 moreElement.previousElementSibling.classList.add("fn__none"); 883}; 884 885const newRound = (countElement: Element, editor: Protyle, actionElements: NodeListOf<Element>, unreviewedCount: number) => { 886 countElement.classList.add("fn__none"); 887 editor.protyle.element.classList.add("fn__none"); 888 const emptyElement = editor.protyle.element.nextElementSibling; 889 emptyElement.innerHTML = `<div>♻️ </div> 890<span>${window.siyuan.languages.continueReview2.replace("${count}", unreviewedCount)}</span> 891<div class="fn__hr"></div> 892<button data-type="newround" class="b3-button fn__size200">${window.siyuan.languages.continueReview1}</button>`; 893 emptyElement.classList.remove("fn__none"); 894 actionElements[0].classList.add("fn__none"); 895 actionElements[1].classList.add("fn__none"); 896};