瀏覽代碼

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

# Conflicts:
#	app/appearance/langs/en_US.json
#	app/appearance/langs/es_ES.json
#	app/appearance/langs/fr_FR.json
#	app/appearance/langs/zh_CHT.json
#	app/appearance/langs/zh_CN.json
Vanessa 2 年之前
父節點
當前提交
cdae86d50b

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

@@ -1,4 +1,9 @@
 {
 {
+  "cardShowAnswer": "Show Answer",
+  "cardRatingAgain": "Again",
+  "cardRatingHard": "Hard",
+  "cardRatingGood": "Good",
+  "cardRatingEasy": "Easy",
   "pdfIsLoading": "PDF is loading, please try again later",
   "pdfIsLoading": "PDF is loading, please try again later",
   "addToDeck": "Add to Deck...",
   "addToDeck": "Add to Deck...",
   "quickMakeCard": "Quick make card",
   "quickMakeCard": "Quick make card",

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

@@ -1,5 +1,10 @@
 {
 {
   "pdfIsLoading": "El PDF se está cargando, inténtalo de nuevo más tarde",
   "pdfIsLoading": "El PDF se está cargando, inténtalo de nuevo más tarde",
+  "cardShowAnswer": "Afficher la réponse",
+  "cardRatingAgain": "Otra vez",
+  "cardRatingHard": "Difícil",
+  "cardRatingGood": "Bueno",
+  "cardRatingEasy": "Fácil",
   "addToDeck": "Agregar a la plataforma...",
   "addToDeck": "Agregar a la plataforma...",
   "quickMakeCard": "Tarjeta de creación rápida",
   "quickMakeCard": "Tarjeta de creación rápida",
   "allAttrs": "Todos los nombres de atributos y valores de atributos",
   "allAttrs": "Todos los nombres de atributos y valores de atributos",

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

@@ -1,4 +1,9 @@
 {
 {
+  "cardShowAnswer": "Afficher la réponse",
+  "cardRatingAgain": "Encore",
+  "cardRatingHard": "Difficile",
+  "cardRatingGood": "Bien",
+  "cardRatingEasy": "Facile",
   "pdfIsLoading": "Le PDF est en cours de chargement, veuillez réessayer plus tard",
   "pdfIsLoading": "Le PDF est en cours de chargement, veuillez réessayer plus tard",
   "addToDeck": "Ajouter au deck...",
   "addToDeck": "Ajouter au deck...",
   "quickMakeCard": "Carte de création rapide",
   "quickMakeCard": "Carte de création rapide",

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

@@ -1,5 +1,10 @@
 {
 {
   "pdfIsLoading": "PDF 正在加載中,請稍後再試",
   "pdfIsLoading": "PDF 正在加載中,請稍後再試",
+  "cardShowAnswer": "顯示答案",
+  "cardRatingAgain": "重來",
+  "cardRatingHard": "困難",
+  "cardRatingGood": "一般",
+  "cardRatingEasy": "輕鬆",
   "addToDeck": "添加到卡包...",
   "addToDeck": "添加到卡包...",
   "quickMakeCard": "快速制卡",
   "quickMakeCard": "快速制卡",
   "allAttrs": "所有屬性名和屬性值",
   "allAttrs": "所有屬性名和屬性值",

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

@@ -1,5 +1,10 @@
 {
 {
   "pdfIsLoading": "PDF 正在加载中,请稍后再试",
   "pdfIsLoading": "PDF 正在加载中,请稍后再试",
+  "cardShowAnswer": "显示答案",
+  "cardRatingAgain": "重来",
+  "cardRatingHard": "困难",
+  "cardRatingGood": "一般",
+  "cardRatingEasy": "轻松",
   "addToDeck": "添加到卡包...",
   "addToDeck": "添加到卡包...",
   "quickMakeCard": "快速制卡",
   "quickMakeCard": "快速制卡",
   "allAttrs": "所有属性名和属性值",
   "allAttrs": "所有属性名和属性值",

+ 1 - 1
app/src/assets/scss/business/_card.scss

@@ -24,7 +24,7 @@
       width: 90%;
       width: 90%;
 
 
       & > div {
       & > div {
-        font-size: 46px;
+        font-size: 32px;
         display: block;
         display: block;
         line-height: 46px;
         line-height: 46px;
         margin-bottom: 4px;
         margin-bottom: 4px;

+ 5 - 5
app/src/card/openCard.ts

@@ -54,35 +54,35 @@ export const openCardByData = (cardsData: ICard[], html = "") => {
         ${window.siyuan.languages.noDueCard}
         ${window.siyuan.languages.noDueCard}
     </div>
     </div>
     <div class="fn__flex card__action${blocks.length === 0 ? " fn__none" : ""}">
     <div class="fn__flex card__action${blocks.length === 0 ? " fn__none" : ""}">
-        <button data-type="-1" class="b3-button fn__flex-1">Show (S)</button>
+        <button data-type="-1" class="b3-button fn__flex-1">${window.siyuan.languages.cardShowAnswer} (S)</button>
     </div>
     </div>
     <div class="fn__flex card__action fn__none">
     <div class="fn__flex card__action fn__none">
         <div>
         <div>
             <span></span>
             <span></span>
             <button data-type="0" aria-label="1 / j" class="b3-button b3-button--error b3-tooltips__s b3-tooltips">
             <button data-type="0" aria-label="1 / j" class="b3-button b3-button--error b3-tooltips__s b3-tooltips">
                 <div>❌</div>
                 <div>❌</div>
-                Again
+                ${window.siyuan.languages.cardRatingAgain} (1)
             </button>
             </button>
         </div>
         </div>
         <div>
         <div>
             <span></span>
             <span></span>
             <button data-type="1" aria-label="2 / k" class="b3-button b3-button--warning b3-tooltips__s b3-tooltips">
             <button data-type="1" aria-label="2 / k" class="b3-button b3-button--warning b3-tooltips__s b3-tooltips">
                 <div>😬</div>
                 <div>😬</div>
-                Hard
+                ${window.siyuan.languages.cardRatingHard} (2)
             </button>
             </button>
         </div>
         </div>
         <div>
         <div>
             <span></span>
             <span></span>
             <button data-type="2" aria-label="3 / l" class="b3-button b3-button--info b3-tooltips__s b3-tooltips">
             <button data-type="2" aria-label="3 / l" class="b3-button b3-button--info b3-tooltips__s b3-tooltips">
                 <div>😊</div>
                 <div>😊</div>
-                Good
+                ${window.siyuan.languages.cardRatingGood} (3)
             </button>
             </button>
         </div>
         </div>
         <div>
         <div>
             <span></span>
             <span></span>
             <button data-type="3" aria-label="4 / ;" class="b3-button b3-button--success b3-tooltips__s b3-tooltips">
             <button data-type="3" aria-label="4 / ;" class="b3-button b3-button--success b3-tooltips__s b3-tooltips">
                 <div>🌈</div>
                 <div>🌈</div>
-                Easy
+                ${window.siyuan.languages.cardRatingEasy} (4)
             </button>
             </button>
         </div>
         </div>
     </div>
     </div>

+ 1 - 1
app/src/mobile/util/initFramework.ts

@@ -128,7 +128,7 @@ export const initFramework = () => {
         closePanel();
         closePanel();
     });
     });
     initEditorName();
     initEditorName();
-    if (window.siyuan.config.newbie) {
+    if (window.siyuan.config.openHelp) {
         mountHelp();
         mountHelp();
     }
     }
     const transactionTipElement = document.getElementById("transactionTip");
     const transactionTipElement = document.getElementById("transactionTip");

+ 1 - 1
app/src/types/index.d.ts

@@ -413,7 +413,7 @@ declare interface IConfig {
     api: {
     api: {
         token: string
         token: string
     }
     }
-    newbie: boolean
+    openHelp: boolean
     system: {
     system: {
         networkProxy: {
         networkProxy: {
             host: string
             host: string

+ 1 - 1
app/src/util/onGetConfig.ts

@@ -176,7 +176,7 @@ export const onGetConfig = (isStart: boolean) => {
             resizeDrag();
             resizeDrag();
         }, 200);
         }, 200);
     });
     });
-    if (window.siyuan.config.newbie) {
+    if (window.siyuan.config.openHelp) {
         mountHelp();
         mountHelp();
     }
     }
     addGA();
     addGA();

+ 12 - 2
kernel/api/extension.go

@@ -30,6 +30,7 @@ import (
 	"github.com/88250/lute"
 	"github.com/88250/lute"
 	"github.com/88250/lute/ast"
 	"github.com/88250/lute/ast"
 	"github.com/88250/lute/parse"
 	"github.com/88250/lute/parse"
+	"github.com/gabriel-vasile/mimetype"
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
 	"github.com/siyuan-note/filelock"
 	"github.com/siyuan-note/filelock"
 	"github.com/siyuan-note/logging"
 	"github.com/siyuan-note/logging"
@@ -80,7 +81,7 @@ func extensionCopy(c *gin.Context) {
 			continue
 			continue
 		}
 		}
 		fName := path.Base(u.Path)
 		fName := path.Base(u.Path)
-		fName = util.FilterUploadFileName(fName)
+
 		f, err := file[0].Open()
 		f, err := file[0].Open()
 		if nil != err {
 		if nil != err {
 			ret.Code = -1
 			ret.Code = -1
@@ -96,10 +97,19 @@ func extensionCopy(c *gin.Context) {
 		}
 		}
 
 
 		ext := path.Ext(fName)
 		ext := path.Ext(fName)
-		fName = fName[0 : len(fName)-len(ext)]
+		originalExt := ext
+		if "" == ext || strings.Contains(ext, "!") {
+			// 改进浏览器剪藏扩展转换本地图片后缀 https://github.com/siyuan-note/siyuan/issues/7467
+			if mtype := mimetype.Detect(data); nil != mtype {
+				ext = mtype.Extension()
+			}
+		}
 		if "" == ext && bytes.HasPrefix(data, []byte("<svg ")) && bytes.HasSuffix(data, []byte("</svg>")) {
 		if "" == ext && bytes.HasPrefix(data, []byte("<svg ")) && bytes.HasSuffix(data, []byte("</svg>")) {
 			ext = ".svg"
 			ext = ".svg"
 		}
 		}
+
+		fName = fName[0 : len(fName)-len(originalExt)]
+		fName = util.FilterUploadFileName(fName)
 		fName = fName + "-" + ast.NewNodeID() + ext
 		fName = fName + "-" + ast.NewNodeID() + ext
 		writePath := filepath.Join(assets, fName)
 		writePath := filepath.Join(assets, fName)
 		if err = filelock.WriteFile(writePath, data); nil != err {
 		if err = filelock.WriteFile(writePath, data); nil != err {

+ 5 - 1
kernel/api/format.go

@@ -35,7 +35,11 @@ func netImg2LocalAssets(c *gin.Context) {
 	}
 	}
 
 
 	id := arg["id"].(string)
 	id := arg["id"].(string)
-	err := model.NetImg2LocalAssets(id)
+	var url string
+	if urlArg := arg["url"]; nil != urlArg {
+		url = urlArg.(string)
+	}
+	err := model.NetImg2LocalAssets(id, url)
 	if nil != err {
 	if nil != err {
 		ret.Code = -1
 		ret.Code = -1
 		ret.Msg = err.Error()
 		ret.Msg = err.Error()

+ 1 - 1
kernel/go.mod

@@ -47,7 +47,7 @@ require (
 	github.com/siyuan-note/filelock v0.0.0-20230223100551-200cbe1cf84e
 	github.com/siyuan-note/filelock v0.0.0-20230223100551-200cbe1cf84e
 	github.com/siyuan-note/httpclient v0.0.0-20230223101139-409ed0b4c5ff
 	github.com/siyuan-note/httpclient v0.0.0-20230223101139-409ed0b4c5ff
 	github.com/siyuan-note/logging v0.0.0-20230223101545-ec2cbf198ffb
 	github.com/siyuan-note/logging v0.0.0-20230223101545-ec2cbf198ffb
-	github.com/siyuan-note/riff v0.0.0-20221228031102-17d458a1217b
+	github.com/siyuan-note/riff v0.0.0-20230224070227-4514ccc3e496
 	github.com/steambap/captcha v1.4.1
 	github.com/steambap/captcha v1.4.1
 	github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2
 	github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2
 	github.com/vmihailenco/msgpack/v5 v5.3.5
 	github.com/vmihailenco/msgpack/v5 v5.3.5

+ 2 - 4
kernel/go.sum

@@ -10,8 +10,6 @@ github.com/88250/gulu v1.2.3-0.20230223100136-26e5f16ac3c0 h1:hZn2F/kNKcxoK41Jhf
 github.com/88250/gulu v1.2.3-0.20230223100136-26e5f16ac3c0/go.mod h1:pTWnjt+6qUqNnP9xltswsJxgCBVu3C7eW09u48LWX0k=
 github.com/88250/gulu v1.2.3-0.20230223100136-26e5f16ac3c0/go.mod h1:pTWnjt+6qUqNnP9xltswsJxgCBVu3C7eW09u48LWX0k=
 github.com/88250/lute v1.7.6-0.20230223100349-d4c62da413ce h1:PGos/Sz/SRVDPzToUgn/SBttEsAO5livLUWzoI+/bZ4=
 github.com/88250/lute v1.7.6-0.20230223100349-d4c62da413ce h1:PGos/Sz/SRVDPzToUgn/SBttEsAO5livLUWzoI+/bZ4=
 github.com/88250/lute v1.7.6-0.20230223100349-d4c62da413ce/go.mod h1:+wUqx/1kdFDbWtxn9LYJlaCOAeol2pjSO6w+WJTVQsg=
 github.com/88250/lute v1.7.6-0.20230223100349-d4c62da413ce/go.mod h1:+wUqx/1kdFDbWtxn9LYJlaCOAeol2pjSO6w+WJTVQsg=
-github.com/88250/pdfcpu v0.3.14-0.20230223050947-68dec81c7661 h1:s8YOfk7TpajM8SBivP0ReIHmNfMQu20hWgEBc98D14w=
-github.com/88250/pdfcpu v0.3.14-0.20230223050947-68dec81c7661/go.mod h1:S5YT38L/GCjVjmB4PB84PymA1qfopjEhfhTNQilLpv4=
 github.com/88250/pdfcpu v0.3.14-0.20230224021324-e51076eb6390 h1:q2AR33VoQ87WYtvZ4pEvwj5gZkv22HK/yMlPWwF1oyc=
 github.com/88250/pdfcpu v0.3.14-0.20230224021324-e51076eb6390 h1:q2AR33VoQ87WYtvZ4pEvwj5gZkv22HK/yMlPWwF1oyc=
 github.com/88250/pdfcpu v0.3.14-0.20230224021324-e51076eb6390/go.mod h1:S5YT38L/GCjVjmB4PB84PymA1qfopjEhfhTNQilLpv4=
 github.com/88250/pdfcpu v0.3.14-0.20230224021324-e51076eb6390/go.mod h1:S5YT38L/GCjVjmB4PB84PymA1qfopjEhfhTNQilLpv4=
 github.com/88250/vitess-sqlparser v0.0.0-20210205111146-56a2ded2aba1 h1:48T899JQDwyyRu9yXHePYlPdHtpJfrJEUGBMH3SMBWY=
 github.com/88250/vitess-sqlparser v0.0.0-20210205111146-56a2ded2aba1 h1:48T899JQDwyyRu9yXHePYlPdHtpJfrJEUGBMH3SMBWY=
@@ -288,8 +286,8 @@ github.com/siyuan-note/httpclient v0.0.0-20230223101139-409ed0b4c5ff h1:3G48J/tG
 github.com/siyuan-note/httpclient v0.0.0-20230223101139-409ed0b4c5ff/go.mod h1:/fjYEiYPN2ZNR2zVTopobwzo3rOychV2qbsutxiV0jI=
 github.com/siyuan-note/httpclient v0.0.0-20230223101139-409ed0b4c5ff/go.mod h1:/fjYEiYPN2ZNR2zVTopobwzo3rOychV2qbsutxiV0jI=
 github.com/siyuan-note/logging v0.0.0-20230223101545-ec2cbf198ffb h1:qzz7ZQw7/tHJd1IST+8UymXFF8RacokMLD7VZgyS+ww=
 github.com/siyuan-note/logging v0.0.0-20230223101545-ec2cbf198ffb h1:qzz7ZQw7/tHJd1IST+8UymXFF8RacokMLD7VZgyS+ww=
 github.com/siyuan-note/logging v0.0.0-20230223101545-ec2cbf198ffb/go.mod h1:6mRFtAAvYPn3cDzqvyv+t8BVPGqpONDMMb5ywOhY1D4=
 github.com/siyuan-note/logging v0.0.0-20230223101545-ec2cbf198ffb/go.mod h1:6mRFtAAvYPn3cDzqvyv+t8BVPGqpONDMMb5ywOhY1D4=
-github.com/siyuan-note/riff v0.0.0-20221228031102-17d458a1217b h1:JDpKOdiyocNsgKFfrF3mB7UoBJz4qcHBUKBig78kVjc=
-github.com/siyuan-note/riff v0.0.0-20221228031102-17d458a1217b/go.mod h1:WnNt0JPjfXp2fjAgbF9rS5W7JC2W0YVcaVmLXIeYF8A=
+github.com/siyuan-note/riff v0.0.0-20230224070227-4514ccc3e496 h1:6u9vlE4EhRja4abccUPGNmG+aMBm/D+5lVomkoYuSmo=
+github.com/siyuan-note/riff v0.0.0-20230224070227-4514ccc3e496/go.mod h1:XJtLlKCr8cZE+lzykM4edHHih92M9M50UNw/nDLYRN8=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY=
 github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY=

+ 2 - 1
kernel/model/assets.go

@@ -70,7 +70,7 @@ func DocImageAssets(rootID string) (ret []string, err error) {
 	return
 	return
 }
 }
 
 
-func NetImg2LocalAssets(rootID string) (err error) {
+func NetImg2LocalAssets(rootID, originalURL string) (err error) {
 	tree, err := loadTreeByBlockID(rootID)
 	tree, err := loadTreeByBlockID(rootID)
 	if nil != err {
 	if nil != err {
 		return
 		return
@@ -138,6 +138,7 @@ func NetImg2LocalAssets(rootID string) (err error) {
 				}
 				}
 				util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(119), u), 15000)
 				util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(119), u), 15000)
 				request := httpclient.NewBrowserRequest()
 				request := httpclient.NewBrowserRequest()
+				request.SetHeader("Referer", originalURL) // 改进浏览器剪藏扩展转换本地图片成功率 https://github.com/siyuan-note/siyuan/issues/7464
 				resp, reqErr := request.Get(u)
 				resp, reqErr := request.Get(u)
 				if nil != reqErr {
 				if nil != reqErr {
 					logging.LogErrorf("download net img [%s] failed: %s", u, reqErr)
 					logging.LogErrorf("download net img [%s] failed: %s", u, reqErr)

+ 3 - 2
kernel/model/conf.go

@@ -70,7 +70,7 @@ type AppConf struct {
 	Stat           *conf.Stat       `json:"stat"`           // 统计
 	Stat           *conf.Stat       `json:"stat"`           // 统计
 	Api            *conf.API        `json:"api"`            // API
 	Api            *conf.API        `json:"api"`            // API
 	Repo           *conf.Repo       `json:"repo"`           // 数据仓库
 	Repo           *conf.Repo       `json:"repo"`           // 数据仓库
-	Newbie         bool             `json:"newbie"`         // 是否是安装后第一次启动
+	OpenHelp       bool             `json:"openHelp"`       // 启动后是否需要打开用户指南
 }
 }
 
 
 func InitConf() {
 func InitConf() {
@@ -211,7 +211,9 @@ func InitConf() {
 	}
 	}
 	if nil == Conf.System {
 	if nil == Conf.System {
 		Conf.System = conf.NewSystem()
 		Conf.System = conf.NewSystem()
+		Conf.OpenHelp = true
 	} else {
 	} else {
+		Conf.OpenHelp = Conf.System.KernelVersion != util.Ver
 		Conf.System.KernelVersion = util.Ver
 		Conf.System.KernelVersion = util.Ver
 		Conf.System.IsInsider = util.IsInsider
 		Conf.System.IsInsider = util.IsInsider
 	}
 	}
@@ -237,7 +239,6 @@ func InitConf() {
 	}
 	}
 	Conf.System.OS = runtime.GOOS
 	Conf.System.OS = runtime.GOOS
 	Conf.System.OSPlatform, _ = util.GetOSPlatform()
 	Conf.System.OSPlatform, _ = util.GetOSPlatform()
-	Conf.Newbie = util.IsNewbie
 
 
 	if "" != Conf.UserData {
 	if "" != Conf.UserData {
 		Conf.User = loadUserFromConf()
 		Conf.User = loadUserFromConf()

+ 1 - 27
kernel/model/file.go

@@ -19,7 +19,6 @@ package model
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"math"
 	"os"
 	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
@@ -93,35 +92,10 @@ func (box *Box) docFromFileInfo(fileInfo *FileInfo, ial map[string]string) (ret
 	}
 	}
 
 
 	ret.Mtime = mTime.Unix()
 	ret.Mtime = mTime.Unix()
-	ret.HMtime = HumanizeTime(mTime)
+	ret.HMtime = util.HumanizeTime(mTime, Conf.Lang)
 	return
 	return
 }
 }
 
 
-func HumanizeTime(then time.Time) string {
-	labels := util.TimeLangs[Conf.Lang]
-
-	defaultMagnitudes := []humanize.RelTimeMagnitude{
-		{time.Second, labels["now"].(string), time.Second},
-		{2 * time.Second, labels["1s"].(string), 1},
-		{time.Minute, labels["xs"].(string), time.Second},
-		{2 * time.Minute, labels["1m"].(string), 1},
-		{time.Hour, labels["xm"].(string), time.Minute},
-		{2 * time.Hour, labels["1h"].(string), 1},
-		{humanize.Day, labels["xh"].(string), time.Hour},
-		{2 * humanize.Day, labels["1d"].(string), 1},
-		{humanize.Week, labels["xd"].(string), humanize.Day},
-		{2 * humanize.Week, labels["1w"].(string), 1},
-		{humanize.Month, labels["xw"].(string), humanize.Week},
-		{2 * humanize.Month, labels["1M"].(string), 1},
-		{humanize.Year, labels["xM"].(string), humanize.Month},
-		{18 * humanize.Month, labels["1y"].(string), 1},
-		{2 * humanize.Year, labels["2y"].(string), 1},
-		{humanize.LongTime, labels["xy"].(string), humanize.Year},
-		{math.MaxInt64, labels["max"].(string), 1},
-	}
-	return humanize.CustomRelTime(then, time.Now(), labels["albl"].(string), labels["blbl"].(string), defaultMagnitudes)
-}
-
 func (box *Box) docIAL(p string) (ret map[string]string) {
 func (box *Box) docIAL(p string) (ret map[string]string) {
 	name := strings.ToLower(filepath.Base(p))
 	name := strings.ToLower(filepath.Base(p))
 	if !strings.HasSuffix(name, ".sy") {
 	if !strings.HasSuffix(name, ".sy") {

+ 44 - 6
kernel/model/flashcard.go

@@ -29,7 +29,6 @@ import (
 	"github.com/88250/gulu"
 	"github.com/88250/gulu"
 	"github.com/88250/lute/ast"
 	"github.com/88250/lute/ast"
 	"github.com/88250/lute/parse"
 	"github.com/88250/lute/parse"
-	"github.com/dustin/go-humanize"
 	"github.com/siyuan-note/logging"
 	"github.com/siyuan-note/logging"
 	"github.com/siyuan-note/riff"
 	"github.com/siyuan-note/riff"
 	"github.com/siyuan-note/siyuan/kernel/cache"
 	"github.com/siyuan-note/siyuan/kernel/cache"
@@ -156,6 +155,9 @@ func GetFlashcards(deckID string, page int) (blocks []*Block, total, pageCount i
 	return
 	return
 }
 }
 
 
+// reviewCardCache <cardID, card> 用于复习时缓存卡片,以便支持撤销。
+var reviewCardCache = map[string]riff.Card{}
+
 func ReviewFlashcard(deckID string, blockID string, rating riff.Rating) (err error) {
 func ReviewFlashcard(deckID string, blockID string, rating riff.Rating) (err error) {
 	deckLock.Lock()
 	deckLock.Lock()
 	defer deckLock.Unlock()
 	defer deckLock.Unlock()
@@ -166,17 +168,39 @@ func ReviewFlashcard(deckID string, blockID string, rating riff.Rating) (err err
 	}
 	}
 
 
 	deck := Decks[deckID]
 	deck := Decks[deckID]
+	card := deck.GetCard(blockID)
+	if nil == card {
+		logging.LogErrorf("card not found [%s]", blockID)
+		return
+	}
+
+	if cachedCard := reviewCardCache[card.ID()]; nil != cachedCard {
+		// 命中缓存说明这张卡片已经复习过了,这次调用复习是撤销后再次复习
+		// 将缓存的卡片重新覆盖回卡包中,以恢复最开始复习前的状态
+		deck.SetCard(cachedCard)
+	} else {
+		// 首次复习该卡片,将卡片缓存以便后续支持撤销后再次复习
+		reviewCardCache[card.ID()] = card
+	}
+
 	deck.Review(blockID, rating)
 	deck.Review(blockID, rating)
 	err = deck.Save()
 	err = deck.Save()
 	if nil != err {
 	if nil != err {
 		logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
 		logging.LogErrorf("save deck [%s] failed: %s", deckID, err)
 		return
 		return
 	}
 	}
+
+	dueCards := getDueFlashcards(deckID)
+	if 1 > len(dueCards) {
+		// 该卡包中没有待复习的卡片了,说明最后一张卡片已经复习完了,清空撤销缓存
+		reviewCardCache = map[string]riff.Card{}
+	}
 	return
 	return
 }
 }
 
 
 type Flashcard struct {
 type Flashcard struct {
 	DeckID   string                 `json:"deckID"`
 	DeckID   string                 `json:"deckID"`
+	CardID   string                 `json:"cardID"`
 	BlockID  string                 `json:"blockID"`
 	BlockID  string                 `json:"blockID"`
 	NextDues map[riff.Rating]string `json:"nextDues"`
 	NextDues map[riff.Rating]string `json:"nextDues"`
 }
 }
@@ -207,11 +231,12 @@ func GetTreeDueFlashcards(rootID string) (ret []*Flashcard, err error) {
 
 
 		nextDues := map[riff.Rating]string{}
 		nextDues := map[riff.Rating]string{}
 		for rating, due := range card.NextDues() {
 		for rating, due := range card.NextDues() {
-			nextDues[rating] = strings.TrimSpace(humanize.RelTime(due, now, "", ""))
+			nextDues[rating] = strings.TrimSpace(util.HumanizeRelTime(due, now, Conf.Lang))
 		}
 		}
 
 
 		ret = append(ret, &Flashcard{
 		ret = append(ret, &Flashcard{
 			DeckID:   builtinDeckID,
 			DeckID:   builtinDeckID,
+			CardID:   card.ID(),
 			BlockID:  blockID,
 			BlockID:  blockID,
 			NextDues: nextDues,
 			NextDues: nextDues,
 		})
 		})
@@ -270,10 +295,21 @@ func GetDueFlashcards(deckID string) (ret []*Flashcard, err error) {
 	}
 	}
 
 
 	if "" == deckID {
 	if "" == deckID {
-		return getAllDueFlashcards()
+		ret = getAllDueFlashcards()
+		return
 	}
 	}
 
 
+	ret = getDueFlashcards(deckID)
+	return
+}
+
+func getDueFlashcards(deckID string) (ret []*Flashcard) {
 	deck := Decks[deckID]
 	deck := Decks[deckID]
+	if nil == deck {
+		logging.LogWarnf("deck not found [%s]", deckID)
+		return
+	}
+
 	cards := deck.Dues()
 	cards := deck.Dues()
 	now := time.Now()
 	now := time.Now()
 	for _, card := range cards {
 	for _, card := range cards {
@@ -285,11 +321,12 @@ func GetDueFlashcards(deckID string) (ret []*Flashcard, err error) {
 
 
 		nextDues := map[riff.Rating]string{}
 		nextDues := map[riff.Rating]string{}
 		for rating, due := range card.NextDues() {
 		for rating, due := range card.NextDues() {
-			nextDues[rating] = strings.TrimSpace(humanize.RelTime(due, now, "", ""))
+			nextDues[rating] = strings.TrimSpace(util.HumanizeRelTime(due, now, Conf.Lang))
 		}
 		}
 
 
 		ret = append(ret, &Flashcard{
 		ret = append(ret, &Flashcard{
 			DeckID:   deckID,
 			DeckID:   deckID,
+			CardID:   card.ID(),
 			BlockID:  blockID,
 			BlockID:  blockID,
 			NextDues: nextDues,
 			NextDues: nextDues,
 		})
 		})
@@ -300,7 +337,7 @@ func GetDueFlashcards(deckID string) (ret []*Flashcard, err error) {
 	return
 	return
 }
 }
 
 
-func getAllDueFlashcards() (ret []*Flashcard, err error) {
+func getAllDueFlashcards() (ret []*Flashcard) {
 	blockIDs := map[string]bool{}
 	blockIDs := map[string]bool{}
 	now := time.Now()
 	now := time.Now()
 	for _, deck := range Decks {
 	for _, deck := range Decks {
@@ -317,11 +354,12 @@ func getAllDueFlashcards() (ret []*Flashcard, err error) {
 
 
 			nextDues := map[riff.Rating]string{}
 			nextDues := map[riff.Rating]string{}
 			for rating, due := range card.NextDues() {
 			for rating, due := range card.NextDues() {
-				nextDues[rating] = strings.TrimSpace(humanize.RelTime(due, now, "", ""))
+				nextDues[rating] = strings.TrimSpace(util.HumanizeRelTime(due, now, Conf.Lang))
 			}
 			}
 
 
 			ret = append(ret, &Flashcard{
 			ret = append(ret, &Flashcard{
 				DeckID:   deck.ID,
 				DeckID:   deck.ID,
+				CardID:   card.ID(),
 				BlockID:  blockID,
 				BlockID:  blockID,
 				NextDues: nextDues,
 				NextDues: nextDues,
 			})
 			})

+ 2 - 2
kernel/model/mount.go

@@ -162,8 +162,8 @@ func Mount(boxID string) (alreadyMount bool, err error) {
 			box.SaveConf(boxConf)
 			box.SaveConf(boxConf)
 		}
 		}
 
 
-		if Conf.Newbie {
-			Conf.Newbie = false
+		if Conf.OpenHelp {
+			Conf.OpenHelp = false
 			Conf.Save()
 			Conf.Save()
 		}
 		}
 
 

+ 38 - 0
kernel/util/time.go

@@ -17,7 +17,11 @@
 package util
 package util
 
 
 import (
 import (
+	"math"
+	"strings"
 	"time"
 	"time"
+
+	"github.com/dustin/go-humanize"
 )
 )
 
 
 func Millisecond2Time(t int64) time.Time {
 func Millisecond2Time(t int64) time.Time {
@@ -33,3 +37,37 @@ func CurrentTimeMillis() int64 {
 func CurrentTimeSecondsStr() string {
 func CurrentTimeSecondsStr() string {
 	return time.Now().Format("20060102150405")
 	return time.Now().Format("20060102150405")
 }
 }
+
+func HumanizeRelTime(a time.Time, b time.Time, lang string) string {
+	_, magnitudes := humanizeTimeMagnitudes(lang)
+	return strings.TrimSpace(humanize.CustomRelTime(a, b, "", "", magnitudes))
+}
+
+func HumanizeTime(then time.Time, lang string) string {
+	labels, magnitudes := humanizeTimeMagnitudes(lang)
+	return strings.TrimSpace(humanize.CustomRelTime(then, time.Now(), labels["albl"].(string), labels["blbl"].(string), magnitudes))
+}
+
+func humanizeTimeMagnitudes(lang string) (labels map[string]interface{}, magnitudes []humanize.RelTimeMagnitude) {
+	labels = TimeLangs[lang]
+	magnitudes = []humanize.RelTimeMagnitude{
+		{time.Second, labels["now"].(string), time.Second},
+		{2 * time.Second, labels["1s"].(string), 1},
+		{time.Minute, labels["xs"].(string), time.Second},
+		{2 * time.Minute, labels["1m"].(string), 1},
+		{time.Hour, labels["xm"].(string), time.Minute},
+		{2 * time.Hour, labels["1h"].(string), 1},
+		{humanize.Day, labels["xh"].(string), time.Hour},
+		{2 * humanize.Day, labels["1d"].(string), 1},
+		{humanize.Week, labels["xd"].(string), humanize.Day},
+		{2 * humanize.Week, labels["1w"].(string), 1},
+		{humanize.Month, labels["xw"].(string), humanize.Week},
+		{2 * humanize.Month, labels["1M"].(string), 1},
+		{humanize.Year, labels["xM"].(string), humanize.Month},
+		{18 * humanize.Month, labels["1y"].(string), 1},
+		{2 * humanize.Year, labels["2y"].(string), 1},
+		{humanize.LongTime, labels["xy"].(string), humanize.Year},
+		{math.MaxInt64, labels["max"].(string), 1},
+	}
+	return
+}

+ 0 - 3
kernel/util/working.go

@@ -180,15 +180,12 @@ var (
 	SnippetsPath   string        // 数据目录下的 snippets/ 路径
 	SnippetsPath   string        // 数据目录下的 snippets/ 路径
 
 
 	UIProcessIDs = sync.Map{} // UI 进程 ID
 	UIProcessIDs = sync.Map{} // UI 进程 ID
-
-	IsNewbie bool // 是否是第一次安装
 )
 )
 
 
 func initWorkspaceDir(workspaceArg string) {
 func initWorkspaceDir(workspaceArg string) {
 	userHomeConfDir := filepath.Join(HomeDir, ".config", "siyuan")
 	userHomeConfDir := filepath.Join(HomeDir, ".config", "siyuan")
 	workspaceConf := filepath.Join(userHomeConfDir, "workspace.json")
 	workspaceConf := filepath.Join(userHomeConfDir, "workspace.json")
 	if !gulu.File.IsExist(workspaceConf) {
 	if !gulu.File.IsExist(workspaceConf) {
-		IsNewbie = ContainerStd == Container // 只有桌面端需要设置新手标识,前端自动挂载帮助文档
 		if err := os.MkdirAll(userHomeConfDir, 0755); nil != err && !os.IsExist(err) {
 		if err := os.MkdirAll(userHomeConfDir, 0755); nil != err && !os.IsExist(err) {
 			log.Printf("create user home conf folder [%s] failed: %s", userHomeConfDir, err)
 			log.Printf("create user home conf folder [%s] failed: %s", userHomeConfDir, err)
 			os.Exit(ExitCodeCreateConfDirErr)
 			os.Exit(ExitCodeCreateConfDirErr)