siyuan/kernel/av/av.go

378 lines
9.5 KiB
Go
Raw Normal View History

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Package av 包含了属性视图Attribute View相关的实现。
package av
import (
2023-07-11 22:11:15 +08:00
"errors"
"fmt"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
)
// AttributeView 描述了属性视图的结构。
type AttributeView struct {
2023-07-12 21:39:55 +08:00
Spec int `json:"spec"` // 格式版本
ID string `json:"id"` // 属性视图 ID
Name string `json:"name"` // 属性视图名称
KeyValues []*KeyValues `json:"keyValues"` // 属性视图属性列值
ViewID string `json:"viewID"` // 当前视图 ID
Views []*View `json:"views"` // 视图
2023-07-12 19:10:05 +08:00
}
// KeyValues 描述了属性视图属性列值的结构。
type KeyValues struct {
2023-07-12 23:52:25 +08:00
Key *Key `json:"key"` // 属性视图属性列
Values []*Value `json:"values,omitempty"` // 属性视图属性列值
2023-07-12 19:10:05 +08:00
}
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"
2023-07-12 19:10:05 +08:00
)
// Key 描述了属性视图属性列的基础结构。
type Key struct {
ID string `json:"id"` // 列 ID
Name string `json:"name"` // 列名
Type KeyType `json:"type"` // 列类型
Icon string `json:"icon"` // 列图标
// 以下是某些列类型的特有属性
2023-07-12 23:52:25 +08:00
Options []*KeySelectOption `json:"options,omitempty"` // 选项列表
2023-07-12 19:10:05 +08:00
}
2023-07-17 11:21:46 +08:00
func NewKey(id, name string, keyType KeyType) *Key {
2023-07-12 19:10:05 +08:00
return &Key{
2023-07-17 11:21:46 +08:00
ID: id,
2023-07-12 19:10:05 +08:00
Name: name,
Type: keyType,
}
}
type KeySelectOption struct {
Name string `json:"name"`
Color string `json:"color"`
}
2023-07-12 19:10:05 +08:00
type Value struct {
2023-07-13 11:01:31 +08:00
ID string `json:"id,omitempty"`
KeyID string `json:"keyID,omitempty"`
BlockID string `json:"blockID,omitempty"`
Type KeyType `json:"type,omitempty"`
2023-07-12 19:10:05 +08:00
Block *ValueBlock `json:"block,omitempty"`
Text *ValueText `json:"text,omitempty"`
Number *ValueNumber `json:"number,omitempty"`
Date *ValueDate `json:"date,omitempty"`
MSelect []*ValueSelect `json:"mSelect,omitempty"`
URL *ValueURL `json:"url,omitempty"`
2023-07-12 19:10:05 +08:00
}
func (value *Value) ToJSONString() string {
data, err := gulu.JSON.MarshalJSON(value)
if nil != err {
return ""
}
return string(data)
}
type ValueBlock struct {
ID string `json:"id"`
Content string `json:"content"`
}
type ValueText struct {
Content string `json:"content"`
}
type ValueNumber struct {
Content float64 `json:"content"`
IsNotEmpty bool `json:"isNotEmpty"`
Format NumberFormat `json:"format"`
FormattedContent string `json:"formattedContent"`
}
type NumberFormat string
const (
NumberFormatNone NumberFormat = ""
NumberFormatPercent NumberFormat = "percent"
)
func NewValueNumber(content float64) *ValueNumber {
return &ValueNumber{
Content: content,
IsNotEmpty: true,
Format: NumberFormatNone,
FormattedContent: fmt.Sprintf("%f", content),
}
}
func NewFormattedValueNumber(content float64, format NumberFormat) (ret *ValueNumber) {
ret = &ValueNumber{
Content: content,
IsNotEmpty: true,
Format: format,
FormattedContent: fmt.Sprintf("%f", content),
}
switch format {
case NumberFormatNone:
s := fmt.Sprintf("%.5f", content)
ret.FormattedContent = strings.TrimRight(strings.TrimRight(s, "0"), ".")
case NumberFormatPercent:
s := fmt.Sprintf("%.2f", content*100)
ret.FormattedContent = strings.TrimRight(strings.TrimRight(s, "0"), ".") + "%"
}
return
}
func (number *ValueNumber) FormatNumber() {
switch number.Format {
case NumberFormatNone:
number.FormattedContent = strconv.FormatFloat(number.Content, 'f', -1, 64)
case NumberFormatPercent:
s := fmt.Sprintf("%.2f", number.Content*100)
number.FormattedContent = strings.TrimRight(strings.TrimRight(s, "0"), ".") + "%"
}
}
type ValueDate struct {
Content int64 `json:"content"`
Content2 int64 `json:"content2"`
HasEndDate bool `json:"hasEndDate"`
FormattedContent string `json:"formattedContent"`
}
type DateFormat string
const (
DateFormatNone DateFormat = ""
DateFormatDuration DateFormat = "duration"
)
func NewFormattedValueDate(content, content2 int64, format DateFormat) (ret *ValueDate) {
formatted := time.UnixMilli(content).Format("2006-01-02 15:04")
if 0 < content2 {
formatted += " → " + time.UnixMilli(content2).Format("2006-01-02 15:04")
}
switch format {
case DateFormatNone:
case DateFormatDuration:
t1 := time.UnixMilli(content)
t2 := time.UnixMilli(content2)
formatted = util.HumanizeRelTime(t1, t2, util.Lang)
}
ret = &ValueDate{
Content: content,
Content2: content2,
HasEndDate: false,
FormattedContent: formatted,
}
return
}
// RoundUp rounds like 12.3416 -> 12.35
func RoundUp(val float64, precision int) float64 {
return math.Ceil(val*(math.Pow10(precision))) / math.Pow10(precision)
}
// RoundDown rounds like 12.3496 -> 12.34
func RoundDown(val float64, precision int) float64 {
return math.Floor(val*(math.Pow10(precision))) / math.Pow10(precision)
}
// Round rounds to nearest like 12.3456 -> 12.35
func Round(val float64, precision int) float64 {
return math.Round(val*(math.Pow10(precision))) / math.Pow10(precision)
2023-07-12 19:10:05 +08:00
}
type ValueSelect struct {
Content string `json:"content"`
Color string `json:"color"`
}
type ValueURL struct {
Content string `json:"content"`
}
2023-07-11 19:45:27 +08:00
// View 描述了视图的结构。
type View struct {
2023-07-12 10:35:17 +08:00
ID string `json:"id"` // 视图 ID
Name string `json:"name"` // 视图名称
2023-07-11 19:45:27 +08:00
2023-07-12 21:39:55 +08:00
LayoutType LayoutType `json:"type"` // 当前布局类型
Table *LayoutTable `json:"table,omitempty"` // 表格布局
2023-07-11 19:45:27 +08:00
}
2023-07-12 10:35:17 +08:00
// LayoutType 描述了视图布局的类型。
type LayoutType string
const (
2023-07-12 10:35:17 +08:00
LayoutTypeTable LayoutType = "table" // 属性视图类型 - 表格
)
2023-07-11 23:40:05 +08:00
func NewView() *View {
name := "Table"
return &View{
2023-07-12 21:39:55 +08:00
ID: ast.NewNodeID(),
Name: name,
LayoutType: LayoutTypeTable,
2023-07-12 10:35:17 +08:00
Table: &LayoutTable{
2023-07-11 23:40:05 +08:00
Spec: 0,
2023-07-12 21:52:41 +08:00
ID: ast.NewNodeID(),
2023-07-11 23:40:05 +08:00
Filters: []*ViewFilter{},
Sorts: []*ViewSort{},
},
}
}
2023-07-11 19:45:27 +08:00
// Viewable 描述了视图的接口。
type Viewable interface {
Filterable
Sortable
Calculable
2023-07-12 10:35:17 +08:00
GetType() LayoutType
GetID() string
2023-07-11 19:45:27 +08:00
}
2023-07-12 19:55:18 +08:00
func NewAttributeView(id string) (ret *AttributeView) {
2023-07-11 23:40:05 +08:00
view := NewView()
2023-07-17 11:21:46 +08:00
key := NewKey(ast.NewNodeID(), "Block", KeyTypeBlock)
2023-07-12 19:55:18 +08:00
ret = &AttributeView{
2023-07-12 21:39:55 +08:00
Spec: 0,
ID: id,
KeyValues: []*KeyValues{{Key: key}},
ViewID: view.ID,
Views: []*View{view},
}
2023-07-12 19:55:18 +08:00
view.Table.Columns = []*ViewTableColumn{{ID: key.ID}}
return
}
func ParseAttributeView(avID string) (ret *AttributeView, err error) {
avJSONPath := getAttributeViewDataPath(avID)
2023-07-12 23:52:25 +08:00
toCreate := false
if !gulu.File.IsExist(avJSONPath) {
ret = NewAttributeView(avID)
2023-07-12 23:52:25 +08:00
toCreate = true
}
2023-07-12 23:52:25 +08:00
if !toCreate {
data, readErr := filelock.ReadFile(avJSONPath)
if nil != readErr {
logging.LogErrorf("read attribute view [%s] failed: %s", avID, readErr)
return
}
2023-07-12 23:52:25 +08:00
ret = &AttributeView{}
if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", avID, err)
return
}
} else {
if err = SaveAttributeView(ret); nil != err {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
}
return
}
func SaveAttributeView(av *AttributeView) (err error) {
data, err := gulu.JSON.MarshalIndentJSON(av, "", "\t") // TODO: single-line for production
if nil != err {
logging.LogErrorf("marshal attribute view [%s] failed: %s", av.ID, err)
return
}
avJSONPath := getAttributeViewDataPath(av.ID)
if err = filelock.WriteFile(avJSONPath, data); nil != err {
logging.LogErrorf("save attribute view [%s] failed: %s", av.ID, err)
return
}
return
}
2023-07-12 23:52:25 +08:00
func (av *AttributeView) GetView() (ret *View, err error) {
2023-07-11 22:11:15 +08:00
for _, v := range av.Views {
2023-07-12 23:52:25 +08:00
if v.ID == av.ViewID {
2023-07-11 22:11:15 +08:00
ret = v
return
}
}
2023-07-11 23:47:17 +08:00
err = ErrViewNotFound
2023-07-11 22:11:15 +08:00
return
}
2023-07-12 19:10:05 +08:00
func (av *AttributeView) GetKey(keyID string) (ret *Key, err error) {
for _, kv := range av.KeyValues {
if kv.Key.ID == keyID {
ret = kv.Key
return
}
}
err = ErrKeyNotFound
return
}
func (av *AttributeView) GetBlockKeyValues() (ret *KeyValues) {
for _, kv := range av.KeyValues {
if KeyTypeBlock == kv.Key.Type {
ret = kv
return
}
}
return
}
func getAttributeViewDataPath(avID string) (ret string) {
av := filepath.Join(util.DataDir, "storage", "av")
ret = filepath.Join(av, avID+".json")
if !gulu.File.IsDir(av) {
if err := os.MkdirAll(av, 0755); nil != err {
logging.LogErrorf("create attribute view dir failed: %s", err)
return
}
}
return
}
2023-07-11 22:11:15 +08:00
var (
ErrViewNotFound = errors.New("view not found")
2023-07-12 19:10:05 +08:00
ErrKeyNotFound = errors.New("key not found")
2023-07-11 22:11:15 +08:00
)