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 {
case "string":
var str string
@ -782,7 +784,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: val,
})
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
}
}
@ -829,7 +831,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: strutil.EncodeRedisKey(loadedVal[i+1]),
})
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
}
}
@ -854,7 +856,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
items[i/2].Key = loadedVal[i]
items[i/2].Value = strutil.EncodeRedisKey(loadedVal[i+1])
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
}
}
@ -899,7 +901,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: val,
})
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
}
}
@ -919,7 +921,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
for i, val := range loadedKey {
items[i].Value = val
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
}
}
@ -967,7 +969,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Score: score,
})
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
}
}
@ -1001,7 +1003,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
Value: val,
})
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
}
}
@ -1062,7 +1064,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
if vb, merr := json.Marshal(msg.Values); merr != nil {
it.DisplayValue = "{}"
} 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) {
continue
@ -1096,7 +1098,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
// blank format indicate auto format
func (b *browserService) ConvertValue(value any, decode, format string) (resp types.JSResp) {
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.Data = map[string]any{
"value": value,
@ -1139,7 +1141,7 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
return
} else {
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())
return
}
@ -1250,12 +1252,13 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe
key := strutil.DecodeRedisKey(param.Key)
str := strutil.DecodeRedisKey(param.Value)
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())
return
}
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 replaced []types.HashReplaceItem
@ -1473,7 +1476,8 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
} else {
// replace index value
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())
return
}
@ -1484,7 +1488,7 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
}
var displayStr string
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{
Index: param.Index,
@ -1574,8 +1578,9 @@ func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp types.JSRe
// insert new value
str = strutil.DecodeRedisKey(param.NewValue)
decoder := Preferences().GetDecoder()
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())
return
}
@ -1583,7 +1588,7 @@ func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp types.JSRe
// add new item
var displayStr string
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{
Value: saveStr,
@ -1620,6 +1625,7 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
var added, updated, removed []types.ZSetEntryItem
var replaced []types.ZSetReplaceItem
var affect int64
decoder := Preferences().GetDecoder()
if len(newVal) <= 0 {
// no new value, delete value
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 {
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())
return
}
@ -1640,7 +1646,7 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
Score: param.Score,
Member: saveVal,
}).Result()
displayValue, _, _ := convutil.ConvertTo(val, param.RetDecode, param.RetFormat)
displayValue, _, _ := convutil.ConvertTo(val, param.RetDecode, param.RetFormat, decoder)
if affect > 0 {
// add new item
added = append(added, types.ZSetEntryItem{
@ -1668,7 +1674,7 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
Score: param.Score,
Member: saveVal,
}).Result()
displayValue, _, _ := convutil.ConvertTo(saveVal, param.RetDecode, param.RetFormat)
displayValue, _, _ := convutil.ConvertTo(saveVal, param.RetDecode, param.RetFormat, decoder)
if affect <= 0 {
// no new value added, just update exists item
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]
}
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.Data = struct {

View File

@ -13,11 +13,14 @@ import (
storage2 "tinyrdm/backend/storage"
"tinyrdm/backend/types"
"tinyrdm/backend/utils/coll"
convutil "tinyrdm/backend/utils/convert"
sliceutil "tinyrdm/backend/utils/slice"
)
type preferencesService struct {
pref *storage2.PreferencesStorage
clientVersion string
customDecoder []convutil.CmdConvert
}
var preferences *preferencesService
@ -182,6 +185,23 @@ func (p *preferencesService) GetScanSize() int {
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 {
Name string `json:"name"`
TagName string `json:"tag_name"`

View File

@ -3,10 +3,11 @@ 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"`
Cli PreferencesCli `json:"cli" yaml:"cli"`
Behavior PreferencesBehavior `json:"behavior" yaml:"behavior"`
General PreferencesGeneral `json:"general" yaml:"general"`
Editor PreferencesEditor `json:"editor" yaml:"editor"`
Cli PreferencesCli `json:"cli" yaml:"cli"`
Decoder []PreferencesDecoder `json:"decoder" yaml:"decoder,omitempty"`
}
func NewPreferences() Preferences {
@ -33,6 +34,7 @@ func NewPreferences() Preferences {
FontSize: consts.DEFAULT_FONT_SIZE,
CursorStyle: "block",
},
Decoder: []PreferencesDecoder{},
}
}
@ -72,3 +74,13 @@ type PreferencesCli struct {
FontSize int `json:"fontSize" yaml:"font_size"`
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
// @param decodeType 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 {
// empty content
if len(formatType) <= 0 {
@ -46,13 +47,17 @@ func ConvertTo(str, decodeType, formatType string) (value, resultDecode, resultF
}
// decode first
value, resultDecode = decodeWith(str, decodeType)
value, resultDecode = decodeWith(str, decodeType, customDecoder)
// then format content
value, resultFormat = viewAs(value, formatType)
if len(formatType) <= 0 {
value, resultFormat = autoViewAs(value)
} else {
value, resultFormat = viewAs(value, formatType)
}
return
}
func decodeWith(str, decodeType string) (value, resultDecode string) {
func decodeWith(str, decodeType string, customDecoder []CmdConvert) (value, resultDecode string) {
if len(decodeType) > 0 {
switch decodeType {
case types.DECODE_NONE:
@ -99,17 +104,31 @@ func decodeWith(str, decodeType string) (value, resultDecode string) {
} else {
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
return
}
return autoDecode(str)
value, resultDecode = autoDecode(str, customDecoder)
return
}
// attempt try possible decode method
// 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 {
// pure digit content may incorrect regard as some encoded type, skip decode
if match, _ := regexp.MatchString(`^\d+$`, str); !match {
@ -147,6 +166,16 @@ func autoDecode(str string) (value, resultDecode string) {
resultDecode = types.DECODE_MSGPACK
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) {
if len(formatType) > 0 {
switch formatType {
default:
fallthrough
case types.FORMAT_RAW, types.FORMAT_YAML, types.FORMAT_XML:
value = str
@ -185,8 +216,7 @@ func viewAs(str, formatType string) (value, resultFormat string) {
resultFormat = formatType
return
}
return autoViewAs(str)
return
}
// attempt automatic convert to possible types
@ -222,7 +252,7 @@ func autoViewAs(str string) (value, resultFormat string) {
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
switch format {
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")
}
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
}

View File

@ -2,7 +2,6 @@ package convutil
import (
"gopkg.in/yaml.v3"
"log"
)
type YamlConvert struct{}
@ -14,10 +13,5 @@ func (YamlConvert) Encode(str string) (string, bool) {
func (YamlConvert) Decode(str string) (string, bool) {
var obj map[string]any
err := yaml.Unmarshal([]byte(str), &obj)
if err != nil {
log.Println(err.Error())
} else {
log.Println(obj)
}
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 ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue'
import { Info } from 'wailsjs/go/services/systemService.js'
import DecoderDialog from '@/components/dialogs/DecoderDialog.vue'
const prefStore = usePreferencesStore()
const connectionStore = useConnectionStore()
@ -79,6 +80,7 @@ watch(
<flush-db-dialog />
<set-ttl-dialog />
<preferences-dialog />
<decoder-dialog />
<about-dialog />
</n-dialog-provider>
</n-config-provider>

View File

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

View File

@ -35,6 +35,7 @@ const handleSelectFile = async () => {
<n-input
:disabled="props.disabled"
:placeholder="placeholder"
:title="props.value"
:value="props.value"
clearable
@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 Conversion from '@/components/icons/Conversion.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({
decode: {
@ -17,13 +19,35 @@ const props = defineProps({
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 onFormatChanged = (selDecode, selFormat) => {
if (!some(decodeTypes, (val) => val === selDecode)) {
const [buildin, external] = decodeTypeOption.value
if (!some([...buildin, ...external], (val) => val === selDecode)) {
selDecode = decodeTypes.NONE
}
if (!some(formatTypes, (val) => val === selFormat)) {
selFormat = formatTypes.RAW
// set to auto chose format
selFormat = ''
}
emit('formatChanged', selDecode, selFormat)
if (selDecode !== props.decode) {
@ -41,7 +65,7 @@ const onFormatChanged = (selDecode, selFormat) => {
:default="formatTypes.RAW"
:disabled="props.disabled"
:icon="Code"
:options="formatTypes"
:options="formatTypeOption"
:tooltip="$t('interface.view_as')"
:value="props.format"
@update:value="(f) => onFormatChanged(props.decode, f)" />
@ -50,10 +74,10 @@ const onFormatChanged = (selDecode, selFormat) => {
:default="decodeTypes.NONE"
:disabled="props.disabled"
:icon="Conversion"
:options="decodeTypes"
:options="decodeTypeOption"
:tooltip="$t('interface.decode_with')"
:value="props.decode"
@update:value="(d) => onFormatChanged(d, props.format)" />
@update:value="(d) => onFormatChanged(d, '')" />
</n-space>
</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>
import { computed, ref, watchEffect } from 'vue'
import { computed, h, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import useDialog from 'stores/dialog'
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 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()
@ -23,6 +30,8 @@ const initPreferences = async () => {
prevPreferences.value = {
general: prefStore.general,
editor: prefStore.editor,
cli: prefStore.cli,
decoder: prefStore.decoder,
}
} finally {
loading.value = false
@ -43,6 +52,99 @@ const keyOptions = computed(() => {
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 success = await prefStore.savePreferences()
if (success) {
@ -68,11 +170,17 @@ const onClose = () => {
:show-icon="false"
:title="$t('preferences.name')"
preset="dialog"
style="width: 500px"
style="width: 640px"
transform-origin="center">
<!-- FIXME: set loading will slow down appear animation of dialog in linux -->
<!-- <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-form :disabled="loading" :model="prefStore.general" :show-require-mark="false" label-placement="top">
<n-grid :x-gap="10">
@ -230,6 +338,23 @@ const onClose = () => {
</n-grid>
</n-form>
</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-spin> -->

View File

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

View File

@ -62,6 +62,14 @@
"cursor_style_block": "Block",
"cursor_style_underline": "Underline",
"cursor_style_bar": "Bar"
},
"decoder": {
"name": "Custom Decoder",
"new": "New Decoder",
"decoder_name": "Name",
"cmd_preview": "Preview",
"status": "Status",
"path": "Decoder Execution Path"
}
},
"interface": {
@ -339,6 +347,20 @@
"quick_set": "Quick Settings",
"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": {
"title": "New Version Available",
"new_version_tip": "A new version({ver}) is available. Download now?",

View File

@ -62,6 +62,14 @@
"cursor_style_block": "方块",
"cursor_style_underline": "下划线",
"cursor_style_bar": "竖线"
},
"decoder": {
"name": "自定义解码",
"new": "新增自定义解码",
"decoder_name": "解码器名称",
"cmd_preview": "命令预览",
"status": "状态",
"auto_enabled": "已加入自动解码"
}
},
"interface": {
@ -339,6 +347,20 @@
"quick_set": "快捷设置",
"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": {
"title": "有可用新版本",
"new_version_tip": "新版本({ver}),是否立即下载",

View File

@ -91,6 +91,16 @@ const useDialogStore = defineStore('dialog', {
ttl: 0,
},
decodeDialogVisible: false,
decodeParam: {
name: '',
auto: true,
decodePath: '',
decodeArgs: [],
encodePath: '',
encodeArgs: [],
},
preferencesDialogVisible: false,
aboutDialogVisible: false,
}),
@ -290,6 +300,36 @@ const useDialogStore = defineStore('dialog', {
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() {
this.preferencesDialogVisible = true
},

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
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 {
CheckForUpdate,
GetFontList,
@ -64,6 +64,7 @@ const usePreferencesStore = defineStore('preferences', {
fontSize: 14,
cursorStyle: 'block',
},
decoder: [],
lastPref: {},
fontList: [],
}),
@ -306,7 +307,7 @@ const usePreferencesStore = defineStore('preferences', {
* @returns {Promise<boolean>}
*/
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)
return success === true
},
@ -335,6 +336,81 @@ const usePreferencesStore = defineStore('preferences', {
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) {
let msgRef = null
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
}