Compare commits
2 Commits
7aba27e5f9
...
66ac7194e2
Author | SHA1 | Date |
---|---|---|
tiny-craft | 66ac7194e2 | |
tiny-craft | ffc50a7fcc |
|
@ -17,6 +17,7 @@ import (
|
|||
"tinyrdm/backend/types"
|
||||
maputil "tinyrdm/backend/utils/map"
|
||||
redis2 "tinyrdm/backend/utils/redis"
|
||||
strutil "tinyrdm/backend/utils/string"
|
||||
)
|
||||
|
||||
type cmdHistoryItem struct {
|
||||
|
@ -476,7 +477,7 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str
|
|||
}
|
||||
|
||||
// GetKeyValue get value by key
|
||||
func (c *connectionService) GetKeyValue(connName string, db int, key string) (resp types.JSResp) {
|
||||
func (c *connectionService) GetKeyValue(connName string, db int, key, viewAs string) (resp types.JSResp) {
|
||||
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
|
@ -512,7 +513,9 @@ func (c *connectionService) GetKeyValue(connName string, db int, key string) (re
|
|||
var cursor uint64
|
||||
switch strings.ToLower(keyType) {
|
||||
case "string":
|
||||
value, err = rdb.Get(ctx, key).Result()
|
||||
var str string
|
||||
str, err = rdb.Get(ctx, key).Result()
|
||||
value, viewAs = strutil.ConvertTo(str, viewAs)
|
||||
size, _ = rdb.StrLen(ctx, key).Result()
|
||||
case "list":
|
||||
value, err = rdb.LRange(ctx, key, 0, -1).Result()
|
||||
|
@ -601,17 +604,18 @@ func (c *connectionService) GetKeyValue(connName string, db int, key string) (re
|
|||
}
|
||||
resp.Success = true
|
||||
resp.Data = map[string]any{
|
||||
"type": keyType,
|
||||
"ttl": ttl,
|
||||
"value": value,
|
||||
"size": size,
|
||||
"type": keyType,
|
||||
"ttl": ttl,
|
||||
"value": value,
|
||||
"size": size,
|
||||
"viewAs": viewAs,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetKeyValue set value by key
|
||||
// @param ttl <= 0 means keep current ttl
|
||||
func (c *connectionService) SetKeyValue(connName string, db int, key, keyType string, value any, ttl int64) (resp types.JSResp) {
|
||||
func (c *connectionService) SetKeyValue(connName string, db int, key, keyType string, value any, ttl int64, viewAs string) (resp types.JSResp) {
|
||||
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
|
@ -632,7 +636,12 @@ func (c *connectionService) SetKeyValue(connName string, db int, key, keyType st
|
|||
resp.Msg = "invalid string value"
|
||||
return
|
||||
} else {
|
||||
_, err = rdb.Set(ctx, key, str, 0).Result()
|
||||
var saveStr string
|
||||
if saveStr, err = strutil.SaveAs(str, viewAs); err != nil {
|
||||
resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error())
|
||||
return
|
||||
}
|
||||
_, err = rdb.Set(ctx, key, saveStr, 0).Result()
|
||||
// set expiration lonely, not "keepttl"
|
||||
if err == nil && expiration > 0 {
|
||||
rdb.Expire(ctx, key, expiration)
|
||||
|
|
|
@ -39,8 +39,8 @@ func (p *preferencesService) GetPreferences() (resp types.JSResp) {
|
|||
return
|
||||
}
|
||||
|
||||
func (p *preferencesService) SetPreferences(values map[string]any) (resp types.JSResp) {
|
||||
err := p.pref.SetPreferencesN(values)
|
||||
func (p *preferencesService) SetPreferences(pf types.Preferences) (resp types.JSResp) {
|
||||
err := p.pref.SetPreferences(&pf)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
|
@ -50,6 +50,16 @@ func (p *preferencesService) SetPreferences(values map[string]any) (resp types.J
|
|||
return
|
||||
}
|
||||
|
||||
func (p *preferencesService) UpdatePreferences(value map[string]any) (resp types.JSResp) {
|
||||
err := p.pref.UpdatePreferences(value)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
resp.Success = true
|
||||
return
|
||||
}
|
||||
|
||||
func (p *preferencesService) RestorePreferences() (resp types.JSResp) {
|
||||
defaultPref := p.pref.RestoreDefault()
|
||||
resp.Data = map[string]any{
|
||||
|
@ -103,23 +113,20 @@ func (p *preferencesService) GetAppVersion() (resp types.JSResp) {
|
|||
}
|
||||
|
||||
func (p *preferencesService) SaveWindowSize(width, height int) {
|
||||
p.SetPreferences(map[string]any{
|
||||
"behavior": map[string]any{
|
||||
"window_width": width,
|
||||
"window_height": height,
|
||||
},
|
||||
p.UpdatePreferences(map[string]any{
|
||||
"behavior.windowWidth": width,
|
||||
"behavior.windowHeight": height,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *preferencesService) GetWindowSize() (width, height int) {
|
||||
data := p.pref.GetPreferences()
|
||||
w, h := data["behavior.window_width"], data["behavior.window_height"]
|
||||
var ok bool
|
||||
if width, ok = w.(int); !ok {
|
||||
return consts.DEFAULT_WINDOW_WIDTH, consts.DEFAULT_WINDOW_HEIGHT
|
||||
width, height = data.Behavior.WindowWidth, data.Behavior.WindowHeight
|
||||
if width <= 0 {
|
||||
width = consts.DEFAULT_WINDOW_WIDTH
|
||||
}
|
||||
if height, ok = h.(int); !ok {
|
||||
return consts.DEFAULT_WINDOW_WIDTH, consts.DEFAULT_WINDOW_HEIGHT
|
||||
if height <= 0 {
|
||||
height = consts.DEFAULT_WINDOW_HEIGHT
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@ package storage
|
|||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"tinyrdm/backend/consts"
|
||||
"tinyrdm/backend/types"
|
||||
)
|
||||
|
||||
type PreferencesStorage struct {
|
||||
|
@ -19,30 +21,11 @@ func NewPreferences() *PreferencesStorage {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *PreferencesStorage) DefaultPreferences() map[string]any {
|
||||
return map[string]any{
|
||||
"behavior": map[string]any{
|
||||
"aside_width": consts.DEFAULT_ASIDE_WIDTH,
|
||||
"window_width": consts.DEFAULT_WINDOW_WIDTH,
|
||||
"window_height": consts.DEFAULT_WINDOW_HEIGHT,
|
||||
},
|
||||
"general": map[string]any{
|
||||
"language": "auto",
|
||||
"font": "",
|
||||
"font_size": consts.DEFAULT_FONT_SIZE,
|
||||
"use_sys_proxy": false,
|
||||
"use_sys_proxy_http": false,
|
||||
"check_update": true,
|
||||
"skip_version": "",
|
||||
},
|
||||
"editor": map[string]any{
|
||||
"font": "",
|
||||
"font_size": 14,
|
||||
},
|
||||
}
|
||||
func (p *PreferencesStorage) DefaultPreferences() types.Preferences {
|
||||
return types.NewPreferences()
|
||||
}
|
||||
|
||||
func (p *PreferencesStorage) getPreferences() (ret map[string]any) {
|
||||
func (p *PreferencesStorage) getPreferences() (ret types.Preferences) {
|
||||
b, err := p.storage.Load()
|
||||
if err != nil {
|
||||
ret = p.DefaultPreferences()
|
||||
|
@ -56,79 +39,43 @@ func (p *PreferencesStorage) getPreferences() (ret map[string]any) {
|
|||
return
|
||||
}
|
||||
|
||||
func (p *PreferencesStorage) flatPreferences(data map[string]any, prefix string) map[string]any {
|
||||
flattened := make(map[string]any)
|
||||
for key, value := range data {
|
||||
newKey := key
|
||||
if prefix != "" {
|
||||
newKey = prefix + "." + key
|
||||
}
|
||||
|
||||
if nested, ok := value.(map[string]any); ok {
|
||||
nestedFlattened := p.flatPreferences(nested, newKey)
|
||||
for k, v := range nestedFlattened {
|
||||
flattened[k] = v
|
||||
}
|
||||
} else {
|
||||
flattened[newKey] = value
|
||||
}
|
||||
}
|
||||
return flattened
|
||||
}
|
||||
|
||||
// GetPreferences Get preferences from local
|
||||
func (p *PreferencesStorage) GetPreferences() (ret map[string]any) {
|
||||
func (p *PreferencesStorage) GetPreferences() (ret types.Preferences) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
pref := p.getPreferences()
|
||||
ret = p.flatPreferences(pref, "")
|
||||
ret = p.getPreferences()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *PreferencesStorage) Value(keys ...string) any {
|
||||
kv := p.getPreferences()
|
||||
var ok bool
|
||||
var a any
|
||||
total := len(keys)
|
||||
for i, key := range keys {
|
||||
if a, ok = kv[key]; !ok {
|
||||
return nil
|
||||
}
|
||||
if i == total-1 {
|
||||
// last key, return value
|
||||
return a
|
||||
}
|
||||
if kv, ok = a.(map[string]any); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PreferencesStorage) setPreferences(pf map[string]any, key string, value any) error {
|
||||
keyPath := strings.Split(key, ".")
|
||||
if len(keyPath) <= 0 {
|
||||
return fmt.Errorf("invalid key path(%s)", key)
|
||||
}
|
||||
var node any = pf
|
||||
for _, k := range keyPath[:len(keyPath)-1] {
|
||||
if subNode, ok := node.(map[string]any); ok {
|
||||
node = subNode[k]
|
||||
func (p *PreferencesStorage) setPreferences(pf *types.Preferences, key string, value any) error {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) > 0 {
|
||||
var reflectValue reflect.Value
|
||||
if reflect.TypeOf(pf).Kind() == reflect.Ptr {
|
||||
reflectValue = reflect.ValueOf(pf).Elem()
|
||||
} else {
|
||||
return fmt.Errorf("invalid key path(%s)", key)
|
||||
reflectValue = reflect.ValueOf(pf)
|
||||
}
|
||||
for i, part := range parts {
|
||||
part = strings.ToUpper(part[:1]) + part[1:]
|
||||
reflectValue = reflectValue.FieldByName(part)
|
||||
if reflectValue.IsValid() {
|
||||
if i == len(parts)-1 {
|
||||
reflectValue.Set(reflect.ValueOf(value))
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subNode, ok := node.(map[string]any); ok {
|
||||
subNode[keyPath[len(keyPath)-1]] = value
|
||||
}
|
||||
|
||||
return nil
|
||||
return fmt.Errorf("invalid key path(%s)", key)
|
||||
}
|
||||
|
||||
func (p *PreferencesStorage) savePreferences(pf map[string]any) error {
|
||||
b, err := yaml.Marshal(&pf)
|
||||
func (p *PreferencesStorage) savePreferences(pf *types.Preferences) error {
|
||||
b, err := yaml.Marshal(pf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -139,35 +86,35 @@ func (p *PreferencesStorage) savePreferences(pf map[string]any) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetPreferences assign value to key path, the key path use "." to indicate multiple level
|
||||
func (p *PreferencesStorage) SetPreferences(key string, value any) error {
|
||||
// SetPreferences replace preferences
|
||||
func (p *PreferencesStorage) SetPreferences(pf *types.Preferences) error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
pf := p.getPreferences()
|
||||
if err := p.setPreferences(pf, key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.savePreferences(pf)
|
||||
}
|
||||
|
||||
// SetPreferencesN set multiple key path and value
|
||||
func (p *PreferencesStorage) SetPreferencesN(values map[string]any) error {
|
||||
// UpdatePreferences update values by key paths, the key path use "." to indicate multiple level
|
||||
func (p *PreferencesStorage) UpdatePreferences(values map[string]any) error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
pf := p.getPreferences()
|
||||
for path, v := range values {
|
||||
if err := p.setPreferences(pf, path, v); err != nil {
|
||||
if err := p.setPreferences(&pf, path, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Println("after save", pf)
|
||||
|
||||
return p.savePreferences(pf)
|
||||
return p.savePreferences(&pf)
|
||||
}
|
||||
|
||||
func (p *PreferencesStorage) RestoreDefault() map[string]any {
|
||||
func (p *PreferencesStorage) RestoreDefault() types.Preferences {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
pf := p.DefaultPreferences()
|
||||
p.savePreferences(pf)
|
||||
return p.flatPreferences(pf, "")
|
||||
p.savePreferences(&pf)
|
||||
return pf
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package types
|
||||
|
||||
import "tinyrdm/backend/consts"
|
||||
|
||||
type Preferences struct {
|
||||
Behavior PreferencesBehavior `json:"behavior" yaml:"behavior"`
|
||||
General PreferencesGeneral `json:"general" yaml:"general"`
|
||||
Editor PreferencesEditor `json:"editor" yaml:"editor"`
|
||||
}
|
||||
|
||||
func NewPreferences() Preferences {
|
||||
return Preferences{
|
||||
Behavior: PreferencesBehavior{
|
||||
AsideWidth: consts.DEFAULT_ASIDE_WIDTH,
|
||||
WindowWidth: consts.DEFAULT_WINDOW_WIDTH,
|
||||
WindowHeight: consts.DEFAULT_WINDOW_HEIGHT,
|
||||
},
|
||||
General: PreferencesGeneral{
|
||||
Theme: "auto",
|
||||
Language: "auto",
|
||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||
CheckUpdate: true,
|
||||
},
|
||||
Editor: PreferencesEditor{
|
||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type PreferencesBehavior struct {
|
||||
AsideWidth int `json:"asideWidth" yaml:"aside_width"`
|
||||
WindowWidth int `json:"windowWidth" yaml:"window_width"`
|
||||
WindowHeight int `json:"windowHeight" yaml:"window_height"`
|
||||
}
|
||||
|
||||
type PreferencesGeneral struct {
|
||||
Theme string `json:"theme" yaml:"theme"`
|
||||
Language string `json:"language" yaml:"language"`
|
||||
Font string `json:"font" yaml:"font,omitempty"`
|
||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||
UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"`
|
||||
UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"`
|
||||
CheckUpdate bool `json:"checkUpdate" yaml:"check_update"`
|
||||
SkipVersion string `json:"skipVersion" yaml:"skip_version,omitempty"`
|
||||
}
|
||||
|
||||
type PreferencesEditor struct {
|
||||
Font string `json:"font" yaml:"font,omitempty"`
|
||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package types
|
||||
|
||||
const PLAIN_TEXT = "Plain Text"
|
||||
const JSON = "JSON"
|
||||
const BASE64_TEXT = "Base64 Text"
|
||||
const BASE64_JSON = "Base64 JSON"
|
||||
const HEX = "Hex"
|
||||
const BINARY = "Binary"
|
||||
const GZIP = "GZip"
|
||||
const GZIP_JSON = "GZip JSON"
|
||||
const DEFLATE = "Deflate"
|
|
@ -0,0 +1,275 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"tinyrdm/backend/types"
|
||||
)
|
||||
|
||||
// ConvertTo convert string to specified type
|
||||
// @param targetType empty string indicates automatic detection of the string type
|
||||
func ConvertTo(str, targetType string) (value, resultType string) {
|
||||
if len(str) <= 0 {
|
||||
// empty content
|
||||
if len(targetType) <= 0 {
|
||||
resultType = types.PLAIN_TEXT
|
||||
} else {
|
||||
resultType = targetType
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch targetType {
|
||||
case types.PLAIN_TEXT:
|
||||
value = str
|
||||
resultType = targetType
|
||||
return
|
||||
|
||||
case types.JSON:
|
||||
value, _ = decodeJson(str)
|
||||
resultType = targetType
|
||||
return
|
||||
|
||||
case types.BASE64_TEXT, types.BASE64_JSON:
|
||||
if base64Str, ok := decodeBase64(str); ok {
|
||||
if targetType == types.BASE64_JSON {
|
||||
value, _ = decodeJson(base64Str)
|
||||
} else {
|
||||
value = base64Str
|
||||
}
|
||||
} else {
|
||||
value = str
|
||||
}
|
||||
resultType = targetType
|
||||
return
|
||||
|
||||
case types.HEX:
|
||||
if hexStr, ok := decodeHex(str); ok {
|
||||
log.Print(hexStr)
|
||||
value = hexStr
|
||||
} else {
|
||||
value = str
|
||||
}
|
||||
resultType = targetType
|
||||
return
|
||||
|
||||
case types.BINARY:
|
||||
var binary strings.Builder
|
||||
for _, char := range str {
|
||||
binary.WriteString(fmt.Sprintf("%08b", int(char)))
|
||||
}
|
||||
value = binary.String()
|
||||
resultType = targetType
|
||||
return
|
||||
|
||||
case types.GZIP, types.GZIP_JSON:
|
||||
if gzipStr, ok := decodeGZip(str); ok {
|
||||
if targetType == types.BASE64_JSON {
|
||||
value, _ = decodeJson(gzipStr)
|
||||
} else {
|
||||
value = gzipStr
|
||||
}
|
||||
} else {
|
||||
value = str
|
||||
}
|
||||
resultType = targetType
|
||||
return
|
||||
|
||||
case types.DEFLATE:
|
||||
value, _ = decodeDeflate(str)
|
||||
resultType = targetType
|
||||
return
|
||||
}
|
||||
|
||||
// type isn't specified or unknown, try to automatically detect and return converted value
|
||||
return autoToType(str)
|
||||
}
|
||||
|
||||
// attempt automatic convert to possible types
|
||||
// if no conversion is possible, it will return the origin string value and "plain text" type
|
||||
func autoToType(str string) (value, resultType string) {
|
||||
if len(str) > 0 {
|
||||
var ok bool
|
||||
if value, ok = decodeJson(str); ok {
|
||||
resultType = types.JSON
|
||||
return
|
||||
}
|
||||
|
||||
if value, ok = decodeBase64(str); ok {
|
||||
if value, ok = decodeJson(value); ok {
|
||||
resultType = types.BASE64_JSON
|
||||
return
|
||||
}
|
||||
resultType = types.BASE64_TEXT
|
||||
return
|
||||
}
|
||||
|
||||
if value, ok = decodeGZip(str); ok {
|
||||
resultType = types.GZIP
|
||||
return
|
||||
}
|
||||
|
||||
if value, ok = decodeDeflate(str); ok {
|
||||
resultType = types.DEFLATE
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
value = str
|
||||
resultType = types.PLAIN_TEXT
|
||||
return
|
||||
}
|
||||
|
||||
func decodeJson(str string) (string, bool) {
|
||||
var data any
|
||||
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
|
||||
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
|
||||
if err := json.Unmarshal([]byte(str), &data); err == nil {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.MarshalIndent(data, "", " "); err == nil {
|
||||
return string(jsonByte), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func decodeBase64(str string) (string, bool) {
|
||||
if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil {
|
||||
return string(decodedStr), true
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func decodeHex(str string) (string, bool) {
|
||||
encodeStr := hex.EncodeToString([]byte(str))
|
||||
var resultStr strings.Builder
|
||||
for i := 0; i < len(encodeStr); i += 2 {
|
||||
resultStr.WriteString("\\x")
|
||||
resultStr.WriteString(encodeStr[i : i+2])
|
||||
}
|
||||
return resultStr.String(), true
|
||||
}
|
||||
|
||||
func decodeGZip(str string) (string, bool) {
|
||||
if reader, err := gzip.NewReader(strings.NewReader(str)); err == nil {
|
||||
defer reader.Close()
|
||||
var decompressed []byte
|
||||
if decompressed, err = io.ReadAll(reader); err == nil {
|
||||
return string(decompressed), true
|
||||
}
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func decodeDeflate(str string) (string, bool) {
|
||||
reader := flate.NewReader(strings.NewReader(str))
|
||||
defer reader.Close()
|
||||
if decompressed, err := io.ReadAll(reader); err == nil {
|
||||
return string(decompressed), true
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func SaveAs(str, targetType string) (value string, err error) {
|
||||
switch targetType {
|
||||
case types.PLAIN_TEXT:
|
||||
return str, nil
|
||||
|
||||
case types.BASE64_TEXT:
|
||||
base64Str, _ := encodeBase64(str)
|
||||
return base64Str, nil
|
||||
|
||||
case types.JSON, types.BASE64_JSON, types.GZIP_JSON:
|
||||
if jsonStr, ok := encodeJson(str); ok {
|
||||
if targetType == types.BASE64_JSON {
|
||||
base64Str, _ := encodeBase64(jsonStr)
|
||||
return base64Str, nil
|
||||
} else {
|
||||
return jsonStr, nil
|
||||
}
|
||||
} else {
|
||||
return str, errors.New("invalid json")
|
||||
}
|
||||
|
||||
case types.GZIP:
|
||||
if gzipStr, ok := encodeGZip(str); ok {
|
||||
return gzipStr, nil
|
||||
} else {
|
||||
return str, errors.New("fail to build gzip data")
|
||||
}
|
||||
|
||||
case types.DEFLATE:
|
||||
if deflateStr, ok := encodeDeflate(str); ok {
|
||||
return deflateStr, nil
|
||||
} else {
|
||||
return str, errors.New("fail to build deflate data")
|
||||
}
|
||||
}
|
||||
return str, errors.New("fail to save with unknown error")
|
||||
}
|
||||
|
||||
func encodeJson(str string) (string, bool) {
|
||||
var data any
|
||||
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
|
||||
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
|
||||
if err := json.Unmarshal([]byte(str), &data); err == nil {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(data); err == nil {
|
||||
return string(jsonByte), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func encodeBase64(str string) (string, bool) {
|
||||
return base64.StdEncoding.EncodeToString([]byte(str)), true
|
||||
}
|
||||
|
||||
func encodeGZip(str string) (string, bool) {
|
||||
var compress = func(b []byte) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
writer := gzip.NewWriter(&buf)
|
||||
if _, err := writer.Write([]byte(str)); err != nil {
|
||||
writer.Close()
|
||||
return "", err
|
||||
}
|
||||
writer.Close()
|
||||
return string(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
if gzipStr, err := compress([]byte(str)); err == nil {
|
||||
return gzipStr, true
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func encodeDeflate(str string) (string, bool) {
|
||||
var compress = func(b []byte) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
writer, err := flate.NewWriter(&buf, flate.DefaultCompression)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = writer.Write([]byte(str)); err != nil {
|
||||
writer.Close()
|
||||
return "", err
|
||||
}
|
||||
writer.Close()
|
||||
return string(buf.Bytes()), nil
|
||||
}
|
||||
if deflateStr, err := compress([]byte(str)); err == nil {
|
||||
return deflateStr, true
|
||||
}
|
||||
return str, false
|
||||
}
|
|
@ -98,6 +98,7 @@ const tabContent = computed(() => {
|
|||
ttl: tab.ttl,
|
||||
value: tab.value,
|
||||
size: tab.size || 0,
|
||||
viewAs: tab.viewAs,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -122,7 +123,7 @@ const onReloadKey = async () => {
|
|||
if (tab == null || isEmpty(tab.key)) {
|
||||
return null
|
||||
}
|
||||
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key)
|
||||
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key, tab.viewAs)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -153,7 +154,8 @@ const onReloadKey = async () => {
|
|||
:name="tabContent.name"
|
||||
:ttl="tabContent.ttl"
|
||||
:value="tabContent.value"
|
||||
:size="tabContent.size" />
|
||||
:size="tabContent.size"
|
||||
:view-as="tabContent.viewAs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -8,12 +8,10 @@ import { useThemeVars } from 'naive-ui'
|
|||
import { types } from '@/consts/value_view_type.js'
|
||||
import Close from '@/components/icons/Close.vue'
|
||||
import Edit from '@/components/icons/Edit.vue'
|
||||
import { IsJson } from '@/utils/check_string_format.js'
|
||||
import { types as redisTypes } from '@/consts/support_redis_type.js'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { map, toLower } from 'lodash'
|
||||
import useConnectionStore from 'stores/connections.js'
|
||||
import { fromBase64, fromBase64Json, toBinary, toHex, toJsonText } from '@/utils/string_convert.js'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -28,6 +26,10 @@ const props = defineProps({
|
|||
},
|
||||
value: String,
|
||||
size: Number,
|
||||
viewAs: {
|
||||
type: String,
|
||||
default: types.PLAIN_TEXT,
|
||||
},
|
||||
})
|
||||
|
||||
const viewOption = computed(() =>
|
||||
|
@ -38,15 +40,15 @@ const viewOption = computed(() =>
|
|||
}
|
||||
}),
|
||||
)
|
||||
const viewAs = ref(types.PLAIN_TEXT)
|
||||
// const viewAs = ref(types.PLAIN_TEXT)
|
||||
|
||||
const autoDetectFormat = () => {
|
||||
// auto check format when loaded
|
||||
if (IsJson(props.value)) {
|
||||
viewAs.value = types.JSON
|
||||
} else {
|
||||
viewAs.value = types.PLAIN_TEXT
|
||||
}
|
||||
// if (IsJson(props.value)) {
|
||||
// viewAs.value = types.JSON
|
||||
// } else {
|
||||
// viewAs.value = types.PLAIN_TEXT
|
||||
// }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -60,44 +62,25 @@ watch(
|
|||
)
|
||||
|
||||
const keyType = redisTypes.STRING
|
||||
/**
|
||||
* view value
|
||||
* @type {ComputedRef<string>}
|
||||
*/
|
||||
const viewValue = computed(() => {
|
||||
switch (viewAs.value) {
|
||||
case types.PLAIN_TEXT:
|
||||
return props.value
|
||||
case types.JSON:
|
||||
return toJsonText(props.value)
|
||||
case types.BASE64_TO_TEXT:
|
||||
return fromBase64(props.value)
|
||||
case types.BASE64_TO_JSON:
|
||||
return fromBase64Json(props.value)
|
||||
case types.HEX:
|
||||
return toHex(props.value)
|
||||
case types.BINARY:
|
||||
return toBinary(props.value)
|
||||
default:
|
||||
return props.value
|
||||
}
|
||||
})
|
||||
|
||||
const viewLanguage = computed(() => {
|
||||
switch (viewAs.value) {
|
||||
switch (props.viewAs) {
|
||||
case types.JSON:
|
||||
case types.BASE64_TO_JSON:
|
||||
case types.BASE64_JSON:
|
||||
return 'json'
|
||||
default:
|
||||
return 'plaintext'
|
||||
}
|
||||
})
|
||||
|
||||
const onViewTypeUpdate = (viewType) => {
|
||||
connectionStore.loadKeyValue(props.name, props.db, props.keyPath, viewType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy value
|
||||
*/
|
||||
const onCopyValue = () => {
|
||||
ClipboardSetText(viewValue.value)
|
||||
ClipboardSetText(props.value)
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
$message.success(i18n.t('dialogue.copy_succ'))
|
||||
|
@ -111,7 +94,7 @@ const onCopyValue = () => {
|
|||
const editValue = ref('')
|
||||
const inEdit = ref(false)
|
||||
const onEditValue = () => {
|
||||
editValue.value = viewValue.value
|
||||
editValue.value = props.value
|
||||
inEdit.value = true
|
||||
}
|
||||
|
||||
|
@ -134,6 +117,7 @@ const onSaveValue = async () => {
|
|||
toLower(keyType),
|
||||
editValue.value,
|
||||
-1,
|
||||
props.viewAs,
|
||||
)
|
||||
if (success) {
|
||||
await connectionStore.loadKeyValue(props.name, props.db, props.keyPath)
|
||||
|
@ -155,7 +139,11 @@ const onSaveValue = async () => {
|
|||
<content-toolbar :db="props.db" :key-path="keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
|
||||
<div class="tb2 flex-box-h">
|
||||
<n-text>{{ $t('interface.view_as') }}</n-text>
|
||||
<n-select v-model:value="viewAs" :options="viewOption" style="width: 200px" />
|
||||
<n-select
|
||||
:value="props.viewAs"
|
||||
:options="viewOption"
|
||||
style="width: 200px"
|
||||
@update:value="onViewTypeUpdate" />
|
||||
<div class="flex-item-expand"></div>
|
||||
<n-button-group v-if="!inEdit">
|
||||
<n-button :focusable="false" @click="onCopyValue">
|
||||
|
@ -188,7 +176,7 @@ const onSaveValue = async () => {
|
|||
</div>
|
||||
<div class="value-wrapper flex-item-expand flex-box-v">
|
||||
<n-scrollbar v-if="!inEdit" class="flex-item-expand">
|
||||
<n-code :code="viewValue" :language="viewLanguage" show-line-numbers style="cursor: text" word-wrap />
|
||||
<n-code :code="props.value" :language="viewLanguage" show-line-numbers style="cursor: text" word-wrap />
|
||||
</n-scrollbar>
|
||||
<n-input
|
||||
v-else
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import { computed, h, reactive, ref, watch } from 'vue'
|
||||
import { types, typesColor } from '@/consts/support_redis_type.js'
|
||||
import { types as viewTypes } from '@/consts/value_view_type.js'
|
||||
import useDialog from 'stores/dialog'
|
||||
import { isEmpty, keys, map } from 'lodash'
|
||||
import NewStringValue from '@/components/new_value/NewStringValue.vue'
|
||||
|
@ -116,7 +117,15 @@ const onAdd = async () => {
|
|||
if (value == null) {
|
||||
value = defaultValue[type]
|
||||
}
|
||||
const { success, msg, nodeKey } = await connectionStore.setKey(server, db, key, type, value, ttl)
|
||||
const { success, msg, nodeKey } = await connectionStore.setKey(
|
||||
server,
|
||||
db,
|
||||
key,
|
||||
type,
|
||||
value,
|
||||
ttl,
|
||||
viewTypes.PLAIN_TEXT,
|
||||
)
|
||||
if (success) {
|
||||
// select current key
|
||||
tabStore.setSelectedKeys(server, nodeKey)
|
||||
|
|
|
@ -5,8 +5,11 @@
|
|||
export const types = {
|
||||
PLAIN_TEXT: 'Plain Text',
|
||||
JSON: 'JSON',
|
||||
BASE64_TO_TEXT: 'Base64 To Text',
|
||||
BASE64_TO_JSON: 'Base64 To JSON',
|
||||
BASE64_TEXT: 'Base64 Text',
|
||||
BASE64_JSON: 'Base64 JSON',
|
||||
HEX: 'Hex',
|
||||
BINARY: 'Binary',
|
||||
GZIP: 'GZip',
|
||||
GZIP_JSON: 'GZip JSON',
|
||||
DEFLATE: 'Deflate',
|
||||
}
|
||||
|
|
|
@ -525,14 +525,15 @@ const useConnectionStore = defineStore('connections', {
|
|||
* @param {string} server
|
||||
* @param {number} db
|
||||
* @param {string} [key] when key is null or blank, update tab to display normal content (blank content or server status)
|
||||
* @param {string} [viewType]
|
||||
*/
|
||||
async loadKeyValue(server, db, key) {
|
||||
async loadKeyValue(server, db, key, viewType) {
|
||||
try {
|
||||
const tab = useTabStore()
|
||||
if (!isEmpty(key)) {
|
||||
const { data, success, msg } = await GetKeyValue(server, db, key)
|
||||
const { data, success, msg } = await GetKeyValue(server, db, key, viewType)
|
||||
if (success) {
|
||||
const { type, ttl, value, size } = data
|
||||
const { type, ttl, value, size, viewAs } = data
|
||||
tab.upsertTab({
|
||||
server,
|
||||
db,
|
||||
|
@ -541,11 +542,16 @@ const useConnectionStore = defineStore('connections', {
|
|||
key,
|
||||
value,
|
||||
size,
|
||||
viewAs,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
if (!isEmpty(msg)) {
|
||||
$message.error('load key fail: ' + msg)
|
||||
}
|
||||
// its danger to delete "non-exists" key, just remove from tree view
|
||||
await this.deleteKey(server, db, key, true)
|
||||
// TODO: show key not found page?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -852,11 +858,12 @@ const useConnectionStore = defineStore('connections', {
|
|||
* @param {string} keyType
|
||||
* @param {any} value
|
||||
* @param {number} ttl
|
||||
* @param {string} [viewAs]
|
||||
* @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>}
|
||||
*/
|
||||
async setKey(connName, db, key, keyType, value, ttl) {
|
||||
async setKey(connName, db, key, keyType, value, ttl, viewAs) {
|
||||
try {
|
||||
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl)
|
||||
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl, viewAs)
|
||||
if (success) {
|
||||
// update tree view data
|
||||
const { newKey = 0 } = this._addKeyNodes(connName, db, [key], true)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { lang } from '@/langs/index.js'
|
||||
import { camelCase, clone, find, get, isEmpty, isObject, map, set, snakeCase, split } from 'lodash'
|
||||
import { clone, find, get, isEmpty, map, pick, set, split } from 'lodash'
|
||||
import {
|
||||
CheckForUpdate,
|
||||
GetFontList,
|
||||
|
@ -38,7 +38,7 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
},
|
||||
general: {
|
||||
theme: 'auto',
|
||||
language: 'en',
|
||||
language: 'auto',
|
||||
font: '',
|
||||
fontSize: 14,
|
||||
useSysProxy: false,
|
||||
|
@ -165,8 +165,7 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
actions: {
|
||||
_applyPreferences(data) {
|
||||
for (const key in data) {
|
||||
const keys = map(split(key, '.'), camelCase)
|
||||
set(this, keys, data[key])
|
||||
set(this, key, data[key])
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -203,30 +202,13 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async savePreferences() {
|
||||
const obj2Map = (prefix, obj) => {
|
||||
const result = {}
|
||||
for (const key in obj) {
|
||||
if (isObject(obj[key])) {
|
||||
const subResult = obj2Map(`${prefix}.${snakeCase(key)}`, obj[key])
|
||||
Object.assign(result, subResult)
|
||||
} else {
|
||||
result[`${prefix}.${snakeCase(key)}`] = obj[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
const pf = Object.assign(
|
||||
{},
|
||||
obj2Map('behavior', this.behavior),
|
||||
obj2Map('general', this.general),
|
||||
obj2Map('editor', this.editor),
|
||||
)
|
||||
const pf = pick(this, ['behavior', 'general', 'editor'])
|
||||
const { success, msg } = await SetPreferences(pf)
|
||||
return success === true
|
||||
},
|
||||
|
||||
/**
|
||||
* reset to last loaded preferences
|
||||
* reset to last-loaded preferences
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetToLastPreferences() {
|
||||
|
|
|
@ -85,8 +85,9 @@ const useTabStore = defineStore('tab', {
|
|||
* @param {string} [key]
|
||||
* @param {number} [size]
|
||||
* @param {*} [value]
|
||||
* @param {string} [viewAs]
|
||||
*/
|
||||
upsertTab({ server, db, type, ttl, key, size, value }) {
|
||||
upsertTab({ server, db, type, ttl, key, size, value, viewAs }) {
|
||||
let tabIndex = findIndex(this.tabList, { name: server })
|
||||
if (tabIndex === -1) {
|
||||
this.tabList.push({
|
||||
|
@ -98,6 +99,7 @@ const useTabStore = defineStore('tab', {
|
|||
key,
|
||||
size,
|
||||
value,
|
||||
viewAs,
|
||||
})
|
||||
tabIndex = this.tabList.length - 1
|
||||
}
|
||||
|
@ -112,6 +114,7 @@ const useTabStore = defineStore('tab', {
|
|||
tab.key = key
|
||||
tab.size = size
|
||||
tab.value = value
|
||||
tab.viewAs = viewAs
|
||||
this._setActivatedIndex(tabIndex, true)
|
||||
// this.activatedTab = tab.name
|
||||
},
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { endsWith, isEmpty, size, startsWith } from 'lodash'
|
||||
|
||||
export const IsRedisKey = (str, separator) => {
|
||||
if (isEmpty(separator)) {
|
||||
separator = ':'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check string is json
|
||||
* @param str
|
||||
* @returns {boolean}
|
||||
* @constructor
|
||||
*/
|
||||
export const IsJson = (str) => {
|
||||
if (size(str) >= 2) {
|
||||
if (startsWith(str, '{') && endsWith(str, '}')) {
|
||||
return true
|
||||
}
|
||||
if (startsWith(str, '[') && endsWith(str, ']')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import { map, padStart } from 'lodash'
|
||||
|
||||
/**
|
||||
* convert string to json
|
||||
* @param str
|
||||
* @return {string}
|
||||
*/
|
||||
export const toJsonText = (str) => {
|
||||
try {
|
||||
const jsonObj = JSON.parse(str)
|
||||
return JSON.stringify(jsonObj, null, 2)
|
||||
} catch (e) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* convert string from base64
|
||||
* @param str
|
||||
* @return {string}
|
||||
*/
|
||||
export const fromBase64 = (str) => {
|
||||
try {
|
||||
return atob(str)
|
||||
} catch (e) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* convert string from base64 to json
|
||||
* @param str
|
||||
* @return {string}
|
||||
*/
|
||||
export const fromBase64Json = (str) => {
|
||||
try {
|
||||
const text = atob(str)
|
||||
const jsonObj = JSON.parse(text)
|
||||
return JSON.stringify(jsonObj, null, 2)
|
||||
} catch (e) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* convert string to hex string
|
||||
* @param str
|
||||
* @return {string}
|
||||
*/
|
||||
export const toHex = (str) => {
|
||||
const hexArr = map(str, (char) => {
|
||||
const charCode = char.charCodeAt(0)
|
||||
return charCode.toString(16)
|
||||
})
|
||||
return hexArr.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* convert string to binary string
|
||||
* @param str
|
||||
* @return {string}
|
||||
*/
|
||||
export const toBinary = (str) => {
|
||||
const codeUnits = map(str, (char) => {
|
||||
let code = char.charCodeAt(0).toString(2)
|
||||
code = padStart(code, 8, '0')
|
||||
return code
|
||||
})
|
||||
return codeUnits.join(' ')
|
||||
}
|
10
main.go
10
main.go
|
@ -57,6 +57,16 @@ func main() {
|
|||
app.startup(ctx)
|
||||
connSvc.Start(ctx)
|
||||
},
|
||||
//OnBeforeClose: func(ctx context.Context) (prevent bool) {
|
||||
// // save current window size
|
||||
// width, height := runtime2.WindowGetSize(ctx)
|
||||
// if width > 0 && height > 0 {
|
||||
// if w, h := prefSvc.GetWindowSize(); w != width || h != height {
|
||||
// prefSvc.SaveWindowSize(width, height)
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
//},
|
||||
OnShutdown: func(ctx context.Context) {
|
||||
// save current window size
|
||||
width, height := runtime2.WindowGetSize(ctx)
|
||||
|
|
Loading…
Reference in New Issue