A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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};