import {Dialog} from "../dialog"; import {fetchPost} from "../util/fetch"; import {isMobile} from "../util/functions"; import {Protyle} from "../protyle"; import {Constants} from "../constants"; import {onGet} from "../protyle/util/onGet"; import {hasClosestByAttribute, hasClosestByClassName} from "../protyle/util/hasClosest"; import {hideElements} from "../protyle/ui/hideElements"; import {isPaidUser, needSubscribe} from "../util/needSubscribe"; import {fullscreen} from "../protyle/breadcrumb/action"; import {MenuItem} from "../menus/Menu"; import {escapeHtml} from "../util/escape"; /// #if !MOBILE import {openFile} from "../editor/util"; /// #endif /// #if !BROWSER import {ipcRenderer} from "electron"; /// #endif import * as dayjs from "dayjs"; import {getDisplayName, movePathTo} from "../util/pathName"; import {App} from "../index"; import {resize} from "../protyle/util/resize"; import {setStorageVal} from "../protyle/util/compatibility"; import {focusByRange} from "../protyle/util/selection"; import {updateCardHV} from "./util"; import {showMessage} from "../dialog/message"; import {Menu} from "../plugin/Menu"; import {transaction} from "../protyle/wysiwyg/transaction"; const genCardCount = (cardsData: ICardData, allIndex = 0) => { let newIndex = 0; let oldIndex = 0; cardsData.cards.forEach((item, index) => { if (index > allIndex) { return; } if (item.state === 0) { newIndex++; } else { oldIndex++; } }); return ` ${newIndex} / ${cardsData.unreviewedNewCardCount} + ${oldIndex} / ${cardsData.unreviewedOldCardCount} `; }; export const genCardHTML = (options: { id: string, cardType: TCardType, cardsData: ICardData, isTab: boolean }) => { let iconsHTML: string; /// #if MOBILE iconsHTML = `
${window.siyuan.languages.riffCard}
${genCardCount(options.cardsData)}
`; /// #else iconsHTML = `
${options.isTab ? '
' : ``}
${genCardCount(options.cardsData)}
`; /// #endif return `
${iconsHTML}
🔮
${window.siyuan.languages.noDueCard}
`; }; const getEditor = (id: string, protyle: IProtyle, element: Element, currentCard: ICard) => { fetchPost("/api/block/getDocInfo", { id, }, (docResponse) => { protyle.wysiwyg.renderCustom(docResponse.data.ial); fetchPost("/api/filetree/getDoc", { id, mode: 0, size: Constants.SIZE_GET_MAX }, (response) => { onGet({ updateReadonly: true, data: response, protyle, action: response.data.rootID === response.data.id ? [] : [Constants.CB_GET_ALL], afterCB: () => { if (protyle.element.classList.contains("fn__none")) { return; } let hasHide = false; if (!window.siyuan.config.flashcard.superBlock && !window.siyuan.config.flashcard.heading && !window.siyuan.config.flashcard.list && !window.siyuan.config.flashcard.mark) { hasHide = false; } else { if (window.siyuan.config.flashcard.superBlock) { if (protyle.wysiwyg.element.querySelector(":scope > .sb")) { hasHide = true; } } if (window.siyuan.config.flashcard.heading) { if (protyle.wysiwyg.element.querySelector(':scope > [data-type="NodeHeading"]')) { hasHide = true; } } if (window.siyuan.config.flashcard.list) { if (protyle.wysiwyg.element.querySelector(".list, .li")) { hasHide = true; } } if (window.siyuan.config.flashcard.mark) { if (protyle.wysiwyg.element.querySelector('span[data-type~="mark"]')) { hasHide = true; } } } const actionElements = element.querySelectorAll(".card__action"); if (!hasHide) { protyle.element.classList.remove("card__block--hidemark", "card__block--hideli", "card__block--hidesb", "card__block--hideh"); actionElements[0].classList.add("fn__none"); actionElements[1].querySelectorAll("button.b3-button").forEach((element, btnIndex) => { if (btnIndex < 2) { return; } element.previousElementSibling.textContent = currentCard.nextDues[btnIndex - 1]; }); actionElements[1].classList.remove("fn__none"); } else { if (window.siyuan.config.flashcard.superBlock) { protyle.element.classList.add("card__block--hidesb"); } if (window.siyuan.config.flashcard.heading) { protyle.element.classList.add("card__block--hideh"); } if (window.siyuan.config.flashcard.list) { protyle.element.classList.add("card__block--hideli"); } if (window.siyuan.config.flashcard.mark) { protyle.element.classList.add("card__block--hidemark"); } actionElements[0].classList.remove("fn__none"); actionElements[1].classList.add("fn__none"); } } }); }); }); }; export const bindCardEvent = async (options: { app: App, element: Element, title?: string, cardsData: ICardData cardType: TCardType, id?: string, dialog?: Dialog, index?: number, }) => { if (window.siyuan.storage[Constants.LOCAL_FLASHCARD].fullscreen) { fullscreen(options.element.querySelector(".card__main"), options.element.querySelector('[data-type="fullscreen"]')); } let index = 0; if (typeof options.index === "number") { index = options.index; } const editor = new Protyle(options.app, options.element.querySelector("[data-type='render']") as HTMLElement, { blockId: "", action: [Constants.CB_GET_ALL], render: { background: false, gutter: true, breadcrumbDocName: true, title: true, hideTitleOnZoom: true, }, typewriterMode: false }); if (window.siyuan.mobile) { window.siyuan.mobile.popEditor = editor; } if (options.cardsData.cards.length > 0) { getEditor(options.cardsData.cards[index].blockID, editor.protyle, options.element, options.cardsData.cards[index]); } options.element.setAttribute("data-key", Constants.DIALOG_OPENCARD); const actionElements = options.element.querySelectorAll(".card__action"); if (options.index === 0 || typeof options.index === "undefined") { actionElements[0].firstElementChild.setAttribute("disabled", "disabled"); actionElements[1].querySelector(".b3-button").setAttribute("disabled", "disabled"); } else { actionElements[0].firstElementChild.removeAttribute("disabled"); actionElements[1].querySelector(".b3-button").removeAttribute("disabled"); } const countElement = options.element.querySelector('[data-type="count"]'); const filterElement = options.element.querySelector('[data-type="filter"]'); const fetchNewRound = () => { const currentCardType = filterElement.getAttribute("data-cardtype"); const docId = filterElement.getAttribute("data-id"); fetchPost(currentCardType === "all" ? "/api/riff/getRiffDueCards" : (currentCardType === "doc" ? "/api/riff/getTreeRiffDueCards" : "/api/riff/getNotebookRiffDueCards"), { rootID: docId, deckID: docId, notebook: docId, }, async (treeCards) => { index = 0; options.cardsData = treeCards.data; for (let i = 0; i < options.app.plugins.length; i++) { options.cardsData = await options.app.plugins[i].updateCards(options.cardsData); } if (options.cardsData.cards.length > 0) { nextCard({ countElement, editor, actionElements, index, cardsData: options.cardsData }); } else { allDone(countElement, editor, actionElements); } }); }; countElement.innerHTML = genCardCount(options.cardsData, index); options.element.firstChild.addEventListener("click", (event: MouseEvent) => { const target = event.target as HTMLElement; let type = ""; const currentCard = options.cardsData.cards[index]; const docId = filterElement.getAttribute("data-id"); if (typeof event.detail === "string") { if (["1", "j", "a"].includes(event.detail)) { type = "1"; } else if (["2", "k", "s"].includes(event.detail)) { type = "2"; } else if (["3", "l", "d"].includes(event.detail)) { type = "3"; } else if (["4", ";", "f"].includes(event.detail)) { type = "4"; } else if ([" ", "enter"].includes(event.detail)) { type = "-1"; } else if (["p", "q"].includes(event.detail)) { type = "-2"; } else if (["0", "x"].includes(event.detail)) { type = "-3"; } } else { const fullscreenElement = hasClosestByAttribute(target, "data-type", "fullscreen"); if (fullscreenElement) { fullscreen(options.element.querySelector(".card__main"), options.element.querySelector('[data-type="fullscreen"]')); resize(editor.protyle); window.siyuan.storage[Constants.LOCAL_FLASHCARD].fullscreen = !window.siyuan.storage[Constants.LOCAL_FLASHCARD].fullscreen; setStorageVal(Constants.LOCAL_FLASHCARD, window.siyuan.storage[Constants.LOCAL_FLASHCARD]); event.stopPropagation(); event.preventDefault(); return; } const moreElement = hasClosestByAttribute(target, "data-type", "more"); if (moreElement && currentCard) { event.stopPropagation(); event.preventDefault(); if (filterElement.getAttribute("data-cardtype") === "all" && filterElement.getAttribute("data-id")) { showMessage(window.siyuan.languages.noSupportTip); return; } const menu = new Menu(); menu.addItem({ id: "setDueTime", icon: "iconClock", label: window.siyuan.languages.setDueTime, click() { const timedialog = new Dialog({ title: window.siyuan.languages.setDueTime, content: `
${window.siyuan.languages.showCardDay}
`, width: isMobile() ? "92vw" : "520px", }); const inputElement = timedialog.element.querySelector("input") as HTMLInputElement; const btnsElement = timedialog.element.querySelectorAll(".b3-button"); timedialog.bindInput(inputElement, () => { (btnsElement[1] as HTMLButtonElement).click(); }); inputElement.focus(); inputElement.select(); btnsElement[0].addEventListener("click", () => { timedialog.destroy(); }); btnsElement[1].addEventListener("click", () => { fetchPost("/api/riff/batchSetRiffCardsDueTime", { cardDues: [{ id: currentCard.cardID, due: dayjs().add(parseInt(inputElement.value), "day").format("YYYYMMDDHHmmss") }] }, () => { actionElements[0].classList.add("fn__none"); actionElements[1].classList.remove("fn__none"); if (currentCard.state === 0) { options.cardsData.unreviewedNewCardCount--; } else { options.cardsData.unreviewedOldCardCount--; } options.element.firstElementChild.dispatchEvent(new CustomEvent("click", {detail: "0"})); options.cardsData.cards.splice(index, 1); index--; timedialog.destroy(); }); }); } }); if (currentCard.state !== 0) { menu.addItem({ id: "reset", icon: "iconRefresh", label: window.siyuan.languages.reset, click() { fetchPost("/api/riff/resetRiffCards", { type: filterElement.getAttribute("data-cardtype"), id: docId, deckID: Constants.QUICK_DECK_ID, blockIDs: [currentCard.blockID], }, () => { const minLang = window.siyuan.languages._time["1m"].replace("%s", ""); currentCard.lapses = 0; currentCard.lastReview = -62135596800000; currentCard.reps = 0; currentCard.state = 0; currentCard.nextDues = { 1: minLang, 2: minLang.replace("1", "5"), 3: minLang.replace("1", "10"), 4: window.siyuan.languages._time["1d"].replace("%s", "").replace("1", "6") }; actionElements[1].querySelectorAll("button.b3-button").forEach((element, btnIndex) => { if (btnIndex < 2) { return; } element.previousElementSibling.textContent = currentCard.nextDues[btnIndex - 1]; }); options.cardsData.unreviewedOldCardCount--; options.cardsData.unreviewedNewCardCount++; countElement.innerHTML = genCardCount(options.cardsData, index); }); } }); } menu.addItem({ id: "removeRiffCard", icon: "iconTrashcan", label: `${window.siyuan.languages.remove} ${window.siyuan.languages.riffCard}`, click() { actionElements[0].classList.add("fn__none"); actionElements[1].classList.remove("fn__none"); if (currentCard.state === 0) { options.cardsData.unreviewedNewCardCount--; } else { options.cardsData.unreviewedOldCardCount--; } options.element.firstElementChild.dispatchEvent(new CustomEvent("click", {detail: "0"})); transaction(undefined, [{ action: "removeFlashcards", deckID: Constants.QUICK_DECK_ID, blockIDs: [currentCard.blockID] }]); options.cardsData.cards.splice(index, 1); index--; } }); menu.addSeparator(); menu.addItem({ id: "forgetCountAndRevisionCountAndCardStatusAndLastReviewTime", iconHTML: "", type: "readonly", label: `
${window.siyuan.languages.forgetCount}
${currentCard.lapses}
${window.siyuan.languages.revisionCount}
${currentCard.reps}
${window.siyuan.languages.cardStatus}
${currentCard.state === 0 ? window.siyuan.languages.flashcardNewCard : window.siyuan.languages.flashcardReviewCard}
${window.siyuan.languages.lastReviewTime}
${dayjs(currentCard.lastReview).format("YYYY-MM-DD")}
`, }); /// #if MOBILE menu.fullscreen(); /// #else const rect = moreElement.getBoundingClientRect(); menu.open({ x: rect.left, y: rect.bottom }); /// #endif return; } /// #if !MOBILE const sticktabElement = hasClosestByAttribute(target, "data-type", "sticktab"); if (sticktabElement) { const stickMenu = new Menu(); stickMenu.addItem({ id: "openInNewTab", icon: "iconOpen", label: window.siyuan.languages.openInNewTab, click() { openFile({ app: options.app, custom: { icon: "iconRiffCard", title: window.siyuan.languages.spaceRepetition, data: { cardsData: options.cardsData, index, cardType: filterElement.getAttribute("data-cardtype") as TCardType, id: docId, title: options.title }, id: "siyuan-card" }, }); options.dialog.destroy(); } }); stickMenu.addItem({ id: "insertRight", icon: "iconLayoutRight", label: window.siyuan.languages.insertRight, click() { openFile({ app: options.app, position: "right", custom: { icon: "iconRiffCard", title: window.siyuan.languages.spaceRepetition, data: { cardsData: options.cardsData, index, cardType: filterElement.getAttribute("data-cardtype") as TCardType, id: docId, title: options.title }, id: "siyuan-card" }, }); options.dialog.destroy(); } }); /// #if !BROWSER stickMenu.addItem({ id: "openByNewWindow", icon: "iconOpenWindow", label: window.siyuan.languages.openByNewWindow, click() { const json = [{ "title": window.siyuan.languages.spaceRepetition, "icon": "iconRiffCard", "instance": "Tab", "children": { "instance": "Custom", "customModelType": "siyuan-card", "customModelData": { "cardsData": options.cardsData, "index": index, "cardType": filterElement.getAttribute("data-cardtype"), "id": docId, "title": options.title } } }]; ipcRenderer.send(Constants.SIYUAN_OPEN_WINDOW, { // 需要 encode, 否则 https://github.com/siyuan-note/siyuan/issues/9343 url: `${window.location.protocol}//${window.location.host}/stage/build/app/window.html?v=${Constants.SIYUAN_VERSION}&json=${encodeURIComponent(JSON.stringify(json))}` }); options.dialog.destroy(); } }); /// #endif const rect = sticktabElement.getBoundingClientRect(); stickMenu.open({ x: rect.left, y: rect.bottom }); event.stopPropagation(); event.preventDefault(); return; } /// #endif const closeElement = hasClosestByAttribute(target, "data-type", "close"); if (closeElement) { if (options.dialog) { options.dialog.destroy(); } event.stopPropagation(); event.preventDefault(); return; } const filterTempElement = hasClosestByAttribute(target, "data-type", "filter"); if (filterTempElement) { fetchPost("/api/riff/getRiffDecks", {}, (response) => { window.siyuan.menus.menu.remove(); window.siyuan.menus.menu.append(new MenuItem({ id: "all", iconHTML: "", label: window.siyuan.languages.all, click() { filterElement.setAttribute("data-id", ""); filterElement.setAttribute("data-cardtype", "all"); fetchNewRound(); }, }).element); window.siyuan.menus.menu.append(new MenuItem({ id: "fileTree", iconHTML: "", label: window.siyuan.languages.fileTree, click() { movePathTo({ cb: (toPath, toNotebook) => { filterElement.setAttribute("data-id", toPath[0] === "/" ? toNotebook[0] : getDisplayName(toPath[0], true, true)); filterElement.setAttribute("data-cardtype", toPath[0] === "/" ? "notebook" : "doc"); fetchNewRound(); }, title: window.siyuan.languages.specifyPath, flashcard: true }); } }).element); if (options.title || response.data.length > 0) { window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element); } if (options.title) { window.siyuan.menus.menu.append(new MenuItem({ iconHTML: "", label: escapeHtml(options.title), click() { filterElement.setAttribute("data-id", options.id); filterElement.setAttribute("data-cardtype", options.cardType); fetchNewRound(); }, }).element); if (response.data.length > 0) { window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element); } } response.data.forEach((deck: { id: string, name: string }) => { window.siyuan.menus.menu.append(new MenuItem({ iconHTML: "", label: escapeHtml(deck.name), click() { filterElement.setAttribute("data-id", deck.id); filterElement.setAttribute("data-cardtype", "all"); fetchNewRound(); }, }).element); }); const filterRect = filterTempElement.getBoundingClientRect(); window.siyuan.menus.menu.popup({x: filterRect.left, y: filterRect.bottom}); }); event.stopPropagation(); event.preventDefault(); return; } const newroundElement = hasClosestByAttribute(target, "data-type", "newround"); if (newroundElement) { fetchNewRound(); event.stopPropagation(); event.preventDefault(); return; } } if (!type) { const buttonElement = hasClosestByClassName(target, "b3-button"); if (buttonElement) { type = buttonElement.getAttribute("data-type"); } } if (!type || !currentCard) { return; } event.preventDefault(); event.stopPropagation(); hideElements(["toolbar", "hint", "util", "gutter"], editor.protyle); if (type === "-1") { // 显示答案 if (actionElements[0].classList.contains("fn__none")) { type = "3"; } else { editor.protyle.element.classList.remove("card__block--hidemark", "card__block--hideli", "card__block--hidesb", "card__block--hideh"); actionElements[0].classList.add("fn__none"); actionElements[1].querySelectorAll("button.b3-button").forEach((element, btnIndex) => { if (btnIndex < 2) { return; } element.previousElementSibling.textContent = currentCard.nextDues[btnIndex - 1]; }); actionElements[1].classList.remove("fn__none"); emitEvent(options.app, currentCard, type); return; } } else if (type === "-2") { // 上一步 if (index > 0) { index--; nextCard({ countElement, editor, actionElements, index, cardsData: options.cardsData }); emitEvent(options.app, options.cardsData.cards[index + 1], type); } return; } if (["1", "2", "3", "4", "-3"].includes(type) && actionElements[0].classList.contains("fn__none")) { fetchPost(type === "-3" ? "/api/riff/skipReviewRiffCard" : "/api/riff/reviewRiffCard", { deckID: currentCard.deckID, cardID: currentCard.cardID, rating: parseInt(type), reviewedCards: options.cardsData.cards }, () => { /// #if MOBILE if (type !== "-3" && ((0 !== window.siyuan.config.sync.provider && isPaidUser()) || (0 === window.siyuan.config.sync.provider && !needSubscribe(""))) && window.siyuan.config.repo.key && window.siyuan.config.sync.enabled) { document.getElementById("toolbarSync").classList.remove("fn__none"); } /// #endif index++; if (index > options.cardsData.cards.length - 1) { const currentCardType = filterElement.getAttribute("data-cardtype"); fetchPost(currentCardType === "all" ? "/api/riff/getRiffDueCards" : (currentCardType === "doc" ? "/api/riff/getTreeRiffDueCards" : "/api/riff/getNotebookRiffDueCards"), { rootID: docId, deckID: docId, notebook: docId, reviewedCards: options.cardsData.cards }, async (result) => { emitEvent(options.app, options.cardsData.cards[index - 1], type); index = 0; options.cardsData = result.data; for (let i = 0; i < options.app.plugins.length; i++) { options.cardsData = await options.app.plugins[i].updateCards(options.cardsData); } if (options.cardsData.cards.length === 0) { if (options.cardsData.unreviewedCount > 0) { newRound(countElement, editor, actionElements, result.data.unreviewedCount); } else { allDone(countElement, editor, actionElements); } } else { nextCard({ countElement, editor, actionElements, index, cardsData: options.cardsData }); } }); return; } nextCard({ countElement, editor, actionElements, index, cardsData: options.cardsData }); emitEvent(options.app, options.cardsData.cards[index - 1], type); }); } }); return editor; }; const emitEvent = (app: App, card: ICard, type: string) => { app.plugins.forEach(item => { item.eventBus.emit("click-flashcard-action", { type, card }); }); }; export const openCard = (app: App) => { if (window.siyuan.config.readonly) { return; } fetchPost("/api/riff/getRiffDueCards", {deckID: ""}, (cardsResponse) => { openCardByData(app, cardsResponse.data, "all"); }); }; export const openCardByData = async (app: App, cardsData: ICardData, cardType: TCardType, id?: string, title?: string) => { const exit = window.siyuan.dialogs.find(item => { if (item.element.getAttribute("data-key") === Constants.DIALOG_OPENCARD) { item.destroy(); return true; } }); if (exit) { return; } let lastRange: Range; if (getSelection().rangeCount > 0) { lastRange = getSelection().getRangeAt(0); } for (let i = 0; i < app.plugins.length; i++) { cardsData = await app.plugins[i].updateCards(cardsData); } const dialog = new Dialog({ positionId: Constants.DIALOG_OPENCARD, content: genCardHTML({id, cardType, cardsData, isTab: false}), width: isMobile() ? "100vw" : "80vw", height: isMobile() ? "100vh" : "70vh", destroyCallback() { if (editor) { editor.destroy(); if (window.siyuan.mobile) { window.siyuan.mobile.popEditor = null; } } if (lastRange) { focusByRange(lastRange); } }, resizeCallback(type: string) { if (type !== "d" && type !== "t" && editor) { editor.resize(); } } }); (dialog.element.querySelector(".b3-dialog__scrim") as HTMLElement).style.backgroundColor = "var(--b3-theme-surface)"; (dialog.element.querySelector(".b3-dialog__container") as HTMLElement).style.maxWidth = "1024px"; const editor = await bindCardEvent({ app, element: dialog.element, cardsData, title, id, cardType, dialog }); editor.resize(); dialog.editors = { card: editor }; /// #if !MOBILE const focusElement = dialog.element.querySelector(".block__icons button.block__icon") as HTMLElement; focusElement.focus(); const range = document.createRange(); range.selectNodeContents(focusElement); range.collapse(); focusByRange(range); /// #endif updateCardHV(); }; const nextCard = (options: { countElement: Element, editor: Protyle, actionElements: NodeListOf, index: number, cardsData: ICardData }) => { options.editor.protyle.element.classList.remove("fn__none"); options.editor.protyle.element.nextElementSibling.classList.add("fn__none"); options.countElement.innerHTML = genCardCount(options.cardsData, options.index); options.countElement.classList.remove("fn__none"); if (options.index === 0) { options.actionElements[0].firstElementChild.setAttribute("disabled", "disabled"); options.actionElements[1].querySelector(".b3-button").setAttribute("disabled", "disabled"); } else { options.actionElements[0].firstElementChild.removeAttribute("disabled"); options.actionElements[1].querySelector(".b3-button").removeAttribute("disabled"); } getEditor(options.cardsData.cards[options.index].blockID, options.editor.protyle, hasClosestByAttribute(options.countElement, "data-key", Constants.DIALOG_OPENCARD) as HTMLElement, options.cardsData.cards[options.index]); }; const allDone = (countElement: Element, editor: Protyle, actionElements: NodeListOf) => { countElement.classList.add("fn__none"); editor.protyle.element.classList.add("fn__none"); const emptyElement = editor.protyle.element.nextElementSibling; emptyElement.innerHTML = `
🔮
${window.siyuan.languages.noDueCard}`; emptyElement.classList.remove("fn__none"); actionElements[0].classList.add("fn__none"); actionElements[1].classList.add("fn__none"); const moreElement = countElement.parentElement.querySelector('[data-type="more"]'); moreElement.classList.add("fn__none"); moreElement.previousElementSibling.classList.add("fn__none"); }; const newRound = (countElement: Element, editor: Protyle, actionElements: NodeListOf, unreviewedCount: number) => { countElement.classList.add("fn__none"); editor.protyle.element.classList.add("fn__none"); const emptyElement = editor.protyle.element.nextElementSibling; emptyElement.innerHTML = `
♻️
${window.siyuan.languages.continueReview2.replace("${count}", unreviewedCount)}
`; emptyElement.classList.remove("fn__none"); actionElements[0].classList.add("fn__none"); actionElements[1].classList.add("fn__none"); };