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

This commit is contained in:
Vanessa 2025-07-25 19:43:26 +08:00
commit 5f91cffc4d
22 changed files with 169 additions and 64 deletions

View file

@ -1642,6 +1642,7 @@
"262": "غدًا",
"263": "التالي %d أيام",
"264": "الحقل [%s] فارغ",
"265": "خارج النطاق"
"265": "خارج النطاق",
"266": "Tesseract OCR غير مثبت أو غير مهيأ، يرجى الرجوع إلى دليل المستخدم - قسم ملفات الموارد لإجراء الإعداد"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Morgen",
"263": "Nächste %d Tage",
"264": "Das Feld [%s] ist leer",
"265": "Außerhalb des Bereichs"
"265": "Außerhalb des Bereichs",
"266": "Tesseract OCR ist nicht installiert oder konfiguriert, bitte lesen Sie das Benutzerhandbuch - Abschnitt Ressourcen-Dateien zur Konfiguration"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Tomorrow",
"263": "Next %d days",
"264": "Field [%s] is empty",
"265": "Out of range"
"265": "Out of range",
"266": "Tesseract OCR is not installed or configured, please refer to the User Guide - Assets section for configuration"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Mañana",
"263": "Próximos %d días",
"264": "El campo [%s] está vacío",
"265": "Fuera de rango"
"265": "Fuera de rango",
"266": "Tesseract OCR no está instalado o configurado, consulte la Guía del Usuario - Sección de archivos de recursos para la configuración"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Demain",
"263": "Les %d prochains jours",
"264": "Le champ [%s] est vide",
"265": "Hors de portée"
"265": "Hors de portée",
"266": "Tesseract OCR n'est pas installé ou configuré, veuillez consulter le Guide de l'utilisateur - Section des fichiers de ressources pour la configuration"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "מחר",
"263": "ה-%d ימים הבאים",
"264": "השדה [%s] ריק",
"265": "מחוץ לטווח"
"265": "מחוץ לטווח",
"266": "Tesseract OCR לא הותקן או הוגדר, אנא עיין במדריך למשתמש - פרק קבצי משאבים לצורך הגדרה"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Domani",
"263": "Prossimi %d giorni",
"264": "Il campo [%s] è vuoto",
"265": "Fuori intervallo"
"265": "Fuori intervallo",
"266": "Tesseract OCR non è installato o configurato, fare riferimento alla Guida utente - Sezione file di risorse per la configurazione"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "明日",
"263": "次の %d 日間",
"264": "フィールド [%s] の値が空です",
"265": "範囲外"
"265": "範囲外",
"266": "Tesseract OCR がインストールされていないか、設定されていません。ユーザーガイド - リソースファイルセクションを参照して設定してください"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Jutro",
"263": "Następne %d dni",
"264": "Pole [%s] jest puste",
"265": "Poza zakresem"
"265": "Poza zakresem",
"266": "Tesseract OCR nie jest zainstalowany lub skonfigurowany, zapoznaj się z Podręcznikiem użytkownika - Sekcja plików zasobów, aby skonfigurować"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Amanhã",
"263": "Próximos %d dias",
"264": "O campo [%s] está vazio",
"265": "Fora do intervalo"
"265": "Fora do intervalo",
"266": "Tesseract OCR não está instalado ou configurado, consulte o Guia do Usuário - Seção de Arquivos de Recursos para configuração"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "Завтра",
"263": "Следующие %d дней",
"264": "Поле [%s] имеет пустое значение",
"265": "Вне диапазона"
"265": "Вне диапазона",
"266": "Tesseract OCR не установлен или не настроен, пожалуйста, обратитесь к Руководству пользователя - Раздел ресурсов для настройки"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "明天",
"263": "未來 %d 天",
"264": "字段 [%s] 值為空",
"265": "不在範圍內"
"265": "不在範圍內",
"266": "Tesseract OCR 未安裝或未配置,請參考 使用者指南-資料文件 章節進行配置"
}
}

View file

@ -1642,6 +1642,7 @@
"262": "明天",
"263": "未来 %d 天",
"264": "字段 [%s] 值为空",
"265": "不在范围内"
"265": "不在范围内",
"266": "Tesseract OCR 未安装或未配置,请参考 用户指南-资源文件 章节进行配置"
}
}

View file

@ -153,7 +153,14 @@ func ocr(c *gin.Context) {
path := arg["path"].(string)
ocrJSON := util.OcrAsset(path)
ocrJSON, err := util.OcrAsset(path)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 7000}
return
}
ret.Data = map[string]interface{}{
"text": util.GetOcrJsonText(ocrJSON),
"ocrJSON": ocrJSON,

View file

@ -79,26 +79,27 @@ func GetKeyBlockValue(blockKeyValues []*KeyValues) (ret *Value) {
return
}
// KeyType 描述了属性视图属性字段的类型。
type KeyType string
const (
KeyTypeBlock KeyType = "block"
KeyTypeText KeyType = "text"
KeyTypeNumber KeyType = "number"
KeyTypeDate KeyType = "date"
KeyTypeSelect KeyType = "select"
KeyTypeMSelect KeyType = "mSelect"
KeyTypeURL KeyType = "url"
KeyTypeEmail KeyType = "email"
KeyTypePhone KeyType = "phone"
KeyTypeMAsset KeyType = "mAsset"
KeyTypeTemplate KeyType = "template"
KeyTypeCreated KeyType = "created"
KeyTypeUpdated KeyType = "updated"
KeyTypeCheckbox KeyType = "checkbox"
KeyTypeRelation KeyType = "relation"
KeyTypeRollup KeyType = "rollup"
KeyTypeLineNumber KeyType = "lineNumber"
KeyTypeBlock KeyType = "block" // 主键
KeyTypeText KeyType = "text" // 文本
KeyTypeNumber KeyType = "number" // 数字
KeyTypeDate KeyType = "date" // 日期
KeyTypeSelect KeyType = "select" // 单选
KeyTypeMSelect KeyType = "mSelect" // 多选
KeyTypeURL KeyType = "url" // URL
KeyTypeEmail KeyType = "email" // Email
KeyTypePhone KeyType = "phone" // 电话
KeyTypeMAsset KeyType = "mAsset" // 资源
KeyTypeTemplate KeyType = "template" // 模板
KeyTypeCreated KeyType = "created" // 创建时间
KeyTypeUpdated KeyType = "updated" // 更新时间
KeyTypeCheckbox KeyType = "checkbox" // 复选框
KeyTypeRelation KeyType = "relation" // 关联
KeyTypeRollup KeyType = "rollup" // 汇总
KeyTypeLineNumber KeyType = "lineNumber" // 行号
)
// Key 描述了属性视图属性字段的基础结构。
@ -191,14 +192,14 @@ type View struct {
Gallery *LayoutGallery `json:"gallery,omitempty"` // 卡片布局
ItemIDs []string `json:"itemIds,omitempty"` // 项目 ID 列表,用于维护所有项目
Group *ViewGroup `json:"group,omitempty"` // 分组规则
GroupUpdated int64 `json:"groupUpdated"` // 分组规则更新时间戳
Groups []*View `json:"groups,omitempty"` // 分组视图列表
GroupItemIDs []string `json:"groupItemIds,omitempty"` // 分组项目 ID 列表,用于维护分组中的所有项目
GroupCalc *GroupCalc `json:"groupCalc,omitempty"` // 分组计算规则
GroupName string `json:"groupName,omitempty"` // 分组名称
GroupFolded bool `json:"groupFolded"` // 分组是否折叠
GroupHidden bool `json:"groupHidden"` // 分组是否隐藏
Group *ViewGroup `json:"group,omitempty"` // 分组规则
GroupUpdated int64 `json:"groupUpdated"` // 分组规则更新时间戳
Groups []*View `json:"groups,omitempty"` // 分组视图列表
GroupItemIDs []string `json:"groupItemIds"` // 分组项目 ID 列表,用于维护分组中的所有项目
GroupCalc *GroupCalc `json:"groupCalc,omitempty"` // 分组计算规则
GroupName string `json:"groupName,omitempty"` // 分组名称
GroupFolded bool `json:"groupFolded"` // 分组是否折叠
GroupHidden int `json:"groupHidden"` // 分组是否隐藏0显示1空白隐藏2手动隐藏
}
// GroupCalc 描述了分组计算规则和结果的结构。
@ -289,7 +290,8 @@ type Viewable interface {
SetGroupFolded(folded bool)
// SetGroupHidden 设置分组是否隐藏。
SetGroupHidden(hidden bool)
// hidden 0显示1空白隐藏2手动隐藏
SetGroupHidden(hidden int)
}
func NewAttributeView(id string) (ret *AttributeView) {

View file

@ -64,11 +64,11 @@ type BaseInstance struct {
ShowIcon bool `json:"showIcon"` // 是否显示字段图标
WrapField bool `json:"wrapField"` // 是否换行字段内容
Groups []Viewable `json:"groups,omitempty"` // 分组实例列表
GroupCalc *GroupCalc `json:"groupCalc,omitempty"` // 分组计算规则和结果
GroupName string `json:"groupName,omitempty"` // 分组名称
GroupFolded bool `json:"groupFolded,omitempty"` // 分组是否折叠
GroupHidden bool `json:"groupHidden,omitempty"` // 分组是否隐藏
Groups []Viewable `json:"groups,omitempty"` // 分组实例列表
GroupCalc *GroupCalc `json:"groupCalc,omitempty"` // 分组计算规则和结果
GroupName string `json:"groupName,omitempty"` // 分组名称
GroupFolded bool `json:"groupFolded"` // 分组是否折叠
GroupHidden int `json:"groupHidden"` // 分组是否隐藏0显示1空白隐藏2手动隐藏
}
func NewViewBaseInstance(view *View) *BaseInstance {
@ -127,7 +127,7 @@ func (baseInstance *BaseInstance) SetGroupFolded(folded bool) {
baseInstance.GroupFolded = folded
}
func (baseInstance *BaseInstance) SetGroupHidden(hidden bool) {
func (baseInstance *BaseInstance) SetGroupHidden(hidden int) {
baseInstance.GroupHidden = hidden
}

View file

@ -67,7 +67,7 @@ func Sort(viewable Viewable, attrView *AttributeView) {
val := items[i].GetValues()[fieldIndexSort.Index]
if KeyTypeCheckbox == val.Type {
if block := item.GetBlockValue(); nil != block && block.IsEdited() {
// 如果主键编辑过,则选框也算作编辑过,参与排序 https://github.com/siyuan-note/siyuan/issues/11016
// 如果主键编辑过,则选框也算作编辑过,参与排序 https://github.com/siyuan-note/siyuan/issues/11016
editedValItems[item.GetID()] = true
break
}

View file

@ -210,7 +210,7 @@ func (value *Value) IsEdited() bool {
}
if KeyTypeCheckbox == value.Type {
// 勾选框不会为空,即使勾选框未勾选,也不算是空,所以不能用下面的 IsEmpty 判断,这里使用更新时间判断是否编辑过 https://github.com/siyuan-note/siyuan/issues/11016
// 复选框不会为空,即使复选框未勾选,也不算是空,所以不能用下面的 IsEmpty 判断,这里使用更新时间判断是否编辑过 https://github.com/siyuan-note/siyuan/issues/11016
return value.CreatedAt != value.UpdatedAt
}
@ -289,7 +289,7 @@ func (value *Value) IsEmpty() bool {
if nil == value.Checkbox {
return true
}
return false // 选框不会为空
return false // 选框不会为空
case KeyTypeRelation:
return 1 > len(value.Relation.Contents)
case KeyTypeRollup:

View file

@ -44,6 +44,51 @@ import (
"github.com/xrash/smetrics"
)
func (tx *Transaction) doSortAttrViewGroup(operation *Operation) (ret *TxErr) {
if err := sortAttributeViewGroup(operation.AvID, operation.BlockID, operation.PreviousID, operation.ID); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func sortAttributeViewGroup(avID, blockID, previousGroupID, groupID string) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return err
}
var group *av.View
var index, previousIndex int
for i, g := range view.Groups {
if g.ID == groupID {
group = g
index = i
break
}
}
if nil == group {
return
}
view.Groups = append(view.Groups[:index], view.Groups[index+1:]...)
for i, g := range group.Groups {
if g.ID == previousGroupID {
previousIndex = i + 1
break
}
}
view.Groups = util.InsertElem(view.Groups, previousIndex, group)
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doRemoveAttrViewGroup(operation *Operation) (ret *TxErr) {
if err := removeAttributeViewGroup(operation.AvID, operation.BlockID); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
@ -121,13 +166,13 @@ func syncAttrViewTableColWidth(operation *Operation) (err error) {
}
func (tx *Transaction) doHideAttrViewGroup(operation *Operation) (ret *TxErr) {
if err := hideAttributeViewGroup(operation.AvID, operation.BlockID, operation.ID, operation.Data.(bool)); nil != err {
if err := hideAttributeViewGroup(operation.AvID, operation.BlockID, operation.ID, int(operation.Data.(float64))); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func hideAttributeViewGroup(avID, blockID, groupID string, hidden bool) (err error) {
func hideAttributeViewGroup(avID, blockID, groupID string, hidden int) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return err
@ -186,16 +231,29 @@ func SetAttributeViewGroup(avID, blockID string, group *av.ViewGroup) (err error
return err
}
oldHideEmpty := false
if nil != view.Group {
oldHideEmpty = view.Group.HideEmpty
}
view.Group = group
for _, g := range view.Groups {
if group.HideEmpty {
g.GroupHidden = true
} else {
g.GroupHidden = false
genAttrViewViewGroups(view, attrView)
if view.Group.HideEmpty != oldHideEmpty {
for _, g := range view.Groups {
if view.Group.HideEmpty {
if 2 != g.GroupHidden && 1 > len(g.GroupItemIDs) {
g.GroupHidden = 1
}
} else {
if 2 != g.GroupHidden {
g.GroupHidden = 0
}
}
}
}
genAttrViewViewGroups(view, attrView)
err = av.SaveAttributeView(attrView)
return
}
@ -1414,6 +1472,7 @@ func renderAttributeView(attrView *av.AttributeView, blockID, viewID, query stri
updatedDate := time.UnixMilli(view.GroupUpdated).Format("2006-01-02")
if time.Now().Format("2006-01-02") != updatedDate {
genAttrViewViewGroups(view, attrView)
av.SaveAttributeView(attrView)
}
for _, groupView := range view.Groups {
@ -1476,7 +1535,10 @@ func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
}
// 如果是按日期分组,则需要记录每个分组视图的一些状态字段,以便后面重新计算分组后可以恢复这些状态
type GroupState struct{ Folded, Hidden bool }
type GroupState struct {
Folded bool
Hidden int
}
groupStates := map[string]*GroupState{}
if isGroupByDate(view) {
for _, groupView := range view.Groups {
@ -1598,6 +1660,14 @@ func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
groupItemsMap[groupName] = append(groupItemsMap[groupName], item)
}
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
for _, o := range groupKey.Options {
if _, ok := groupItemsMap[o.Name]; !ok {
groupItemsMap[o.Name] = []av.Item{}
}
}
}
for name, groupItems := range groupItemsMap {
var v *av.View
switch view.LayoutType {
@ -1611,6 +1681,8 @@ func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
logging.LogWarnf("unknown layout type [%s] for group view", view.LayoutType)
return
}
v.GroupItemIDs = []string{}
for _, item := range groupItems {
v.GroupItemIDs = append(v.GroupItemIDs, item.GetID())
}
@ -1636,7 +1708,14 @@ func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
}
}
av.SaveAttributeView(attrView)
if av.GroupOrderMan != view.Group.Order {
sort.SliceStable(view.Groups, func(i, j int) bool {
if av.GroupOrderAsc == view.Group.Order {
return view.Groups[i].Name < view.Groups[j].Name
}
return view.Groups[i].Name > view.Groups[j].Name
})
}
}
func isGroupByDate(view *av.View) bool {
@ -2100,9 +2179,6 @@ func (tx *Transaction) doSortAttrViewView(operation *Operation) (ret *TxErr) {
break
}
}
if nil == view {
return
}
attrView.Views = append(attrView.Views[:index], attrView.Views[index+1:]...)
for i, v := range attrView.Views {

View file

@ -305,6 +305,8 @@ func performTx(tx *Transaction) (ret *TxErr) {
ret = tx.doSyncAttrViewTableColWidth(op)
case "removeAttrViewGroup":
ret = tx.doRemoveAttrViewGroup(op)
case "sortAttrViewGroup":
ret = tx.doSortAttrViewGroup(op)
}
if nil != ret {

View file

@ -202,7 +202,7 @@ func generateAttrViewItems(attrView *av.AttributeView, view *av.View) (ret map[s
}
// 如果是分组视图,则需要过滤掉不在分组中的项目
if 0 < len(view.GroupItemIDs) {
if nil != view.GroupItemIDs {
tmp := map[string][]*av.KeyValues{}
for _, groupItemID := range view.GroupItemIDs {
if _, ok := ret[groupItemID]; ok {

View file

@ -149,7 +149,12 @@ func ExistsAssetText(asset string) (ret bool) {
return
}
func OcrAsset(asset string) (ret []map[string]interface{}) {
func OcrAsset(asset string) (ret []map[string]interface{}, err error) {
if !TesseractEnabled {
err = fmt.Errorf(Langs[Lang][266])
return
}
assetsPath := GetDataAssetsAbsPath()
assetAbsPath := strings.TrimPrefix(asset, "assets")
assetAbsPath = filepath.Join(assetsPath, assetAbsPath)