Forráskód Böngészése

Merge remote-tracking branch 'origin/dev' into dev

Vanessa 2 éve
szülő
commit
7ad5cdd067

+ 0 - 3
app/appearance/langs/en_US.json

@@ -507,9 +507,6 @@
   "useDefault": "Open with default program",
   "previous": "Previous",
   "next": "Next",
-  "lockFile0": "Unable to access data",
-  "lockFile1": "The data file has been locked by another program",
-  "lockFile2": "If the problem still occurs frequently in subsequent use, please report it via <a href=\"https://github.com/siyuan-note/siyuan/issues\" target=\"_blank\">GitHub Issues</a>",
   "kernelFault0": "Kernel connection interrupted...",
   "kernelFault1": "Please check if the network connection and kernel process is normal",
   "kernelFault2": "If the problem still occurs after restarting, please report it via <a href=\"https://github.com/siyuan-note/siyuan/issues\" target=\"_blank\">GitHub Issues</a>",

+ 0 - 3
app/appearance/langs/es_ES.json

@@ -507,9 +507,6 @@
   "useDefault": "Abrir con el programa por defecto",
   "previous": "Anterior",
   "next": "Siguiente",
-  "lockFile0": "No se puede acceder a los datos",
-  "lockFile1": "El archivo de datos ha sido bloqueado por otro programa",
-  "lockFile2": "Si el problema sigue ocurriendo con frecuencia en el uso posterior, infórmelo a través de <a href=\"https://github.com/siyuan-note/siyuan/issues\" target=\"_blank\">Problemas en GitHub</a>",
   "kernelFault0": "Conexión del kernel interrumpida...",
   "kernelFault1": "Verifique si la conexión de red y los procesos del kernel son normales",
   "kernelFault2": "Si el problema sigue produciéndose después de reiniciar, comuníquelo a través de <a href=\"https://github.com/siyuan-note/siyuan/issues\" target=\"_blank\">Problemas en GitHub</a>",

+ 0 - 3
app/appearance/langs/fr_FR.json

@@ -507,9 +507,6 @@
   "useDefault": "Ouvrir avec le programme par défaut",
   "previous": "Précédent",
   "next": "Prochain",
-  "lockFile0": "Impossible d'accéder aux données",
-  "lockFile1": "Le fichier de données a été verrouillé par un autre programme",
-  "lockFile2": "Si le problème se produit encore fréquemment lors d'une utilisation ultérieure, veuillez le signaler via <a href=\"https://github.com/siyuan-note/siyuan/issues\" target=\"_blank\">GitHub Issues</a>",
   "kernelFault0": "Connexion au noyau interrompue...",
   "kernelFault1": "Veuillez vérifier si la connexion réseau et les processus du noyau sont normaux",
   "kernelFault2": "Si le problème persiste après le redémarrage, veuillez le signaler via <a href=\"https://github.com/siyuan-note/siyuan/issues\" target=\"_blank\">GitHub Issues</a>",

+ 0 - 3
app/appearance/langs/zh_CHT.json

@@ -507,9 +507,6 @@
   "useDefault": "使用預設程式打開",
   "previous": "上一個",
   "next": "下一個",
-  "lockFile0": "無法存取資料",
-  "lockFile1": "資料檔案已被其他程式鎖定",
-  "lockFile2": "如果後續使用仍然頻繁出現該問題,請通過<a href=\"https://ld246.com/article/1649901726096\" target=\"_blank\">這裡回饋</a>",
   "kernelFault0": "kernel連接中斷...",
   "kernelFault1": "請檢查網絡連接和內核進程是否正常",
   "kernelFault2": "如果重啟後仍然出現該問題,請通過<a href=\"https://ld246.com/article/1649901726096\" target=\"_blank\">這裡回饋</a>",

+ 0 - 3
app/appearance/langs/zh_CN.json

@@ -507,9 +507,6 @@
   "useDefault": "使用默认程序打开",
   "previous": "上一个",
   "next": "下一个",
-  "lockFile0": "无法存取数据",
-  "lockFile1": "数据文件已被其他程序锁定",
-  "lockFile2": "如果后续使用仍然频繁出现该问题,请通过<a href=\"https://ld246.com/article/1649901726096\" target=\"_blank\">这里反馈</a>",
   "kernelFault0": "内核连接中断...",
   "kernelFault1": "请检查网络连接和内核进程是否正常",
   "kernelFault2": "如果重启后仍然出现该问题,请通过<a href=\"https://ld246.com/article/1649901726096\" target=\"_blank\">这里反馈</a>",

+ 3 - 1
app/electron-builder.yml

@@ -36,7 +36,9 @@ nsis:
   createStartMenuShortcut: true
   shortcutName: "SiYuan"
   license: "../LICENSE"
-  include: "installer.nsh"
+  include: "nsis/installer.nsh"
+  installerSidebar: "nsis/installerSidebar.bmp"
+  uninstallerSidebar: "nsis/uninstallerSidebar.bmp"
 
 extraResources:
   - from: "appearance/boot"

+ 0 - 0
app/installer.nsh → app/nsis/installer.nsh


BIN
app/nsis/installerSidebar.bmp


BIN
app/nsis/uninstallerSidebar.bmp


BIN
app/pandoc/pandoc-darwin-amd64.zip


BIN
app/pandoc/pandoc-linux-amd64.zip


BIN
app/pandoc/pandoc-windows-amd64.zip


+ 0 - 6
app/src/block/Panel.ts

@@ -10,7 +10,6 @@ import {openNewWindowById} from "../window/openNewWindow";
 /// #endif
 import {disabledProtyle} from "../protyle/util/onGet";
 import {fetchPost} from "../util/fetch";
-import {lockFile} from "../dialog/processSystem";
 import {showMessage} from "../dialog/message";
 
 export class BlockPanel {
@@ -245,11 +244,6 @@ export class BlockPanel {
     private initProtyle(editorElement: HTMLElement) {
         const index = parseInt(editorElement.getAttribute("data-index"));
         fetchPost("api/block/getBlockInfo", {id: this.nodeIds[index]}, (response) => {
-            if (response.code === 2) {
-                // 文件被锁定
-                lockFile(response.data);
-                return false;
-            }
             if (response.code === 3) {
                 showMessage(response.msg);
                 return;

+ 0 - 46
app/src/dialog/processSystem.ts

@@ -1,7 +1,6 @@
 import {Constants} from "../constants";
 import {fetchPost} from "../util/fetch";
 /// #if !MOBILE
-import {getAllModels} from "../layout/getAll";
 import {exportLayout} from "../layout/util";
 /// #endif
 /// #if !BROWSER
@@ -29,47 +28,6 @@ export const lockScreen = () => {
     /// #endif
 };
 
-export const lockFile = (id: string) => {
-    const html = `<div class="b3-dialog__scrim"></div>
-<div class="b3-dialog__container">
-    <div class="b3-dialog__header" onselectstart="return false;">🔒 ${window.siyuan.languages.lockFile0} <small>v${Constants.SIYUAN_VERSION}</small></div>
-    <div class="b3-dialog__content">
-        <p>${window.siyuan.languages.lockFile1}</p>
-        <p>${window.siyuan.languages.lockFile2}</p>
-    </div>
-    <div class="b3-dialog__action">
-        <button class="b3-button b3-button--cancel">${window.siyuan.languages.closeTab}</button>
-        <div class="fn__space"></div>
-        <button class="b3-button b3-button--text">${window.siyuan.languages.retry}</button>
-    </div>
-</div>`;
-    let logElement = document.getElementById("errorLog");
-    if (logElement) {
-        logElement.innerHTML = html;
-    } else {
-        document.body.insertAdjacentHTML("beforeend", `<div id="errorLog" class="b3-dialog b3-dialog--open">${html}</div>`);
-        logElement = document.getElementById("errorLog");
-    }
-    logElement.querySelector(".b3-button--cancel").addEventListener("click", () => {
-        /// #if !MOBILE
-        getAllModels().editor.find((item) => {
-            if (item.editor.protyle.block.rootID === id) {
-                item.parent.parent.removeTab(item.parent.id, false, false);
-                return true;
-            }
-        });
-        logElement.remove();
-        /// #endif
-    });
-    logElement.querySelector(".b3-button--text").addEventListener("click", () => {
-        fetchPost("/api/filetree/lockFile", {id}, (response) => {
-            if (response.code === 0) {
-                window.location.reload();
-            }
-        });
-    });
-};
-
 export const kernelError = () => {
     let iosReStart = "";
     if (window.siyuan.config.system.container === "ios" && window.webkit?.messageHandlers) {
@@ -162,10 +120,6 @@ export const exitSiYuan = () => {
 };
 
 export const transactionError = (data: { code: number, data: string }) => {
-    if (data.code === 1) {
-        lockFile(data.data);
-        return;
-    }
     if (document.getElementById("transactionError")) {
         return;
     }

+ 1 - 6
app/src/editor/util.ts

@@ -19,7 +19,7 @@ import {pushBack} from "../util/backForward";
 import {Asset} from "../asset";
 import {Layout} from "../layout";
 import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName,} from "../protyle/util/hasClosest";
-import {lockFile, setTitle} from "../dialog/processSystem";
+import {setTitle} from "../dialog/processSystem";
 import {zoomOut} from "../menus/protyle";
 import {countBlockWord, countSelectWord} from "../layout/status";
 import {showMessage} from "../dialog/message";
@@ -34,11 +34,6 @@ export const openFileById = (options: {
     removeCurrentTab?: boolean
 }) => {
     fetchPost("/api/block/getBlockInfo", {id: options.id}, (data) => {
-        if (data.code === 2) {
-            // 文件被锁定
-            lockFile(data.data);
-            return;
-        }
         if (data.code === 3) {
             showMessage(data.msg);
             return;

+ 0 - 6
app/src/mobile/editor.ts

@@ -7,7 +7,6 @@ import {disabledProtyle, onGet} from "../protyle/util/onGet";
 import {addLoading} from "../protyle/ui/initUI";
 import {focusBlock} from "../protyle/util/selection";
 import {scrollCenter} from "../util/highlightById";
-import {lockFile} from "../dialog/processSystem";
 import {hasClosestByAttribute} from "../protyle/util/hasClosest";
 import {setEditMode} from "../protyle/util/setEditMode";
 import {hideElements} from "../protyle/ui/hideElements";
@@ -40,11 +39,6 @@ export const openMobileFileById = (id: string, action = [Constants.CB_GET_HL]) =
     }
 
     fetchPost("/api/block/getBlockInfo", {id}, (data) => {
-        if (data.code === 2) {
-            // 文件被锁定
-            lockFile(data.data);
-            return;
-        }
         if (data.code === 3) {
             showMessage(data.msg);
             return;

+ 0 - 6
app/src/protyle/export/index.ts

@@ -11,7 +11,6 @@ import {confirmDialog} from "../../dialog/confirmDialog";
 import {getThemeMode, setInlineStyle} from "../../util/assets";
 import {fetchPost} from "../../util/fetch";
 import {Dialog} from "../../dialog";
-import {lockFile} from "../../dialog/processSystem";
 import {pathPosix} from "../../util/pathName";
 import {replaceLocalPath} from "../../editor/rename";
 import {setStorageVal} from "../util/compatibility";
@@ -468,11 +467,6 @@ const getExportPath = (option: { type: string, id: string }, removeAssets?: bool
     fetchPost("/api/block/getBlockInfo", {
         id: option.id
     }, (response) => {
-        if (response.code === 2) {
-            // 文件被锁定
-            lockFile(response.data);
-            return;
-        }
         if (response.code === 3) {
             showMessage(response.msg);
             return;

+ 1 - 7
app/src/protyle/util/onGet.ts

@@ -1,4 +1,4 @@
-import {lockFile, setTitle} from "../../dialog/processSystem";
+import {setTitle} from "../../dialog/processSystem";
 import {Constants} from "../../constants";
 import {hideElements} from "../ui/hideElements";
 import {genEmptyElement} from "../../block/util";
@@ -36,12 +36,6 @@ export const onGet = (data: IWebSocketData, protyle: IProtyle, action: string[]
     }
     protyle.notebookId = data.data.box;
     protyle.path = data.data.path;
-    if (data.code === 2) {
-        // 文件被锁定
-        protyle.block.rootID = data.data;
-        lockFile(data.data);
-        return;
-    }
 
     if (data.data.eof) {
         if (action.includes(Constants.CB_GET_BEFORE)) {

+ 1 - 5
app/src/protyle/wysiwyg/transaction.ts

@@ -6,7 +6,6 @@ import {blockRender} from "../markdown/blockRender";
 import {processRender} from "../util/processCode";
 import {highlightRender} from "../markdown/highlightRender";
 import {hasClosestBlock, hasClosestByAttribute} from "../util/hasClosest";
-import {lockFile} from "../../dialog/processSystem";
 import {setFold} from "../../menus/protyle";
 import {onGet} from "../util/onGet";
 /// #if !MOBILE
@@ -69,10 +68,7 @@ const promiseTransaction = () => {
         } else {
             promiseTransaction();
         }
-        if (response.code === 1) {
-            lockFile(protyle.block.rootID);
-            return;
-        }
+
         countBlockWord([], protyle.block.rootID, true);
         /// #if MOBILE
         if ((0 !== window.siyuan.config.sync.provider || (0 === window.siyuan.config.sync.provider && !needSubscribe(""))) &&

+ 0 - 6
app/src/util/backForward.ts

@@ -10,7 +10,6 @@ import {Tab} from "../layout/Tab";
 import {Editor} from "../editor";
 import {onGet} from "../protyle/util/onGet";
 import {scrollCenter} from "./highlightById";
-import {lockFile} from "../dialog/processSystem";
 import {zoomOut} from "../menus/protyle";
 import {showMessage} from "../dialog/message";
 import {saveScroll} from "../protyle/scroll/saveScroll";
@@ -39,11 +38,6 @@ const focusStack = async (stack: IBackStack) => {
         }
         if (wnd) {
             const info = await fetchSyncPost("/api/block/getBlockInfo", {id: stack.id});
-            if (info.code === 2) {
-                // 文件被锁定
-                lockFile(info.data);
-                return false;
-            }
             if (info.code === 3) {
                 showMessage(info.msg);
                 return;

+ 0 - 6
app/src/window/openNewWindow.ts

@@ -6,7 +6,6 @@ import {getCurrentWindow} from "@electron/remote";
 import {Constants} from "../constants";
 import {Tab} from "../layout/Tab";
 import {fetchPost} from "../util/fetch";
-import {lockFile} from "../dialog/processSystem";
 import {showMessage} from "../dialog/message";
 
 export const openNewWindow = (tab: Tab) => {
@@ -23,11 +22,6 @@ export const openNewWindow = (tab: Tab) => {
 
 export const openNewWindowById = (id: string) => {
     fetchPost("api/block/getBlockInfo", {id}, (response) => {
-        if (response.code === 2) {
-            // 文件被锁定
-            lockFile(response.data);
-            return false;
-        }
         if (response.code === 3) {
             showMessage(response.msg);
             return;

+ 0 - 16
kernel/api/block.go

@@ -194,12 +194,6 @@ func checkBlockExist(c *gin.Context) {
 
 	id := arg["id"].(string)
 	b, err := model.GetBlock(id, nil)
-	// TODO 文件被锁的情况已经在 filelock 中做了退出进程处理,不会走到应用层,所以 code 为 2 的情况应该移除
-	//if errors.Is(err, filelock.ErrUnableAccessFile) {
-	//	ret.Code = 2
-	//	ret.Data = id
-	//	return
-	//}
 	if errors.Is(err, model.ErrIndexing) {
 		ret.Code = 0
 		ret.Data = false
@@ -405,11 +399,6 @@ func getBlockInfo(c *gin.Context) {
 	id := arg["id"].(string)
 
 	tree, err := model.LoadTreeByID(id)
-	//if errors.Is(err, filelock.ErrUnableAccessFile) {
-	//	ret.Code = 2
-	//	ret.Data = id
-	//	return
-	//}
 	if errors.Is(err, model.ErrIndexing) {
 		ret.Code = 3
 		ret.Msg = model.Conf.Language(56)
@@ -438,11 +427,6 @@ func getBlockInfo(c *gin.Context) {
 	}
 
 	root, err := model.GetBlock(block.RootID, tree)
-	//if errors.Is(err, filelock.ErrUnableAccessFile) {
-	//	ret.Code = 2
-	//	ret.Data = id
-	//	return
-	//}
 	if errors.Is(err, model.ErrIndexing) {
 		ret.Code = 3
 		ret.Data = model.Conf.Language(56)

+ 0 - 23
kernel/api/filetree.go

@@ -501,24 +501,6 @@ func createDocWithMd(c *gin.Context) {
 	pushCreate(box, p, id, arg)
 }
 
-func lockFile(c *gin.Context) {
-	ret := gulu.Ret.NewResult()
-	defer c.JSON(http.StatusOK, ret)
-
-	arg, ok := util.JsonArg(c, ret)
-	if !ok {
-		return
-	}
-
-	id := arg["id"].(string)
-	locked := model.TryAccessFileByBlockID(id)
-	if !locked {
-		ret.Code = -1
-		ret.Msg = fmt.Sprintf(model.Conf.Language(75))
-		ret.Data = map[string]interface{}{"closeTimeout": 5000}
-	}
-}
-
 func getDocCreateSavePath(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)
@@ -689,11 +671,6 @@ func getDoc(c *gin.Context) {
 	}
 
 	blockCount, content, parentID, parent2ID, rootID, typ, eof, scroll, boxID, docPath, isBacklinkExpand, err := model.GetDoc(startID, endID, id, index, keyword, mode, size, isBacklink)
-	//if errors.Is(err, filelock.ErrUnableAccessFile) {
-	//	ret.Code = 2
-	//	ret.Data = id
-	//	return
-	//}
 	if model.ErrBlockNotFound == err {
 		ret.Code = 3
 		return

+ 1 - 1
kernel/api/router.go

@@ -89,7 +89,6 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/filetree/getDocCreateSavePath", model.CheckAuth, getDocCreateSavePath)
 	ginServer.Handle("POST", "/api/filetree/getRefCreateSavePath", model.CheckAuth, getRefCreateSavePath)
 	ginServer.Handle("POST", "/api/filetree/changeSort", model.CheckAuth, model.CheckReadonly, changeSort)
-	ginServer.Handle("POST", "/api/filetree/lockFile", model.CheckAuth, lockFile)
 	ginServer.Handle("POST", "/api/filetree/createDocWithMd", model.CheckAuth, model.CheckReadonly, createDocWithMd)
 	ginServer.Handle("POST", "/api/filetree/createDailyNote", model.CheckAuth, model.CheckReadonly, createDailyNote)
 	ginServer.Handle("POST", "/api/filetree/createDoc", model.CheckAuth, model.CheckReadonly, createDoc)
@@ -263,6 +262,7 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/setting/getCustomCSS", model.CheckAuth, getCustomCSS)
 	ginServer.Handle("POST", "/api/setting/setCustomCSS", model.CheckAuth, model.CheckReadonly, setCustomCSS)
 	ginServer.Handle("POST", "/api/setting/setEmoji", model.CheckAuth, model.CheckReadonly, setEmoji)
+	ginServer.Handle("POST", "/api/setting/setFlashcard", model.CheckAuth, model.CheckReadonly, setFlashcard)
 
 	ginServer.Handle("POST", "/api/graph/resetGraph", model.CheckAuth, model.CheckReadonly, resetGraph)
 	ginServer.Handle("POST", "/api/graph/resetLocalGraph", model.CheckAuth, model.CheckReadonly, resetLocalGraph)

+ 37 - 0
kernel/api/setting.go

@@ -29,6 +29,43 @@ import (
 	"github.com/siyuan-note/siyuan/kernel/util"
 )
 
+func setFlashcard(c *gin.Context) {
+	ret := gulu.Ret.NewResult()
+	defer c.JSON(http.StatusOK, ret)
+
+	arg, ok := util.JsonArg(c, ret)
+	if !ok {
+		return
+	}
+
+	param, err := gulu.JSON.MarshalJSON(arg)
+	if nil != err {
+		ret.Code = -1
+		ret.Msg = err.Error()
+		return
+	}
+
+	flashcard := &conf.Flashcard{}
+	if err = gulu.JSON.UnmarshalJSON(param, flashcard); nil != err {
+		ret.Code = -1
+		ret.Msg = err.Error()
+		return
+	}
+
+	if 1 > flashcard.DailyNewCardLimit {
+		flashcard.DailyNewCardLimit = 1
+	}
+
+	if 1 > flashcard.DailyReviewCardLimit {
+		flashcard.DailyReviewCardLimit = 1
+	}
+
+	model.Conf.Flashcard = flashcard
+	model.Conf.Save()
+
+	ret.Data = flashcard
+}
+
 func setAccount(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)

+ 6 - 6
kernel/go.mod

@@ -37,12 +37,13 @@ require (
 	github.com/mitchellh/go-ps v1.0.0
 	github.com/mssola/user_agent v0.6.0
 	github.com/olahol/melody v1.1.3
+	github.com/open-spaced-repetition/go-fsrs v0.1.0
 	github.com/panjf2000/ants/v2 v2.7.1
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/radovskyb/watcher v1.0.7
 	github.com/sashabaranov/go-gpt3 v1.4.0
 	github.com/shirou/gopsutil/v3 v3.23.2
-	github.com/siyuan-note/dejavu v0.0.0-20230315034343-e9513a7e1999
+	github.com/siyuan-note/dejavu v0.0.0-20230319084158-c8c1eaadf7bd
 	github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75
 	github.com/siyuan-note/eventbus v0.0.0-20230216103454-41885eac6c2b
 	github.com/siyuan-note/filelock v0.0.0-20230319015503-9ac4a0ee675b
@@ -66,7 +67,7 @@ require (
 	github.com/alecthomas/chroma v0.10.0 // indirect
 	github.com/andybalholm/cascadia v1.3.1 // indirect
 	github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef // indirect
-	github.com/aws/aws-sdk-go v1.44.221 // indirect
+	github.com/aws/aws-sdk-go v1.44.224 // indirect
 	github.com/bytedance/sonic v1.8.5 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
@@ -107,14 +108,13 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/onsi/ginkgo/v2 v2.9.1 // indirect
-	github.com/open-spaced-repetition/go-fsrs v0.1.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.7 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
 	github.com/qiniu/go-sdk/v7 v7.14.0 // indirect
 	github.com/quic-go/qpack v0.4.0 // indirect
-	github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
-	github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
+	github.com/quic-go/qtls-go1-19 v0.3.0 // indirect
+	github.com/quic-go/qtls-go1-20 v0.2.0 // indirect
 	github.com/quic-go/quic-go v0.33.0 // indirect
 	github.com/restic/chunker v0.4.0 // indirect
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
@@ -128,7 +128,7 @@ require (
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
 	golang.org/x/arch v0.3.0 // indirect
 	golang.org/x/crypto v0.7.0 // indirect
-	golang.org/x/exp v0.0.0-20230314191032-db074128a8ec // indirect
+	golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
 	golang.org/x/net v0.8.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
 	golang.org/x/sys v0.6.0 // indirect

+ 10 - 10
kernel/go.sum

@@ -37,8 +37,8 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
 github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
 github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
-github.com/aws/aws-sdk-go v1.44.221 h1:yndn4uvLolKXPoXIwKHhO5XtwlTnJfXLBKXs84C5+hQ=
-github.com/aws/aws-sdk-go v1.44.221/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.224 h1:09CiaaF35nRmxrzWZ2uRq5v6Ghg/d2RiPjZnSgtt+RQ=
+github.com/aws/aws-sdk-go v1.44.224/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/bytedance/sonic v1.8.5 h1:kjX0/vo5acEQ/sinD/18SkA/lDDUk23F0RcaHvI7omc=
 github.com/bytedance/sonic v1.8.5/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -250,10 +250,10 @@ github.com/qiniu/go-sdk/v7 v7.14.0/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFs
 github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
 github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
 github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
-github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
-github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
-github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
+github.com/quic-go/qtls-go1-19 v0.3.0 h1:aUBoQdpHzUWtPw5tQZbsD2GnrWCNu7/RIX1PtqGeLYY=
+github.com/quic-go/qtls-go1-19 v0.3.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
+github.com/quic-go/qtls-go1-20 v0.2.0 h1:jUHn+obJ6WI5JudqBO0Iy1ra5Vh5vsitQ1gXQvkmN+E=
+github.com/quic-go/qtls-go1-20 v0.2.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
 github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
 github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
 github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
@@ -277,8 +277,8 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/shurcooL/gofontwoff v0.0.0-20181114050219-180f79e6909d h1:lvCTyBbr36+tqMccdGMwuEU+hjux/zL6xSmf5S9ITaA=
 github.com/shurcooL/gofontwoff v0.0.0-20181114050219-180f79e6909d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
-github.com/siyuan-note/dejavu v0.0.0-20230315034343-e9513a7e1999 h1:t4FSwNVaa2CJbAIJNyF5egCdgqAeBDbP0WmGXcPBbGs=
-github.com/siyuan-note/dejavu v0.0.0-20230315034343-e9513a7e1999/go.mod h1:KUsHkTYpibU30rUArZOakCCHF1SGHQlxPWUu2LqW72s=
+github.com/siyuan-note/dejavu v0.0.0-20230319084158-c8c1eaadf7bd h1:h46K2Da4gmnDtXniCTX9p0NEP5toLd4qeTJ4teRiyns=
+github.com/siyuan-note/dejavu v0.0.0-20230319084158-c8c1eaadf7bd/go.mod h1:8fvMTYkfHRBn13YyZxbRfk2GCxDFszYlJfyyYFkvvNU=
 github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75 h1:Bi7/7f29LW+Fm0cHc0J1NO1cZqyJwljSWVmfOqVZgaE=
 github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75/go.mod h1:H8fyqqAbp9XreANjeSbc72zEdFfKTXYN34tc1TjZwtw=
 github.com/siyuan-note/eventbus v0.0.0-20230216103454-41885eac6c2b h1:828lTUW2C0uNiolODqoACu7J8sDUzswD4Xo04mUombg=
@@ -351,8 +351,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4
 golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
-golang.org/x/exp v0.0.0-20230314191032-db074128a8ec h1:pAv+d8BM2JNnNctsLJ6nnZ6NqXT8N4+eauvZSb3P0I0=
-golang.org/x/exp v0.0.0-20230314191032-db074128a8ec/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
+golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=

+ 0 - 13
kernel/model/box.go

@@ -527,19 +527,6 @@ func (box *Box) UpdateHistoryGenerated() {
 	boxLatestHistoryTime[box.ID] = time.Now()
 }
 
-func TryAccessFileByBlockID(id string) (ok bool) {
-	bt := treenode.GetBlockTree(id)
-	if nil == bt {
-		return
-	}
-	p := filepath.Join(util.DataDir, bt.BoxID, bt.Path)
-
-	if !gulu.File.IsExist(p) {
-		return false
-	}
-	return true
-}
-
 func getBoxesByPaths(paths []string) (ret map[string]*Box) {
 	ret = map[string]*Box{}
 	for _, p := range paths {

+ 14 - 0
kernel/model/flashcard.go

@@ -29,6 +29,7 @@ import (
 	"github.com/88250/gulu"
 	"github.com/88250/lute/ast"
 	"github.com/88250/lute/parse"
+	"github.com/open-spaced-repetition/go-fsrs"
 	"github.com/siyuan-note/logging"
 	"github.com/siyuan-note/riff"
 	"github.com/siyuan-note/siyuan/kernel/cache"
@@ -634,6 +635,10 @@ func removeFlashcardsByBlockIDs(blockIDs []string, deck *riff.Deck) {
 	}
 
 	cards := deck.GetCardsByBlockIDs(blockIDs)
+	if 1 > len(cards) {
+		return
+	}
+
 	for _, card := range cards {
 		deck.RemoveCard(card.ID())
 	}
@@ -900,11 +905,20 @@ func getDeckDueCards(deck *riff.Deck) (ret []riff.Card) {
 	ret = []riff.Card{}
 	dues := deck.Dues()
 
+	newCount := 0
+	reviewCount := 0
 	for _, c := range dues {
 		if nil != skipCardCache[c.ID()] {
 			continue
 		}
 
+		fsrsCard := c.Impl().(*fsrs.Card)
+		if fsrs.New == fsrsCard.State {
+			newCount++
+		} else {
+			reviewCount++
+		}
+
 		ret = append(ret, c)
 	}
 	return

+ 77 - 66
kernel/util/runtime.go

@@ -130,105 +130,116 @@ func ReportFileSysFatalError(err error) {
 	os.Exit(logging.ExitCodeFileSysErr)
 }
 
+var checkFileSysStatusLock = sync.Mutex{}
+
 func CheckFileSysStatus() {
 	if ContainerStd != Container {
 		return
 	}
 
-	const fileSysStatusCheckFile = ".siyuan/filesys_status_check"
-
 	for {
 		<-thirdPartySyncCheckTicker.C
+		checkFileSysStatus()
+	}
+}
 
-		if IsCloudDrivePath(WorkspaceDir) {
-			ReportFileSysFatalError(fmt.Errorf("workspace dir [%s] is in third party sync dir", WorkspaceDir))
-			return
-		}
+func checkFileSysStatus() {
+	if IsMutexLocked(&checkFileSysStatusLock) {
+		logging.LogWarnf("check file system status is locked, skip")
+		return
+	}
+
+	checkFileSysStatusLock.Lock()
+	defer checkFileSysStatusLock.Unlock()
 
-		dir := filepath.Join(DataDir, fileSysStatusCheckFile)
-		if err := os.RemoveAll(dir); nil != err {
+	const fileSysStatusCheckFile = ".siyuan/filesys_status_check"
+	if IsCloudDrivePath(WorkspaceDir) {
+		ReportFileSysFatalError(fmt.Errorf("workspace dir [%s] is in third party sync dir", WorkspaceDir))
+		return
+	}
+
+	dir := filepath.Join(DataDir, fileSysStatusCheckFile)
+	if err := os.RemoveAll(dir); nil != err {
+		ReportFileSysFatalError(err)
+		return
+	}
+
+	if err := os.MkdirAll(dir, 0755); nil != err {
+		ReportFileSysFatalError(err)
+		return
+	}
+
+	for i := 0; i < 7; i++ {
+		tmp := filepath.Join(dir, "check_consistency")
+		data := make([]byte, 1024*4)
+		_, err := rand.Read(data)
+		if nil != err {
 			ReportFileSysFatalError(err)
 			return
 		}
 
-		if err := os.MkdirAll(dir, 0755); nil != err {
+		if err = os.WriteFile(tmp, data, 0644); nil != err {
 			ReportFileSysFatalError(err)
 			return
 		}
 
-		for i := 0; i < 7; i++ {
-			tmp := filepath.Join(dir, "check_consistency")
-			data := make([]byte, 1024*4)
-			_, err := rand.Read(data)
+		time.Sleep(5 * time.Second)
+
+		for j := 0; j < 32; j++ {
+			renamed := tmp + "_renamed"
+			if err = os.Rename(tmp, renamed); nil != err {
+				ReportFileSysFatalError(err)
+				break
+			}
+
+			time.Sleep(time.Second)
+
+			f, err := os.Open(renamed)
 			if nil != err {
 				ReportFileSysFatalError(err)
 				return
 			}
 
-			if err = os.WriteFile(tmp, data, 0644); nil != err {
+			if err = f.Close(); nil != err {
 				ReportFileSysFatalError(err)
 				return
 			}
 
-			time.Sleep(5 * time.Second)
-
-			for j := 0; j < 32; j++ {
-				renamed := tmp + "_renamed"
-				if err = os.Rename(tmp, renamed); nil != err {
-					ReportFileSysFatalError(err)
-					break
-				}
-
-				time.Sleep(time.Second)
-
-				f, err := os.Open(renamed)
-				if nil != err {
-					ReportFileSysFatalError(err)
-					return
-				}
-
-				if err = f.Close(); nil != err {
-					ReportFileSysFatalError(err)
-					return
-				}
-
-				if err = os.Rename(renamed, tmp); nil != err {
-					ReportFileSysFatalError(err)
-					return
-				}
+			if err = os.Rename(renamed, tmp); nil != err {
+				ReportFileSysFatalError(err)
+				return
+			}
 
-				entries, err := os.ReadDir(dir)
-				if nil != err {
-					ReportFileSysFatalError(err)
-					return
-				}
+			entries, err := os.ReadDir(dir)
+			if nil != err {
+				ReportFileSysFatalError(err)
+				return
+			}
 
-				checkFilenames := bytes.Buffer{}
-				for _, entry := range entries {
-					if !entry.IsDir() && strings.Contains(entry.Name(), "check_") {
-						checkFilenames.WriteString(entry.Name())
-						checkFilenames.WriteString("\n")
-					}
-				}
-				lines := strings.Split(strings.TrimSpace(checkFilenames.String()), "\n")
-				if 1 < len(lines) {
-					buf := bytes.Buffer{}
-					for _, line := range lines {
-						buf.WriteString("  ")
-						buf.WriteString(line)
-						buf.WriteString("\n")
-					}
-					output := buf.String()
-					ReportFileSysFatalError(fmt.Errorf("dir [%s] has more than 1 file:\n%s", dir, output))
-					return
+			checkFilenames := bytes.Buffer{}
+			for _, entry := range entries {
+				if !entry.IsDir() && strings.Contains(entry.Name(), "check_") {
+					checkFilenames.WriteString(entry.Name())
+					checkFilenames.WriteString("\n")
 				}
 			}
-
-			if err = os.RemoveAll(tmp); nil != err {
-				ReportFileSysFatalError(err)
+			lines := strings.Split(strings.TrimSpace(checkFilenames.String()), "\n")
+			if 1 < len(lines) {
+				buf := bytes.Buffer{}
+				for _, line := range lines {
+					buf.WriteString("  ")
+					buf.WriteString(line)
+					buf.WriteString("\n")
+				}
+				output := buf.String()
+				ReportFileSysFatalError(fmt.Errorf("dir [%s] has more than 1 file:\n%s", dir, output))
 				return
 			}
+		}
 
+		if err = os.RemoveAll(tmp); nil != err {
+			ReportFileSysFatalError(err)
+			return
 		}
 	}
 }

+ 1 - 1
kernel/util/working.go

@@ -397,7 +397,7 @@ func initPandoc() {
 	}
 
 	pandocZip := filepath.Join(WorkingDir, "pandoc.zip")
-	if "dev" == Mode {
+	if "dev" == Mode || !gulu.File.IsExist(pandocZip) {
 		if gulu.OS.IsWindows() {
 			pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-windows-amd64.zip")
 		} else if gulu.OS.IsDarwin() {