feat: add custom decoder/encoder for value content

This commit is contained in:
Lykin 2024-02-03 15:06:23 +08:00
parent 7faca878a3
commit 450e451781
18 changed files with 783 additions and 63 deletions

View File

@ -740,6 +740,8 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
} }
} }
decoder := Preferences().GetDecoder()
switch data.KeyType { switch data.KeyType {
case "string": case "string":
var str string var str string
@ -782,7 +784,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: val, Value: val,
}) })
if doConvert { if doConvert {
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format); dv != val { if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
items[len(items)-1].DisplayValue = dv items[len(items)-1].DisplayValue = dv
} }
} }
@ -829,7 +831,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: strutil.EncodeRedisKey(loadedVal[i+1]), Value: strutil.EncodeRedisKey(loadedVal[i+1]),
}) })
if doConvert { if doConvert {
if dv, _, _ := convutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format); dv != loadedVal[i+1] { if dv, _, _ := convutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format, decoder); dv != loadedVal[i+1] {
items[len(items)-1].DisplayValue = dv items[len(items)-1].DisplayValue = dv
} }
} }
@ -854,7 +856,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
items[i/2].Key = loadedVal[i] items[i/2].Key = loadedVal[i]
items[i/2].Value = strutil.EncodeRedisKey(loadedVal[i+1]) items[i/2].Value = strutil.EncodeRedisKey(loadedVal[i+1])
if doConvert { if doConvert {
if dv, _, _ := convutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format); dv != loadedVal[i+1] { if dv, _, _ := convutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format, decoder); dv != loadedVal[i+1] {
items[i/2].DisplayValue = dv items[i/2].DisplayValue = dv
} }
} }
@ -899,7 +901,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: val, Value: val,
}) })
if doConvert { if doConvert {
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format); dv != val { if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
items[len(items)-1].DisplayValue = dv items[len(items)-1].DisplayValue = dv
} }
} }
@ -919,7 +921,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
for i, val := range loadedKey { for i, val := range loadedKey {
items[i].Value = val items[i].Value = val
if doConvert { if doConvert {
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format); dv != val { if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
items[i].DisplayValue = dv items[i].DisplayValue = dv
} }
} }
@ -967,7 +969,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Score: score, Score: score,
}) })
if doConvert { if doConvert {
if dv, _, _ := convutil.ConvertTo(loadedVal[i], param.Decode, param.Format); dv != loadedVal[i] { if dv, _, _ := convutil.ConvertTo(loadedVal[i], param.Decode, param.Format, decoder); dv != loadedVal[i] {
items[len(items)-1].DisplayValue = dv items[len(items)-1].DisplayValue = dv
} }
} }
@ -1001,7 +1003,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: val, Value: val,
}) })
if doConvert { if doConvert {
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format); dv != val { if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
items[len(items)-1].DisplayValue = dv items[len(items)-1].DisplayValue = dv
} }
} }
@ -1062,7 +1064,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
if vb, merr := json.Marshal(msg.Values); merr != nil { if vb, merr := json.Marshal(msg.Values); merr != nil {
it.DisplayValue = "{}" it.DisplayValue = "{}"
} else { } else {
it.DisplayValue, _, _ = convutil.ConvertTo(string(vb), types.DECODE_NONE, types.FORMAT_JSON) it.DisplayValue, _, _ = convutil.ConvertTo(string(vb), types.DECODE_NONE, types.FORMAT_JSON, decoder)
} }
if doFilter && !strings.Contains(it.DisplayValue, param.MatchPattern) { if doFilter && !strings.Contains(it.DisplayValue, param.MatchPattern) {
continue continue
@ -1096,7 +1098,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
// blank format indicate auto format // blank format indicate auto format
func (b *browserService) ConvertValue(value any, decode, format string) (resp types.JSResp) { func (b *browserService) ConvertValue(value any, decode, format string) (resp types.JSResp) {
str := strutil.DecodeRedisKey(value) str := strutil.DecodeRedisKey(value)
value, decode, format = convutil.ConvertTo(str, decode, format) value, decode, format = convutil.ConvertTo(str, decode, format, Preferences().GetDecoder())
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
"value": value, "value": value,
@ -1139,7 +1141,7 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
return return
} else { } else {
var saveStr string var saveStr string
if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode); err != nil { if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode, Preferences().GetDecoder()); err != nil {
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
@ -1250,12 +1252,13 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe
key := strutil.DecodeRedisKey(param.Key) key := strutil.DecodeRedisKey(param.Key)
str := strutil.DecodeRedisKey(param.Value) str := strutil.DecodeRedisKey(param.Value)
var saveStr, displayStr string var saveStr, displayStr string
if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode); err != nil { decoder := Preferences().GetDecoder()
if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode, decoder); err != nil {
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 { if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 {
displayStr, _, _ = convutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat) displayStr, _, _ = convutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat, decoder)
} }
var updated, added, removed []types.HashEntryItem var updated, added, removed []types.HashEntryItem
var replaced []types.HashReplaceItem var replaced []types.HashReplaceItem
@ -1473,7 +1476,8 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
} else { } else {
// replace index value // replace index value
var saveStr string var saveStr string
if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode); err != nil { decoder := Preferences().GetDecoder()
if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode, decoder); err != nil {
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
@ -1484,7 +1488,7 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
} }
var displayStr string var displayStr string
if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 { if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 {
displayStr, _, _ = convutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat) displayStr, _, _ = convutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat, decoder)
} }
replaced = append(replaced, types.ListReplaceItem{ replaced = append(replaced, types.ListReplaceItem{
Index: param.Index, Index: param.Index,
@ -1574,8 +1578,9 @@ func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp types.JSRe
// insert new value // insert new value
str = strutil.DecodeRedisKey(param.NewValue) str = strutil.DecodeRedisKey(param.NewValue)
decoder := Preferences().GetDecoder()
var saveStr string var saveStr string
if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode); err != nil { if saveStr, err = convutil.SaveAs(str, param.Format, param.Decode, decoder); err != nil {
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
@ -1583,7 +1588,7 @@ func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp types.JSRe
// add new item // add new item
var displayStr string var displayStr string
if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 { if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 {
displayStr, _, _ = convutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat) displayStr, _, _ = convutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat, decoder)
} }
added = append(added, types.SetEntryItem{ added = append(added, types.SetEntryItem{
Value: saveStr, Value: saveStr,
@ -1620,6 +1625,7 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
var added, updated, removed []types.ZSetEntryItem var added, updated, removed []types.ZSetEntryItem
var replaced []types.ZSetReplaceItem var replaced []types.ZSetReplaceItem
var affect int64 var affect int64
decoder := Preferences().GetDecoder()
if len(newVal) <= 0 { if len(newVal) <= 0 {
// no new value, delete value // no new value, delete value
if affect, err = client.ZRem(ctx, key, val).Result(); affect > 0 { if affect, err = client.ZRem(ctx, key, val).Result(); affect > 0 {
@ -1630,7 +1636,7 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
} }
} else { } else {
var saveVal string var saveVal string
if saveVal, err = convutil.SaveAs(newVal, param.Format, param.Decode); err != nil { if saveVal, err = convutil.SaveAs(newVal, param.Format, param.Decode, decoder); err != nil {
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
@ -1640,7 +1646,7 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
Score: param.Score, Score: param.Score,
Member: saveVal, Member: saveVal,
}).Result() }).Result()
displayValue, _, _ := convutil.ConvertTo(val, param.RetDecode, param.RetFormat) displayValue, _, _ := convutil.ConvertTo(val, param.RetDecode, param.RetFormat, decoder)
if affect > 0 { if affect > 0 {
// add new item // add new item
added = append(added, types.ZSetEntryItem{ added = append(added, types.ZSetEntryItem{
@ -1668,7 +1674,7 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
Score: param.Score, Score: param.Score,
Member: saveVal, Member: saveVal,
}).Result() }).Result()
displayValue, _, _ := convutil.ConvertTo(saveVal, param.RetDecode, param.RetFormat) displayValue, _, _ := convutil.ConvertTo(saveVal, param.RetDecode, param.RetFormat, decoder)
if affect <= 0 { if affect <= 0 {
// no new value added, just update exists item // no new value added, just update exists item
removed = append(removed, types.ZSetEntryItem{ removed = append(removed, types.ZSetEntryItem{
@ -1794,7 +1800,7 @@ func (b *browserService) AddStreamValue(server string, db int, k any, ID string,
updateValues[fieldItems[i].(string)] = fieldItems[i+1] updateValues[fieldItems[i].(string)] = fieldItems[i+1]
} }
vb, _ := json.Marshal(updateValues) vb, _ := json.Marshal(updateValues)
displayValue, _, _ := convutil.ConvertTo(string(vb), types.DECODE_NONE, types.FORMAT_JSON) displayValue, _, _ := convutil.ConvertTo(string(vb), types.DECODE_NONE, types.FORMAT_JSON, Preferences().GetDecoder())
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = struct {

View File

@ -13,11 +13,14 @@ import (
storage2 "tinyrdm/backend/storage" storage2 "tinyrdm/backend/storage"
"tinyrdm/backend/types" "tinyrdm/backend/types"
"tinyrdm/backend/utils/coll" "tinyrdm/backend/utils/coll"
convutil "tinyrdm/backend/utils/convert"
sliceutil "tinyrdm/backend/utils/slice"
) )
type preferencesService struct { type preferencesService struct {
pref *storage2.PreferencesStorage pref *storage2.PreferencesStorage
clientVersion string clientVersion string
customDecoder []convutil.CmdConvert
} }
var preferences *preferencesService var preferences *preferencesService
@ -182,6 +185,23 @@ func (p *preferencesService) GetScanSize() int {
return size return size
} }
func (p *preferencesService) GetDecoder() []convutil.CmdConvert {
data := p.pref.GetPreferences()
return sliceutil.FilterMap(data.Decoder, func(i int) (convutil.CmdConvert, bool) {
//if !data.Decoder[i].Enable {
// return convutil.CmdConvert{}, false
//}
return convutil.CmdConvert{
Name: data.Decoder[i].Name,
Auto: data.Decoder[i].Auto,
DecodePath: data.Decoder[i].DecodePath,
DecodeArgs: data.Decoder[i].DecodeArgs,
EncodePath: data.Decoder[i].EncodePath,
EncodeArgs: data.Decoder[i].EncodeArgs,
}, true
})
}
type latestRelease struct { type latestRelease struct {
Name string `json:"name"` Name string `json:"name"`
TagName string `json:"tag_name"` TagName string `json:"tag_name"`

View File

@ -7,6 +7,7 @@ type Preferences struct {
General PreferencesGeneral `json:"general" yaml:"general"` General PreferencesGeneral `json:"general" yaml:"general"`
Editor PreferencesEditor `json:"editor" yaml:"editor"` Editor PreferencesEditor `json:"editor" yaml:"editor"`
Cli PreferencesCli `json:"cli" yaml:"cli"` Cli PreferencesCli `json:"cli" yaml:"cli"`
Decoder []PreferencesDecoder `json:"decoder" yaml:"decoder,omitempty"`
} }
func NewPreferences() Preferences { func NewPreferences() Preferences {
@ -33,6 +34,7 @@ func NewPreferences() Preferences {
FontSize: consts.DEFAULT_FONT_SIZE, FontSize: consts.DEFAULT_FONT_SIZE,
CursorStyle: "block", CursorStyle: "block",
}, },
Decoder: []PreferencesDecoder{},
} }
} }
@ -72,3 +74,13 @@ type PreferencesCli struct {
FontSize int `json:"fontSize" yaml:"font_size"` FontSize int `json:"fontSize" yaml:"font_size"`
CursorStyle string `json:"cursorStyle" yaml:"cursor_style,omitempty"` CursorStyle string `json:"cursorStyle" yaml:"cursor_style,omitempty"`
} }
type PreferencesDecoder struct {
Name string `json:"name" yaml:"name"`
Enable bool `json:"enable" yaml:"enable"`
Auto bool `json:"auto" yaml:"auto"`
DecodePath string `json:"decodePath" yaml:"decode_path"`
DecodeArgs []string `json:"decodeArgs" yaml:"decode_args,omitempty"`
EncodePath string `json:"encodePath" yaml:"encode_path"`
EncodeArgs []string `json:"encodeArgs" yaml:"encode_args,omitempty"`
}

View File

@ -0,0 +1,75 @@
package convutil
import (
"encoding/base64"
"os/exec"
"strings"
sliceutil "tinyrdm/backend/utils/slice"
)
type CmdConvert struct {
Name string
Auto bool
DecodePath string
DecodeArgs []string
EncodePath string
EncodeArgs []string
}
const replaceholder = "{VALUE}"
func (c CmdConvert) Encode(str string) (string, bool) {
base64Content := base64.StdEncoding.EncodeToString([]byte(str))
var containHolder bool
args := sliceutil.Map(c.EncodeArgs, func(i int) string {
arg := strings.TrimSpace(c.EncodeArgs[i])
if strings.Contains(arg, replaceholder) {
arg = strings.ReplaceAll(arg, replaceholder, base64Content)
containHolder = true
}
return arg
})
if len(args) <= 0 || !containHolder {
args = append(args, base64Content)
}
cmd := exec.Command(c.EncodePath, args...)
output, err := cmd.Output()
if err != nil || len(output) <= 0 || string(output) == "[RDM-ERROR]" {
return str, false
}
outputContent := make([]byte, base64.StdEncoding.DecodedLen(len(output)))
n, err := base64.StdEncoding.Decode(outputContent, output)
if err != nil {
return str, false
}
return string(outputContent[:n]), true
}
func (c CmdConvert) Decode(str string) (string, bool) {
base64Content := base64.StdEncoding.EncodeToString([]byte(str))
var containHolder bool
args := sliceutil.Map(c.DecodeArgs, func(i int) string {
arg := strings.TrimSpace(c.DecodeArgs[i])
if strings.Contains(arg, replaceholder) {
arg = strings.ReplaceAll(arg, replaceholder, base64Content)
containHolder = true
}
return arg
})
if len(args) <= 0 || !containHolder {
args = append(args, base64Content)
}
cmd := exec.Command(c.DecodePath, args...)
output, err := cmd.Output()
if err != nil || len(output) <= 0 || string(output) == "[RDM-ERROR]" {
return str, false
}
outputContent := make([]byte, base64.StdEncoding.DecodedLen(len(output)))
n, err := base64.StdEncoding.Decode(outputContent, output)
if err != nil {
return str, false
}
return string(outputContent[:n]), true
}

View File

@ -29,7 +29,8 @@ var (
// ConvertTo convert string to specified type // ConvertTo convert string to specified type
// @param decodeType empty string indicates automatic detection // @param decodeType empty string indicates automatic detection
// @param formatType empty string indicates automatic detection // @param formatType empty string indicates automatic detection
func ConvertTo(str, decodeType, formatType string) (value, resultDecode, resultFormat string) { // @param custom decoder if any
func ConvertTo(str, decodeType, formatType string, customDecoder []CmdConvert) (value, resultDecode, resultFormat string) {
if len(str) <= 0 { if len(str) <= 0 {
// empty content // empty content
if len(formatType) <= 0 { if len(formatType) <= 0 {
@ -46,13 +47,17 @@ func ConvertTo(str, decodeType, formatType string) (value, resultDecode, resultF
} }
// decode first // decode first
value, resultDecode = decodeWith(str, decodeType) value, resultDecode = decodeWith(str, decodeType, customDecoder)
// then format content // then format content
if len(formatType) <= 0 {
value, resultFormat = autoViewAs(value)
} else {
value, resultFormat = viewAs(value, formatType) value, resultFormat = viewAs(value, formatType)
}
return return
} }
func decodeWith(str, decodeType string) (value, resultDecode string) { func decodeWith(str, decodeType string, customDecoder []CmdConvert) (value, resultDecode string) {
if len(decodeType) > 0 { if len(decodeType) > 0 {
switch decodeType { switch decodeType {
case types.DECODE_NONE: case types.DECODE_NONE:
@ -99,17 +104,31 @@ func decodeWith(str, decodeType string) (value, resultDecode string) {
} else { } else {
value = str value = str
} }
default:
for _, decoder := range customDecoder {
if decoder.Name == decodeType {
if decodedStr, ok := decoder.Decode(str); ok {
value = decodedStr
} else {
value = str
} }
break
}
}
}
resultDecode = decodeType resultDecode = decodeType
return return
} }
return autoDecode(str) value, resultDecode = autoDecode(str, customDecoder)
return
} }
// attempt try possible decode method // attempt try possible decode method
// if no decode is possible, it will return the origin string value and "none" decode type // if no decode is possible, it will return the origin string value and "none" decode type
func autoDecode(str string) (value, resultDecode string) { func autoDecode(str string, customDecoder []CmdConvert) (value, resultDecode string) {
if len(str) > 0 { if len(str) > 0 {
// pure digit content may incorrect regard as some encoded type, skip decode // pure digit content may incorrect regard as some encoded type, skip decode
if match, _ := regexp.MatchString(`^\d+$`, str); !match { if match, _ := regexp.MatchString(`^\d+$`, str); !match {
@ -147,6 +166,16 @@ func autoDecode(str string) (value, resultDecode string) {
resultDecode = types.DECODE_MSGPACK resultDecode = types.DECODE_MSGPACK
return return
} }
// try decode with custom decoder
for _, decoder := range customDecoder {
if decoder.Auto {
if value, ok = decoder.Decode(str); ok {
resultDecode = decoder.Name
return
}
}
}
} }
} }
@ -158,6 +187,8 @@ func autoDecode(str string) (value, resultDecode string) {
func viewAs(str, formatType string) (value, resultFormat string) { func viewAs(str, formatType string) (value, resultFormat string) {
if len(formatType) > 0 { if len(formatType) > 0 {
switch formatType { switch formatType {
default:
fallthrough
case types.FORMAT_RAW, types.FORMAT_YAML, types.FORMAT_XML: case types.FORMAT_RAW, types.FORMAT_YAML, types.FORMAT_XML:
value = str value = str
@ -185,8 +216,7 @@ func viewAs(str, formatType string) (value, resultFormat string) {
resultFormat = formatType resultFormat = formatType
return return
} }
return
return autoViewAs(str)
} }
// attempt automatic convert to possible types // attempt automatic convert to possible types
@ -222,7 +252,7 @@ func autoViewAs(str string) (value, resultFormat string) {
return return
} }
func SaveAs(str, format, decode string) (value string, err error) { func SaveAs(str, format, decode string, customDecoder []CmdConvert) (value string, err error) {
value = str value = str
switch format { switch format {
case types.FORMAT_JSON: case types.FORMAT_JSON:
@ -297,6 +327,18 @@ func SaveAs(str, format, decode string) (value string, err error) {
err = errors.New("fail to build msgpack") err = errors.New("fail to build msgpack")
} }
return return
default:
for _, decoder := range customDecoder {
if decoder.Name == decode {
if encodedStr, ok := decoder.Encode(str); ok {
value = encodedStr
} else {
value = str
}
return
}
}
} }
return str, nil return str, nil
} }

View File

@ -2,7 +2,6 @@ package convutil
import ( import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"log"
) )
type YamlConvert struct{} type YamlConvert struct{}
@ -14,10 +13,5 @@ func (YamlConvert) Encode(str string) (string, bool) {
func (YamlConvert) Decode(str string) (string, bool) { func (YamlConvert) Decode(str string) (string, bool) {
var obj map[string]any var obj map[string]any
err := yaml.Unmarshal([]byte(str), &obj) err := yaml.Unmarshal([]byte(str), &obj)
if err != nil {
log.Println(err.Error())
} else {
log.Println(obj)
}
return str, err == nil return str, err == nil
} }

View File

@ -21,6 +21,7 @@ import FlushDbDialog from '@/components/dialogs/FlushDbDialog.vue'
import ExportKeyDialog from '@/components/dialogs/ExportKeyDialog.vue' import ExportKeyDialog from '@/components/dialogs/ExportKeyDialog.vue'
import ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue' import ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue'
import { Info } from 'wailsjs/go/services/systemService.js' import { Info } from 'wailsjs/go/services/systemService.js'
import DecoderDialog from '@/components/dialogs/DecoderDialog.vue'
const prefStore = usePreferencesStore() const prefStore = usePreferencesStore()
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
@ -79,6 +80,7 @@ watch(
<flush-db-dialog /> <flush-db-dialog />
<set-ttl-dialog /> <set-ttl-dialog />
<preferences-dialog /> <preferences-dialog />
<decoder-dialog />
<about-dialog /> <about-dialog />
</n-dialog-provider> </n-dialog-provider>
</n-config-provider> </n-config-provider>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { computed, h, ref } from 'vue' import { computed, h, ref } from 'vue'
import { get, map } from 'lodash' import { get, isEmpty } from 'lodash'
import { NIcon, NText } from 'naive-ui' import { NIcon, NText } from 'naive-ui'
import { useRender } from '@/utils/render.js' import { useRender } from '@/utils/render.js'
@ -10,8 +10,8 @@ const props = defineProps({
value: '', value: '',
}, },
options: { options: {
type: Object, type: Array,
value: {}, value: () => [],
}, },
tooltip: { tooltip: {
type: String, type: String,
@ -40,15 +40,32 @@ const dropdownOption = computed(() => {
type: 'divider', type: 'divider',
}, },
] ]
return [ if (get(props.options, 0) instanceof Array) {
...options, // multiple group
...map(props.options, (t) => { for (let i = 0; i < props.options.length; i++) {
return { if (i !== 0 && !isEmpty(props.options[i])) {
key: t, // add divider
label: t, options.push({
key: 'header-divider' + (i + 1),
type: 'divider',
})
} }
}), for (const option of props.options[i]) {
] options.push({
key: option,
label: option,
})
}
}
} else {
for (const option of props.options) {
options.push({
key: option,
label: option,
})
}
}
return options
}) })
const onDropdownSelect = (key) => { const onDropdownSelect = (key) => {

View File

@ -35,6 +35,7 @@ const handleSelectFile = async () => {
<n-input <n-input
:disabled="props.disabled" :disabled="props.disabled"
:placeholder="placeholder" :placeholder="placeholder"
:title="props.value"
:value="props.value" :value="props.value"
clearable clearable
@clear="onClear" @clear="onClear"

View File

@ -3,7 +3,9 @@ import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import Code from '@/components/icons/Code.vue' import Code from '@/components/icons/Code.vue'
import Conversion from '@/components/icons/Conversion.vue' import Conversion from '@/components/icons/Conversion.vue'
import DropdownSelector from '@/components/common/DropdownSelector.vue' import DropdownSelector from '@/components/common/DropdownSelector.vue'
import { some } from 'lodash' import { isEmpty, map, some } from 'lodash'
import { computed } from 'vue'
import usePreferencesStore from 'stores/preferences.js'
const props = defineProps({ const props = defineProps({
decode: { decode: {
@ -17,13 +19,35 @@ const props = defineProps({
disabled: Boolean, disabled: Boolean,
}) })
const prefStore = usePreferencesStore()
const formatTypeOption = computed(() => {
return map(formatTypes, (t) => t)
})
const decodeTypeOption = computed(() => {
const customTypes = []
// has custom decoder
if (!isEmpty(prefStore.decoder)) {
for (const decoder of prefStore.decoder) {
// types[decoder.name] = types[decoder.name] || decoder.name
if (!decodeTypes.hasOwnProperty(decoder.name)) {
customTypes.push(decoder.name)
}
}
}
return [map(decodeTypes, (t) => t), customTypes]
})
const emit = defineEmits(['formatChanged', 'update:decode', 'update:format']) const emit = defineEmits(['formatChanged', 'update:decode', 'update:format'])
const onFormatChanged = (selDecode, selFormat) => { const onFormatChanged = (selDecode, selFormat) => {
if (!some(decodeTypes, (val) => val === selDecode)) { const [buildin, external] = decodeTypeOption.value
if (!some([...buildin, ...external], (val) => val === selDecode)) {
selDecode = decodeTypes.NONE selDecode = decodeTypes.NONE
} }
if (!some(formatTypes, (val) => val === selFormat)) { if (!some(formatTypes, (val) => val === selFormat)) {
selFormat = formatTypes.RAW // set to auto chose format
selFormat = ''
} }
emit('formatChanged', selDecode, selFormat) emit('formatChanged', selDecode, selFormat)
if (selDecode !== props.decode) { if (selDecode !== props.decode) {
@ -41,7 +65,7 @@ const onFormatChanged = (selDecode, selFormat) => {
:default="formatTypes.RAW" :default="formatTypes.RAW"
:disabled="props.disabled" :disabled="props.disabled"
:icon="Code" :icon="Code"
:options="formatTypes" :options="formatTypeOption"
:tooltip="$t('interface.view_as')" :tooltip="$t('interface.view_as')"
:value="props.format" :value="props.format"
@update:value="(f) => onFormatChanged(props.decode, f)" /> @update:value="(f) => onFormatChanged(props.decode, f)" />
@ -50,10 +74,10 @@ const onFormatChanged = (selDecode, selFormat) => {
:default="decodeTypes.NONE" :default="decodeTypes.NONE"
:disabled="props.disabled" :disabled="props.disabled"
:icon="Conversion" :icon="Conversion"
:options="decodeTypes" :options="decodeTypeOption"
:tooltip="$t('interface.decode_with')" :tooltip="$t('interface.decode_with')"
:value="props.decode" :value="props.decode"
@update:value="(d) => onFormatChanged(d, props.format)" /> @update:value="(d) => onFormatChanged(d, '')" />
</n-space> </n-space>
</template> </template>

View File

@ -0,0 +1,206 @@
<script setup>
import useDialog from 'stores/dialog.js'
import { computed, reactive, ref, toRaw, watch } from 'vue'
import FileOpenInput from '@/components/common/FileOpenInput.vue'
import Delete from '@/components/icons/Delete.vue'
import Add from '@/components/icons/Add.vue'
import IconButton from '@/components/common/IconButton.vue'
import { cloneDeep, get, isEmpty } from 'lodash'
import usePreferencesStore from 'stores/preferences.js'
import { joinCommand } from '@/utils/decoder_cmd.js'
import Help from '@/components/icons/Help.vue'
const editName = ref('')
const decoderForm = reactive({
name: '',
auto: true,
decodePath: '',
decodeArgs: [],
encodePath: '',
encodeArgs: [],
})
const dialogStore = useDialog()
const prefStore = usePreferencesStore()
watch(
() => dialogStore.decodeDialogVisible,
(visible) => {
if (visible) {
const name = get(dialogStore.decodeParam, 'name', '')
if (!isEmpty(name)) {
editName.value = decoderForm.name = name
decoderForm.auto = dialogStore.decodeParam.auto !== false
decoderForm.decodePath = get(dialogStore.decodeParam, 'decodePath', '')
decoderForm.decodeArgs = get(dialogStore.decodeParam, 'decodeArgs', [])
decoderForm.encodePath = get(dialogStore.decodeParam, 'encodePath', '')
decoderForm.encodeArgs = get(dialogStore.decodeParam, 'encodeArgs', [])
} else {
editName.value = ''
decoderForm.decodePath = ''
decoderForm.encodePath = ''
decoderForm.decodeArgs = []
decoderForm.encodeArgs = []
}
} else {
editName.value = ''
}
},
)
const decodeCmdPreview = computed(() => {
return joinCommand(decoderForm.decodePath, decoderForm.decodeArgs, '')
})
const encodeCmdPreview = computed(() => {
return joinCommand(decoderForm.encodePath, decoderForm.encodeArgs, '')
})
const onAddOrUpdate = () => {
if (isEmpty(editName.value)) {
// add decoder
prefStore.addCustomDecoder(toRaw(decoderForm))
} else {
// update decoder
const param = cloneDeep(toRaw(decoderForm))
param.newName = param.name
param.name = editName.value
prefStore.updateCustomDecoder(param)
}
}
const onClose = () => {}
</script>
<template>
<n-modal
v-model:show="dialogStore.decodeDialogVisible"
:closable="false"
:close-on-esc="false"
:mask-closable="false"
:negative-button-props="{ focusable: false, size: 'medium' }"
:negative-text="$t('common.cancel')"
:positive-button-props="{ focusable: false, size: 'medium' }"
:positive-text="$t('common.confirm')"
:show-icon="false"
:title="editName ? $t('dialogue.decoder.edit_name') : $t('dialogue.decoder.name')"
preset="dialog"
transform-origin="center"
@positive-click="onAddOrUpdate"
@negative-click="onClose">
<n-form :model="decoderForm" :show-require-mark="false" label-align="left" label-placement="top">
<n-form-item :label="$t('dialogue.decoder.decoder_name')" required show-require-mark>
<n-input v-model:value="decoderForm.name" />
</n-form-item>
<n-tabs type="line">
<!-- decode pane -->
<n-tab-pane :tab="$t('dialogue.decoder.decoder')" name="decode">
<n-form-item required show-require-mark>
<template #label>
<n-space :size="5" :wrap-item="false" align="center" justify="center">
<span>{{ $t('dialogue.decoder.decode_path') }}</span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="Help" />
</template>
<div class="text-block" style="max-width: 600px">
{{ $t('dialogue.decoder.path_help') }}
</div>
</n-tooltip>
</n-space>
</template>
<file-open-input
v-model:value="decoderForm.decodePath"
:placeholder="$t('dialogue.decoder.decode_path')" />
</n-form-item>
<n-form-item required>
<template #label>
<n-space :size="5" :wrap-item="false" align="center" justify="center">
<span>{{ $t('dialogue.decoder.args') }}</span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="Help" />
</template>
<div class="text-block" style="max-width: 600px">
{{ $t('dialogue.decoder.args_help').replace('[', '{').replace(']', '}') }}
</div>
</n-tooltip>
</n-space>
</template>
<n-dynamic-input v-model:value="decoderForm.decodeArgs" @create="() => ''">
<template #action="{ index, create, remove, move }">
<icon-button :icon="Add" size="18" @click="() => create(index)" />
<icon-button :icon="Delete" size="18" @click="() => remove(index)" />
</template>
</n-dynamic-input>
</n-form-item>
<n-card
v-if="decodeCmdPreview"
content-class="cmd-line"
content-style="padding: 10px;"
embedded
size="small">
{{ decodeCmdPreview }}
</n-card>
</n-tab-pane>
<!-- encode pane -->
<n-tab-pane :tab="$t('dialogue.decoder.encoder')" name="encode">
<n-form-item required show-require-mark>
<template #label>
<n-space :size="5" :wrap-item="false" align="center" justify="center">
<span>{{ $t('dialogue.decoder.encode_path') }}</span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="Help" />
</template>
<div class="text-block" style="max-width: 600px">
{{ $t('dialogue.decoder.path_help') }}
</div>
</n-tooltip>
</n-space>
</template>
<file-open-input
v-model:value="decoderForm.encodePath"
:placeholder="$t('dialogue.decoder.encode_path')" />
</n-form-item>
<n-form-item :label="$t('dialogue.decoder.args')" required>
<template #label>
<n-space :size="5" :wrap-item="false" align="center" justify="center">
<span>{{ $t('dialogue.decoder.args') }}</span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="Help" />
</template>
<div class="text-block" style="max-width: 600px">
{{ $t('dialogue.decoder.args_help').replace('[', '{').replace(']', '}') }}
</div>
</n-tooltip>
</n-space>
</template>
<n-dynamic-input v-model:value="decoderForm.encodeArgs" @create="() => ''">
<template #action="{ index, create, remove, move }">
<icon-button :icon="Add" size="18" @click="() => create(index)" />
<icon-button :icon="Delete" size="18" @click="() => remove(index)" />
</template>
</n-dynamic-input>
</n-form-item>
<n-card
v-if="encodeCmdPreview"
content-class="cmd-line"
content-style="padding: 10px;"
embedded
size="small">
{{ encodeCmdPreview }}
</n-card>
</n-tab-pane>
</n-tabs>
<n-form-item :show-feedback="false">
<n-checkbox v-model:checked="decoderForm.auto" :label="$t('dialogue.decoder.auto')" />
</n-form-item>
</n-form>
</n-modal>
</template>
<style lang="scss" scoped>
@import '@/styles/content';
</style>

View File

@ -1,11 +1,18 @@
<script setup> <script setup>
import { computed, ref, watchEffect } from 'vue' import { computed, h, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import useDialog from 'stores/dialog' import useDialog from 'stores/dialog'
import usePreferencesStore from 'stores/preferences.js' import usePreferencesStore from 'stores/preferences.js'
import { map, sortBy } from 'lodash' import { find, map, sortBy } from 'lodash'
import { typesIconStyle } from '@/consts/support_redis_type.js' import { typesIconStyle } from '@/consts/support_redis_type.js'
import Help from '@/components/icons/Help.vue' import Help from '@/components/icons/Help.vue'
import Delete from '@/components/icons/Delete.vue'
import IconButton from '@/components/common/IconButton.vue'
import { NButton, NEllipsis, NIcon, NSpace, NTooltip } from 'naive-ui'
import Edit from '@/components/icons/Edit.vue'
import { joinCommand } from '@/utils/decoder_cmd.js'
import AddLink from '@/components/icons/AddLink.vue'
import Checked from '@/components/icons/Checked.vue'
const prefStore = usePreferencesStore() const prefStore = usePreferencesStore()
@ -23,6 +30,8 @@ const initPreferences = async () => {
prevPreferences.value = { prevPreferences.value = {
general: prefStore.general, general: prefStore.general,
editor: prefStore.editor, editor: prefStore.editor,
cli: prefStore.cli,
decoder: prefStore.decoder,
} }
} finally { } finally {
loading.value = false loading.value = false
@ -43,6 +52,99 @@ const keyOptions = computed(() => {
return sortBy(opts, (o) => o.value) return sortBy(opts, (o) => o.value)
}) })
const decoderList = computed(() => {
const decoder = prefStore.decoder || []
const list = []
for (const d of decoder) {
// decode command
list.push({
name: d.name,
auto: d.auto,
decodeCmd: joinCommand(d.decodePath, d.decodeArgs),
encodeCmd: joinCommand(d.encodePath, d.encodeArgs),
})
}
return list
})
const decoderColumns = computed(() => {
return [
{
key: 'name',
title: () => i18n.t('preferences.decoder.decoder_name'),
width: 120,
align: 'center',
titleAlign: 'center',
},
{
key: 'cmd',
title: () => i18n.t('preferences.decoder.cmd_preview'),
titleAlign: 'center',
render: ({ decodeCmd, encodeCmd }, index) => {
return h(NSpace, { vertical: true, wrapItem: false, wrap: false, justify: 'center', size: 15 }, () => [
h(NEllipsis, {}, { default: () => decodeCmd, tooltip: () => decodeCmd + '\n\n' + encodeCmd }),
h(NEllipsis, {}, { default: () => encodeCmd, tooltip: () => decodeCmd + '\n\n' + encodeCmd }),
])
},
},
{
key: 'status',
title: () => i18n.t('preferences.decoder.status'),
width: 80,
align: 'center',
titleAlign: 'center',
render: ({ auto }, index) => {
if (auto) {
return h(
NTooltip,
{ delay: 0, showArrow: false },
{
default: () => i18n.t('preferences.decoder.auto_enabled'),
trigger: () => h(NIcon, { component: Checked, size: 16 }),
},
)
}
return '-'
},
},
{
key: 'action',
title: () => i18n.t('interface.action'),
width: 80,
align: 'center',
titleAlign: 'center',
render: ({ name, auto }, index) => {
return h(NSpace, { wrapItem: false, wrap: false, justify: 'center', size: 'small' }, () => [
h(IconButton, {
icon: Delete,
tTooltip: 'interface.delete_row',
onClick: () => {
prefStore.removeCustomDecoder(name)
},
}),
h(IconButton, {
icon: Edit,
tTooltip: 'interface.edit_row',
onClick: () => {
const decoders = prefStore.decoder || []
const decoder = find(decoders, { name })
const { auto, decodePath, decodeArgs, encodePath, encodeArgs } = decoder
dialogStore.openDecoderDialog({
name,
auto,
decodePath,
decodeArgs,
encodePath,
encodeArgs,
})
},
}),
])
},
},
]
})
const onSavePreferences = async () => { const onSavePreferences = async () => {
const success = await prefStore.savePreferences() const success = await prefStore.savePreferences()
if (success) { if (success) {
@ -68,11 +170,17 @@ const onClose = () => {
:show-icon="false" :show-icon="false"
:title="$t('preferences.name')" :title="$t('preferences.name')"
preset="dialog" preset="dialog"
style="width: 500px" style="width: 640px"
transform-origin="center"> transform-origin="center">
<!-- FIXME: set loading will slow down appear animation of dialog in linux --> <!-- FIXME: set loading will slow down appear animation of dialog in linux -->
<!-- <n-spin :show="loading"> --> <!-- <n-spin :show="loading"> -->
<n-tabs v-model:value="tab" animated type="line"> <n-tabs
v-model:value="tab"
animated
pane-style="min-height: 300px"
placement="left"
tab-style="justify-content: right; font-weight: 420;"
type="line">
<n-tab-pane :tab="$t('preferences.general.name')" display-directive="show" name="general"> <n-tab-pane :tab="$t('preferences.general.name')" display-directive="show" name="general">
<n-form :disabled="loading" :model="prefStore.general" :show-require-mark="false" label-placement="top"> <n-form :disabled="loading" :model="prefStore.general" :show-require-mark="false" label-placement="top">
<n-grid :x-gap="10"> <n-grid :x-gap="10">
@ -230,6 +338,23 @@ const onClose = () => {
</n-grid> </n-grid>
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
<!-- Custom decoder pane -->
<n-tab-pane :tab="$t('preferences.decoder.name')" display-directive="show:lazy" name="decoder">
<n-space>
<n-button @click="dialogStore.openDecoderDialog()">
<template #icon>
<n-icon :component="AddLink" size="18" />
</template>
{{ $t('preferences.decoder.new') }}
</n-button>
<n-data-table
:columns="decoderColumns"
:data="decoderList"
:single-line="false"
max-height="350px" />
</n-space>
</n-tab-pane>
</n-tabs> </n-tabs>
<!-- </n-spin> --> <!-- </n-spin> -->

View File

@ -269,7 +269,7 @@ const exThemeVars = computed(() => {
.nav-menu-item { .nav-menu-item {
align-items: center; align-items: center;
padding: 10px 0 15px; padding: 10px 0 15px;
gap: 18px; gap: 20px;
--wails-draggable: none; --wails-draggable: none;
.nav-menu-button { .nav-menu-button {

View File

@ -62,6 +62,14 @@
"cursor_style_block": "Block", "cursor_style_block": "Block",
"cursor_style_underline": "Underline", "cursor_style_underline": "Underline",
"cursor_style_bar": "Bar" "cursor_style_bar": "Bar"
},
"decoder": {
"name": "Custom Decoder",
"new": "New Decoder",
"decoder_name": "Name",
"cmd_preview": "Preview",
"status": "Status",
"path": "Decoder Execution Path"
} }
}, },
"interface": { "interface": {
@ -339,6 +347,20 @@
"quick_set": "Quick Settings", "quick_set": "Quick Settings",
"success": "All TTL of keys have been updated" "success": "All TTL of keys have been updated"
}, },
"decoder": {
"name": "New Decoder/Encoder",
"edit_name": "Edit Decoder/Encoder",
"new": "New",
"decoder": "Decoder",
"encoder": "Encoder",
"decoder_name": "Name",
"auto": "Automatic Decoding",
"decode_path": "Decoder Execution Path",
"encode_path": "Encoder Execution Path",
"path_help": "The path of executable file, any cli alias like 'sh/php/python' are also supported.",
"args": "Arguments",
"args_help": "Use [VALUE] as a placeholder for encoding/decoding content. The content will be appended to the end if no placeholder is provided."
},
"upgrade": { "upgrade": {
"title": "New Version Available", "title": "New Version Available",
"new_version_tip": "A new version({ver}) is available. Download now?", "new_version_tip": "A new version({ver}) is available. Download now?",

View File

@ -62,6 +62,14 @@
"cursor_style_block": "方块", "cursor_style_block": "方块",
"cursor_style_underline": "下划线", "cursor_style_underline": "下划线",
"cursor_style_bar": "竖线" "cursor_style_bar": "竖线"
},
"decoder": {
"name": "自定义解码",
"new": "新增自定义解码",
"decoder_name": "解码器名称",
"cmd_preview": "命令预览",
"status": "状态",
"auto_enabled": "已加入自动解码"
} }
}, },
"interface": { "interface": {
@ -339,6 +347,20 @@
"quick_set": "快捷设置", "quick_set": "快捷设置",
"success": "已全部更新TTL" "success": "已全部更新TTL"
}, },
"decoder": {
"name": "新增解码/编码器",
"edit_name": "编辑解码/编码器",
"new": "新增",
"decoder": "解码器",
"encoder": "编码器",
"decoder_name": "解码器名称",
"auto": "自动解码",
"decode_path": "解码器执行路径",
"encode_path": "编码器执行路径",
"path_help": "执行文件路径也可以直接填写命令行接口如sh/php/python",
"args": "运行参数",
"args_help": "使用[VALUE]代替编码/解码内容占位符,如果不填内容占位则默认放最后"
},
"upgrade": { "upgrade": {
"title": "有可用新版本", "title": "有可用新版本",
"new_version_tip": "新版本({ver}),是否立即下载", "new_version_tip": "新版本({ver}),是否立即下载",

View File

@ -91,6 +91,16 @@ const useDialogStore = defineStore('dialog', {
ttl: 0, ttl: 0,
}, },
decodeDialogVisible: false,
decodeParam: {
name: '',
auto: true,
decodePath: '',
decodeArgs: [],
encodePath: '',
encodeArgs: [],
},
preferencesDialogVisible: false, preferencesDialogVisible: false,
aboutDialogVisible: false, aboutDialogVisible: false,
}), }),
@ -290,6 +300,36 @@ const useDialogStore = defineStore('dialog', {
this.ttlDialogVisible = false this.ttlDialogVisible = false
}, },
/**
*
* @param {string} name
* @param {boolean} auto
* @param {string} decodePath
* @param {string[]} decodeArgs
* @param {string} encodePath
* @param {string[]} encodeArgs
*/
openDecoderDialog({
name = '',
auto = true,
decodePath = '',
decodeArgs = [],
encodePath = '',
encodeArgs = [],
} = {}) {
this.decodeDialogVisible = true
this.decodeParam.name = name
this.decodeParam.auto = auto !== false
this.decodeParam.decodePath = decodePath
this.decodeParam.decodeArgs = decodeArgs || []
this.decodeParam.encodePath = encodePath
this.decodeParam.encodeArgs = encodeArgs || []
},
closeDecoderDialog() {
this.decodeDialogVisible = false
},
openPreferencesDialog() { openPreferencesDialog() {
this.preferencesDialogVisible = true this.preferencesDialogVisible = true
}, },

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { lang } from '@/langs/index.js' import { lang } from '@/langs/index.js'
import { cloneDeep, get, isEmpty, join, map, pick, set, split } from 'lodash' import { cloneDeep, findIndex, get, isEmpty, join, map, merge, pick, set, some, split } from 'lodash'
import { import {
CheckForUpdate, CheckForUpdate,
GetFontList, GetFontList,
@ -64,6 +64,7 @@ const usePreferencesStore = defineStore('preferences', {
fontSize: 14, fontSize: 14,
cursorStyle: 'block', cursorStyle: 'block',
}, },
decoder: [],
lastPref: {}, lastPref: {},
fontList: [], fontList: [],
}), }),
@ -306,7 +307,7 @@ const usePreferencesStore = defineStore('preferences', {
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
async savePreferences() { async savePreferences() {
const pf = pick(this, ['behavior', 'general', 'editor', 'cli']) const pf = pick(this, ['behavior', 'general', 'editor', 'cli', 'decoder'])
const { success, msg } = await SetPreferences(pf) const { success, msg } = await SetPreferences(pf)
return success === true return success === true
}, },
@ -335,6 +336,81 @@ const usePreferencesStore = defineStore('preferences', {
return false return false
}, },
/**
* add a new custom decoder
* @param {string} name
* @param {boolean} enable
* @param {boolean} auto
* @param {string} encodePath
* @param {string[]} encodeArgs
* @param {string} decodePath
* @param {string[]} decodeArgs
*/
addCustomDecoder({ name, enable = true, auto = true, encodePath, encodeArgs, decodePath, decodeArgs }) {
if (some(this.decoder, { name })) {
return false
}
this.decoder = this.decoder || []
this.decoder.push({ name, enable, auto, encodePath, encodeArgs, decodePath, decodeArgs })
return true
},
/**
* update an existing custom decoder
* @param {string} newName
* @param {boolean} enable
* @param {boolean} auto
* @param {string} name
* @param {string} encodePath
* @param {string[]} encodeArgs
* @param {string} decodePath
* @param {string[]} decodeArgs
*/
updateCustomDecoder({
newName,
enable = true,
auto = true,
name,
encodePath,
encodeArgs,
decodePath,
decodeArgs,
}) {
const idx = findIndex(this.decoder, { name })
if (idx === -1) {
return false
}
// conflicted
if (newName !== name && some(this.decoder, { name: newName })) {
return false
}
this.decoder[idx] = merge(this.decoder[idx], {
name: newName || name,
enable,
auto,
encodePath,
encodeArgs,
decodePath,
decodeArgs,
})
return true
},
/**
* remove an existing custom decoder
* @param {string} name
* @return {boolean}
*/
removeCustomDecoder(name) {
const idx = findIndex(this.decoder, { name })
if (idx === -1) {
return false
}
this.decoder.splice(idx, 1)
return true
},
async checkForUpdate(manual = false) { async checkForUpdate(manual = false) {
let msgRef = null let msgRef = null
if (manual) { if (manual) {

View File

@ -0,0 +1,36 @@
import { includes, isEmpty, toUpper, trim } from 'lodash'
/**
* join execute path and arguments into a command string
* @param {string} path
* @param {string[]} args
* @param {string} [emptyContent]
* @return {string}
*/
export const joinCommand = (path, args = [], emptyContent = '-') => {
let cmd = ''
path = trim(path)
if (!isEmpty(path)) {
let containValuePlaceholder = false
cmd = includes(path, ' ') ? `"${path}"` : path
for (let part of args) {
part = trim(part)
if (isEmpty(part)) {
continue
}
if (includes(part, ' ')) {
cmd += ' "' + part + '"'
} else {
if (toUpper(part) === '{VALUE}') {
part = '{VALUE}'
containValuePlaceholder = true
}
cmd += ' ' + part
}
}
if (!containValuePlaceholder) {
cmd += ' {VALUE}'
}
}
return cmd || emptyContent
}