mirror of
https://github.com/tiny-craft/tiny-rdm.git
synced 2025-04-06 20:38:05 +08:00
Compare commits
238 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3860415429 | ||
|
dec906a69b | ||
|
118b9b8870 | ||
|
864bd74029 | ||
|
aedf4d4222 | ||
|
a4d7c598f9 | ||
|
480e4cc7fd | ||
|
1016df4a25 | ||
|
46d7ba2a9f | ||
|
19c8368dff | ||
|
1c322fdac5 | ||
|
53e8c26380 | ||
|
400a908cf9 | ||
|
edd77182a5 | ||
|
6e44c64441 | ||
|
51beb7249f | ||
|
79fd2a6d39 | ||
|
30c3decd65 | ||
|
23fc32e92f | ||
|
f458a1a0e4 | ||
|
52aaad6339 | ||
|
dd70d6b595 | ||
|
5d425aadb1 | ||
|
c02a24ee94 | ||
|
e03fc8ad28 | ||
|
3367f13d80 | ||
|
b601ba255b | ||
|
ccb4bb85ae | ||
|
b0dfe348bd | ||
|
a3a1a17af3 | ||
|
ca9f0a08e1 | ||
|
3f5b63a36f | ||
|
ab3560fc2b | ||
|
cb428747e2 | ||
|
aa3383db43 | ||
|
026591c8d4 | ||
|
a70b5b56ff | ||
|
eaa68df583 | ||
|
2388f309d8 | ||
|
a9c7cb1cd2 | ||
|
b506e8a6a4 | ||
|
c082a0c41f | ||
|
970ebcf902 | ||
|
b223feb441 | ||
|
469a62333f | ||
|
c38944e948 | ||
|
5efbd4d316 | ||
|
c54567115e | ||
|
a14e7e947e | ||
|
868b0c81b6 | ||
|
a3cb09863a | ||
|
b26f5d2bde | ||
|
0038092193 | ||
|
0739cb8b68 | ||
|
70c38d9aa7 | ||
|
237b25086c | ||
|
71dbda01da | ||
|
5deb6ce443 | ||
|
ee398d4d98 | ||
|
29b51f836f | ||
|
ea8ceba32a | ||
|
9bec3934bb | ||
|
fdfd04d4bf | ||
|
410dcd9e57 | ||
|
ea44253c02 | ||
|
e2093a89bf | ||
|
908d4c7007 | ||
|
6843314bad | ||
|
bdfa31e4b6 | ||
|
aa8c5495c1 | ||
|
65cfdd1bcc | ||
|
6bd1b23a64 | ||
|
8c30daec15 | ||
|
86f42fcc10 | ||
|
1bcde26e35 | ||
|
bf71c6db0e | ||
|
88e2c6cb43 | ||
|
6eeb701439 | ||
|
028a240f49 | ||
|
eefa7b1346 | ||
|
3321fbf6fd | ||
|
4ed93902a6 | ||
|
b4405eb7db | ||
|
04bc103583 | ||
|
f17bb744f4 | ||
|
152fbe962f | ||
|
a2b0fc183f | ||
|
f536b0f23b | ||
|
3c43f960c3 | ||
|
78bfaf6e16 | ||
|
50bec33870 | ||
|
f0c9b74545 | ||
|
e5fed29427 | ||
|
4dd52a8c8e | ||
|
abf5534165 | ||
|
455a911154 | ||
|
e2264b33b0 | ||
|
84b493b26a | ||
|
c9e618d418 | ||
|
e8f76ce8ae | ||
|
70354c14ec | ||
|
d472836d5f | ||
|
f00a959db3 | ||
|
aa98da31d6 | ||
|
18ba04a5b1 | ||
|
c9beceab76 | ||
|
6abf4823a6 | ||
|
2133fe44ca | ||
|
8139481ea7 | ||
|
d41421389c | ||
|
1cf126e78d | ||
|
4d29343c1f | ||
|
a682aabb0b | ||
|
971c89a5cf | ||
|
b72855b707 | ||
|
974477cb49 | ||
|
1bf4b0eee1 | ||
|
33caf873d6 | ||
|
fc0deac074 | ||
|
eb5f69bcbc | ||
|
28c326d608 | ||
|
83319c06d7 | ||
|
dbe3d995b4 | ||
|
8ab82534e1 | ||
|
fc67590bde | ||
|
2e28c1b44b | ||
|
c7c647d728 | ||
|
4fd73411de | ||
|
b8e1c2fca4 | ||
|
cda10ed279 | ||
|
2e620f7050 | ||
|
c0fdfc0ce7 | ||
|
d1958a3290 | ||
|
668620425f | ||
|
cc696f9642 | ||
|
adf9e4347f | ||
|
41d0dc6ffc | ||
|
d99a397ae3 | ||
|
1091f5d34c | ||
|
35c6918b9b | ||
|
006f3b2a05 | ||
|
d2e3392283 | ||
|
3900052adb | ||
|
bb9c25a880 | ||
|
af6748ac59 | ||
|
66715d05a5 | ||
|
656e18afe5 | ||
|
eca6bd523e | ||
|
13b9a38095 | ||
|
de7cdb5bd4 | ||
|
e3ccb06a96 | ||
|
8e48da3cc7 | ||
|
635cd6ca8b | ||
|
aa291f742d | ||
|
2d2954d81c | ||
|
fdf2c477f2 | ||
|
f3c5065259 | ||
|
78f3abaf5e | ||
|
6430deeaea | ||
|
a13bf788eb | ||
|
b745b9f45d | ||
|
b46cc6c436 | ||
|
00b1efc43d | ||
|
f8ef25a323 | ||
|
bb4ab109e7 | ||
|
3a799b7b4a | ||
|
71ffbde648 | ||
|
9bd958ef24 | ||
|
ad2c4c432b | ||
|
c4d41b12dc | ||
|
53563acac0 | ||
|
23b68d9e19 | ||
|
9a10aba67e | ||
|
a431467b5f | ||
|
78b7700b1b | ||
|
28d2336124 | ||
|
2250e15971 | ||
|
70ed556e80 | ||
|
13e80da978 | ||
|
64ae79f565 | ||
|
cb9a9ebb8a | ||
|
434568e66d | ||
|
7cf842ed4c | ||
|
3057012710 | ||
|
1c4462b161 | ||
|
30835ac469 | ||
|
2405a79ace | ||
|
e92eb525e7 | ||
|
094705e87d | ||
|
f1e1a89baf | ||
|
29ffd83486 | ||
|
df71ac7049 | ||
|
32f1b71073 | ||
|
74a6b9b0e1 | ||
|
09264134ec | ||
|
a2331675d7 | ||
|
e1f022908c | ||
|
c0415fe23d | ||
|
e271eafc9e | ||
|
a8109c4bb2 | ||
|
ce1b9b706f | ||
|
450e451781 | ||
|
7faca878a3 | ||
|
74d789ac8e | ||
|
fd58357a04 | ||
|
f09ee89a96 | ||
|
7a579d0d0b | ||
|
f94134faa0 | ||
|
581a1b79ca | ||
|
b361e9b0be | ||
|
4032c80add | ||
|
f3a43c8083 | ||
|
b610bd7861 | ||
|
5a86bab647 | ||
|
fe2f8a0480 | ||
|
7cbdc83884 | ||
|
124d221b9f | ||
|
edaef2a78c | ||
|
d75635bf70 | ||
|
0b37b89f9b | ||
|
cdac3c4496 | ||
|
655cd539ca | ||
|
76783c36fb | ||
|
3c7003291c | ||
|
4cbc0b98e7 | ||
|
a679858478 | ||
|
94d642c4de | ||
|
ff2043c0e2 | ||
|
c2c1e9cfba | ||
|
e3e0ed0a91 | ||
|
74ab68b280 | ||
|
cee25a8015 | ||
|
bbcbc8b661 | ||
|
673159dc85 | ||
|
700a54081b | ||
|
9564272fd8 | ||
|
a2336b4fc8 | ||
|
6aca08de3e |
89
.github/workflows/release-linux.yaml
vendored
89
.github/workflows/release-linux.yaml
vendored
@ -3,6 +3,12 @@ name: Release Linux App
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ published ]
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Version tag'
|
||||||
|
required: true
|
||||||
|
default: '1.0.0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@ -12,6 +18,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@ -23,12 +30,26 @@ jobs:
|
|||||||
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g')
|
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g')
|
||||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Normalise platform arch
|
||||||
|
id: normalise_platform_arch
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.platform }}" == "linux/amd64" ]; then
|
||||||
|
echo "arch=x86_64" >> "$GITHUB_OUTPUT"
|
||||||
|
elif [ "${{ matrix.platform }}" == "linux/aarch64" ]; then
|
||||||
|
echo "arch=aarch64" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Normalise version tag
|
- name: Normalise version tag
|
||||||
id: normalise_version
|
id: normalise_version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
if [ "${{ github.event.release.tag_name }}" == "" ]; then
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
version=$(echo ${{ github.event.inputs.tag }} | sed -e 's/v//g')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@ -43,7 +64,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libfuse-dev libfuse2
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
@ -65,7 +86,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
||||||
-ldflags "-X main.version=${{ github.event.release.tag_name }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.LINUX_GA_SECRET }}" \
|
-ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.LINUX_GA_SECRET }}" \
|
||||||
-o tiny-rdm
|
-o tiny-rdm
|
||||||
|
|
||||||
- name: Setup control template
|
- name: Setup control template
|
||||||
@ -98,11 +119,57 @@ jobs:
|
|||||||
sed -i 's/0.0.0/${{ steps.normalise_version.outputs.version }}/g' "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64/DEBIAN/control"
|
sed -i 's/0.0.0/${{ steps.normalise_version.outputs.version }}/g' "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64/DEBIAN/control"
|
||||||
dpkg-deb --build -Zxz "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"
|
dpkg-deb --build -Zxz "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"
|
||||||
|
|
||||||
- name: Upload release asset
|
- name: Package up appimage file
|
||||||
shell: bash
|
|
||||||
working-directory: ./build/linux/
|
|
||||||
run: |
|
run: |
|
||||||
filepath="tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64.deb"
|
curl https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-${{ steps.normalise_platform_arch.outputs.arch }}.AppImage \
|
||||||
filename="tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.deb"
|
-o linuxdeploy \
|
||||||
upload_url="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${{ github.event.release.id }}/assets"
|
-L
|
||||||
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary @$filepath "$upload_url?name=$filename"
|
chmod u+x linuxdeploy
|
||||||
|
|
||||||
|
./linuxdeploy --appdir AppDir
|
||||||
|
|
||||||
|
pushd AppDir
|
||||||
|
# Copy WebKit files.
|
||||||
|
find /usr/lib* -name WebKitNetworkProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
|
||||||
|
find /usr/lib* -name WebKitWebProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
|
||||||
|
find /usr/lib* -name libwebkit2gtkinjectedbundle.so -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
|
||||||
|
popd
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p AppDir/usr/share/icons/hicolor/512x512/apps
|
||||||
|
build_dir="build/linux/tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"
|
||||||
|
|
||||||
|
cp -r $build_dir/usr/share/icons/hicolor/512x512/apps/tiny-rdm.png AppDir/usr/share/icons/hicolor/512x512/apps/
|
||||||
|
cp $build_dir/usr/local/bin/tiny-rdm AppDir/usr/bin/
|
||||||
|
|
||||||
|
|
||||||
|
sed -i 's#/usr/local/bin/tiny-rdm#tiny-rdm#g' $build_dir/usr/share/applications/tiny-rdm.desktop
|
||||||
|
|
||||||
|
curl -o linuxdeploy-plugin-gtk.sh "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh"
|
||||||
|
|
||||||
|
sed -i '/XDG_DATA_DIRS/a export WEBKIT_DISABLE_COMPOSITING_MODE=1' linuxdeploy-plugin-gtk.sh
|
||||||
|
chmod +x linuxdeploy-plugin-gtk.sh
|
||||||
|
|
||||||
|
curl -o AppDir/AppRun https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${{ steps.normalise_platform_arch.outputs.arch }} -L
|
||||||
|
|
||||||
|
./linuxdeploy --appdir AppDir \
|
||||||
|
--output=appimage \
|
||||||
|
--plugin=gtk \
|
||||||
|
-e $build_dir/usr/local/bin/tiny-rdm \
|
||||||
|
-d $build_dir/usr/share/applications/tiny-rdm.desktop
|
||||||
|
|
||||||
|
- name: Rename deb
|
||||||
|
working-directory: ./build/linux
|
||||||
|
run: mv "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64.deb" "tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.deb"
|
||||||
|
|
||||||
|
- name: Rename appimage
|
||||||
|
run: mv Tiny_RDM-${{ steps.normalise_platform_arch.outputs.arch }}.AppImage "tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.AppImage"
|
||||||
|
|
||||||
|
- name: Upload release asset
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: v${{ steps.normalise_version.outputs.version }}
|
||||||
|
files: |
|
||||||
|
./build/linux/tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.deb
|
||||||
|
tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.AppImage
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
33
.github/workflows/release-macos.yaml
vendored
33
.github/workflows/release-macos.yaml
vendored
@ -3,6 +3,12 @@ name: Release macOS App
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ published ]
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Version tag'
|
||||||
|
required: true
|
||||||
|
default: '1.0.0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@ -29,8 +35,13 @@ jobs:
|
|||||||
id: normalise_version
|
id: normalise_version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
if [ "${{ github.event.release.tag_name }}" == "" ]; then
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
version=$(echo ${{ github.event.inputs.tag }} | sed -e 's/v//g')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@ -71,7 +82,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
||||||
-ldflags "-X main.version=${{ github.event.release.tag_name }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.MAC_GA_SECRET }}"
|
-ldflags "-X main.version=${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.MAC_GA_SECRET }}"
|
||||||
|
|
||||||
# - name: Notarise macOS app + create dmg
|
# - name: Notarise macOS app + create dmg
|
||||||
# shell: bash
|
# shell: bash
|
||||||
@ -109,11 +120,13 @@ jobs:
|
|||||||
"bin/TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg" \
|
"bin/TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg" \
|
||||||
"bin"
|
"bin"
|
||||||
|
|
||||||
|
- name: Rename dmg
|
||||||
|
working-directory: ./build/bin
|
||||||
|
run: mv "TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg" "TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.dmg"
|
||||||
|
|
||||||
- name: Upload release asset (DMG Package)
|
- name: Upload release asset (DMG Package)
|
||||||
shell: bash
|
uses: softprops/action-gh-release@v1
|
||||||
working-directory: ./build/bin/
|
with:
|
||||||
run: |
|
tag_name: v${{ steps.normalise_version.outputs.version }}
|
||||||
filepath="TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg"
|
files: ./build/bin/TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.dmg
|
||||||
filename="TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.dmg"
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
upload_url="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${{ github.event.release.id }}/assets"
|
|
||||||
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/x-apple-diskimage" --data-binary @$filepath "$upload_url?name=$filename"
|
|
||||||
|
69
.github/workflows/release-windows.yaml
vendored
69
.github/workflows/release-windows.yaml
vendored
@ -3,6 +3,12 @@ name: Release Windows App
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ published ]
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Version tag'
|
||||||
|
required: true
|
||||||
|
default: '1.0.0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@ -12,6 +18,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- windows/amd64
|
- windows/amd64
|
||||||
|
- windows/arm64
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@ -20,15 +27,27 @@ jobs:
|
|||||||
id: normalise_platform
|
id: normalise_platform
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g')
|
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g' -e 's/amd64/x64/g')
|
||||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Normalise platform name
|
||||||
|
id: normalise_platform_name
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pname=$(echo "${{ matrix.platform }}" | sed 's/windows\///g')
|
||||||
|
echo "pname=$pname" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Normalise version tag
|
- name: Normalise version tag
|
||||||
id: normalise_version
|
id: normalise_version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
if [ "${{ github.event.release.tag_name }}" == "" ]; then
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
version=$(echo ${{ github.event.inputs.tag }} | sed -e 's/v//g')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@ -38,7 +57,7 @@ jobs:
|
|||||||
- name: Install chocolatey
|
- name: Install chocolatey
|
||||||
uses: crazy-max/ghaction-chocolatey@v2
|
uses: crazy-max/ghaction-chocolatey@v2
|
||||||
with:
|
with:
|
||||||
args: install nsis jq upx
|
args: install nsis jq
|
||||||
|
|
||||||
- name: Install wails
|
- name: Install wails
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -64,27 +83,29 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} \
|
CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} \
|
||||||
-upx -webview2 embed \
|
-webview2 embed \
|
||||||
-ldflags "-X main.version=${{ github.event.release.tag_name }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.WINDOWS_GA_SECRET }}"
|
-ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.WINDOWS_GA_SECRET }}"
|
||||||
|
|
||||||
- name: Compress portable binary
|
- name: Compress portable binary
|
||||||
working-directory: ./build/bin
|
working-directory: ./build/bin
|
||||||
run: Compress-Archive "Tiny RDM.exe" tiny-rdm.zip
|
run: Compress-Archive "Tiny RDM.exe" "TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip"
|
||||||
|
|
||||||
- name: Upload release asset (Portable)
|
- name: Upload release asset (Portable)
|
||||||
shell: bash
|
uses: softprops/action-gh-release@v1
|
||||||
working-directory: ./build/bin
|
with:
|
||||||
run: |
|
tag_name: v${{ steps.normalise_version.outputs.version }}
|
||||||
filepath="tiny-rdm.zip"
|
files: ./build/bin/TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip
|
||||||
filename="TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip"
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
upload_url="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${{ github.event.release.id }}/assets"
|
|
||||||
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/zip" --data-binary @$filepath "$upload_url?name=$filename"
|
|
||||||
|
|
||||||
- name: Build Windows NSIS installer
|
- name: Build Windows NSIS installer
|
||||||
shell: bash
|
shell: bash
|
||||||
run: CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} -nsis -upx -webview2 embed -ldflags "-X main.version=${{ github.event.release.tag_name }}"
|
run: |
|
||||||
|
CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} \
|
||||||
|
-nsis -webview2 embed \
|
||||||
|
-ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }}"
|
||||||
|
|
||||||
- name: Codesign Windows NSIS installer
|
- name: Codesign Windows NSIS installer
|
||||||
|
shell: powershell
|
||||||
working-directory: ./build/bin
|
working-directory: ./build/bin
|
||||||
run: |
|
run: |
|
||||||
echo "Creating certificate file"
|
echo "Creating certificate file"
|
||||||
@ -92,13 +113,15 @@ jobs:
|
|||||||
Set-Content -Path certificate\certificate.txt -Value '${{ secrets.WIN_SIGNING_CERT }}'
|
Set-Content -Path certificate\certificate.txt -Value '${{ secrets.WIN_SIGNING_CERT }}'
|
||||||
certutil -decode certificate\certificate.txt certificate\certificate.pfx
|
certutil -decode certificate\certificate.txt certificate\certificate.pfx
|
||||||
echo "Signing TinyRDM installer"
|
echo "Signing TinyRDM installer"
|
||||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /fd sha256 /tr http://ts.ssl.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' TinyRDM-amd64-installer.exe
|
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /fd sha256 /tr http://timestamp.digicert.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' TinyRDM-${{ steps.normalise_platform_name.outputs.pname }}-installer.exe
|
||||||
|
|
||||||
|
- name: Rename installer
|
||||||
|
working-directory: ./build/bin
|
||||||
|
run: Rename-Item -Path "TinyRDM-${{ steps.normalise_platform_name.outputs.pname }}-installer.exe" -NewName "TinyRDM_Setup_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.exe"
|
||||||
|
|
||||||
- name: Upload release asset (Installer)
|
- name: Upload release asset (Installer)
|
||||||
shell: bash
|
uses: softprops/action-gh-release@v1
|
||||||
working-directory: ./build/bin/
|
with:
|
||||||
run: |
|
tag_name: v${{ steps.normalise_version.outputs.version }}
|
||||||
filepath="TinyRDM-amd64-installer.exe"
|
files: ./build/bin/TinyRDM_Setup_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.exe
|
||||||
filename="TinyRDM_Setup_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.exe"
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
upload_url="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${{ github.event.release.id }}/assets"
|
|
||||||
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary @$filepath "$upload_url?name=$filename"
|
|
||||||
|
27
README.md
27
README.md
@ -3,7 +3,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1 align="center">Tiny RDM</h1>
|
<h1 align="center">Tiny RDM</h1>
|
||||||
<h4 align="center"><strong>English</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">
|
<h4 align="center"><strong>English</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">
|
||||||
简体中文</a></h4>
|
简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a></h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
||||||
@ -37,7 +37,7 @@ Linux.</strong>
|
|||||||
* Provides visually and user-friendly UI, light and dark themes (Thanks to [Naive UI](https://github.com/tusen-ai/naive-ui)
|
* Provides visually and user-friendly UI, light and dark themes (Thanks to [Naive UI](https://github.com/tusen-ai/naive-ui)
|
||||||
and [IconPark](https://iconpark.oceanengine.com)).
|
and [IconPark](https://iconpark.oceanengine.com)).
|
||||||
* Multi-language support ([Need more languages ? Click here to contribute](.github/CONTRIBUTING.md)).
|
* Multi-language support ([Need more languages ? Click here to contribute](.github/CONTRIBUTING.md)).
|
||||||
* Better connection management: supports SSH Tunnel/SSL/Sentinel Mode/Cluster Mode.
|
* Better connection management: supports SSH Tunnel/SSL/Sentinel Mode/Cluster Mode/HTTP proxy/SOCKS5 proxy.
|
||||||
* Visualize key value operations, CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets, and Streams.
|
* Visualize key value operations, CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets, and Streams.
|
||||||
* Support multiple data viewing format and decode/decompression methods.
|
* Support multiple data viewing format and decode/decompression methods.
|
||||||
* Use SCAN for segmented loading, making it easy to list millions of keys.
|
* Use SCAN for segmented loading, making it easy to list millions of keys.
|
||||||
@ -49,11 +49,9 @@ Linux.</strong>
|
|||||||
* Integrate with Monaco Editor
|
* Integrate with Monaco Editor
|
||||||
* Support real-time commands monitoring.
|
* Support real-time commands monitoring.
|
||||||
* Support import/export data.
|
* Support import/export data.
|
||||||
|
* Support publish/subscribe.
|
||||||
## Roadmap
|
* Support import/export connection profile.
|
||||||
|
* Custom data encoder and decoder for value display ([Here are the instructions](https://redis.tinycraft.cc/guide/custom-decoder/)).
|
||||||
- [ ] Pub/Sub operations
|
|
||||||
- [ ] Import/export connection profile
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -72,25 +70,32 @@ Available to download for free from [here](https://github.com/tiny-craft/tiny-rd
|
|||||||
* Node.js >= 16
|
* Node.js >= 16
|
||||||
* NPM >= 9
|
* NPM >= 9
|
||||||
|
|
||||||
### Install wails
|
### Install Wails
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clone the code
|
### Pull the Code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
|
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build frontend
|
### Build Frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --prefix ./frontend
|
npm install --prefix ./frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and run
|
or
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wails dev
|
wails dev
|
||||||
|
111
README_ja.md
Normal file
111
README_ja.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
|
||||||
|
</div>
|
||||||
|
<h1 align="center">Tiny RDM</h1>
|
||||||
|
<h4 align="center"><strong><a href="/">English</a></strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | 日本語</h4>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
||||||
|
[](https://github.com/tiny-craft/tiny-rdm/releases)
|
||||||
|

|
||||||
|
[](https://github.com/tiny-craft/tiny-rdm/stargazers)
|
||||||
|
[](https://github.com/tiny-craft/tiny-rdm/fork)
|
||||||
|
[](https://discord.gg/VTFbBMGjWh)
|
||||||
|
[](https://twitter.com/Lykin53448)
|
||||||
|
|
||||||
|
<strong>Tiny RDMは、Mac、Windows、Linuxで利用可能な、モダンで軽量なクロスプラットフォームのRedisデスクトップマネージャーです。</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
|
||||||
|
<img alt="screenshot" src="screenshots/dark_en.png">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
|
||||||
|
<img alt="screenshot" src="screenshots/dark_en2.png">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
## 特徴
|
||||||
|
|
||||||
|
* 超軽量、Webview2をベースにしており、埋め込みブラウザなし([Wails](https://github.com/wailsapp/wails)に感謝)。
|
||||||
|
* 視覚的でユーザーフレンドリーなUI、ライトとダークテーマを提供([Naive UI](https://github.com/tusen-ai/naive-ui)と[IconPark](https://iconpark.oceanengine.com)に感謝)。
|
||||||
|
* 多言語サポート([もっと多くの言語が必要ですか?ここをクリックして貢献してください](.github/CONTRIBUTING.md))。
|
||||||
|
* より良い接続管理:SSHトンネル/SSL/センチネルモード/クラスターモード/HTTPプロキシ/SOCKS5プロキシをサポート。
|
||||||
|
* キー値操作の可視化、リスト、ハッシュ、文字列、セット、ソートセット、ストリームのCRUDサポート。
|
||||||
|
* 複数のデータ表示形式とデコード/解凍方法をサポート。
|
||||||
|
* SCANを使用してセグメント化された読み込みを行い、数百万のキーを簡単にリスト化。
|
||||||
|
* コマンド操作履歴のログリスト。
|
||||||
|
* コマンドラインモードを提供。
|
||||||
|
* スローログリストを提供。
|
||||||
|
* リスト/ハッシュ/セット/ソートセットのセグメント化された読み込みとクエリ。
|
||||||
|
* リスト/ハッシュ/セット/ソートセットの値のデコード/解凍を提供。
|
||||||
|
* Monaco Editorと統合。
|
||||||
|
* リアルタイムコマンド監視をサポート。
|
||||||
|
* データのインポート/エクスポートをサポート。
|
||||||
|
* パブリッシュ/サブスクライブをサポート。
|
||||||
|
* 接続プロファイルのインポート/エクスポートをサポート。
|
||||||
|
* 値表示のためのカスタムデータエンコーダーとデコーダーをサポート([こちらが手順です](https://redis.tinycraft.cc/guide/custom-decoder/))。
|
||||||
|
|
||||||
|
## インストール
|
||||||
|
|
||||||
|
[こちら](https://github.com/tiny-craft/tiny-rdm/releases)から無料でダウンロードできます。
|
||||||
|
|
||||||
|
> macOSにインストール後に開けない場合は、以下のコマンドを実行してから再度開いてください:
|
||||||
|
> ``` shell
|
||||||
|
> sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
|
||||||
|
> ```
|
||||||
|
|
||||||
|
## ビルドガイドライン
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
|
|
||||||
|
* Go(最新バージョン)
|
||||||
|
* Node.js >= 16
|
||||||
|
* NPM >= 9
|
||||||
|
|
||||||
|
### Wailsのインストール
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### コードの取得
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### フロントエンドのビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --prefix ./frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
または
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### コンパイルと実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wails dev
|
||||||
|
```
|
||||||
|
## について
|
||||||
|
|
||||||
|
### Wechat公式アカウント
|
||||||
|
|
||||||
|
<img src="docs/images/wechat_official.png" alt="wechat" width="360" />
|
||||||
|
|
||||||
|
### スポンサー
|
||||||
|
|
||||||
|
このプロジェクトが役立つ場合は、コーヒーを一杯おごってください ☕️。
|
||||||
|
|
||||||
|
* Wechatスポンサー
|
||||||
|
|
||||||
|
<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />
|
25
README_zh.md
25
README_zh.md
@ -2,7 +2,7 @@
|
|||||||
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
|
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
|
||||||
</div>
|
</div>
|
||||||
<h1 align="center">Tiny RDM</h1>
|
<h1 align="center">Tiny RDM</h1>
|
||||||
<h4 align="center"><strong><a href="/">English</a></strong> | 简体中文</h4>
|
<h4 align="center"><strong><a href="/">English</a></strong> | 简体中文 | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a></h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
||||||
@ -32,7 +32,7 @@
|
|||||||
* 界面精美易用,提供浅色/深色主题(感谢[Naive UI](https://github.com/tusen-ai/naive-ui)
|
* 界面精美易用,提供浅色/深色主题(感谢[Naive UI](https://github.com/tusen-ai/naive-ui)
|
||||||
和 [IconPark](https://iconpark.oceanengine.com))
|
和 [IconPark](https://iconpark.oceanengine.com))
|
||||||
* 多国语言支持:英文/中文([需要更多语言支持?点我贡献语言](.github/CONTRIBUTING_zh.md))
|
* 多国语言支持:英文/中文([需要更多语言支持?点我贡献语言](.github/CONTRIBUTING_zh.md))
|
||||||
* 更好用的连接管理:支持SSH隧道/SSL/哨兵模式/集群模式
|
* 更好用的连接管理:支持SSH隧道/SSL/哨兵模式/集群模式/HTTP代理/SOCKS5代理
|
||||||
* 可视化键值操作,增删查改一应俱全
|
* 可视化键值操作,增删查改一应俱全
|
||||||
* 支持多种数据查看格式以及转码/解压方式
|
* 支持多种数据查看格式以及转码/解压方式
|
||||||
* 采用SCAN分段加载,可轻松处理数百万键列表
|
* 采用SCAN分段加载,可轻松处理数百万键列表
|
||||||
@ -43,12 +43,10 @@
|
|||||||
* List/Hash/Set/Sorted Set值的转码显示
|
* List/Hash/Set/Sorted Set值的转码显示
|
||||||
* 内置高级编辑器Monaco Editor
|
* 内置高级编辑器Monaco Editor
|
||||||
* 支持命令实时监控
|
* 支持命令实时监控
|
||||||
* 支持数据导入/导出
|
* 支持导入/导出数据
|
||||||
|
* 支持发布订阅
|
||||||
## 未来版本规划
|
* 支持导入/导出连接配置
|
||||||
|
* 自定义数据展示编码/解码([这是操作指引](https://redis.tinycraft.cc/zh/guide/custom-decoder/))
|
||||||
- [ ] 发布/订阅支持
|
|
||||||
- [ ] 连接配置导入/导出
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@ -85,6 +83,13 @@ git clone https://github.com/tiny-craft/tiny-rdm --depth=1
|
|||||||
npm install --prefix ./frontend
|
npm install --prefix ./frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
|
或者
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
### 编译运行开发版本
|
### 编译运行开发版本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -101,6 +106,10 @@ wails dev
|
|||||||
|
|
||||||
<img src="docs/images/wechat_official.png" alt="wechat" width="360" />
|
<img src="docs/images/wechat_official.png" alt="wechat" width="360" />
|
||||||
|
|
||||||
|
### B站官方账号
|
||||||
|
|
||||||
|
<img src="docs/images/bilibili_official.png" alt="bilibili" width="360" />
|
||||||
|
|
||||||
### 独立开发互助QQ群
|
### 独立开发互助QQ群
|
||||||
|
|
||||||
```
|
```
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -5,21 +5,24 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/klauspost/compress/zip"
|
"github.com/klauspost/compress/zip"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/vrischmann/userdir"
|
"github.com/vrischmann/userdir"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
. "tinyrdm/backend/storage"
|
. "tinyrdm/backend/storage"
|
||||||
"tinyrdm/backend/types"
|
"tinyrdm/backend/types"
|
||||||
|
_ "tinyrdm/backend/utils/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cmdHistoryItem struct {
|
type cmdHistoryItem struct {
|
||||||
@ -53,11 +56,35 @@ func (c *connectionService) Start(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.Options, error) {
|
func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.Options, error) {
|
||||||
var sshClient *ssh.Client
|
var dialer proxy.Dialer
|
||||||
|
var dialerErr error
|
||||||
|
if config.Proxy.Type == 1 {
|
||||||
|
// use system proxy
|
||||||
|
dialer = proxy.FromEnvironment()
|
||||||
|
} else if config.Proxy.Type == 2 {
|
||||||
|
// use custom proxy
|
||||||
|
proxyUrl := url.URL{
|
||||||
|
Host: net.JoinHostPort(config.Proxy.Addr, strconv.Itoa(config.Proxy.Port)),
|
||||||
|
}
|
||||||
|
if len(config.Proxy.Username) > 0 {
|
||||||
|
proxyUrl.User = url.UserPassword(config.Proxy.Username, config.Proxy.Password)
|
||||||
|
}
|
||||||
|
switch config.Proxy.Schema {
|
||||||
|
case "socks5", "socks5h", "http", "https":
|
||||||
|
proxyUrl.Scheme = config.Proxy.Schema
|
||||||
|
default:
|
||||||
|
proxyUrl.Scheme = "http"
|
||||||
|
}
|
||||||
|
if dialer, dialerErr = proxy.FromURL(&proxyUrl, proxy.Direct); dialerErr != nil {
|
||||||
|
return nil, dialerErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sshConfig *ssh.ClientConfig
|
||||||
|
var sshAddr string
|
||||||
if config.SSH.Enable {
|
if config.SSH.Enable {
|
||||||
sshConfig := &ssh.ClientConfig{
|
sshConfig = &ssh.ClientConfig{
|
||||||
User: config.SSH.Username,
|
User: config.SSH.Username,
|
||||||
Auth: []ssh.AuthMethod{ssh.Password(config.SSH.Password)},
|
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
Timeout: time.Duration(config.ConnTimeout) * time.Second,
|
Timeout: time.Duration(config.ConnTimeout) * time.Second,
|
||||||
}
|
}
|
||||||
@ -83,11 +110,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
|||||||
return nil, errors.New("invalid login type")
|
return nil, errors.New("invalid login type")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
sshAddr = net.JoinHostPort(config.SSH.Addr, strconv.Itoa(config.SSH.Port))
|
||||||
sshClient, err = ssh.Dial("tcp", fmt.Sprintf("%s:%d", config.SSH.Addr, config.SSH.Port), sshConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
@ -121,20 +144,59 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
|||||||
}
|
}
|
||||||
|
|
||||||
option := &redis.Options{
|
option := &redis.Options{
|
||||||
Addr: fmt.Sprintf("%s:%d", config.Addr, config.Port),
|
Username: config.Username,
|
||||||
Username: config.Username,
|
Password: config.Password,
|
||||||
Password: config.Password,
|
DialTimeout: time.Duration(config.ConnTimeout) * time.Second,
|
||||||
DialTimeout: time.Duration(config.ConnTimeout) * time.Second,
|
ReadTimeout: time.Duration(config.ExecTimeout) * time.Second,
|
||||||
ReadTimeout: time.Duration(config.ExecTimeout) * time.Second,
|
WriteTimeout: time.Duration(config.ExecTimeout) * time.Second,
|
||||||
WriteTimeout: time.Duration(config.ExecTimeout) * time.Second,
|
TLSConfig: tlsConfig,
|
||||||
TLSConfig: tlsConfig,
|
DisableIndentity: true,
|
||||||
|
IdentitySuffix: "tinyrdm_",
|
||||||
}
|
}
|
||||||
if config.LastDB > 0 {
|
if config.Network == "unix" {
|
||||||
option.DB = config.LastDB
|
option.Network = "unix"
|
||||||
|
if len(config.Sock) <= 0 {
|
||||||
|
option.Addr = "/tmp/redis.sock"
|
||||||
|
} else {
|
||||||
|
option.Addr = config.Sock
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
option.Network = "tcp"
|
||||||
|
port := 6379
|
||||||
|
if config.Port > 0 {
|
||||||
|
port = config.Port
|
||||||
|
}
|
||||||
|
if len(config.Addr) <= 0 {
|
||||||
|
option.Addr = net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||||
|
} else {
|
||||||
|
option.Addr = net.JoinHostPort(config.Addr, strconv.Itoa(port))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if sshClient != nil {
|
|
||||||
|
if len(sshAddr) > 0 {
|
||||||
|
if dialer != nil {
|
||||||
|
// ssh with proxy
|
||||||
|
conn, err := dialer.Dial("tcp", sshAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sc, chans, reqs, err := ssh.NewClientConn(conn, sshAddr, sshConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dialer = ssh.NewClient(sc, chans, reqs)
|
||||||
|
} else {
|
||||||
|
// ssh without proxy
|
||||||
|
sshClient, err := ssh.Dial("tcp", sshAddr, sshConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dialer = sshClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dialer != nil {
|
||||||
option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return sshClient.Dial(network, addr)
|
return dialer.Dial(network, addr)
|
||||||
}
|
}
|
||||||
option.ReadTimeout = -2
|
option.ReadTimeout = -2
|
||||||
option.WriteTimeout = -2
|
option.WriteTimeout = -2
|
||||||
@ -161,9 +223,17 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (re
|
|||||||
if len(addr) < 2 {
|
if len(addr) < 2 {
|
||||||
return nil, errors.New("cannot get master address")
|
return nil, errors.New("cannot get master address")
|
||||||
}
|
}
|
||||||
option.Addr = fmt.Sprintf("%s:%s", addr[0], addr[1])
|
option.Addr = net.JoinHostPort(addr[0], addr[1])
|
||||||
option.Username = config.Sentinel.Username
|
option.Username = config.Sentinel.Username
|
||||||
option.Password = config.Sentinel.Password
|
option.Password = config.Sentinel.Password
|
||||||
|
if option.Dialer != nil {
|
||||||
|
option.ReadTimeout = -2
|
||||||
|
option.WriteTimeout = -2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.LastDB > 0 {
|
||||||
|
option.DB = config.LastDB
|
||||||
}
|
}
|
||||||
|
|
||||||
rdb := redis.NewClient(option)
|
rdb := redis.NewClient(option)
|
||||||
@ -243,7 +313,7 @@ func (c *connectionService) ListSentinelMasters(config types.ConnectionConfig) (
|
|||||||
if infoMap, ok := info.(map[any]any); ok {
|
if infoMap, ok := info.(map[any]any); ok {
|
||||||
retInfo = append(retInfo, map[string]string{
|
retInfo = append(retInfo, map[string]string{
|
||||||
"name": infoMap["name"].(string),
|
"name": infoMap["name"].(string),
|
||||||
"addr": fmt.Sprintf("%s:%s", infoMap["ip"].(string), infoMap["port"].(string)),
|
"addr": net.JoinHostPort(infoMap["ip"].(string), infoMap["port"].(string)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -512,3 +582,53 @@ func (c *connectionService) ImportConnections() (resp types.JSResp) {
|
|||||||
resp.Success = true
|
resp.Success = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseConnectURL parse connection url string
|
||||||
|
func (c *connectionService) ParseConnectURL(url string) (resp types.JSResp) {
|
||||||
|
urlOpt, err := redis.ParseURL(url)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var network, addr string
|
||||||
|
var port int
|
||||||
|
if urlOpt.Network == "unix" {
|
||||||
|
network = urlOpt.Network
|
||||||
|
addr = urlOpt.Addr
|
||||||
|
} else {
|
||||||
|
network = "tcp"
|
||||||
|
addrPart := strings.Split(urlOpt.Addr, ":")
|
||||||
|
addr = addrPart[0]
|
||||||
|
port = 6379
|
||||||
|
if len(addrPart) > 1 {
|
||||||
|
port, _ = strconv.Atoi(addrPart[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sslServerName string
|
||||||
|
if urlOpt.TLSConfig != nil {
|
||||||
|
sslServerName = urlOpt.TLSConfig.ServerName
|
||||||
|
}
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = struct {
|
||||||
|
Network string `json:"network"`
|
||||||
|
Sock string `json:"sock"`
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
ConnTimeout int64 `json:"connTimeout"`
|
||||||
|
ExecTimeout int64 `json:"execTimeout"`
|
||||||
|
SSLServerName string `json:"sslServerName,omitempty"`
|
||||||
|
}{
|
||||||
|
Network: network,
|
||||||
|
Addr: addr,
|
||||||
|
Port: port,
|
||||||
|
Username: urlOpt.Username,
|
||||||
|
Password: urlOpt.Password,
|
||||||
|
ConnTimeout: int64(urlOpt.DialTimeout.Seconds()),
|
||||||
|
ExecTimeout: int64(urlOpt.ReadTimeout.Seconds()),
|
||||||
|
SSLServerName: sslServerName,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -89,7 +89,7 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
|
|||||||
item.cmd = item.client.Monitor(c.ctx, item.ch)
|
item.cmd = item.client.Monitor(c.ctx, item.ch)
|
||||||
item.cmd.Start()
|
item.cmd.Start()
|
||||||
|
|
||||||
go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.eventName)
|
go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.cmd, item.eventName)
|
||||||
resp.Success = true
|
resp.Success = true
|
||||||
resp.Data = struct {
|
resp.Data = struct {
|
||||||
EventName string `json:"eventName"`
|
EventName string `json:"eventName"`
|
||||||
@ -99,7 +99,7 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, eventName string) {
|
func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, cmd *redis.MonitorCmd, eventName string) {
|
||||||
lastEmitTime := time.Now().Add(-1 * time.Minute)
|
lastEmitTime := time.Now().Add(-1 * time.Minute)
|
||||||
cache := make([]string, 0, 1000)
|
cache := make([]string, 0, 1000)
|
||||||
for {
|
for {
|
||||||
@ -120,6 +120,7 @@ func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, clo
|
|||||||
|
|
||||||
case <-closeCh:
|
case <-closeCh:
|
||||||
// monitor stopped
|
// monitor stopped
|
||||||
|
cmd.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,8 +137,8 @@ func (c *monitorService) StopMonitor(server string) (resp types.JSResp) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
item.cmd.Stop()
|
|
||||||
//close(item.ch)
|
//close(item.ch)
|
||||||
|
item.client.Close()
|
||||||
close(item.closeCh)
|
close(item.closeCh)
|
||||||
delete(c.items, server)
|
delete(c.items, server)
|
||||||
resp.Success = true
|
resp.Success = true
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/adrg/sysfont"
|
"github.com/adrg/sysfont"
|
||||||
runtime2 "github.com/wailsapp/wails/v2/pkg/runtime"
|
runtime2 "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -13,6 +14,8 @@ 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 {
|
||||||
@ -48,6 +51,7 @@ func (p *preferencesService) SetPreferences(pf types.Preferences) (resp types.JS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.UpdateEnv()
|
||||||
resp.Success = true
|
resp.Success = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -98,6 +102,25 @@ func (p *preferencesService) GetFontList() (resp types.JSResp) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *preferencesService) GetBuildInDecoder() (resp types.JSResp) {
|
||||||
|
buildinDecoder := make([]string, 0, len(convutil.BuildInDecoders))
|
||||||
|
for name, convert := range convutil.BuildInDecoders {
|
||||||
|
if convert.Enable() {
|
||||||
|
buildinDecoder = append(buildinDecoder, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"decoder": buildinDecoder,
|
||||||
|
}
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *preferencesService) GetLanguage() string {
|
||||||
|
pref := p.pref.GetPreferences()
|
||||||
|
return pref.General.Language
|
||||||
|
}
|
||||||
|
|
||||||
func (p *preferencesService) SetAppVersion(ver string) {
|
func (p *preferencesService) SetAppVersion(ver string) {
|
||||||
if !strings.HasPrefix(ver, "v") {
|
if !strings.HasPrefix(ver, "v") {
|
||||||
p.clientVersion = "v" + ver
|
p.clientVersion = "v" + ver
|
||||||
@ -182,22 +205,48 @@ func (p *preferencesService) GetScanSize() int {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
type latestRelease struct {
|
func (p *preferencesService) GetDecoder() []convutil.CmdConvert {
|
||||||
Name string `json:"name"`
|
data := p.pref.GetPreferences()
|
||||||
TagName string `json:"tag_name"`
|
return sliceutil.FilterMap(data.Decoder, func(i int) (convutil.CmdConvert, bool) {
|
||||||
Url string `json:"url"`
|
//if !data.Decoder[i].Enable {
|
||||||
HtmlUrl string `json:"html_url"`
|
// 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 sponsorItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Region []string `json:"region"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeInfo struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Changelog map[string]string `json:"changelog"`
|
||||||
|
Description map[string]string `json:"description"`
|
||||||
|
DownloadURl map[string]string `json:"download_url"`
|
||||||
|
DownloadPage map[string]string `json:"download_page"`
|
||||||
|
Sponsor []sponsorItem `json:"sponsor,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
|
func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
|
||||||
// request latest version
|
// request latest version
|
||||||
res, err := http.Get("https://api.github.com/repos/tiny-craft/tiny-rdm/releases/latest")
|
//res, err := http.Get("https://api.github.com/repos/tiny-craft/tiny-rdm/releases/latest")
|
||||||
|
res, err := http.Get("https://redis.tinycraft.cc/client_version.json")
|
||||||
if err != nil || res.StatusCode != http.StatusOK {
|
if err != nil || res.StatusCode != http.StatusOK {
|
||||||
resp.Msg = "network error"
|
resp.Msg = "network error"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var respObj latestRelease
|
var respObj upgradeInfo
|
||||||
err = json.NewDecoder(res.Body).Decode(&respObj)
|
err = json.NewDecoder(res.Body).Decode(&respObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Msg = "invalid content"
|
resp.Msg = "invalid content"
|
||||||
@ -207,9 +256,20 @@ func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
|
|||||||
// compare with current version
|
// compare with current version
|
||||||
resp.Success = true
|
resp.Success = true
|
||||||
resp.Data = map[string]any{
|
resp.Data = map[string]any{
|
||||||
"version": p.clientVersion,
|
"version": p.clientVersion,
|
||||||
"latest": respObj.TagName,
|
"latest": respObj.Version,
|
||||||
"page_url": respObj.HtmlUrl,
|
"description": respObj.Description,
|
||||||
|
"download_page": respObj.DownloadPage,
|
||||||
|
"sponsor": respObj.Sponsor,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateEnv Update System Environment
|
||||||
|
func (p *preferencesService) UpdateEnv() {
|
||||||
|
if p.GetLanguage() == "zh" {
|
||||||
|
os.Setenv("LANG", "zh_CN.UTF-8")
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LANG")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,6 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
@ -13,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type pubsubItem struct {
|
type pubsubItem struct {
|
||||||
client *redis.Client
|
client redis.UniversalClient
|
||||||
pubsub *redis.PubSub
|
pubsub *redis.PubSub
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
closeCh chan struct{}
|
closeCh chan struct{}
|
||||||
@ -62,12 +61,8 @@ func (p *pubsubService) getItem(server string) (*pubsubItem, error) {
|
|||||||
if uniClient, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil {
|
if uniClient, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var client *redis.Client
|
|
||||||
if client, ok = uniClient.(*redis.Client); !ok {
|
|
||||||
return nil, errors.New("create redis client fail")
|
|
||||||
}
|
|
||||||
item = &pubsubItem{
|
item = &pubsubItem{
|
||||||
client: client,
|
client: uniClient,
|
||||||
}
|
}
|
||||||
p.items[server] = item
|
p.items[server] = item
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
runtime2 "runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"tinyrdm/backend/consts"
|
"tinyrdm/backend/consts"
|
||||||
@ -11,7 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type systemService struct {
|
type systemService struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
appVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
var system *systemService
|
var system *systemService
|
||||||
@ -20,15 +22,18 @@ var onceSystem sync.Once
|
|||||||
func System() *systemService {
|
func System() *systemService {
|
||||||
if system == nil {
|
if system == nil {
|
||||||
onceSystem.Do(func() {
|
onceSystem.Do(func() {
|
||||||
system = &systemService{}
|
system = &systemService{
|
||||||
|
appVersion: "0.0.0",
|
||||||
|
}
|
||||||
go system.loopWindowEvent()
|
go system.loopWindowEvent()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemService) Start(ctx context.Context) {
|
func (s *systemService) Start(ctx context.Context, version string) {
|
||||||
s.ctx = ctx
|
s.ctx = ctx
|
||||||
|
s.appVersion = version
|
||||||
|
|
||||||
// maximize the window if screen size is lower than the minimum window size
|
// maximize the window if screen size is lower than the minimum window size
|
||||||
if screen, err := runtime.ScreenGetAll(ctx); err == nil && len(screen) > 0 {
|
if screen, err := runtime.ScreenGetAll(ctx); err == nil && len(screen) > 0 {
|
||||||
@ -43,6 +48,20 @@ func (s *systemService) Start(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *systemService) Info() (resp types.JSResp) {
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = struct {
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}{
|
||||||
|
OS: runtime2.GOOS,
|
||||||
|
Arch: runtime2.GOARCH,
|
||||||
|
Version: s.appVersion,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// SelectFile open file dialog to select a file
|
// SelectFile open file dialog to select a file
|
||||||
func (s *systemService) SelectFile(title string, extensions []string) (resp types.JSResp) {
|
func (s *systemService) SelectFile(title string, extensions []string) (resp types.JSResp) {
|
||||||
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
|
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
|
||||||
|
@ -3,10 +3,10 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"tinyrdm/backend/consts"
|
"tinyrdm/backend/consts"
|
||||||
"tinyrdm/backend/types"
|
"tinyrdm/backend/types"
|
||||||
sliceutil "tinyrdm/backend/utils/slice"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnectionsStorage struct {
|
type ConnectionsStorage struct {
|
||||||
@ -27,6 +27,7 @@ func (c *ConnectionsStorage) defaultConnections() types.Connections {
|
|||||||
func (c *ConnectionsStorage) defaultConnectionItem() types.ConnectionConfig {
|
func (c *ConnectionsStorage) defaultConnectionItem() types.ConnectionConfig {
|
||||||
return types.ConnectionConfig{
|
return types.ConnectionConfig{
|
||||||
Name: "",
|
Name: "",
|
||||||
|
Network: "tcp",
|
||||||
Addr: "127.0.0.1",
|
Addr: "127.0.0.1",
|
||||||
Port: 6379,
|
Port: 6379,
|
||||||
Username: "",
|
Username: "",
|
||||||
@ -255,10 +256,10 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
|
|||||||
|
|
||||||
conns := c.GetConnectionsFlat()
|
conns := c.GetConnectionsFlat()
|
||||||
takeConn := func(name string) (types.Connection, bool) {
|
takeConn := func(name string) (types.Connection, bool) {
|
||||||
idx, ok := sliceutil.Find(conns, func(i int) bool {
|
idx := slices.IndexFunc(conns, func(connection types.Connection) bool {
|
||||||
return conns[i].Name == name
|
return connection.Name == name
|
||||||
})
|
})
|
||||||
if ok {
|
if idx >= 0 {
|
||||||
ret := conns[idx]
|
ret := conns[idx]
|
||||||
conns = append(conns[:idx], conns[idx+1:]...)
|
conns = append(conns[:idx], conns[idx+1:]...)
|
||||||
return ret, true
|
return ret, true
|
||||||
|
@ -17,8 +17,10 @@ type PreferencesStorage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewPreferences() *PreferencesStorage {
|
func NewPreferences() *PreferencesStorage {
|
||||||
|
storage := NewLocalStore("preferences.yaml")
|
||||||
|
log.Printf("preferences path: %s\n", storage.ConfPath)
|
||||||
return &PreferencesStorage{
|
return &PreferencesStorage{
|
||||||
storage: NewLocalStore("preferences.yaml"),
|
storage: storage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,8 +29,8 @@ func (p *PreferencesStorage) DefaultPreferences() types.Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *PreferencesStorage) getPreferences() (ret types.Preferences) {
|
func (p *PreferencesStorage) getPreferences() (ret types.Preferences) {
|
||||||
b, err := p.storage.Load()
|
|
||||||
ret = p.DefaultPreferences()
|
ret = p.DefaultPreferences()
|
||||||
|
b, err := p.storage.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ type ConnectionConfig struct {
|
|||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
Group string `json:"group,omitempty" yaml:"-"`
|
Group string `json:"group,omitempty" yaml:"-"`
|
||||||
LastDB int `json:"lastDB" yaml:"last_db"`
|
LastDB int `json:"lastDB" yaml:"last_db"`
|
||||||
|
Network string `json:"network,omitempty" yaml:"network,omitempty"`
|
||||||
|
Sock string `json:"sock,omitempty" yaml:"sock,omitempty"`
|
||||||
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
|
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
|
||||||
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
||||||
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||||
@ -19,12 +21,13 @@ type ConnectionConfig struct {
|
|||||||
KeyView int `json:"keyView,omitempty" yaml:"key_view,omitempty"`
|
KeyView int `json:"keyView,omitempty" yaml:"key_view,omitempty"`
|
||||||
LoadSize int `json:"loadSize,omitempty" yaml:"load_size,omitempty"`
|
LoadSize int `json:"loadSize,omitempty" yaml:"load_size,omitempty"`
|
||||||
MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"`
|
MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"`
|
||||||
RefreshInterval int `json:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty"`
|
RefreshInterval int `json:"refreshInterval,omitempty" yaml:"refresh_interval,omitempty"`
|
||||||
Alias map[int]string `json:"alias,omitempty" yaml:"alias,omitempty"`
|
Alias map[int]string `json:"alias,omitempty" yaml:"alias,omitempty"`
|
||||||
SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"`
|
SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"`
|
||||||
SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`
|
SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`
|
||||||
Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"`
|
Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"`
|
||||||
Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"`
|
Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"`
|
||||||
|
Proxy ConnectionProxy `json:"proxy,omitempty" yaml:"proxy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
@ -46,10 +49,10 @@ type ConnectionDB struct {
|
|||||||
|
|
||||||
type ConnectionSSL struct {
|
type ConnectionSSL struct {
|
||||||
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
||||||
KeyFile string `json:"keyFile,omitempty" yaml:"keyFile,omitempty"`
|
KeyFile string `json:"keyFile,omitempty" yaml:"keyfile,omitempty"`
|
||||||
CertFile string `json:"certFile,omitempty" yaml:"certFile,omitempty"`
|
CertFile string `json:"certFile,omitempty" yaml:"certfile,omitempty"`
|
||||||
CAFile string `json:"caFile,omitempty" yaml:"caFile,omitempty"`
|
CAFile string `json:"caFile,omitempty" yaml:"cafile,omitempty"`
|
||||||
AllowInsecure bool `json:"allowInsecure,omitempty" yaml:"allowInsecure,omitempty"`
|
AllowInsecure bool `json:"allowInsecure,omitempty" yaml:"allow_insecure,omitempty"`
|
||||||
SNI string `json:"sni,omitempty" yaml:"sni,omitempty"`
|
SNI string `json:"sni,omitempty" yaml:"sni,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,3 +77,12 @@ type ConnectionSentinel struct {
|
|||||||
type ConnectionCluster struct {
|
type ConnectionCluster struct {
|
||||||
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConnectionProxy struct {
|
||||||
|
Type int `json:"type,omitempty" yaml:"type,omitempty"`
|
||||||
|
Schema string `json:"schema,omitempty" yaml:"schema,omitempty"`
|
||||||
|
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
|
||||||
|
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
||||||
|
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||||
|
}
|
||||||
|
@ -56,7 +56,7 @@ type SetListParam struct {
|
|||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
DB int `json:"db"`
|
DB int `json:"db"`
|
||||||
Key any `json:"key"`
|
Key any `json:"key"`
|
||||||
Index int64 `json:"index"`
|
Index int `json:"index"`
|
||||||
Value any `json:"value"`
|
Value any `json:"value"`
|
||||||
Format string `json:"format,omitempty"`
|
Format string `json:"format,omitempty"`
|
||||||
Decode string `json:"decode,omitempty"`
|
Decode string `json:"decode,omitempty"`
|
||||||
@ -101,3 +101,12 @@ type SetZSetParam struct {
|
|||||||
RetFormat string `json:"retFormat,omitempty"`
|
RetFormat string `json:"retFormat,omitempty"`
|
||||||
RetDecode string `json:"retDecode,omitempty"`
|
RetDecode string `json:"retDecode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetHashParam struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
DB int `json:"db"`
|
||||||
|
Key any `json:"key"`
|
||||||
|
Field string `json:"field,omitempty"`
|
||||||
|
Format string `json:"format,omitempty"`
|
||||||
|
Decode string `json:"decode,omitempty"`
|
||||||
|
}
|
||||||
|
@ -3,9 +3,11 @@ package types
|
|||||||
import "tinyrdm/backend/consts"
|
import "tinyrdm/backend/consts"
|
||||||
|
|
||||||
type Preferences struct {
|
type Preferences struct {
|
||||||
Behavior PreferencesBehavior `json:"behavior" yaml:"behavior"`
|
Behavior PreferencesBehavior `json:"behavior" yaml:"behavior"`
|
||||||
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"`
|
||||||
|
Decoder []PreferencesDecoder `json:"decoder" yaml:"decoder,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPreferences() Preferences {
|
func NewPreferences() Preferences {
|
||||||
@ -22,16 +24,26 @@ func NewPreferences() Preferences {
|
|||||||
ScanSize: consts.DEFAULT_SCAN_SIZE,
|
ScanSize: consts.DEFAULT_SCAN_SIZE,
|
||||||
KeyIconStyle: 0,
|
KeyIconStyle: 0,
|
||||||
CheckUpdate: true,
|
CheckUpdate: true,
|
||||||
|
AllowTrack: true,
|
||||||
},
|
},
|
||||||
Editor: PreferencesEditor{
|
Editor: PreferencesEditor{
|
||||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||||
ShowLineNum: true,
|
ShowLineNum: true,
|
||||||
ShowFolding: true,
|
ShowFolding: true,
|
||||||
|
DropText: true,
|
||||||
|
Links: true,
|
||||||
|
EntryTextAlign: 0,
|
||||||
},
|
},
|
||||||
|
Cli: PreferencesCli{
|
||||||
|
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||||
|
CursorStyle: "block",
|
||||||
|
},
|
||||||
|
Decoder: []PreferencesDecoder{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreferencesBehavior struct {
|
type PreferencesBehavior struct {
|
||||||
|
Welcomed bool `json:"welcomed" yaml:"welcomed"`
|
||||||
AsideWidth int `json:"asideWidth" yaml:"aside_width"`
|
AsideWidth int `json:"asideWidth" yaml:"aside_width"`
|
||||||
WindowWidth int `json:"windowWidth" yaml:"window_width"`
|
WindowWidth int `json:"windowWidth" yaml:"window_width"`
|
||||||
WindowHeight int `json:"windowHeight" yaml:"window_height"`
|
WindowHeight int `json:"windowHeight" yaml:"window_height"`
|
||||||
@ -41,21 +53,43 @@ type PreferencesBehavior struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PreferencesGeneral struct {
|
type PreferencesGeneral struct {
|
||||||
Theme string `json:"theme" yaml:"theme"`
|
Theme string `json:"theme" yaml:"theme"`
|
||||||
Language string `json:"language" yaml:"language"`
|
Language string `json:"language" yaml:"language"`
|
||||||
Font string `json:"font" yaml:"font,omitempty"`
|
Font string `json:"font" yaml:"font,omitempty"`
|
||||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
|
||||||
ScanSize int `json:"scanSize" yaml:"scan_size"`
|
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||||
KeyIconStyle int `json:"keyIconStyle" yaml:"key_icon_style"`
|
ScanSize int `json:"scanSize" yaml:"scan_size"`
|
||||||
UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"`
|
KeyIconStyle int `json:"keyIconStyle" yaml:"key_icon_style"`
|
||||||
UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"`
|
UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"`
|
||||||
CheckUpdate bool `json:"checkUpdate" yaml:"check_update"`
|
UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"`
|
||||||
SkipVersion string `json:"skipVersion" yaml:"skip_version,omitempty"`
|
CheckUpdate bool `json:"checkUpdate" yaml:"check_update"`
|
||||||
|
SkipVersion string `json:"skipVersion" yaml:"skip_version,omitempty"`
|
||||||
|
AllowTrack bool `json:"allowTrack" yaml:"allow_track"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreferencesEditor struct {
|
type PreferencesEditor struct {
|
||||||
Font string `json:"font" yaml:"font,omitempty"`
|
Font string `json:"font" yaml:"font,omitempty"`
|
||||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
|
||||||
ShowLineNum bool `json:"showLineNum" yaml:"show_line_num"`
|
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||||
ShowFolding bool `json:"showFolding" yaml:"show_folding"`
|
ShowLineNum bool `json:"showLineNum" yaml:"show_line_num"`
|
||||||
|
ShowFolding bool `json:"showFolding" yaml:"show_folding"`
|
||||||
|
DropText bool `json:"dropText" yaml:"drop_text"`
|
||||||
|
Links bool `json:"links" yaml:"links"`
|
||||||
|
EntryTextAlign int `json:"entryTextAlign" yaml:"entry_text_align"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreferencesCli struct {
|
||||||
|
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
type ListEntryItem struct {
|
type ListEntryItem struct {
|
||||||
|
Index int `json:"index"`
|
||||||
Value any `json:"v"`
|
Value any `json:"v"`
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
DisplayValue string `json:"dv,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListReplaceItem struct {
|
type ListReplaceItem struct {
|
||||||
Index int64 `json:"index"`
|
Index int `json:"index"`
|
||||||
Value any `json:"v,omitempty"`
|
Value any `json:"v,omitempty"`
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
DisplayValue string `json:"dv,omitempty"`
|
||||||
}
|
}
|
||||||
@ -31,7 +32,8 @@ type SetEntryItem struct {
|
|||||||
|
|
||||||
type ZSetEntryItem struct {
|
type ZSetEntryItem struct {
|
||||||
Score float64 `json:"s"`
|
Score float64 `json:"s"`
|
||||||
Value string `json:"v"`
|
ScoreStr string `json:"ss,omitempty"`
|
||||||
|
Value any `json:"v"`
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
DisplayValue string `json:"dv,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package types
|
|||||||
|
|
||||||
const FORMAT_RAW = "Raw"
|
const FORMAT_RAW = "Raw"
|
||||||
const FORMAT_JSON = "JSON"
|
const FORMAT_JSON = "JSON"
|
||||||
|
const FORMAT_UNICODE_JSON = "Unicode JSON"
|
||||||
const FORMAT_YAML = "YAML"
|
const FORMAT_YAML = "YAML"
|
||||||
const FORMAT_XML = "XML"
|
const FORMAT_XML = "XML"
|
||||||
const FORMAT_HEX = "Hex"
|
const FORMAT_HEX = "Hex"
|
||||||
@ -12,4 +13,8 @@ const DECODE_BASE64 = "Base64"
|
|||||||
const DECODE_GZIP = "GZip"
|
const DECODE_GZIP = "GZip"
|
||||||
const DECODE_DEFLATE = "Deflate"
|
const DECODE_DEFLATE = "Deflate"
|
||||||
const DECODE_ZSTD = "ZStd"
|
const DECODE_ZSTD = "ZStd"
|
||||||
|
const DECODE_LZ4 = "LZ4"
|
||||||
const DECODE_BROTLI = "Brotli"
|
const DECODE_BROTLI = "Brotli"
|
||||||
|
const DECODE_MSGPACK = "Msgpack"
|
||||||
|
const DECODE_PHP = "PHP"
|
||||||
|
const DECODE_PICKLE = "Pickle"
|
||||||
|
25
backend/utils/convert/base64_convert.go
Normal file
25
backend/utils/convert/base64_convert.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
strutil "tinyrdm/backend/utils/string"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Base64Convert struct{}
|
||||||
|
|
||||||
|
func (Base64Convert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Base64Convert) Encode(str string) (string, bool) {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(str)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Base64Convert) Decode(str string) (string, bool) {
|
||||||
|
if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil {
|
||||||
|
if s := string(decodedStr); !strutil.ContainsBinary(s) {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
31
backend/utils/convert/binary_convert.go
Normal file
31
backend/utils/convert/binary_convert.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BinaryConvert struct{}
|
||||||
|
|
||||||
|
func (BinaryConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BinaryConvert) Encode(str string) (string, bool) {
|
||||||
|
var result strings.Builder
|
||||||
|
total := len(str)
|
||||||
|
for i := 0; i < total; i += 8 {
|
||||||
|
b, _ := strconv.ParseInt(str[i:i+8], 2, 8)
|
||||||
|
result.WriteByte(byte(b))
|
||||||
|
}
|
||||||
|
return result.String(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BinaryConvert) Decode(str string) (string, bool) {
|
||||||
|
var binary strings.Builder
|
||||||
|
for _, char := range []byte(str) {
|
||||||
|
binary.WriteString(fmt.Sprintf("%08b", int(char)))
|
||||||
|
}
|
||||||
|
return binary.String(), true
|
||||||
|
}
|
39
backend/utils/convert/brotli_convert.go
Normal file
39
backend/utils/convert/brotli_convert.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/andybalholm/brotli"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BrotliConvert struct{}
|
||||||
|
|
||||||
|
func (BrotliConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BrotliConvert) Encode(str string) (string, bool) {
|
||||||
|
var compress = func(b []byte) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := brotli.NewWriter(&buf)
|
||||||
|
if _, err := writer.Write([]byte(str)); err != nil {
|
||||||
|
writer.Close()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
return string(buf.Bytes()), nil
|
||||||
|
}
|
||||||
|
if brotliStr, err := compress([]byte(str)); err == nil {
|
||||||
|
return brotliStr, true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BrotliConvert) Decode(str string) (string, bool) {
|
||||||
|
reader := brotli.NewReader(strings.NewReader(str))
|
||||||
|
if decompressed, err := io.ReadAll(reader); err == nil {
|
||||||
|
return string(decompressed), true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
76
backend/utils/convert/cmd_convert.go
Normal file
76
backend/utils/convert/cmd_convert.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"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) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
output, err := runCommand(c.EncodePath, args...)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
output, err := runCommand(c.DecodePath, args...)
|
||||||
|
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
|
||||||
|
}
|
17
backend/utils/convert/common.go
Normal file
17
backend/utils/convert/common.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vrischmann/userdir"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeExecuteFile(content []byte, filename string) (string, error) {
|
||||||
|
filepath := path.Join(userdir.GetConfigHome(), "TinyRDM", "decoder", filename)
|
||||||
|
_ = os.Mkdir(path.Dir(filepath), 0777)
|
||||||
|
err := os.WriteFile(filepath, content, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath, nil
|
||||||
|
}
|
12
backend/utils/convert/common_nonwindows.go
Normal file
12
backend/utils/convert/common_nonwindows.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runCommand(name string, arg ...string) ([]byte, error) {
|
||||||
|
cmd := exec.Command(name, arg...)
|
||||||
|
return cmd.Output()
|
||||||
|
}
|
14
backend/utils/convert/common_windows.go
Normal file
14
backend/utils/convert/common_windows.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runCommand(name string, arg ...string) ([]byte, error) {
|
||||||
|
cmd := exec.Command(name, arg...)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
return cmd.Output()
|
||||||
|
}
|
264
backend/utils/convert/convert.go
Normal file
264
backend/utils/convert/convert.go
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"tinyrdm/backend/types"
|
||||||
|
strutil "tinyrdm/backend/utils/string"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataConvert interface {
|
||||||
|
Enable() bool
|
||||||
|
Encode(string) (string, bool)
|
||||||
|
Decode(string) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
jsonConv JsonConvert
|
||||||
|
uniJsonConv UnicodeJsonConvert
|
||||||
|
yamlConv YamlConvert
|
||||||
|
xmlConv XmlConvert
|
||||||
|
base64Conv Base64Convert
|
||||||
|
binaryConv BinaryConvert
|
||||||
|
hexConv HexConvert
|
||||||
|
gzipConv GZipConvert
|
||||||
|
deflateConv DeflateConvert
|
||||||
|
zstdConv ZStdConvert
|
||||||
|
lz4Conv LZ4Convert
|
||||||
|
brotliConv BrotliConvert
|
||||||
|
msgpackConv MsgpackConvert
|
||||||
|
phpConv = NewPhpConvert()
|
||||||
|
pickleConv = NewPickleConvert()
|
||||||
|
)
|
||||||
|
|
||||||
|
var BuildInFormatters = map[string]DataConvert{
|
||||||
|
types.FORMAT_JSON: jsonConv,
|
||||||
|
types.FORMAT_UNICODE_JSON: uniJsonConv,
|
||||||
|
types.FORMAT_YAML: yamlConv,
|
||||||
|
types.FORMAT_XML: xmlConv,
|
||||||
|
types.FORMAT_HEX: hexConv,
|
||||||
|
types.FORMAT_BINARY: binaryConv,
|
||||||
|
}
|
||||||
|
|
||||||
|
var BuildInDecoders = map[string]DataConvert{
|
||||||
|
types.DECODE_BASE64: base64Conv,
|
||||||
|
types.DECODE_GZIP: gzipConv,
|
||||||
|
types.DECODE_DEFLATE: deflateConv,
|
||||||
|
types.DECODE_ZSTD: zstdConv,
|
||||||
|
types.DECODE_LZ4: lz4Conv,
|
||||||
|
types.DECODE_BROTLI: brotliConv,
|
||||||
|
types.DECODE_MSGPACK: msgpackConv,
|
||||||
|
types.DECODE_PHP: phpConv,
|
||||||
|
types.DECODE_PICKLE: pickleConv,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertTo convert string to specified type
|
||||||
|
// @param decodeType empty string indicates automatic detection
|
||||||
|
// @param formatType empty string indicates automatic detection
|
||||||
|
// @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 {
|
||||||
|
resultFormat = types.FORMAT_RAW
|
||||||
|
} else {
|
||||||
|
resultFormat = formatType
|
||||||
|
}
|
||||||
|
if len(decodeType) <= 0 {
|
||||||
|
resultDecode = types.DECODE_NONE
|
||||||
|
} else {
|
||||||
|
resultDecode = decodeType
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode first
|
||||||
|
value, resultDecode = decodeWith(str, decodeType, customDecoder)
|
||||||
|
// then format content
|
||||||
|
if len(formatType) <= 0 {
|
||||||
|
value, resultFormat = autoViewAs(value)
|
||||||
|
} else {
|
||||||
|
value, resultFormat = viewAs(value, formatType)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeWith(str, decodeType string, customDecoder []CmdConvert) (value, resultDecode string) {
|
||||||
|
if len(decodeType) > 0 {
|
||||||
|
value = str
|
||||||
|
|
||||||
|
if buildinDecoder, ok := BuildInDecoders[decodeType]; ok {
|
||||||
|
if decodedStr, ok := buildinDecoder.Decode(str); ok {
|
||||||
|
value = decodedStr
|
||||||
|
}
|
||||||
|
} else if decodeType != types.DECODE_NONE {
|
||||||
|
for _, decoder := range customDecoder {
|
||||||
|
if decoder.Name == decodeType {
|
||||||
|
if decodedStr, ok := decoder.Decode(str); ok {
|
||||||
|
value = decodedStr
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDecode = decodeType
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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 {
|
||||||
|
var ok bool
|
||||||
|
if len(str)%4 == 0 && len(str) >= 12 && !strutil.IsSameChar(str) {
|
||||||
|
if value, ok = base64Conv.Decode(str); ok {
|
||||||
|
resultDecode = types.DECODE_BASE64
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = gzipConv.Decode(str); ok {
|
||||||
|
resultDecode = types.DECODE_GZIP
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: skip decompress with deflate due to incorrect format checking
|
||||||
|
//if value, ok = decodeDeflate(str); ok {
|
||||||
|
// resultDecode = types.DECODE_DEFLATE
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
|
if value, ok = zstdConv.Decode(str); ok {
|
||||||
|
resultDecode = types.DECODE_ZSTD
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = lz4Conv.Decode(str); ok {
|
||||||
|
resultDecode = types.DECODE_LZ4
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: skip decompress with brotli due to incorrect format checking
|
||||||
|
//if value, ok = decodeBrotli(str); ok {
|
||||||
|
// resultDecode = types.DECODE_BROTLI
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
|
if value, ok = msgpackConv.Decode(str); ok {
|
||||||
|
resultDecode = types.DECODE_MSGPACK
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = phpConv.Decode(str); ok {
|
||||||
|
resultDecode = types.DECODE_PHP
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = pickleConv.Decode(str); ok {
|
||||||
|
resultDecode = types.DECODE_PICKLE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try decode with custom decoder
|
||||||
|
for _, decoder := range customDecoder {
|
||||||
|
if decoder.Auto {
|
||||||
|
if value, ok = decoder.Decode(str); ok {
|
||||||
|
resultDecode = decoder.Name
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = str
|
||||||
|
resultDecode = types.DECODE_NONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewAs(str, formatType string) (value, resultFormat string) {
|
||||||
|
if len(formatType) > 0 {
|
||||||
|
value = str
|
||||||
|
if buildinFormatter, ok := BuildInFormatters[formatType]; ok {
|
||||||
|
if formattedStr, ok := buildinFormatter.Decode(str); ok {
|
||||||
|
value = formattedStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultFormat = formatType
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt automatic convert to possible types
|
||||||
|
// if no conversion is possible, it will return the origin string value and "plain text" type
|
||||||
|
func autoViewAs(str string) (value, resultFormat string) {
|
||||||
|
if len(str) > 0 {
|
||||||
|
var ok bool
|
||||||
|
if value, ok = jsonConv.Decode(str); ok {
|
||||||
|
resultFormat = types.FORMAT_JSON
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = yamlConv.Decode(str); ok {
|
||||||
|
resultFormat = types.FORMAT_YAML
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = xmlConv.Decode(str); ok {
|
||||||
|
resultFormat = types.FORMAT_XML
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strutil.ContainsBinary(str) {
|
||||||
|
if value, ok = hexConv.Decode(str); ok {
|
||||||
|
resultFormat = types.FORMAT_HEX
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = str
|
||||||
|
resultFormat = types.FORMAT_RAW
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveAs(str, format, decode string, customDecoder []CmdConvert) (value string, err error) {
|
||||||
|
value = str
|
||||||
|
if buildingFormatter, ok := BuildInFormatters[format]; ok {
|
||||||
|
if formattedStr, ok := buildingFormatter.Encode(str); ok {
|
||||||
|
value = formattedStr
|
||||||
|
} else {
|
||||||
|
err = errors.New("invalid " + format + " data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if buildinDecoder, ok := BuildInDecoders[decode]; ok {
|
||||||
|
if encodedValue, ok := buildinDecoder.Encode(str); ok {
|
||||||
|
value = encodedValue
|
||||||
|
} else {
|
||||||
|
err = errors.New("fail to build " + decode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if decode != types.DECODE_NONE {
|
||||||
|
for _, decoder := range customDecoder {
|
||||||
|
if decoder.Name == decode {
|
||||||
|
if encodedStr, ok := decoder.Encode(str); ok {
|
||||||
|
value = encodedStr
|
||||||
|
} else {
|
||||||
|
err = errors.New("fail to build " + decode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
43
backend/utils/convert/deflate_convert.go
Normal file
43
backend/utils/convert/deflate_convert.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/klauspost/compress/flate"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeflateConvert struct{}
|
||||||
|
|
||||||
|
func (d DeflateConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DeflateConvert) Encode(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DeflateConvert) Decode(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
|
||||||
|
}
|
43
backend/utils/convert/gzip_convert.go
Normal file
43
backend/utils/convert/gzip_convert.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/klauspost/compress/gzip"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GZipConvert struct{}
|
||||||
|
|
||||||
|
func (GZipConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (GZipConvert) Encode(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 (GZipConvert) Decode(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
|
||||||
|
}
|
33
backend/utils/convert/hex_convert.go
Normal file
33
backend/utils/convert/hex_convert.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HexConvert struct{}
|
||||||
|
|
||||||
|
func (HexConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (HexConvert) Encode(str string) (string, bool) {
|
||||||
|
hexStrArr := strings.Split(str, "\\x")
|
||||||
|
hexStr := strings.Join(hexStrArr, "")
|
||||||
|
if decodeStr, err := hex.DecodeString(hexStr); err == nil {
|
||||||
|
return string(decodeStr), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (HexConvert) Decode(str string) (string, bool) {
|
||||||
|
decodeStr := hex.EncodeToString([]byte(str))
|
||||||
|
decodeStr = strings.ToUpper(decodeStr)
|
||||||
|
var resultStr strings.Builder
|
||||||
|
for i := 0; i < len(decodeStr); i += 2 {
|
||||||
|
resultStr.WriteString("\\x")
|
||||||
|
resultStr.WriteString(decodeStr[i : i+2])
|
||||||
|
}
|
||||||
|
return resultStr.String(), true
|
||||||
|
}
|
25
backend/utils/convert/json_convert.go
Normal file
25
backend/utils/convert/json_convert.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
strutil "tinyrdm/backend/utils/string"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JsonConvert struct{}
|
||||||
|
|
||||||
|
func (JsonConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JsonConvert) Decode(str string) (string, bool) {
|
||||||
|
trimedStr := strings.TrimSpace(str)
|
||||||
|
if (strings.HasPrefix(trimedStr, "{") && strings.HasSuffix(trimedStr, "}")) ||
|
||||||
|
(strings.HasPrefix(trimedStr, "[") && strings.HasSuffix(trimedStr, "]")) {
|
||||||
|
return strutil.JSONBeautify(trimedStr, " "), true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JsonConvert) Encode(str string) (string, bool) {
|
||||||
|
return strutil.JSONMinify(str), true
|
||||||
|
}
|
39
backend/utils/convert/lz4_convert.go
Normal file
39
backend/utils/convert/lz4_convert.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/pierrec/lz4/v4"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LZ4Convert struct{}
|
||||||
|
|
||||||
|
func (LZ4Convert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LZ4Convert) Encode(str string) (string, bool) {
|
||||||
|
var compress = func(b []byte) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := lz4.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 (LZ4Convert) Decode(str string) (string, bool) {
|
||||||
|
reader := lz4.NewReader(bytes.NewReader([]byte(str)))
|
||||||
|
if decompressed, err := io.ReadAll(reader); err == nil {
|
||||||
|
return string(decompressed), true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
70
backend/utils/convert/msgpack_convert.go
Normal file
70
backend/utils/convert/msgpack_convert.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MsgpackConvert struct{}
|
||||||
|
|
||||||
|
func (MsgpackConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MsgpackConvert) Encode(str string) (string, bool) {
|
||||||
|
var obj map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(str), &obj); err == nil {
|
||||||
|
for k, v := range obj {
|
||||||
|
obj[k] = c.TryFloatToInt(v)
|
||||||
|
}
|
||||||
|
if b, err := msgpack.Marshal(obj); err == nil {
|
||||||
|
return string(b), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b, err := msgpack.Marshal(str); err != nil {
|
||||||
|
return string(b), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MsgpackConvert) Decode(str string) (string, bool) {
|
||||||
|
var decodedStr string
|
||||||
|
if err := msgpack.Unmarshal([]byte(str), &decodedStr); err == nil {
|
||||||
|
return decodedStr, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj map[string]any
|
||||||
|
if err := msgpack.Unmarshal([]byte(str), &obj); err == nil {
|
||||||
|
if b, err := json.Marshal(obj); err == nil {
|
||||||
|
if len(b) >= 10 {
|
||||||
|
return string(b), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MsgpackConvert) TryFloatToInt(input any) any {
|
||||||
|
switch val := input.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for k, v := range val {
|
||||||
|
val[k] = c.TryFloatToInt(v)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
case []any:
|
||||||
|
for i, v := range val {
|
||||||
|
val[i] = c.TryFloatToInt(v)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
case float64:
|
||||||
|
if val == float64(int(val)) {
|
||||||
|
return int(val)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
default:
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
89
backend/utils/convert/php_convert.go
Normal file
89
backend/utils/convert/php_convert.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PhpConvert struct {
|
||||||
|
CmdConvert
|
||||||
|
}
|
||||||
|
|
||||||
|
const phpDecodeCode = `
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$action = strtolower($argv[1]);
|
||||||
|
$content = $argv[2];
|
||||||
|
|
||||||
|
if ($action === 'decode') {
|
||||||
|
$decoded = base64_decode($content);
|
||||||
|
if ($decoded !== false) {
|
||||||
|
$obj = unserialize($decoded);
|
||||||
|
if ($obj !== false) {
|
||||||
|
$unserialized = json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($unserialized !== false) {
|
||||||
|
echo base64_encode($unserialized);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($action === 'encode') {
|
||||||
|
$decoded = base64_decode($content);
|
||||||
|
if ($decoded !== false) {
|
||||||
|
$json = json_decode($decoded, true);
|
||||||
|
if ($json !== false) {
|
||||||
|
$serialized = serialize($json);
|
||||||
|
if ($serialized !== false) {
|
||||||
|
echo base64_encode($serialized);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo '[RDM-ERROR]';
|
||||||
|
`
|
||||||
|
|
||||||
|
func NewPhpConvert() *PhpConvert {
|
||||||
|
c := CmdConvert{
|
||||||
|
Name: "PHP",
|
||||||
|
Auto: true,
|
||||||
|
DecodePath: "php",
|
||||||
|
EncodePath: "php",
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if _, err = exec.LookPath(c.DecodePath); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var filepath string
|
||||||
|
if filepath, err = writeExecuteFile([]byte(phpDecodeCode), "php_decoder.php"); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.DecodeArgs = []string{filepath, "decode"}
|
||||||
|
c.EncodeArgs = []string{filepath, "encode"}
|
||||||
|
|
||||||
|
return &PhpConvert{
|
||||||
|
CmdConvert: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PhpConvert) Enable() bool {
|
||||||
|
if p == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PhpConvert) Encode(str string) (string, bool) {
|
||||||
|
if !p.Enable() {
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
return p.CmdConvert.Encode(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PhpConvert) Decode(str string) (string, bool) {
|
||||||
|
if !p.Enable() {
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
return p.CmdConvert.Decode(str)
|
||||||
|
}
|
97
backend/utils/convert/pickle_convert.go
Normal file
97
backend/utils/convert/pickle_convert.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PickleConvert struct {
|
||||||
|
CmdConvert
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickleDecodeCode = `
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) >= 3:
|
||||||
|
action = sys.argv[1].lower()
|
||||||
|
content = sys.argv[2]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if action == 'decode':
|
||||||
|
decoded = base64.b64decode(content)
|
||||||
|
obj = pickle.loads(decoded)
|
||||||
|
unserialized = json.dumps(obj, ensure_ascii=False)
|
||||||
|
print(base64.b64encode(unserialized.encode('utf-8')).decode('utf-8'))
|
||||||
|
elif action == 'encode':
|
||||||
|
decoded = base64.b64decode(content)
|
||||||
|
obj = json.loads(decoded)
|
||||||
|
serialized = pickle.dumps(obj)
|
||||||
|
print(base64.b64encode(serialized).decode('utf-8'))
|
||||||
|
except:
|
||||||
|
print('[RDM-ERROR]')
|
||||||
|
else:
|
||||||
|
print('[RDM-ERROR]')
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func NewPickleConvert() *PickleConvert {
|
||||||
|
c := CmdConvert{
|
||||||
|
Name: "Pickle",
|
||||||
|
Auto: true,
|
||||||
|
}
|
||||||
|
c.DecodePath, c.EncodePath = "python3", "python3"
|
||||||
|
var err error
|
||||||
|
if _, err = exec.LookPath(c.DecodePath); err != nil {
|
||||||
|
c.DecodePath, c.EncodePath = "python", "python"
|
||||||
|
if _, err = exec.LookPath(c.DecodePath); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if pickle available
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
// the xcode-select installation prompt may appear on macOS
|
||||||
|
// so check it manually in advance
|
||||||
|
if _, err = exec.LookPath("xcode-select"); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = runCommand(c.DecodePath, "-c", "import pickle"); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var filepath string
|
||||||
|
if filepath, err = writeExecuteFile([]byte(pickleDecodeCode), "pickle_decoder.py"); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.DecodeArgs = []string{filepath, "decode"}
|
||||||
|
c.EncodeArgs = []string{filepath, "encode"}
|
||||||
|
|
||||||
|
return &PickleConvert{
|
||||||
|
CmdConvert: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PickleConvert) Enable() bool {
|
||||||
|
if p == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PickleConvert) Encode(str string) (string, bool) {
|
||||||
|
if !p.Enable() {
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
return p.CmdConvert.Encode(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PickleConvert) Decode(str string) (string, bool) {
|
||||||
|
if !p.Enable() {
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
return p.CmdConvert.Decode(str)
|
||||||
|
}
|
200
backend/utils/convert/unicode_json_convert.go
Normal file
200
backend/utils/convert/unicode_json_convert.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
strutil "tinyrdm/backend/utils/string"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnicodeJsonConvert struct{}
|
||||||
|
|
||||||
|
func (UnicodeJsonConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnicodeJsonConvert) Decode(str string) (string, bool) {
|
||||||
|
trimedStr := strings.TrimSpace(str)
|
||||||
|
if (strings.HasPrefix(trimedStr, "{") && strings.HasSuffix(trimedStr, "}")) ||
|
||||||
|
(strings.HasPrefix(trimedStr, "[") && strings.HasSuffix(trimedStr, "]")) {
|
||||||
|
resultStr := strutil.JSONBeautify(trimedStr, " ")
|
||||||
|
if quoteStr, ok := UnquoteUnicodeJson([]byte(resultStr)); ok {
|
||||||
|
return string(quoteStr), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnicodeJsonConvert) Encode(str string) (string, bool) {
|
||||||
|
return strutil.JSONMinify(str), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnquoteUnicodeJson(s []byte) ([]byte, bool) {
|
||||||
|
var unquoted bytes.Buffer
|
||||||
|
r := 0
|
||||||
|
ls := len(s)
|
||||||
|
for r < ls {
|
||||||
|
c := s[r]
|
||||||
|
offset := 1
|
||||||
|
if c == '"' {
|
||||||
|
// find next '"'
|
||||||
|
for ; r+offset < ls; offset++ {
|
||||||
|
if s[r+offset] == '"' && s[r+offset-1] != '\\' {
|
||||||
|
offset += 1
|
||||||
|
if ub, ok := unquoteBytes(s[r : r+offset]); ok {
|
||||||
|
unquoted.WriteString(strconv.Quote(string(ub)))
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// can not find close '"' until reach to the end of content
|
||||||
|
if r+offset >= ls {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unquoted.WriteByte(c)
|
||||||
|
}
|
||||||
|
r += offset
|
||||||
|
}
|
||||||
|
return unquoted.Bytes(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getu4(s []byte) rune {
|
||||||
|
if len(s) < 6 || s[0] != '\\' || s[1] != 'u' {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
var r rune
|
||||||
|
for _, c := range s[2:6] {
|
||||||
|
switch {
|
||||||
|
case '0' <= c && c <= '9':
|
||||||
|
c = c - '0'
|
||||||
|
case 'a' <= c && c <= 'f':
|
||||||
|
c = c - 'a' + 10
|
||||||
|
case 'A' <= c && c <= 'F':
|
||||||
|
c = c - 'A' + 10
|
||||||
|
default:
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
r = r*16 + rune(c)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func unquoteBytes(s []byte) (t []byte, ok bool) {
|
||||||
|
if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = s[1 : len(s)-1]
|
||||||
|
|
||||||
|
// Check for unusual characters. If there are none,
|
||||||
|
// then no unquoting is needed, so return a slice of the
|
||||||
|
// original bytes.
|
||||||
|
r := 0
|
||||||
|
for r < len(s) {
|
||||||
|
c := s[r]
|
||||||
|
if c == '\\' || c == '"' || c < ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if c < utf8.RuneSelf {
|
||||||
|
r++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rr, size := utf8.DecodeRune(s[r:])
|
||||||
|
if rr == utf8.RuneError && size == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
r += size
|
||||||
|
}
|
||||||
|
if r == len(s) {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, len(s)+2*utf8.UTFMax)
|
||||||
|
w := copy(b, s[0:r])
|
||||||
|
for r < len(s) {
|
||||||
|
// Out of room? Can only happen if s is full of
|
||||||
|
// malformed UTF-8 and we're replacing each
|
||||||
|
// byte with RuneError.
|
||||||
|
if w >= len(b)-2*utf8.UTFMax {
|
||||||
|
nb := make([]byte, (len(b)+utf8.UTFMax)*2)
|
||||||
|
copy(nb, b[0:w])
|
||||||
|
b = nb
|
||||||
|
}
|
||||||
|
switch c := s[r]; {
|
||||||
|
case c == '\\':
|
||||||
|
r++
|
||||||
|
if r >= len(s) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch s[r] {
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
case '"', '\\', '/', '\'':
|
||||||
|
b[w] = s[r]
|
||||||
|
r++
|
||||||
|
w++
|
||||||
|
case 'b':
|
||||||
|
b[w] = '\b'
|
||||||
|
r++
|
||||||
|
w++
|
||||||
|
case 'f':
|
||||||
|
b[w] = '\f'
|
||||||
|
r++
|
||||||
|
w++
|
||||||
|
case 'n':
|
||||||
|
b[w] = '\n'
|
||||||
|
r++
|
||||||
|
w++
|
||||||
|
case 'r':
|
||||||
|
b[w] = '\r'
|
||||||
|
r++
|
||||||
|
w++
|
||||||
|
case 't':
|
||||||
|
b[w] = '\t'
|
||||||
|
r++
|
||||||
|
w++
|
||||||
|
case 'u':
|
||||||
|
r--
|
||||||
|
rr := getu4(s[r:])
|
||||||
|
if rr < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r += 6
|
||||||
|
if utf16.IsSurrogate(rr) {
|
||||||
|
rr1 := getu4(s[r:])
|
||||||
|
if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar {
|
||||||
|
// A valid pair; consume.
|
||||||
|
r += 6
|
||||||
|
w += utf8.EncodeRune(b[w:], dec)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Invalid surrogate; fall back to replacement rune.
|
||||||
|
rr = unicode.ReplacementChar
|
||||||
|
}
|
||||||
|
w += utf8.EncodeRune(b[w:], rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote, control characters are invalid.
|
||||||
|
case c == '"', c < ' ':
|
||||||
|
return
|
||||||
|
|
||||||
|
// ASCII
|
||||||
|
case c < utf8.RuneSelf:
|
||||||
|
b[w] = c
|
||||||
|
r++
|
||||||
|
w++
|
||||||
|
|
||||||
|
// Coerce to well-formed UTF-8.
|
||||||
|
default:
|
||||||
|
rr, size := utf8.DecodeRune(s[r:])
|
||||||
|
r += size
|
||||||
|
w += utf8.EncodeRune(b[w:], rr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b[0:w], true
|
||||||
|
}
|
26
backend/utils/convert/xml_convert.go
Normal file
26
backend/utils/convert/xml_convert.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XmlConvert struct{}
|
||||||
|
|
||||||
|
func (XmlConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (XmlConvert) Encode(str string) (string, bool) {
|
||||||
|
return str, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (XmlConvert) Decode(str string) (string, bool) {
|
||||||
|
trimedStr := strings.TrimSpace(str)
|
||||||
|
if !strings.HasPrefix(trimedStr, "<") && !strings.HasSuffix(trimedStr, ">") {
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
var obj any
|
||||||
|
err := xml.Unmarshal([]byte(trimedStr), &obj)
|
||||||
|
return str, err == nil
|
||||||
|
}
|
21
backend/utils/convert/yaml_convert.go
Normal file
21
backend/utils/convert/yaml_convert.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YamlConvert struct{}
|
||||||
|
|
||||||
|
func (YamlConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YamlConvert) Encode(str string) (string, bool) {
|
||||||
|
return str, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YamlConvert) Decode(str string) (string, bool) {
|
||||||
|
var obj map[string]any
|
||||||
|
err := yaml.Unmarshal([]byte(str), &obj)
|
||||||
|
return str, err == nil
|
||||||
|
}
|
44
backend/utils/convert/zstd_convert.go
Normal file
44
backend/utils/convert/zstd_convert.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package convutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZStdConvert struct{}
|
||||||
|
|
||||||
|
func (ZStdConvert) Enable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ZStdConvert) Encode(str string) (string, bool) {
|
||||||
|
var compress = func(b []byte) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer, err := zstd.NewWriter(&buf)
|
||||||
|
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 zstdStr, err := compress([]byte(str)); err == nil {
|
||||||
|
return zstdStr, true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ZStdConvert) Decode(str string) (string, bool) {
|
||||||
|
if reader, err := zstd.NewReader(strings.NewReader(str)); err == nil {
|
||||||
|
defer reader.Close()
|
||||||
|
if decompressed, err := io.ReadAll(reader); err == nil {
|
||||||
|
return string(decompressed), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
@ -107,27 +107,6 @@ func Merge[M ~map[K]V, K Hashable, V any](mapArr ...M) M {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepMerge 深度递归覆盖src值到dst中
|
|
||||||
// 将返回新的值
|
|
||||||
func DeepMerge[M ~map[K]any, K Hashable](src1, src2 M) M {
|
|
||||||
out := make(map[K]any, len(src1))
|
|
||||||
for k, v := range src1 {
|
|
||||||
out[k] = v
|
|
||||||
}
|
|
||||||
for k, v := range src2 {
|
|
||||||
if v1, ok := v.(map[K]any); ok {
|
|
||||||
if bv, ok := out[k]; ok {
|
|
||||||
if bv1, ok := bv.(map[K]any); ok {
|
|
||||||
out[k] = DeepMerge(bv1, v1)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out[k] = v
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Omit 根据条件省略指定元素
|
// Omit 根据条件省略指定元素
|
||||||
func Omit[M ~map[K]V, K Hashable, V any](m M, omitFunc func(k K, v V) bool) (M, []K) {
|
func Omit[M ~map[K]V, K Hashable, V any](m M, omitFunc func(k K, v V) bool) (M, []K) {
|
||||||
result := M{}
|
result := M{}
|
||||||
|
96
backend/utils/proxy/http.go
Normal file
96
backend/utils/proxy/http.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpProxy struct {
|
||||||
|
scheme string // HTTP Proxy scheme
|
||||||
|
host string // HTTP Proxy host or host:port
|
||||||
|
auth *proxy.Auth // authentication
|
||||||
|
forward proxy.Dialer // forwarding Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HttpProxy) Dial(network, addr string) (net.Conn, error) {
|
||||||
|
c, err := p.forward.Dial(network, p.host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.SetDeadline(time.Now().Add(15 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqUrl := &url.URL{
|
||||||
|
Scheme: "",
|
||||||
|
Host: addr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create with CONNECT method
|
||||||
|
req, err := http.NewRequest("CONNECT", reqUrl.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Close = false
|
||||||
|
|
||||||
|
// authentication
|
||||||
|
if p.auth != nil {
|
||||||
|
req.SetBasicAuth(p.auth.User, p.auth.Password)
|
||||||
|
req.Header.Add("Proxy-Authorization", req.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// send request
|
||||||
|
err = req.Write(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := http.ReadResponse(bufio.NewReader(c), req)
|
||||||
|
if err != nil {
|
||||||
|
res.Body.Close()
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
c.Close()
|
||||||
|
return nil, fmt.Errorf("proxy connection error: StatusCode[%d]", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpProxyDialer(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
|
||||||
|
var auth *proxy.Auth
|
||||||
|
if u.User != nil {
|
||||||
|
pwd, _ := u.User.Password()
|
||||||
|
auth = &proxy.Auth{
|
||||||
|
User: u.User.Username(),
|
||||||
|
Password: pwd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hp := &HttpProxy{
|
||||||
|
scheme: u.Scheme,
|
||||||
|
host: u.Host,
|
||||||
|
auth: auth,
|
||||||
|
forward: forward,
|
||||||
|
}
|
||||||
|
return hp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proxy.RegisterDialerType("http", NewHttpProxyDialer)
|
||||||
|
proxy.RegisterDialerType("https", NewHttpProxyDialer)
|
||||||
|
}
|
@ -1,137 +1,11 @@
|
|||||||
package sliceutil
|
package sliceutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
. "tinyrdm/backend/utils"
|
. "tinyrdm/backend/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get 获取指定索引的值, 如果不存在则返回默认值
|
// Map map items to new array
|
||||||
func Get[S ~[]T, T any](arr S, index int, defaultVal T) T {
|
|
||||||
if index < 0 || index >= len(arr) {
|
|
||||||
return defaultVal
|
|
||||||
}
|
|
||||||
return arr[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 删除指定索引的元素
|
|
||||||
func Remove[S ~[]T, T any](arr S, index int) S {
|
|
||||||
return append(arr[:index], arr[index+1:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveIf 移除指定条件的元素
|
|
||||||
func RemoveIf[S ~[]T, T any](arr S, cond func(T) bool) S {
|
|
||||||
l := len(arr)
|
|
||||||
if l <= 0 {
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
for i := l - 1; i >= 0; i-- {
|
|
||||||
if cond(arr[i]) {
|
|
||||||
arr = append(arr[:i], arr[i+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveRange 删除从[from, to]部分元素
|
|
||||||
func RemoveRange[S ~[]T, T any](arr S, from, to int) S {
|
|
||||||
return append(arr[:from], arr[to:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find 查找指定条件的元素第一个出现位置
|
|
||||||
func Find[S ~[]T, T any](arr S, matchFunc func(int) bool) (int, bool) {
|
|
||||||
total := len(arr)
|
|
||||||
for i := 0; i < total; i++ {
|
|
||||||
if matchFunc(i) {
|
|
||||||
return i, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnyMatch 判断是否有任意元素符合条件
|
|
||||||
func AnyMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
|
|
||||||
total := len(arr)
|
|
||||||
if total > 0 {
|
|
||||||
for i := 0; i < total; i++ {
|
|
||||||
if matchFunc(i) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllMatch 判断是否所有元素都符合条件
|
|
||||||
func AllMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
|
|
||||||
total := len(arr)
|
|
||||||
for i := 0; i < total; i++ {
|
|
||||||
if !matchFunc(i) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals 比较两个切片内容是否完全一致
|
|
||||||
func Equals[S ~[]T, T comparable](arr1, arr2 S) bool {
|
|
||||||
if &arr1 == &arr2 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
len1, len2 := len(arr1), len(arr2)
|
|
||||||
if len1 != len2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < len1; i++ {
|
|
||||||
if arr1[i] != arr2[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains 判断数组是否包含指定元素
|
|
||||||
func Contains[S ~[]T, T Hashable](arr S, elem T) bool {
|
|
||||||
return AnyMatch(arr, func(idx int) bool {
|
|
||||||
return arr[idx] == elem
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsAny 判断数组是否包含任意指定元素
|
|
||||||
func ContainsAny[S ~[]T, T Hashable](arr S, elems ...T) bool {
|
|
||||||
for _, elem := range elems {
|
|
||||||
if Contains(arr, elem) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsAll 判断数组是否包含所有指定元素
|
|
||||||
func ContainsAll[S ~[]T, T Hashable](arr S, elems ...T) bool {
|
|
||||||
for _, elem := range elems {
|
|
||||||
if !Contains(arr, elem) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter 筛选出符合指定条件的所有元素
|
|
||||||
func Filter[S ~[]T, T any](arr S, filterFunc func(int) bool) []T {
|
|
||||||
total := len(arr)
|
|
||||||
var result []T
|
|
||||||
for i := 0; i < total; i++ {
|
|
||||||
if filterFunc(i) {
|
|
||||||
result = append(result, arr[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map 数组映射转换
|
|
||||||
func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
||||||
total := len(arr)
|
total := len(arr)
|
||||||
result := make([]R, total)
|
result := make([]R, total)
|
||||||
@ -141,7 +15,7 @@ func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterMap 数组过滤和映射转换
|
// FilterMap filter and map items to new array
|
||||||
func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R {
|
func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R {
|
||||||
total := len(arr)
|
total := len(arr)
|
||||||
result := make([]R, 0, total)
|
result := make([]R, 0, total)
|
||||||
@ -155,68 +29,7 @@ func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap 数组转键值对
|
// Join join any array to a single string by custom function
|
||||||
func ToMap[S ~[]T, T any, K Hashable, V any](arr S, mappingFunc func(int) (K, V)) map[K]V {
|
|
||||||
total := len(arr)
|
|
||||||
result := map[K]V{}
|
|
||||||
for i := 0; i < total; i++ {
|
|
||||||
key, val := mappingFunc(i)
|
|
||||||
result[key] = val
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flat 二维数组扁平化
|
|
||||||
func Flat[T any](arr [][]T) []T {
|
|
||||||
total := len(arr)
|
|
||||||
var result []T
|
|
||||||
for i := 0; i < total; i++ {
|
|
||||||
subTotal := len(arr[i])
|
|
||||||
for j := 0; j < subTotal; j++ {
|
|
||||||
result = append(result, arr[i][j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlatMap 二维数组扁平化映射
|
|
||||||
func FlatMap[T any, R any](arr [][]T, mappingFunc func(int, int) R) []R {
|
|
||||||
total := len(arr)
|
|
||||||
var result []R
|
|
||||||
for i := 0; i < total; i++ {
|
|
||||||
subTotal := len(arr[i])
|
|
||||||
for j := 0; j < subTotal; j++ {
|
|
||||||
result = append(result, mappingFunc(i, j))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func FlatValueMap[T Hashable](arr [][]T) []T {
|
|
||||||
return FlatMap(arr, func(i, j int) T {
|
|
||||||
return arr[i][j]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce 数组累计
|
|
||||||
func Reduce[S ~[]T, T any, R any](arr S, init R, reduceFunc func(R, T) R) R {
|
|
||||||
result := init
|
|
||||||
for _, item := range arr {
|
|
||||||
result = reduceFunc(result, item)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse 反转数组(会修改原数组)
|
|
||||||
func Reverse[S ~[]T, T any](arr S) S {
|
|
||||||
total := len(arr)
|
|
||||||
for i := 0; i < total/2; i++ {
|
|
||||||
arr[i], arr[total-i-1] = arr[total-i-1], arr[i]
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join 数组拼接转字符串
|
|
||||||
func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) string {
|
func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) string {
|
||||||
total := len(arr)
|
total := len(arr)
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
@ -236,21 +49,14 @@ func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) strin
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinString 字符串数组拼接成字符串
|
// JoinString join string array to a single string
|
||||||
func JoinString(arr []string, sep string) string {
|
func JoinString(arr []string, sep string) string {
|
||||||
return Join(arr, sep, func(idx int) string {
|
return Join(arr, sep, func(idx int) string {
|
||||||
return arr[idx]
|
return arr[idx]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinInt 整形数组拼接转字符串
|
// Unique filter unique item
|
||||||
func JoinInt(arr []int, sep string) string {
|
|
||||||
return Join(arr, sep, func(idx int) string {
|
|
||||||
return strconv.Itoa(arr[idx])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unique 数组去重
|
|
||||||
func Unique[S ~[]T, T Hashable](arr S) S {
|
func Unique[S ~[]T, T Hashable](arr S) S {
|
||||||
result := make(S, 0, len(arr))
|
result := make(S, 0, len(arr))
|
||||||
uniKeys := map[T]struct{}{}
|
uniKeys := map[T]struct{}{}
|
||||||
@ -263,136 +69,3 @@ func Unique[S ~[]T, T Hashable](arr S) S {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// UniqueEx 数组去重(任意类型)
|
|
||||||
// @param toKeyFunc 数组元素转为唯一标识字符串函数, 如转为哈希值等
|
|
||||||
func UniqueEx[S ~[]T, T any](arr S, toKeyFunc func(i int) string) S {
|
|
||||||
result := make(S, 0, len(arr))
|
|
||||||
keyArr := Map(arr, toKeyFunc)
|
|
||||||
uniKeys := map[string]struct{}{}
|
|
||||||
var exists bool
|
|
||||||
for i, item := range arr {
|
|
||||||
if _, exists = uniKeys[keyArr[i]]; !exists {
|
|
||||||
uniKeys[keyArr[i]] = struct{}{}
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort 顺序排序(会修改原数组)
|
|
||||||
func Sort[S ~[]T, T Hashable](arr S) S {
|
|
||||||
sort.Slice(arr, func(i, j int) bool {
|
|
||||||
return arr[i] <= arr[j]
|
|
||||||
})
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// SortDesc 倒序排序(会修改原数组)
|
|
||||||
func SortDesc[S ~[]T, T Hashable](arr S) S {
|
|
||||||
sort.Slice(arr, func(i, j int) bool {
|
|
||||||
return arr[i] > arr[j]
|
|
||||||
})
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Union 返回两个切片共同拥有的元素
|
|
||||||
func Union[S ~[]T, T Hashable](arr1 S, arr2 S) S {
|
|
||||||
hashArr, compArr := arr1, arr2
|
|
||||||
if len(arr1) < len(arr2) {
|
|
||||||
hashArr, compArr = compArr, hashArr
|
|
||||||
}
|
|
||||||
hash := map[T]struct{}{}
|
|
||||||
for _, item := range hashArr {
|
|
||||||
hash[item] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
uniq := map[T]struct{}{}
|
|
||||||
ret := make(S, 0, len(compArr))
|
|
||||||
exists := false
|
|
||||||
for _, item := range compArr {
|
|
||||||
if _, exists = hash[item]; exists {
|
|
||||||
if _, exists = uniq[item]; !exists {
|
|
||||||
ret = append(ret, item)
|
|
||||||
uniq[item] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exclude 返回不包含的元素
|
|
||||||
func Exclude[S ~[]T, T Hashable](arr1 S, arr2 S) S {
|
|
||||||
diff := make([]T, 0, len(arr1))
|
|
||||||
hash := map[T]struct{}{}
|
|
||||||
for _, item := range arr2 {
|
|
||||||
hash[item] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range arr1 {
|
|
||||||
if _, exists := hash[item]; !exists {
|
|
||||||
diff = append(diff, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// PadLeft 左边填充指定数量
|
|
||||||
func PadLeft[S ~[]T, T any](arr S, val T, count int) S {
|
|
||||||
prefix := make(S, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
prefix[i] = val
|
|
||||||
}
|
|
||||||
arr = append(prefix, arr...)
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// PadRight 右边填充指定数量
|
|
||||||
func PadRight[S ~[]T, T any](arr S, val T, count int) S {
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
arr = append(arr, val)
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveLeft 移除左侧相同元素
|
|
||||||
func RemoveLeft[S ~[]T, T comparable](arr S, val T) S {
|
|
||||||
for len(arr) > 0 && arr[0] == val {
|
|
||||||
arr = arr[1:]
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveRight 移除右侧相同元素
|
|
||||||
func RemoveRight[S ~[]T, T comparable](arr S, val T) S {
|
|
||||||
for {
|
|
||||||
length := len(arr)
|
|
||||||
if length > 0 && arr[length-1] == val {
|
|
||||||
arr = arr[:length]
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count 统计制定条件元素数量
|
|
||||||
func Count[S ~[]T, T any](arr S, filter func(int) bool) int {
|
|
||||||
count := 0
|
|
||||||
for i := range arr {
|
|
||||||
if filter(i) {
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group 根据分组函数对数组进行分组汇总
|
|
||||||
func Group[S ~[]T, T any, K Hashable, R any](arr S, groupFunc func(int) (K, R)) map[K][]R {
|
|
||||||
ret := map[K][]R{}
|
|
||||||
for i := range arr {
|
|
||||||
key, val := groupFunc(i)
|
|
||||||
ret[key] = append(ret[key], val)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,6 @@ package strutil
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
sliceutil "tinyrdm/backend/utils/slice"
|
sliceutil "tinyrdm/backend/utils/slice"
|
||||||
@ -133,14 +132,41 @@ func AnyToString(value interface{}, prefix string, layer int) (s string) {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
func SplitCmd(cmd string) []string {
|
func SplitCmd(cmd string) []string {
|
||||||
re := regexp.MustCompile(`'[^']+'|"[^"]+"|\S+`)
|
var result []string
|
||||||
args := re.FindAllString(cmd, -1)
|
var curStr strings.Builder
|
||||||
return sliceutil.FilterMap(args, func(i int) (string, bool) {
|
var preChar int32
|
||||||
arg := strings.Trim(args[i], "\"")
|
var quotesChar int32
|
||||||
arg = strings.Trim(arg, "'")
|
|
||||||
if len(arg) <= 0 {
|
cmdRune := []rune(cmd)
|
||||||
|
for _, char := range cmdRune {
|
||||||
|
if (char == '"' || char == '\'') && preChar != '\\' && (quotesChar == 0 || quotesChar == char) {
|
||||||
|
if quotesChar != 0 {
|
||||||
|
quotesChar = 0
|
||||||
|
} else {
|
||||||
|
quotesChar = char
|
||||||
|
}
|
||||||
|
} else if char == ' ' && quotesChar == 0 {
|
||||||
|
result = append(result, curStr.String())
|
||||||
|
curStr.Reset()
|
||||||
|
} else {
|
||||||
|
curStr.WriteRune(char)
|
||||||
|
}
|
||||||
|
preChar = char
|
||||||
|
}
|
||||||
|
result = append(result, curStr.String())
|
||||||
|
|
||||||
|
result = sliceutil.FilterMap(result, func(i int) (string, bool) {
|
||||||
|
var part = strings.TrimSpace(result[i])
|
||||||
|
if len(part) <= 0 {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return arg, true
|
if strings.Contains(part, "\\") {
|
||||||
|
if unquotePart, e := strconv.Unquote(`"` + part + `"`); e == nil {
|
||||||
|
return unquotePart, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return part, true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func containsBinary(str string) bool {
|
func ContainsBinary(str string) bool {
|
||||||
//buf := []byte(str)
|
//buf := []byte(str)
|
||||||
//size := 0
|
//size := 0
|
||||||
//for start := 0; start < len(buf); start += size {
|
//for start := 0; start < len(buf); start += size {
|
||||||
@ -15,6 +15,9 @@ func containsBinary(str string) bool {
|
|||||||
//}
|
//}
|
||||||
rs := []rune(str)
|
rs := []rune(str)
|
||||||
for _, r := range rs {
|
for _, r := range rs {
|
||||||
|
if r == unicode.ReplacementChar {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if !unicode.IsPrint(r) && !unicode.IsSpace(r) {
|
if !unicode.IsPrint(r) && !unicode.IsSpace(r) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -22,7 +25,7 @@ func containsBinary(str string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSameChar(str string) bool {
|
func IsSameChar(str string) bool {
|
||||||
if len(str) <= 0 {
|
if len(str) <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -1,464 +0,0 @@
|
|||||||
package strutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/andybalholm/brotli"
|
|
||||||
"github.com/klauspost/compress/flate"
|
|
||||||
"github.com/klauspost/compress/gzip"
|
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
"io"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"tinyrdm/backend/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
if len(str) <= 0 {
|
|
||||||
// empty content
|
|
||||||
if len(formatType) <= 0 {
|
|
||||||
resultFormat = types.FORMAT_RAW
|
|
||||||
} else {
|
|
||||||
resultFormat = formatType
|
|
||||||
}
|
|
||||||
if len(decodeType) <= 0 {
|
|
||||||
resultDecode = types.DECODE_NONE
|
|
||||||
} else {
|
|
||||||
resultDecode = decodeType
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode first
|
|
||||||
value, resultDecode = decodeWith(str, decodeType)
|
|
||||||
// then format content
|
|
||||||
value, resultFormat = viewAs(value, formatType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeWith(str, decodeType string) (value, resultDecode string) {
|
|
||||||
if len(decodeType) > 0 {
|
|
||||||
switch decodeType {
|
|
||||||
case types.DECODE_NONE:
|
|
||||||
value = str
|
|
||||||
resultDecode = decodeType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_BASE64:
|
|
||||||
if base64Str, ok := decodeBase64(str); ok {
|
|
||||||
value = base64Str
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultDecode = decodeType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_GZIP:
|
|
||||||
if gzipStr, ok := decodeGZip(str); ok {
|
|
||||||
value = gzipStr
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultDecode = decodeType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_DEFLATE:
|
|
||||||
if falteStr, ok := decodeDeflate(str); ok {
|
|
||||||
value = falteStr
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultDecode = decodeType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_ZSTD:
|
|
||||||
if zstdStr, ok := decodeZStd(str); ok {
|
|
||||||
value = zstdStr
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultDecode = decodeType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_BROTLI:
|
|
||||||
if brotliStr, ok := decodeBrotli(str); ok {
|
|
||||||
value = brotliStr
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultDecode = decodeType
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return autoDecode(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
if len(str) > 0 {
|
|
||||||
// pure digit content may incorrect regard as some encoded type, skip decode
|
|
||||||
if match, _ := regexp.MatchString(`^\d+$`, str); !match {
|
|
||||||
var ok bool
|
|
||||||
if len(str)%4 == 0 && len(str) >= 12 && !isSameChar(str) {
|
|
||||||
if value, ok = decodeBase64(str); ok {
|
|
||||||
resultDecode = types.DECODE_BASE64
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if value, ok = decodeGZip(str); ok {
|
|
||||||
resultDecode = types.DECODE_GZIP
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: skip decompress with deflate due to incorrect format checking
|
|
||||||
//if value, ok = decodeDeflate(str); ok {
|
|
||||||
// resultDecode = types.DECODE_DEFLATE
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
|
|
||||||
if value, ok = decodeZStd(str); ok {
|
|
||||||
resultDecode = types.DECODE_ZSTD
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: skip decompress with brotli due to incorrect format checking
|
|
||||||
//if value, ok = decodeBrotli(str); ok {
|
|
||||||
// resultDecode = types.DECODE_BROTLI
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value = str
|
|
||||||
resultDecode = types.DECODE_NONE
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func viewAs(str, formatType string) (value, resultFormat string) {
|
|
||||||
if len(formatType) > 0 {
|
|
||||||
switch formatType {
|
|
||||||
case types.FORMAT_RAW, types.FORMAT_YAML, types.FORMAT_XML:
|
|
||||||
value = str
|
|
||||||
resultFormat = formatType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.FORMAT_JSON:
|
|
||||||
if jsonStr, ok := decodeJson(str); ok {
|
|
||||||
value = jsonStr
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultFormat = formatType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.FORMAT_HEX:
|
|
||||||
if hexStr, ok := decodeToHex(str); ok {
|
|
||||||
value = hexStr
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultFormat = formatType
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.FORMAT_BINARY:
|
|
||||||
if binStr, ok := decodeBinary(str); ok {
|
|
||||||
value = binStr
|
|
||||||
} else {
|
|
||||||
value = str
|
|
||||||
}
|
|
||||||
resultFormat = formatType
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return autoViewAs(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// attempt automatic convert to possible types
|
|
||||||
// if no conversion is possible, it will return the origin string value and "plain text" type
|
|
||||||
func autoViewAs(str string) (value, resultFormat string) {
|
|
||||||
if len(str) > 0 {
|
|
||||||
var ok bool
|
|
||||||
if value, ok = decodeJson(str); ok {
|
|
||||||
resultFormat = types.FORMAT_JSON
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if containsBinary(str) {
|
|
||||||
if value, ok = decodeToHex(str); ok {
|
|
||||||
resultFormat = types.FORMAT_HEX
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value = str
|
|
||||||
resultFormat = types.FORMAT_RAW
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeJson(str string) (string, bool) {
|
|
||||||
trimedStr := strings.TrimSpace(str)
|
|
||||||
if (strings.HasPrefix(trimedStr, "{") && strings.HasSuffix(trimedStr, "}")) ||
|
|
||||||
(strings.HasPrefix(trimedStr, "[") && strings.HasSuffix(trimedStr, "]")) {
|
|
||||||
var out bytes.Buffer
|
|
||||||
if err := json.Indent(&out, []byte(trimedStr), "", " "); err == nil {
|
|
||||||
return out.String(), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBase64(str string) (string, bool) {
|
|
||||||
if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil {
|
|
||||||
if s := string(decodedStr); !containsBinary(s) {
|
|
||||||
return s, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBinary(str string) (string, bool) {
|
|
||||||
var binary strings.Builder
|
|
||||||
for _, char := range str {
|
|
||||||
binary.WriteString(fmt.Sprintf("%08b", int(char)))
|
|
||||||
}
|
|
||||||
return binary.String(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeToHex(str string) (string, bool) {
|
|
||||||
decodeStr := hex.EncodeToString([]byte(str))
|
|
||||||
var resultStr strings.Builder
|
|
||||||
for i := 0; i < len(decodeStr); i += 2 {
|
|
||||||
resultStr.WriteString("\\x")
|
|
||||||
resultStr.WriteString(decodeStr[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 decodeZStd(str string) (string, bool) {
|
|
||||||
if reader, err := zstd.NewReader(strings.NewReader(str)); err == nil {
|
|
||||||
defer reader.Close()
|
|
||||||
if decompressed, err := io.ReadAll(reader); err == nil {
|
|
||||||
return string(decompressed), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBrotli(str string) (string, bool) {
|
|
||||||
reader := brotli.NewReader(strings.NewReader(str))
|
|
||||||
if decompressed, err := io.ReadAll(reader); err == nil {
|
|
||||||
return string(decompressed), true
|
|
||||||
}
|
|
||||||
return str, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveAs(str, format, decode string) (value string, err error) {
|
|
||||||
value = str
|
|
||||||
switch format {
|
|
||||||
case types.FORMAT_JSON:
|
|
||||||
if jsonStr, ok := encodeJson(str); ok {
|
|
||||||
value = jsonStr
|
|
||||||
} else {
|
|
||||||
err = errors.New("invalid json data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case types.FORMAT_HEX:
|
|
||||||
if hexStr, ok := encodeHex(str); ok {
|
|
||||||
value = hexStr
|
|
||||||
} else {
|
|
||||||
err = errors.New("invalid hex data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case types.FORMAT_BINARY:
|
|
||||||
if binStr, ok := encodeBinary(str); ok {
|
|
||||||
value = binStr
|
|
||||||
} else {
|
|
||||||
err = errors.New("invalid binary data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch decode {
|
|
||||||
case types.DECODE_NONE:
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_BASE64:
|
|
||||||
value, _ = encodeBase64(value)
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_GZIP:
|
|
||||||
if gzipStr, ok := encodeGZip(str); ok {
|
|
||||||
value = gzipStr
|
|
||||||
} else {
|
|
||||||
err = errors.New("fail to build gzip")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_DEFLATE:
|
|
||||||
if deflateStr, ok := encodeDeflate(str); ok {
|
|
||||||
value = deflateStr
|
|
||||||
} else {
|
|
||||||
err = errors.New("fail to build deflate")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_ZSTD:
|
|
||||||
if zstdStr, ok := encodeZStd(str); ok {
|
|
||||||
value = zstdStr
|
|
||||||
} else {
|
|
||||||
err = errors.New("fail to build zstd")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
case types.DECODE_BROTLI:
|
|
||||||
if brotliStr, ok := encodeBrotli(str); ok {
|
|
||||||
value = brotliStr
|
|
||||||
} else {
|
|
||||||
err = errors.New("fail to build brotli")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeJson(str string) (string, bool) {
|
|
||||||
var dst bytes.Buffer
|
|
||||||
if err := json.Compact(&dst, []byte(str)); err != nil {
|
|
||||||
return str, false
|
|
||||||
}
|
|
||||||
return dst.String(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeBase64(str string) (string, bool) {
|
|
||||||
return base64.StdEncoding.EncodeToString([]byte(str)), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeBinary(str string) (string, bool) {
|
|
||||||
var result strings.Builder
|
|
||||||
total := len(str)
|
|
||||||
for i := 0; i < total; i += 8 {
|
|
||||||
b, _ := strconv.ParseInt(str[i:i+8], 2, 8)
|
|
||||||
result.WriteByte(byte(b))
|
|
||||||
}
|
|
||||||
return result.String(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeHex(str string) (string, bool) {
|
|
||||||
hexStrArr := strings.Split(str, "\\x")
|
|
||||||
hexStr := strings.Join(hexStrArr, "")
|
|
||||||
if decodeStr, err := hex.DecodeString(hexStr); err == nil {
|
|
||||||
return string(decodeStr), true
|
|
||||||
}
|
|
||||||
|
|
||||||
return str, false
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeZStd(str string) (string, bool) {
|
|
||||||
var compress = func(b []byte) (string, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
writer, err := zstd.NewWriter(&buf)
|
|
||||||
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 zstdStr, err := compress([]byte(str)); err == nil {
|
|
||||||
return zstdStr, true
|
|
||||||
}
|
|
||||||
return str, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeBrotli(str string) (string, bool) {
|
|
||||||
var compress = func(b []byte) (string, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
writer := brotli.NewWriter(&buf)
|
|
||||||
if _, err := writer.Write([]byte(str)); err != nil {
|
|
||||||
writer.Close()
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
writer.Close()
|
|
||||||
return string(buf.Bytes()), nil
|
|
||||||
}
|
|
||||||
if brotliStr, err := compress([]byte(str)); err == nil {
|
|
||||||
return brotliStr, true
|
|
||||||
}
|
|
||||||
return str, false
|
|
||||||
}
|
|
158
backend/utils/string/json_formatter.go
Normal file
158
backend/utils/string/json_formatter.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package strutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Convert from https://github.com/ObuchiYuki/SwiftJSONFormatter
|
||||||
|
|
||||||
|
// ArrayIterator defines the iterator for an array
|
||||||
|
type ArrayIterator[T any] struct {
|
||||||
|
array []T
|
||||||
|
head int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArrayIterator initializes a new ArrayIterator with the given array
|
||||||
|
func NewArrayIterator[T any](array []T) *ArrayIterator[T] {
|
||||||
|
return &ArrayIterator[T]{
|
||||||
|
array: array,
|
||||||
|
head: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNext returns true if there are more elements to iterate over
|
||||||
|
func (it *ArrayIterator[T]) HasNext() bool {
|
||||||
|
return it.head+1 < len(it.array)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekNext returns the next element without advancing the iterator
|
||||||
|
func (it *ArrayIterator[T]) PeekNext() *T {
|
||||||
|
if it.head+1 < len(it.array) {
|
||||||
|
return &it.array[it.head+1]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next element and advances the iterator
|
||||||
|
func (it *ArrayIterator[T]) Next() *T {
|
||||||
|
defer func() {
|
||||||
|
it.head++
|
||||||
|
}()
|
||||||
|
return it.PeekNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONBeautify formats a JSON string with indentation
|
||||||
|
func JSONBeautify(value string, indent string) string {
|
||||||
|
if len(indent) <= 0 {
|
||||||
|
indent = " "
|
||||||
|
}
|
||||||
|
return format(value, indent, "\n", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONMinify formats a JSON string by removing all unnecessary whitespace
|
||||||
|
func JSONMinify(value string) string {
|
||||||
|
return format(value, "", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// format applies the specified formatting to a JSON string
|
||||||
|
func format(value string, indent string, newLine string, separator string) string {
|
||||||
|
var formatted strings.Builder
|
||||||
|
chars := NewArrayIterator([]rune(value))
|
||||||
|
indentLevel := 0
|
||||||
|
|
||||||
|
for chars.HasNext() {
|
||||||
|
if char := chars.Next(); char != nil {
|
||||||
|
switch *char {
|
||||||
|
case '{', '[':
|
||||||
|
formatted.WriteRune(*char)
|
||||||
|
consumeWhitespaces(chars)
|
||||||
|
peeked := chars.PeekNext()
|
||||||
|
if peeked != nil && (*peeked == '}' || *peeked == ']') {
|
||||||
|
chars.Next()
|
||||||
|
formatted.WriteRune(*peeked)
|
||||||
|
} else {
|
||||||
|
indentLevel++
|
||||||
|
formatted.WriteString(newLine)
|
||||||
|
formatted.WriteString(strings.Repeat(indent, indentLevel))
|
||||||
|
}
|
||||||
|
case '}', ']':
|
||||||
|
indentLevel--
|
||||||
|
formatted.WriteString(newLine)
|
||||||
|
formatted.WriteString(strings.Repeat(indent, max(0, indentLevel)))
|
||||||
|
formatted.WriteRune(*char)
|
||||||
|
case '"':
|
||||||
|
str := consumeString(chars)
|
||||||
|
//str = convertUnicodeString(str)
|
||||||
|
formatted.WriteString(str)
|
||||||
|
case ',':
|
||||||
|
consumeWhitespaces(chars)
|
||||||
|
formatted.WriteRune(',')
|
||||||
|
peeked := chars.PeekNext()
|
||||||
|
if peeked != nil && *peeked != '}' && *peeked != ']' {
|
||||||
|
formatted.WriteString(newLine)
|
||||||
|
formatted.WriteString(strings.Repeat(indent, max(0, indentLevel)))
|
||||||
|
}
|
||||||
|
case ':':
|
||||||
|
formatted.WriteString(":" + separator)
|
||||||
|
default:
|
||||||
|
if !unicode.IsSpace(*char) {
|
||||||
|
formatted.WriteRune(*char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeWhitespaces advances the iterator past any whitespace characters
|
||||||
|
func consumeWhitespaces(iter *ArrayIterator[rune]) {
|
||||||
|
for iter.HasNext() {
|
||||||
|
if peeked := iter.PeekNext(); peeked != nil && unicode.IsSpace(*peeked) {
|
||||||
|
iter.Next()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeString consumes a JSON string value from the iterator
|
||||||
|
func consumeString(iter *ArrayIterator[rune]) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteRune('"')
|
||||||
|
escaping := false
|
||||||
|
|
||||||
|
for iter.HasNext() {
|
||||||
|
if char := iter.Next(); char != nil {
|
||||||
|
if *char == '\n' {
|
||||||
|
return sb.String() // Unterminated string
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteRune(*char)
|
||||||
|
|
||||||
|
if escaping {
|
||||||
|
escaping = false
|
||||||
|
} else {
|
||||||
|
if *char == '\\' {
|
||||||
|
escaping = true
|
||||||
|
}
|
||||||
|
if *char == '"' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUnicodeString(str string) string {
|
||||||
|
// TODO: quote UTF-16 characters
|
||||||
|
//if len(str) > 2 {
|
||||||
|
// if unqStr, err := strconv.Unquote(str); err == nil {
|
||||||
|
// return strconv.Quote(unqStr)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
return str
|
||||||
|
}
|
@ -8,7 +8,7 @@ import (
|
|||||||
// EncodeRedisKey encode the redis key to integer array
|
// EncodeRedisKey encode the redis key to integer array
|
||||||
// if key contains binary which could not display on ui, convert the key to char array
|
// if key contains binary which could not display on ui, convert the key to char array
|
||||||
func EncodeRedisKey(key string) any {
|
func EncodeRedisKey(key string) any {
|
||||||
if containsBinary(key) {
|
if ContainsBinary(key) {
|
||||||
b := []byte(key)
|
b := []byte(key)
|
||||||
arr := make([]int, len(b))
|
arr := make([]int, len(b))
|
||||||
for i, bb := range b {
|
for i, bb := range b {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>iconfile</string>
|
<string>iconfile</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.13.0</string>
|
<string>11.7.0</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>iconfile</string>
|
<string>iconfile</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.13.0</string>
|
<string>11.7.0</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
@ -3,6 +3,7 @@ Version: {{.Info.ProductVersion}}
|
|||||||
Section: base
|
Section: base
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
|
Depends: libwebkit2gtk-4.0-37
|
||||||
Maintainer: {{.Author.Name}} <{{.Author.Email}}>
|
Maintainer: {{.Author.Name}} <{{.Author.Email}}>
|
||||||
Homepage: https://redis.tinycraft.cc/
|
Homepage: https://redis.tinycraft.cc/
|
||||||
Description: {{.Info.Comments}}
|
Description: {{.Info.Comments}}
|
||||||
|
BIN
docs/images/bilibili_official.png
Normal file
BIN
docs/images/bilibili_official.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@ -1,14 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang='en'>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset='UTF-8' />
|
<meta charset="UTF-8" />
|
||||||
<meta content='width=device-width, initial-scale=1.0' name='viewport' />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Tiny RDM</title>
|
<title>Tiny RDM</title>
|
||||||
<!-- <link href="./src/styles/style.scss" rel="stylesheet">-->
|
<!-- <link href="./src/styles/style.scss" rel="stylesheet">-->
|
||||||
</head>
|
</head>
|
||||||
<body spellcheck="false">
|
<body spellcheck="false">
|
||||||
<div id='app'></div>
|
<div id="app"></div>
|
||||||
<script src='./src/main.js' type='module'></script>
|
<script src="./src/main.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
4121
frontend/package-lock.json
generated
4121
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,25 +9,26 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.8",
|
||||||
"dayjs": "^1.11.10",
|
"copy-text-to-clipboard": "^3.2.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"monaco-editor": "^0.45.0",
|
"monaco-editor": "^0.47.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^3.0.1",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.85.1",
|
||||||
"vue": "^3.4.14",
|
"vue": "^3.5.13",
|
||||||
"vue-chartjs": "^5.3.0",
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-i18n": "^9.9.0",
|
"vue-i18n": "^11.1.2",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0"
|
"xterm-addon-fit": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"naive-ui": "^2.37.3",
|
"naive-ui": "^2.41.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.5.3",
|
||||||
"unplugin-auto-import": "^0.17.3",
|
"unplugin-auto-import": "^19.1.1",
|
||||||
"unplugin-icons": "^0.18.2",
|
"unplugin-icons": "^22.1.0",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^28.4.1",
|
||||||
"vite": "^5.0.11"
|
"vite": "^6.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
43b12c3794341237a522ac6457ab81da
|
47ebcfd89e9e219e5b4ccf43ca2aa197
|
@ -8,18 +8,21 @@ import AddFieldsDialog from './components/dialogs/AddFieldsDialog.vue'
|
|||||||
import AppContent from './AppContent.vue'
|
import AppContent from './AppContent.vue'
|
||||||
import GroupDialog from './components/dialogs/GroupDialog.vue'
|
import GroupDialog from './components/dialogs/GroupDialog.vue'
|
||||||
import DeleteKeyDialog from './components/dialogs/DeleteKeyDialog.vue'
|
import DeleteKeyDialog from './components/dialogs/DeleteKeyDialog.vue'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { h, onMounted, ref, watch } from 'vue'
|
||||||
import usePreferencesStore from './stores/preferences.js'
|
import usePreferencesStore from './stores/preferences.js'
|
||||||
import useConnectionStore from './stores/connections.js'
|
import useConnectionStore from './stores/connections.js'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { darkTheme } from 'naive-ui'
|
import { darkTheme, NButton, NSpace } from 'naive-ui'
|
||||||
import KeyFilterDialog from './components/dialogs/KeyFilterDialog.vue'
|
import KeyFilterDialog from './components/dialogs/KeyFilterDialog.vue'
|
||||||
import { WindowSetDarkTheme, WindowSetLightTheme } from 'wailsjs/runtime/runtime.js'
|
import { Environment, WindowSetDarkTheme, WindowSetLightTheme } from 'wailsjs/runtime/runtime.js'
|
||||||
import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
|
import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
|
||||||
import AboutDialog from '@/components/dialogs/AboutDialog.vue'
|
import AboutDialog from '@/components/dialogs/AboutDialog.vue'
|
||||||
import FlushDbDialog from '@/components/dialogs/FlushDbDialog.vue'
|
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 DecoderDialog from '@/components/dialogs/DecoderDialog.vue'
|
||||||
|
import { loadModule, trackEvent } from '@/utils/analytics.js'
|
||||||
|
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
const connectionStore = useConnectionStore()
|
const connectionStore = useConnectionStore()
|
||||||
@ -29,10 +32,66 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
initializing.value = true
|
initializing.value = true
|
||||||
await prefStore.loadFontList()
|
await prefStore.loadFontList()
|
||||||
|
await prefStore.loadBuildInDecoder()
|
||||||
await connectionStore.initConnections()
|
await connectionStore.initConnections()
|
||||||
if (prefStore.autoCheckUpdate) {
|
if (prefStore.autoCheckUpdate) {
|
||||||
prefStore.checkForUpdate()
|
prefStore.checkForUpdate()
|
||||||
}
|
}
|
||||||
|
const env = await Environment()
|
||||||
|
loadModule(env.buildType !== 'dev' && prefStore.general.allowTrack !== false).then(() => {
|
||||||
|
Info().then(({ data }) => {
|
||||||
|
trackEvent('startup', data, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// show greetings and user behavior tracking statements
|
||||||
|
if (!!!prefStore.behavior.welcomed) {
|
||||||
|
const n = $notification.show({
|
||||||
|
title: () => i18n.t('dialogue.welcome.title'),
|
||||||
|
content: () => i18n.t('dialogue.welcome.content'),
|
||||||
|
// duration: 5000,
|
||||||
|
keepAliveOnHover: true,
|
||||||
|
closable: false,
|
||||||
|
meta: ' ',
|
||||||
|
action: () =>
|
||||||
|
h(
|
||||||
|
NSpace,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
default: () => [
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
secondary: true,
|
||||||
|
type: 'tertiary',
|
||||||
|
onClick: () => {
|
||||||
|
prefStore.setAsWelcomed(false)
|
||||||
|
n.destroy()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => i18n.t('dialogue.welcome.reject'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
secondary: true,
|
||||||
|
type: 'primary',
|
||||||
|
onClick: () => {
|
||||||
|
prefStore.setAsWelcomed(true)
|
||||||
|
n.destroy()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => i18n.t('dialogue.welcome.accept'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
initializing.value = false
|
initializing.value = false
|
||||||
}
|
}
|
||||||
@ -74,6 +133,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>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ContentPane from './components/content/ContentPane.vue'
|
import ContentPane from './components/content/ContentPane.vue'
|
||||||
import BrowserPane from './components/sidebar/BrowserPane.vue'
|
import BrowserPane from './components/sidebar/BrowserPane.vue'
|
||||||
import { computed, onMounted, reactive, ref, watchEffect } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, watchEffect } from 'vue'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import Ribbon from './components/sidebar/Ribbon.vue'
|
import Ribbon from './components/sidebar/Ribbon.vue'
|
||||||
@ -13,7 +13,7 @@ import ContentLogPane from './components/content/ContentLogPane.vue'
|
|||||||
import ContentValueTab from '@/components/content/ContentValueTab.vue'
|
import ContentValueTab from '@/components/content/ContentValueTab.vue'
|
||||||
import ToolbarControlWidget from '@/components/common/ToolbarControlWidget.vue'
|
import ToolbarControlWidget from '@/components/common/ToolbarControlWidget.vue'
|
||||||
import { EventsOn, WindowIsFullscreen, WindowIsMaximised, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
|
import { EventsOn, WindowIsFullscreen, WindowIsMaximised, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
|
||||||
import { isMacOS } from '@/utils/platform.js'
|
import { isMacOS, isWindows } from '@/utils/platform.js'
|
||||||
import iconUrl from '@/assets/images/icon.png'
|
import iconUrl from '@/assets/images/icon.png'
|
||||||
import ResizeableWrapper from '@/components/common/ResizeableWrapper.vue'
|
import ResizeableWrapper from '@/components/common/ResizeableWrapper.vue'
|
||||||
import { extraTheme } from '@/utils/extra_theme.js'
|
import { extraTheme } from '@/utils/extra_theme.js'
|
||||||
@ -57,6 +57,9 @@ const logoPaddingLeft = ref(10)
|
|||||||
const maximised = ref(false)
|
const maximised = ref(false)
|
||||||
const hideRadius = ref(false)
|
const hideRadius = ref(false)
|
||||||
const wrapperStyle = computed(() => {
|
const wrapperStyle = computed(() => {
|
||||||
|
if (isWindows()) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
return hideRadius.value
|
return hideRadius.value
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
@ -65,6 +68,11 @@ const wrapperStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
const spinStyle = computed(() => {
|
const spinStyle = computed(() => {
|
||||||
|
if (isWindows()) {
|
||||||
|
return {
|
||||||
|
backgroundColor: themeVars.value.bodyColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
return hideRadius.value
|
return hideRadius.value
|
||||||
? {
|
? {
|
||||||
backgroundColor: themeVars.value.bodyColor,
|
backgroundColor: themeVars.value.bodyColor,
|
||||||
@ -109,7 +117,28 @@ onMounted(async () => {
|
|||||||
onToggleFullscreen(fullscreen === true)
|
onToggleFullscreen(fullscreen === true)
|
||||||
const maximised = await WindowIsMaximised()
|
const maximised = await WindowIsMaximised()
|
||||||
onToggleMaximize(maximised)
|
onToggleMaximize(maximised)
|
||||||
|
window.addEventListener('keydown', onKeyShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('keydown', onKeyShortcut)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onKeyShortcut = (e) => {
|
||||||
|
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
|
||||||
|
switch (e.key) {
|
||||||
|
case 'w':
|
||||||
|
if (isCtrlOn) {
|
||||||
|
// close current tab
|
||||||
|
const tabStore = useTabStore()
|
||||||
|
const currentTab = tabStore.currentTab
|
||||||
|
if (currentTab != null) {
|
||||||
|
tabStore.closeTab(currentTab.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -133,7 +162,7 @@ onMounted(async () => {
|
|||||||
}">
|
}">
|
||||||
<n-space :size="3" :wrap="false" :wrap-item="false" align="center">
|
<n-space :size="3" :wrap="false" :wrap-item="false" align="center">
|
||||||
<n-avatar :size="32" :src="iconUrl" color="#0000" style="min-width: 32px" />
|
<n-avatar :size="32" :src="iconUrl" color="#0000" style="min-width: 32px" />
|
||||||
<div style="min-width: 68px; font-weight: 800">Tiny RDM</div>
|
<div style="min-width: 68px; white-space: nowrap; font-weight: 800">Tiny RDM</div>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px">
|
<n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px">
|
||||||
- {{ tabStore.currentTabName }}
|
- {{ tabStore.currentTabName }}
|
||||||
@ -145,7 +174,7 @@ onMounted(async () => {
|
|||||||
<div v-show="tabStore.nav === 'browser'" class="app-toolbar-tab flex-item-expand">
|
<div v-show="tabStore.nav === 'browser'" class="app-toolbar-tab flex-item-expand">
|
||||||
<content-value-tab />
|
<content-value-tab />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-expand"></div>
|
<div class="flex-item-expand" style="min-width: 15px"></div>
|
||||||
<!-- simulate window control buttons -->
|
<!-- simulate window control buttons -->
|
||||||
<toolbar-control-widget
|
<toolbar-control-widget
|
||||||
v-if="!isMacOS()"
|
v-if="!isMacOS()"
|
||||||
@ -233,6 +262,7 @@ onMounted(async () => {
|
|||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-content {
|
#app-content {
|
||||||
|
BIN
frontend/src/assets/images/bilibili_official.png
Normal file
BIN
frontend/src/assets/images/bilibili_official.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h, ref } from 'vue'
|
import { computed, h, ref } from 'vue'
|
||||||
import { get, map } from 'lodash'
|
import { get, isEmpty, some } 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'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: {
|
value: {
|
||||||
@ -10,8 +11,12 @@ const props = defineProps({
|
|||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Array,
|
||||||
value: {},
|
value: () => [],
|
||||||
|
},
|
||||||
|
menuOption: {
|
||||||
|
type: Array,
|
||||||
|
value: () => [],
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -21,7 +26,8 @@ const props = defineProps({
|
|||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:value'])
|
const emit = defineEmits(['update:value', 'menu'])
|
||||||
|
const i18n = useI18n()
|
||||||
const render = useRender()
|
const render = useRender()
|
||||||
|
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
@ -40,19 +46,53 @@ 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty(props.menuOption)) {
|
||||||
|
options.push({
|
||||||
|
key: 'header-divider',
|
||||||
|
type: 'divider',
|
||||||
|
})
|
||||||
|
for (const { key, label } of props.menuOption) {
|
||||||
|
options.push({
|
||||||
|
key,
|
||||||
|
label: i18n.t(label),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
const onDropdownSelect = (key) => {
|
const onDropdownSelect = (key) => {
|
||||||
emit('update:value', key)
|
if (some(props.menuOption, { key })) {
|
||||||
|
emit('menu', key)
|
||||||
|
} else {
|
||||||
|
emit('update:value', key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
@ -71,7 +111,6 @@ const onDropdownShow = (show) => {
|
|||||||
:options="dropdownOption"
|
:options="dropdownOption"
|
||||||
:render-label="({ label }) => render.renderLabel(label, { class: 'type-selector-item' })"
|
:render-label="({ label }) => render.renderLabel(label, { class: 'type-selector-item' })"
|
||||||
:show-arrow="true"
|
:show-arrow="true"
|
||||||
:title="props.tooltip"
|
|
||||||
:value="props.value"
|
:value="props.value"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@select="onDropdownSelect"
|
@select="onDropdownSelect"
|
||||||
|
@ -5,14 +5,16 @@ import Edit from '@/components/icons/Edit.vue'
|
|||||||
import Close from '@/components/icons/Close.vue'
|
import Close from '@/components/icons/Close.vue'
|
||||||
import Save from '@/components/icons/Save.vue'
|
import Save from '@/components/icons/Save.vue'
|
||||||
import Copy from '@/components/icons/Copy.vue'
|
import Copy from '@/components/icons/Copy.vue'
|
||||||
|
import Refresh from '@/components/icons/Refresh.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
bindKey: String,
|
bindKey: String,
|
||||||
editing: Boolean,
|
editing: Boolean,
|
||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
|
canRefresh: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['edit', 'delete', 'copy', 'save', 'cancel'])
|
const emit = defineEmits(['edit', 'delete', 'copy', 'refresh', 'save', 'cancel'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -22,6 +24,7 @@ const emit = defineEmits(['edit', 'delete', 'copy', 'save', 'cancel'])
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="flex-box-h edit-column-func">
|
<div v-else class="flex-box-h edit-column-func">
|
||||||
<icon-button :icon="Copy" :title="$t('interface.copy_value')" @click="emit('copy')" />
|
<icon-button :icon="Copy" :title="$t('interface.copy_value')" @click="emit('copy')" />
|
||||||
|
<icon-button v-if="props.canRefresh" :icon="Refresh" :title="$t('interface.reload')" @click="emit('refresh')" />
|
||||||
<icon-button v-if="!props.readonly" :icon="Edit" :title="$t('interface.edit_row')" @click="emit('edit')" />
|
<icon-button v-if="!props.readonly" :icon="Edit" :title="$t('interface.edit_row')" @click="emit('edit')" />
|
||||||
<n-popconfirm
|
<n-popconfirm
|
||||||
:negative-text="$t('common.cancel')"
|
:negative-text="$t('common.cancel')"
|
||||||
|
@ -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"
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, useSlots } from 'vue'
|
||||||
import { NIcon } from 'naive-ui'
|
import { NIcon } from 'naive-ui'
|
||||||
|
|
||||||
const emit = defineEmits(['click'])
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tooltip: String,
|
tooltip: String,
|
||||||
tTooltip: String,
|
tTooltip: String,
|
||||||
@ -35,13 +33,17 @@ const props = defineProps({
|
|||||||
tertiary: Boolean,
|
tertiary: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['click'])
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
const hasTooltip = computed(() => {
|
const hasTooltip = computed(() => {
|
||||||
return props.tooltip || props.tTooltip
|
return props.tooltip || props.tTooltip || slots.tooltip
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tooltip v-if="hasTooltip" :delay="tooltipDelay" :show-arrow="false">
|
<n-tooltip v-if="hasTooltip" :delay="tooltipDelay" :keep-alive-on-hover="false" :show-arrow="false">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button
|
<n-button
|
||||||
:class="props.buttonClass"
|
:class="props.buttonClass"
|
||||||
@ -65,7 +67,9 @@ const hasTooltip = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
|
<slot name="tooltip">
|
||||||
|
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
|
||||||
|
</slot>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
<n-button
|
<n-button
|
||||||
v-else
|
v-else
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { NSpace, useThemeVars } from 'naive-ui'
|
import { NSpace, useThemeVars } from 'naive-ui'
|
||||||
import { types, typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js'
|
import { types, typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js'
|
||||||
import { get, map, toUpper } from 'lodash'
|
import { get, isEmpty, map, toUpper } from 'lodash'
|
||||||
import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -14,6 +14,14 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'bottom-start',
|
default: 'bottom-start',
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
disableTip: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:value', 'select'])
|
const emit = defineEmits(['update:value', 'select'])
|
||||||
@ -82,22 +90,42 @@ const handleSelect = (select) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-dropdown
|
<template v-if="props.disabled">
|
||||||
:options="options"
|
<n-tooltip :disabled="isEmpty(props.disableTip)">
|
||||||
:placement="props.placement"
|
<div>{{ props.disableTip }}</div>
|
||||||
:render-icon="renderIcon"
|
<template #trigger>
|
||||||
:render-label="renderLabel"
|
<n-tag
|
||||||
show-arrow
|
:bordered="true"
|
||||||
@select="handleSelect">
|
:color="{ color: backgroundColor, textColor: fontColor }"
|
||||||
<n-tag
|
class="redis-tag"
|
||||||
:bordered="true"
|
disabled
|
||||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
size="medium"
|
||||||
class="redis-tag"
|
strong>
|
||||||
size="medium"
|
{{ displayValue }}
|
||||||
strong>
|
</n-tag>
|
||||||
{{ displayValue }}
|
</template>
|
||||||
</n-tag>
|
</n-tooltip>
|
||||||
</n-dropdown>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<n-dropdown
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:options="options"
|
||||||
|
:placement="props.placement"
|
||||||
|
:render-icon="renderIcon"
|
||||||
|
:render-label="renderLabel"
|
||||||
|
show-arrow
|
||||||
|
@select="handleSelect">
|
||||||
|
<n-tag
|
||||||
|
:bordered="true"
|
||||||
|
:color="{ color: backgroundColor, textColor: fontColor }"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
class="redis-tag"
|
||||||
|
size="medium"
|
||||||
|
strong>
|
||||||
|
{{ displayValue }}
|
||||||
|
</n-tag>
|
||||||
|
</n-dropdown>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -68,6 +68,7 @@ const label = computed(() => {
|
|||||||
'redis-type-tag-small': !props.short && props.size === 'small',
|
'redis-type-tag-small': !props.short && props.size === 'small',
|
||||||
'redis-type-tag-round': props.round,
|
'redis-type-tag-round': props.round,
|
||||||
'redis-type-tag-loading': props.loading,
|
'redis-type-tag-loading': props.loading,
|
||||||
|
'redis-type-tag': props.short,
|
||||||
}"
|
}"
|
||||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
:color="{ color: backgroundColor, textColor: fontColor }"
|
||||||
:size="props.size"
|
:size="props.size"
|
||||||
@ -112,4 +113,13 @@ const label = computed(() => {
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.redis-type-tag {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -12,6 +12,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
icons: Array,
|
icons: Array,
|
||||||
tTooltips: Array,
|
tTooltips: Array,
|
||||||
|
tTooltipPlacement: {
|
||||||
|
type: String,
|
||||||
|
default: 'bottom',
|
||||||
|
},
|
||||||
iconSize: {
|
iconSize: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: 20,
|
default: 20,
|
||||||
@ -45,6 +49,7 @@ const handleSwitch = (idx) => {
|
|||||||
v-for="(icon, i) in props.icons"
|
v-for="(icon, i) in props.icons"
|
||||||
:key="i"
|
:key="i"
|
||||||
:disabled="!(props.tTooltips && props.tTooltips[i])"
|
:disabled="!(props.tTooltips && props.tTooltips[i])"
|
||||||
|
:placement="props.tTooltipPlacement"
|
||||||
:show-arrow="false">
|
:show-arrow="false">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button :focusable="false" :size="props.size" :tertiary="i !== props.value" @click="handleSwitch(i)">
|
<n-button :focusable="false" :size="props.size" :tertiary="i !== props.value" @click="handleSwitch(i)">
|
||||||
|
@ -168,5 +168,5 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@use '@/styles/content';
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,7 +13,6 @@ import ContentValueWrapper from '@/components/content_value/ContentValueWrapper.
|
|||||||
import ContentCli from '@/components/content_value/ContentCli.vue'
|
import ContentCli from '@/components/content_value/ContentCli.vue'
|
||||||
import Monitor from '@/components/icons/Monitor.vue'
|
import Monitor from '@/components/icons/Monitor.vue'
|
||||||
import ContentSlog from '@/components/content_value/ContentSlog.vue'
|
import ContentSlog from '@/components/content_value/ContentSlog.vue'
|
||||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
|
||||||
import ContentMonitor from '@/components/content_value/ContentMonitor.vue'
|
import ContentMonitor from '@/components/content_value/ContentMonitor.vue'
|
||||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||||
import ContentPubsub from '@/components/content_value/ContentPubsub.vue'
|
import ContentPubsub from '@/components/content_value/ContentPubsub.vue'
|
||||||
@ -58,8 +57,8 @@ const tabContent = computed(() => {
|
|||||||
value: tab.value,
|
value: tab.value,
|
||||||
size: tab.size || 0,
|
size: tab.size || 0,
|
||||||
length: tab.length || 0,
|
length: tab.length || 0,
|
||||||
decode: tab.decode || decodeTypes.NONE,
|
decode: tab.decode,
|
||||||
format: tab.format || formatTypes.RAW,
|
format: tab.format,
|
||||||
matchPattern: tab.matchPattern || '',
|
matchPattern: tab.matchPattern || '',
|
||||||
end: tab.end === true,
|
end: tab.end === true,
|
||||||
loading: tab.loading === true,
|
loading: tab.loading === true,
|
||||||
@ -71,7 +70,7 @@ const isBlankValue = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectedSubTab = computed(() => {
|
const selectedSubTab = computed(() => {
|
||||||
const { subTab = 'status' } = tabStore.currentTab || {}
|
const { subTab = BrowserTabType.Status } = tabStore.currentTab || {}
|
||||||
return subTab
|
return subTab
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -105,7 +104,7 @@ watch(
|
|||||||
}"
|
}"
|
||||||
:value="selectedSubTab"
|
:value="selectedSubTab"
|
||||||
class="content-sub-tab"
|
class="content-sub-tab"
|
||||||
default-value="status"
|
:default-value="BrowserTabType.Status.toString()"
|
||||||
pane-class="content-sub-tab-pane"
|
pane-class="content-sub-tab-pane"
|
||||||
placement="top"
|
placement="top"
|
||||||
tab-style="padding-left: 10px; padding-right: 10px;"
|
tab-style="padding-left: 10px; padding-right: 10px;"
|
||||||
@ -213,7 +212,7 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@use '@/styles/content';
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
//padding: 5px 5px 0;
|
//padding: 5px 5px 0;
|
||||||
|
@ -1,8 +1,32 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
import AddLink from '@/components/icons/AddLink.vue'
|
import AddLink from '@/components/icons/AddLink.vue'
|
||||||
import useDialogStore from 'stores/dialog.js'
|
import useDialogStore from 'stores/dialog.js'
|
||||||
|
import { NButton, useThemeVars } from 'naive-ui'
|
||||||
|
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
|
||||||
|
import { find, includes, isEmpty } from 'lodash'
|
||||||
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
|
|
||||||
|
const themeVars = useThemeVars()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
const prefStore = usePreferencesStore()
|
||||||
|
|
||||||
|
const onOpenSponsor = (link) => {
|
||||||
|
BrowserOpenURL(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sponsorAd = computed(() => {
|
||||||
|
try {
|
||||||
|
const content = localStorage.getItem('sponsor_ad')
|
||||||
|
const ads = JSON.parse(content)
|
||||||
|
const ad = find(ads, ({ region }) => {
|
||||||
|
return isEmpty(region) || includes(region, prefStore.currentLanguage)
|
||||||
|
})
|
||||||
|
return ad || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -18,16 +42,27 @@ const dialogStore = useDialogStore()
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-empty>
|
</n-empty>
|
||||||
|
|
||||||
|
<n-button v-if="sponsorAd != null" class="sponsor-ad" style="" text @click="onOpenSponsor(sponsorAd.link)">
|
||||||
|
{{ sponsorAd.name }}
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@use '@/styles/content';
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
& > .sponsor-ad {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
vertical-align: bottom;
|
||||||
|
color: v-bind('themeVars.textColor3');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-preset-item {
|
.color-preset-item {
|
||||||
|
@ -2,32 +2,24 @@
|
|||||||
import Server from '@/components/icons/Server.vue'
|
import Server from '@/components/icons/Server.vue'
|
||||||
import useTabStore from 'stores/tab.js'
|
import useTabStore from 'stores/tab.js'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { get, map } from 'lodash'
|
import { get, map } from 'lodash'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import useConnectionStore from 'stores/connections.js'
|
import useConnectionStore from 'stores/connections.js'
|
||||||
import { extraTheme } from '@/utils/extra_theme.js'
|
import { extraTheme } from '@/utils/extra_theme.js'
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value content tab on head
|
* Value content tab on head
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const i18n = useI18n()
|
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const connectionStore = useConnectionStore()
|
const connectionStore = useConnectionStore()
|
||||||
const browserStore = useBrowserStore()
|
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
|
|
||||||
const onCloseTab = (tabIndex) => {
|
const onCloseTab = (tabIndex) => {
|
||||||
const tab = get(tabStore.tabs, tabIndex)
|
const tab = get(tabStore.tabs, tabIndex)
|
||||||
if (tab != null) {
|
tabStore.closeTab(tab.name)
|
||||||
$dialog.warning(i18n.t('dialogue.close_confirm', { name: tab.name }), () => {
|
|
||||||
browserStore.closeConnection(tab.name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabMarkColor = computed(() => {
|
const tabMarkColor = computed(() => {
|
||||||
|
@ -32,9 +32,12 @@ let fitAddonInst = null
|
|||||||
* @return {{fitAddon: xterm-addon-fit.FitAddon, term: Terminal}}
|
* @return {{fitAddon: xterm-addon-fit.FitAddon, term: Terminal}}
|
||||||
*/
|
*/
|
||||||
const newTerm = () => {
|
const newTerm = () => {
|
||||||
|
const { fontSize = 14, fontFamily = 'Courier New' } = prefStore.cliFont
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
fontSize: prefStore.general.fontSize || 14,
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
cursorStyle: prefStore.cli.cursorStyle || 'block',
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
screenReaderMode: true,
|
screenReaderMode: true,
|
||||||
@ -51,6 +54,7 @@ const newTerm = () => {
|
|||||||
term.loadAddon(fitAddon)
|
term.loadAddon(fitAddon)
|
||||||
|
|
||||||
term.onData(onTermData)
|
term.onData(onTermData)
|
||||||
|
term.attachCustomKeyEventHandler(onTermKey)
|
||||||
return { term, fitAddon }
|
return { term, fitAddon }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,10 +93,21 @@ defineExpose({
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => prefStore.general.fontSize,
|
() => prefStore.cliFont,
|
||||||
(fontSize) => {
|
({ fontSize = 14, fontFamily = 'Courier New' }) => {
|
||||||
if (termInst != null) {
|
if (termInst != null) {
|
||||||
termInst.options.fontSize = fontSize
|
termInst.options.fontSize = fontSize
|
||||||
|
termInst.options.fontFamily = fontFamily
|
||||||
|
}
|
||||||
|
resizeTerm()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => prefStore.cli.cursorStyle,
|
||||||
|
(style) => {
|
||||||
|
if (termInst != null) {
|
||||||
|
termInst.options.cursorStyle = style || 'block'
|
||||||
}
|
}
|
||||||
resizeTerm()
|
resizeTerm()
|
||||||
},
|
},
|
||||||
@ -177,6 +192,81 @@ const onTermData = (data) => {
|
|||||||
// term.write(data)
|
// term.write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param e
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
const onTermKey = (e) => {
|
||||||
|
if (e.type === 'keydown') {
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'a': // move to head of line
|
||||||
|
moveInputCursorTo(0)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'e': // move to tail of line
|
||||||
|
moveInputCursorTo(Number.MAX_SAFE_INTEGER)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'f': // move forward
|
||||||
|
moveInputCursor(1)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'b': // move backward
|
||||||
|
moveInputCursor(-1)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'd': // delete char
|
||||||
|
deleteInput(false)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'h': // back delete
|
||||||
|
deleteInput(true)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'u': // delete all text before cursor
|
||||||
|
deleteInput2(false)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'k': // delete all text after cursor
|
||||||
|
deleteInput2(true)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'w': // delete word before cursor
|
||||||
|
deleteWord(false)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'p': // previous history
|
||||||
|
changeHistory(true)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'n': // next history
|
||||||
|
changeHistory(false)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case 'l': // clear screen
|
||||||
|
termInst.clear()
|
||||||
|
replaceTermInput()
|
||||||
|
newInputLine()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// block all ctrl key combinations input
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Home': // move to head of line
|
||||||
|
moveInputCursorTo(0)
|
||||||
|
return false
|
||||||
|
case 'End': // move to tail of line
|
||||||
|
moveInputCursorTo(Number.MAX_SAFE_INTEGER)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* move input cursor by step
|
* move input cursor by step
|
||||||
* @param {number} step above 0 indicate move right; 0 indicate move to last
|
* @param {number} step above 0 indicate move right; 0 indicate move to last
|
||||||
@ -211,7 +301,7 @@ const moveInputCursor = (step) => {
|
|||||||
* move cursor to the end of current line
|
* move cursor to the end of current line
|
||||||
*/
|
*/
|
||||||
const moveInputCursorToEnd = () => {
|
const moveInputCursorToEnd = () => {
|
||||||
moveInputCursorTo(Number.MAX_VALUE)
|
moveInputCursorTo(Number.MAX_SAFE_INTEGER)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -232,6 +322,8 @@ const updateInput = (data) => {
|
|||||||
if (data == null || data.length <= 0) {
|
if (data == null || data.length <= 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// replace (Non-Breaking Space) with normal blank space
|
||||||
|
data = data.replace(/\u00A0/g, ' ')
|
||||||
|
|
||||||
if (termInst == null) {
|
if (termInst == null) {
|
||||||
return
|
return
|
||||||
@ -283,6 +375,85 @@ const deleteInput = (back = false) => {
|
|||||||
moveInputCursorTo(inputCursor)
|
moveInputCursorTo(inputCursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete to the end
|
||||||
|
* @param back
|
||||||
|
*/
|
||||||
|
const deleteInput2 = (back = false) => {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentLine = getCurrentInput()
|
||||||
|
if (back) {
|
||||||
|
// delete until tail
|
||||||
|
currentLine = currentLine.substring(0, inputCursor - 1)
|
||||||
|
inputCursor = currentLine.length
|
||||||
|
} else {
|
||||||
|
// delete until head
|
||||||
|
currentLine = currentLine.substring(inputCursor)
|
||||||
|
inputCursor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceTermInput(currentLine)
|
||||||
|
updateCurrentInput(currentLine)
|
||||||
|
moveInputCursorTo(inputCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete one word
|
||||||
|
* @param back
|
||||||
|
*/
|
||||||
|
const deleteWord = (back = false) => {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentLine = getCurrentInput()
|
||||||
|
if (back) {
|
||||||
|
const prefix = currentLine.substring(0, inputCursor)
|
||||||
|
let firstNonChar = false
|
||||||
|
let cursor = inputCursor
|
||||||
|
while (cursor < currentLine.length) {
|
||||||
|
const isChar =
|
||||||
|
(currentLine[cursor] >= 'a' && currentLine[cursor] <= 'z') ||
|
||||||
|
(currentLine[cursor] >= 'A' && currentLine[cursor] <= 'Z') ||
|
||||||
|
(currentLine[cursor] >= '0' && currentLine[cursor] <= '9')
|
||||||
|
if (!firstNonChar || isChar) {
|
||||||
|
if (!isChar) {
|
||||||
|
firstNonChar = true
|
||||||
|
}
|
||||||
|
cursor++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLine = prefix + currentLine.substring(cursor)
|
||||||
|
} else {
|
||||||
|
const suffix = currentLine.substring(inputCursor)
|
||||||
|
let firstNonChar = false
|
||||||
|
while (inputCursor >= 0) {
|
||||||
|
const isChar =
|
||||||
|
(currentLine[inputCursor] >= 'a' && currentLine[inputCursor] <= 'z') ||
|
||||||
|
(currentLine[inputCursor] >= 'A' && currentLine[inputCursor] <= 'Z') ||
|
||||||
|
(currentLine[inputCursor] >= '0' && currentLine[inputCursor] <= '9')
|
||||||
|
if (!firstNonChar || isChar) {
|
||||||
|
if (!isChar) {
|
||||||
|
firstNonChar = true
|
||||||
|
}
|
||||||
|
inputCursor--
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLine = currentLine.substring(0, inputCursor) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceTermInput(currentLine)
|
||||||
|
updateCurrentInput(currentLine)
|
||||||
|
moveInputCursorTo(inputCursor)
|
||||||
|
}
|
||||||
|
|
||||||
const getCurrentInput = () => {
|
const getCurrentInput = () => {
|
||||||
return get(inputHistory, historyIndex, '')
|
return get(inputHistory, historyIndex, '')
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|||||||
import * as monaco from 'monaco-editor'
|
import * as monaco from 'monaco-editor'
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
content: {
|
content: {
|
||||||
@ -18,18 +19,22 @@ const props = defineProps({
|
|||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
showLineNum: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showFolding: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
border: {
|
border: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
resetKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
offsetKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
keepOffset: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['reset', 'input', 'save'])
|
const emit = defineEmits(['reset', 'input', 'save'])
|
||||||
@ -39,6 +44,18 @@ const themeVars = useThemeVars()
|
|||||||
const editorRef = ref(null)
|
const editorRef = ref(null)
|
||||||
/** @type monaco.editor.IStandaloneCodeEditor */
|
/** @type monaco.editor.IStandaloneCodeEditor */
|
||||||
let editorNode = null
|
let editorNode = null
|
||||||
|
const scrollOffset = { top: 0, left: 0 }
|
||||||
|
|
||||||
|
const updateScroll = () => {
|
||||||
|
if (editorNode != null) {
|
||||||
|
if (props.keepOffset && !isEmpty(props.offsetKey)) {
|
||||||
|
editorNode.setScrollPosition({ scrollTop: scrollOffset.top, scrollLeft: scrollOffset.left })
|
||||||
|
} else {
|
||||||
|
// reset offset if not needed
|
||||||
|
editorNode.setScrollPosition({ scrollTop: 0, scrollLeft: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const destroyEditor = () => {
|
const destroyEditor = () => {
|
||||||
if (editorNode != null && editorNode.dispose != null) {
|
if (editorNode != null && editorNode.dispose != null) {
|
||||||
@ -58,18 +75,20 @@ const readonlyValue = computed(() => {
|
|||||||
const pref = usePreferencesStore()
|
const pref = usePreferencesStore()
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (editorRef.value != null) {
|
if (editorRef.value != null) {
|
||||||
const { fontSize, fontFamily = undefined } = pref.generalFont
|
const { fontSize, fontFamily = ['monaco'] } = pref.editorFont
|
||||||
editorNode = monaco.editor.create(editorRef.value, {
|
editorNode = monaco.editor.create(editorRef.value, {
|
||||||
// value: props.content,
|
// value: props.content,
|
||||||
theme: pref.isDark ? 'rdm-dark' : 'rdm-light',
|
theme: pref.isDark ? 'rdm-dark' : 'rdm-light',
|
||||||
language: props.language,
|
language: props.language,
|
||||||
lineNumbers: props.showLineNum ? 'on' : 'off',
|
lineNumbers: pref.showLineNum ? 'on' : 'off',
|
||||||
|
links: pref.editorLinks,
|
||||||
readOnly: readonlyValue.value,
|
readOnly: readonlyValue.value,
|
||||||
colorDecorators: true,
|
colorDecorators: true,
|
||||||
accessibilitySupport: 'off',
|
accessibilitySupport: 'off',
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
folding: props.showFolding !== false,
|
folding: pref.showFolding,
|
||||||
|
dragAndDrop: pref.dropText,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
@ -94,6 +113,18 @@ onMounted(async () => {
|
|||||||
emit('save')
|
emit('save')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
editorNode.onDidScrollChange((event) => {
|
||||||
|
// save scroll offset when changes, ie. content changes
|
||||||
|
if (props.keepOffset && !event.scrollHeightChanged) {
|
||||||
|
scrollOffset.top = event.scrollTop
|
||||||
|
scrollOffset.left = event.scrollLeft
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
editorNode.onDidLayoutChange((event) => {
|
||||||
|
updateScroll()
|
||||||
|
})
|
||||||
|
|
||||||
// editorNode.onDidChangeModelLanguageConfiguration(() => {
|
// editorNode.onDidChangeModelLanguageConfiguration(() => {
|
||||||
// editorNode?.getAction('editor.action.formatDocument')?.run()
|
// editorNode?.getAction('editor.action.formatDocument')?.run()
|
||||||
// })
|
// })
|
||||||
@ -112,6 +143,30 @@ watch(
|
|||||||
if (editorNode != null) {
|
if (editorNode != null) {
|
||||||
editorNode.setValue(content)
|
editorNode.setValue(content)
|
||||||
await nextTick(() => emit('reset', content))
|
await nextTick(() => emit('reset', content))
|
||||||
|
updateScroll()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.resetKey,
|
||||||
|
async () => {
|
||||||
|
if (editorNode != null) {
|
||||||
|
editorNode.setValue(props.content)
|
||||||
|
await nextTick(() => emit('reset', props.content))
|
||||||
|
updateScroll()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.offsetKey,
|
||||||
|
() => {
|
||||||
|
// reset scroll offset when key changed
|
||||||
|
if (editorNode != null) {
|
||||||
|
scrollOffset.top = 0
|
||||||
|
scrollOffset.left = 0
|
||||||
|
editorNode.setScrollPosition({ scrollTop: 0, scrollLeft: 0 })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -150,6 +205,24 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => pref.editor,
|
||||||
|
({ showLineNum = true, showFolding = true, dropText = true, links = true }) => {
|
||||||
|
if (editorNode != null) {
|
||||||
|
const { fontSize, fontFamily } = pref.editorFont
|
||||||
|
editorNode.updateOptions({
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
lineNumbers: showLineNum ? 'on' : 'off',
|
||||||
|
folding: showFolding,
|
||||||
|
dragAndDrop: dropText,
|
||||||
|
links,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
destroyEditor()
|
destroyEditor()
|
||||||
})
|
})
|
||||||
@ -176,4 +249,8 @@ onUnmounted(() => {
|
|||||||
left: 2px;
|
left: 2px;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.line-numbers) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -12,10 +12,10 @@ import WindowClose from '@/components/icons/WindowClose.vue'
|
|||||||
import Pin from '@/components/icons/Pin.vue'
|
import Pin from '@/components/icons/Pin.vue'
|
||||||
import OffScreen from '@/components/icons/OffScreen.vue'
|
import OffScreen from '@/components/icons/OffScreen.vue'
|
||||||
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
import { isEmpty, toString } from 'lodash'
|
||||||
import { toString } from 'lodash'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
keyPath: String,
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
@ -23,7 +23,7 @@ const props = defineProps({
|
|||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: [String, Array],
|
||||||
},
|
},
|
||||||
fieldLabel: {
|
fieldLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -47,7 +47,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const browserStore = useBrowserStore()
|
const browserStore = useBrowserStore()
|
||||||
const prefStore = usePreferencesStore()
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:field',
|
'update:field',
|
||||||
'update:value',
|
'update:value',
|
||||||
@ -60,8 +59,8 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
watchEffect(
|
watchEffect(
|
||||||
() => {
|
() => {
|
||||||
if (props.show && props.value != null) {
|
if (props.show && !isEmpty(props.keyPath)) {
|
||||||
onFormatChanged()
|
onFormatChanged(props.decode, props.format)
|
||||||
} else {
|
} else {
|
||||||
viewAs.value = ''
|
viewAs.value = ''
|
||||||
}
|
}
|
||||||
@ -96,6 +95,7 @@ const enableSave = computed(() => {
|
|||||||
const viewLanguage = computed(() => {
|
const viewLanguage = computed(() => {
|
||||||
switch (viewAs.format) {
|
switch (viewAs.format) {
|
||||||
case formatTypes.JSON:
|
case formatTypes.JSON:
|
||||||
|
case formatTypes.UNICODE_JSON:
|
||||||
return 'json'
|
return 'json'
|
||||||
case formatTypes.YAML:
|
case formatTypes.YAML:
|
||||||
return 'yaml'
|
return 'yaml'
|
||||||
@ -128,6 +128,8 @@ const onFormatChanged = async (decode = null, format = null) => {
|
|||||||
editingContent.value = viewAs.value = value
|
editingContent.value = viewAs.value = value
|
||||||
viewAs.decode = decode || retDecode
|
viewAs.decode = decode || retDecode
|
||||||
viewAs.format = format || retFormat
|
viewAs.format = format || retFormat
|
||||||
|
emit('update:decode', viewAs.decode)
|
||||||
|
emit('update:format', viewAs.format)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -175,9 +177,8 @@ const onSave = () => {
|
|||||||
<content-editor
|
<content-editor
|
||||||
:border="true"
|
:border="true"
|
||||||
:content="displayValue"
|
:content="displayValue"
|
||||||
|
:key-path="viewAs.field"
|
||||||
:language="viewLanguage"
|
:language="viewLanguage"
|
||||||
:show-folding="prefStore.showFolding"
|
|
||||||
:show-line-num="prefStore.showLineNum"
|
|
||||||
class="flex-item-expand"
|
class="flex-item-expand"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@reset="onInput"
|
@reset="onInput"
|
||||||
|
@ -3,20 +3,19 @@ import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|||||||
import { debounce, filter, get, includes, isEmpty, join } from 'lodash'
|
import { debounce, filter, get, includes, isEmpty, join } from 'lodash'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
|
||||||
import Play from '@/components/icons/Play.vue'
|
import Play from '@/components/icons/Play.vue'
|
||||||
import Pause from '@/components/icons/Pause.vue'
|
import Pause from '@/components/icons/Pause.vue'
|
||||||
import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js'
|
import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js'
|
||||||
import { ClipboardSetText, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
import { EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||||
import Copy from '@/components/icons/Copy.vue'
|
import Copy from '@/components/icons/Copy.vue'
|
||||||
import Export from '@/components/icons/Export.vue'
|
import Export from '@/components/icons/Export.vue'
|
||||||
import Delete from '@/components/icons/Delete.vue'
|
import Delete from '@/components/icons/Delete.vue'
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
import IconButton from '@/components/common/IconButton.vue'
|
||||||
import Bottom from '@/components/icons/Bottom.vue'
|
import Bottom from '@/components/icons/Bottom.vue'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
|
|
||||||
const browserStore = useBrowserStore()
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
server: {
|
server: {
|
||||||
@ -95,15 +94,8 @@ const onStopMonitor = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCopyLog = async () => {
|
const onCopyLog = async () => {
|
||||||
try {
|
copy(join(data.list, '\n'))
|
||||||
const content = join(data.list, '\n')
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
const succ = await ClipboardSetText(content)
|
|
||||||
if (succ) {
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$message.error(e.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onExportLog = () => {
|
const onExportLog = () => {
|
||||||
@ -189,7 +181,7 @@ const onCleanLog = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@use '@/styles/content';
|
||||||
|
|
||||||
.line-item {
|
.line-item {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
@ -281,7 +281,7 @@ const onPublish = async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@use '@/styles/content';
|
||||||
|
|
||||||
.total-message {
|
.total-message {
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive } from 'vue'
|
import { computed, nextTick, reactive } from 'vue'
|
||||||
import { debounce, isEmpty, trim } from 'lodash'
|
import { debounce, isEmpty, trim } from 'lodash'
|
||||||
import { NButton, NInput } from 'naive-ui'
|
import { NButton, NInput } from 'naive-ui'
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
import IconButton from '@/components/common/IconButton.vue'
|
||||||
import Help from '@/components/icons/Help.vue'
|
import SpellCheck from '@/components/icons/SpellCheck.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fullSearchIcon: {
|
fullSearchIcon: {
|
||||||
@ -22,17 +22,22 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
exact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['filterChanged', 'matchChanged'])
|
const emit = defineEmits(['filterChanged', 'matchChanged', 'exactChanged'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {UnwrapNestedRefs<{filter: string, match: string}>}
|
* @type {UnwrapNestedRefs<{filter: string, match: string, exact: boolean}>}
|
||||||
*/
|
*/
|
||||||
const inputData = reactive({
|
const inputData = reactive({
|
||||||
match: '',
|
match: '',
|
||||||
filter: '',
|
filter: '',
|
||||||
|
exact: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasMatch = computed(() => {
|
const hasMatch = computed(() => {
|
||||||
@ -43,38 +48,49 @@ const hasFilter = computed(() => {
|
|||||||
return !isEmpty(trim(inputData.filter))
|
return !isEmpty(trim(inputData.filter))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onExactChecked = () => {
|
||||||
|
// update search search result
|
||||||
|
if (hasMatch.value) {
|
||||||
|
nextTick(() => onForceFullSearch())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onFullSearch = () => {
|
const onFullSearch = () => {
|
||||||
inputData.filter = trim(inputData.filter)
|
inputData.filter = trim(inputData.filter)
|
||||||
if (!isEmpty(inputData.filter)) {
|
if (!isEmpty(inputData.filter)) {
|
||||||
inputData.match = inputData.filter
|
inputData.match = inputData.filter
|
||||||
inputData.filter = ''
|
inputData.filter = ''
|
||||||
emit('matchChanged', inputData.match, inputData.filter)
|
emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onForceFullSearch = () => {
|
||||||
|
inputData.filter = trim(inputData.filter)
|
||||||
|
emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
|
||||||
|
}
|
||||||
|
|
||||||
const _onInput = () => {
|
const _onInput = () => {
|
||||||
emit('filterChanged', inputData.filter)
|
emit('filterChanged', inputData.filter, inputData.exact)
|
||||||
}
|
}
|
||||||
const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true })
|
const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true })
|
||||||
|
|
||||||
const onKeyup = (evt) => {
|
|
||||||
if (evt.key === 'Enter') {
|
|
||||||
onFullSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClearFilter = () => {
|
const onClearFilter = () => {
|
||||||
inputData.filter = ''
|
inputData.filter = ''
|
||||||
onClearMatch()
|
onClearMatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onUpdateMatch = () => {
|
||||||
|
inputData.filter = inputData.match
|
||||||
|
onClearMatch()
|
||||||
|
}
|
||||||
|
|
||||||
const onClearMatch = () => {
|
const onClearMatch = () => {
|
||||||
const changed = !isEmpty(inputData.match)
|
const changed = !isEmpty(inputData.match)
|
||||||
inputData.match = ''
|
inputData.match = ''
|
||||||
if (changed) {
|
if (changed) {
|
||||||
emit('matchChanged', inputData.match, inputData.filter)
|
emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
|
||||||
} else {
|
} else {
|
||||||
emit('filterChanged', inputData.filter)
|
emit('filterChanged', inputData.filter, inputData.exact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +100,7 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-input-group>
|
<n-input-group style="overflow: hidden">
|
||||||
<slot name="prepend" />
|
<slot name="prepend" />
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="inputData.filter"
|
v-model:value="inputData.filter"
|
||||||
@ -94,12 +110,12 @@ defineExpose({
|
|||||||
clearable
|
clearable
|
||||||
@clear="onClearFilter"
|
@clear="onClearFilter"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@keyup.enter="onKeyup">
|
@keyup.enter="onFullSearch">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<slot name="prefix" />
|
<slot name="prefix" />
|
||||||
<n-tooltip v-if="hasMatch">
|
<n-tooltip v-if="hasMatch" placement="bottom">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-tag closable size="small" @close="onClearMatch">
|
<n-tag closable size="small" @close="onClearMatch" @dblclick="onUpdateMatch">
|
||||||
{{ inputData.match }}
|
{{ inputData.match }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
@ -112,12 +128,23 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<template v-if="props.useGlob">
|
<template v-if="props.useGlob">
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip placement="bottom" trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-icon :component="Help" />
|
<n-tag
|
||||||
|
v-model:checked="inputData.exact"
|
||||||
|
:checkable="true"
|
||||||
|
:type="props.exact ? 'primary' : 'default'"
|
||||||
|
size="small"
|
||||||
|
strong
|
||||||
|
style="padding: 0 5px"
|
||||||
|
@updateChecked="onExactChecked">
|
||||||
|
<n-icon :size="14">
|
||||||
|
<spell-check :stroke-width="2" />
|
||||||
|
</n-icon>
|
||||||
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
<div class="text-block" style="max-width: 600px">
|
<div class="text-block" style="max-width: 600px">
|
||||||
{{ $t('dialogue.filter.filter_pattern_tip') }}
|
{{ $t('dialogue.filter.exact_match_tip') }}
|
||||||
</div>
|
</div>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@ -129,11 +156,17 @@ defineExpose({
|
|||||||
:disabled="hasMatch && !hasFilter"
|
:disabled="hasMatch && !hasFilter"
|
||||||
:icon="props.fullSearchIcon"
|
:icon="props.fullSearchIcon"
|
||||||
:size="small ? 16 : 20"
|
:size="small ? 16 : 20"
|
||||||
|
:tooltip-delay="1"
|
||||||
border
|
border
|
||||||
small
|
small
|
||||||
stroke-width="4"
|
stroke-width="4"
|
||||||
t-tooltip="interface.full_search"
|
@click="onFullSearch">
|
||||||
@click="onFullSearch" />
|
<template #tooltip>
|
||||||
|
<div class="text-block" style="max-width: 600px">
|
||||||
|
{{ $t('dialogue.filter.filter_pattern_tip') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</icon-button>
|
||||||
<n-button v-else :disabled="hasMatch && !hasFilter" :focusable="false" @click="onFullSearch">
|
<n-button v-else :disabled="hasMatch && !hasFilter" :focusable="false" @click="onFullSearch">
|
||||||
{{ $t('interface.full_search') }}
|
{{ $t('interface.full_search') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@ -141,4 +174,23 @@ defineExpose({
|
|||||||
</n-input-group>
|
</n-input-group>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped>
|
||||||
|
:deep(.n-input) {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input__prefix) {
|
||||||
|
max-width: 50%;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tag__content) {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -14,20 +14,21 @@ import {
|
|||||||
toArray,
|
toArray,
|
||||||
toNumber,
|
toNumber,
|
||||||
} from 'lodash'
|
} from 'lodash'
|
||||||
import { computed, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from 'vue'
|
import { computed, h, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from 'vue'
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
import IconButton from '@/components/common/IconButton.vue'
|
||||||
import Filter from '@/components/icons/Filter.vue'
|
import Filter from '@/components/icons/Filter.vue'
|
||||||
import Refresh from '@/components/icons/Refresh.vue'
|
import Refresh from '@/components/icons/Refresh.vue'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
import { timeout } from '@/utils/promise.js'
|
import { timeout } from '@/utils/promise.js'
|
||||||
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
||||||
import { NIcon, useThemeVars } from 'naive-ui'
|
import { NButton, NIcon, NSpace, useThemeVars } from 'naive-ui'
|
||||||
import { Line } from 'vue-chartjs'
|
import { Line } from 'vue-chartjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { convertBytes, formatBytes } from '@/utils/byte_convert.js'
|
import { convertBytes, formatBytes } from '@/utils/byte_convert.js'
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import useConnectionStore from 'stores/connections.js'
|
import useConnectionStore from 'stores/connections.js'
|
||||||
|
import { toHumanReadable } from '@/utils/date.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
server: String,
|
server: String,
|
||||||
@ -79,9 +80,11 @@ const refreshInfo = async (force) => {
|
|||||||
}
|
}
|
||||||
if (!isEmpty(props.server) && browserStore.isConnected(props.server)) {
|
if (!isEmpty(props.server) && browserStore.isConnected(props.server)) {
|
||||||
try {
|
try {
|
||||||
const info = await browserStore.getServerInfo(props.server)
|
const info = await browserStore.getServerInfo(props.server, true)
|
||||||
serverInfo.value = info
|
if (!isEmpty(info)) {
|
||||||
_updateChart(info)
|
serverInfo.value = info
|
||||||
|
_updateChart(info)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
pageState.loading = false
|
pageState.loading = false
|
||||||
pageState.autoLoading = false
|
pageState.autoLoading = false
|
||||||
@ -91,7 +94,7 @@ const refreshInfo = async (force) => {
|
|||||||
|
|
||||||
const _updateChart = (info) => {
|
const _updateChart = (info) => {
|
||||||
let timeLabels = toRaw(cmdRate.value.labels)
|
let timeLabels = toRaw(cmdRate.value.labels)
|
||||||
timeLabels = timeLabels.concat(dayjs().format('hh:mm:ss'))
|
timeLabels = timeLabels.concat(dayjs().format('HH:mm:ss'))
|
||||||
timeLabels = slice(timeLabels, Math.max(0, timeLabels.length - statusHistory))
|
timeLabels = slice(timeLabels, Math.max(0, timeLabels.length - statusHistory))
|
||||||
|
|
||||||
// commands per seconds
|
// commands per seconds
|
||||||
@ -143,7 +146,7 @@ const _updateChart = (info) => {
|
|||||||
const _mockChart = () => {
|
const _mockChart = () => {
|
||||||
const timeLabels = []
|
const timeLabels = []
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
timeLabels.push(dayjs().add(5, 'seconds').format('hh:mm:ss'))
|
timeLabels.push(dayjs().add(5, 'seconds').format('HH:mm:ss'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// commands per seconds
|
// commands per seconds
|
||||||
@ -237,7 +240,7 @@ onMounted(() => {
|
|||||||
pageState.refreshInterval = interval === 0 ? 5 : interval
|
pageState.refreshInterval = interval === 0 ? 5 : interval
|
||||||
onToggleRefresh(true)
|
onToggleRefresh(true)
|
||||||
} else {
|
} else {
|
||||||
setTimeout(refreshInfo, 5000)
|
setTimeout(refreshInfo, 3000)
|
||||||
// setTimeout(_mockChart, 1000)
|
// setTimeout(_mockChart, 1000)
|
||||||
}
|
}
|
||||||
refreshInfo()
|
refreshInfo()
|
||||||
@ -426,6 +429,7 @@ const networkRate = shallowRef({
|
|||||||
|
|
||||||
const chartOption = computed(() => {
|
const chartOption = computed(() => {
|
||||||
return {
|
return {
|
||||||
|
animation: false,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
events: [],
|
events: [],
|
||||||
@ -463,6 +467,7 @@ const chartOption = computed(() => {
|
|||||||
|
|
||||||
const byteChartOption = computed(() => {
|
const byteChartOption = computed(() => {
|
||||||
return {
|
return {
|
||||||
|
animation: false,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
events: [],
|
events: [],
|
||||||
@ -501,6 +506,87 @@ const byteChartOption = computed(() => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const clientInfo = reactive({
|
||||||
|
loading: false,
|
||||||
|
content: [],
|
||||||
|
})
|
||||||
|
const onShowClients = async (show) => {
|
||||||
|
if (show) {
|
||||||
|
try {
|
||||||
|
clientInfo.loading = true
|
||||||
|
clientInfo.content = await browserStore.getClientList(props.server)
|
||||||
|
} finally {
|
||||||
|
clientInfo.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientTableColumns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
title: () => {
|
||||||
|
return h(NSpace, { wrap: false, wrapItem: false, justify: 'center' }, () => [
|
||||||
|
h('span', { style: { fontWeight: '550', fontSize: '15px' } }, i18n.t('status.client.title')),
|
||||||
|
h(IconButton, {
|
||||||
|
icon: Refresh,
|
||||||
|
size: 16,
|
||||||
|
onClick: () => onShowClients(true),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
title: '#',
|
||||||
|
width: 60,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
render: (row, index) => {
|
||||||
|
return index + 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'addr',
|
||||||
|
title: () => i18n.t('status.client.addr'),
|
||||||
|
sorter: 'default',
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'db',
|
||||||
|
title: () => i18n.t('status.client.db'),
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'age',
|
||||||
|
title: () => i18n.t('status.client.age'),
|
||||||
|
sorter: (row1, row2) => row1.age - row2.age,
|
||||||
|
defaultSortOrder: 'descend',
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
render: ({ age }, index) => {
|
||||||
|
return toHumanReadable(age)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'idle',
|
||||||
|
title: () => i18n.t('status.client.idle'),
|
||||||
|
sorter: (row1, row2) => row1.age - row2.age,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
render: ({ idle }, index) => {
|
||||||
|
return toHumanReadable(idle)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -568,7 +654,28 @@ const byteChartOption = computed(() => {
|
|||||||
<n-gi :span="6">
|
<n-gi :span="6">
|
||||||
<n-statistic
|
<n-statistic
|
||||||
:label="$t('status.connected_clients')"
|
:label="$t('status.connected_clients')"
|
||||||
:value="get(serverInfo, 'Clients.connected_clients', '0')" />
|
:value="get(serverInfo, 'Clients.connected_clients', '0')">
|
||||||
|
<template #suffix>
|
||||||
|
<n-tooltip
|
||||||
|
:content-style="{ backgroundColor: themeVars.tableColor }"
|
||||||
|
trigger="click"
|
||||||
|
width="70vw"
|
||||||
|
@update-show="onShowClients">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button :bordered="false" size="small">↘</n-button>
|
||||||
|
</template>
|
||||||
|
<n-data-table
|
||||||
|
:columns="clientTableColumns"
|
||||||
|
:data="clientInfo.content"
|
||||||
|
:loading="clientInfo.loading"
|
||||||
|
:single-column="false"
|
||||||
|
:single-line="false"
|
||||||
|
max-height="50vh"
|
||||||
|
size="small"
|
||||||
|
striped />
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi :span="6">
|
<n-gi :span="6">
|
||||||
<n-statistic :value="totalKeys">
|
<n-statistic :value="totalKeys">
|
||||||
@ -669,7 +776,7 @@ const byteChartOption = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@use '@/styles/content';
|
||||||
|
|
||||||
.line-chart {
|
.line-chart {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -179,6 +179,7 @@ const onListLimitChanged = (limit) => {
|
|||||||
<n-input-number
|
<n-input-number
|
||||||
v-model:value="data.listLimit"
|
v-model:value="data.listLimit"
|
||||||
:max="9999"
|
:max="9999"
|
||||||
|
:min="1"
|
||||||
style="width: 120px"
|
style="width: 120px"
|
||||||
@update:value="onListLimitChanged" />
|
@update:value="onListLimitChanged" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@ -216,11 +217,10 @@ const onListLimitChanged = (limit) => {
|
|||||||
class="flex-item-expand"
|
class="flex-item-expand"
|
||||||
flex-height
|
flex-height
|
||||||
striped
|
striped
|
||||||
virtual-scroll
|
|
||||||
@update:sorter="({ order }) => (data.sortOrder = order)" />
|
@update:sorter="({ order }) => (data.sortOrder = order)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@use '@/styles/content';
|
||||||
</style>
|
</style>
|
||||||
|
@ -9,12 +9,12 @@ import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
import IconButton from '@/components/common/IconButton.vue'
|
||||||
import Copy from '@/components/icons/Copy.vue'
|
import Copy from '@/components/icons/Copy.vue'
|
||||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
import { computed, onMounted, onUnmounted, reactive, watch } from 'vue'
|
||||||
import { computed, onUnmounted, reactive, watch } from 'vue'
|
|
||||||
import { NIcon, useThemeVars } from 'naive-ui'
|
import { NIcon, useThemeVars } from 'naive-ui'
|
||||||
import { timeout } from '@/utils/promise.js'
|
import { timeout } from '@/utils/promise.js'
|
||||||
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
||||||
import dayjs from 'dayjs'
|
import { toHumanReadable } from '@/utils/date.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
server: String,
|
server: String,
|
||||||
@ -45,6 +45,12 @@ const autoRefresh = reactive({
|
|||||||
interval: 2,
|
interval: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ttl = reactive({
|
||||||
|
value: 0,
|
||||||
|
expire: 0,
|
||||||
|
intervalID: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const dialogStore = useDialog()
|
const dialogStore = useDialog()
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
@ -54,15 +60,9 @@ const binaryKey = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ttlString = computed(() => {
|
const ttlString = computed(() => {
|
||||||
if (props.ttl > 0) {
|
if (ttl.value > 0) {
|
||||||
const dur = dayjs.duration(props.ttl, 'seconds')
|
return toHumanReadable(ttl.value)
|
||||||
const days = dur.days()
|
} else if (ttl.value < 0) {
|
||||||
if (days > 0) {
|
|
||||||
return days + i18n.t('common.unit_day') + ' ' + dur.format('HH:mm:ss')
|
|
||||||
} else {
|
|
||||||
return dur.format('HH:mm:ss')
|
|
||||||
}
|
|
||||||
} else if (props.ttl < 0) {
|
|
||||||
return i18n.t('interface.forever')
|
return i18n.t('interface.forever')
|
||||||
} else {
|
} else {
|
||||||
return '00:00:00'
|
return '00:00:00'
|
||||||
@ -89,15 +89,46 @@ const stopAutoRefresh = () => {
|
|||||||
autoRefresh.on = false
|
autoRefresh.on = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syncTTL = (seconds) => {
|
||||||
|
ttl.value = seconds
|
||||||
|
if (seconds >= 0) {
|
||||||
|
ttl.expire = Math.floor(Date.now() / 1000 + seconds)
|
||||||
|
} else {
|
||||||
|
ttl.expire = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.keyPath,
|
() => props.keyPath,
|
||||||
() => {
|
() => {
|
||||||
stopAutoRefresh()
|
stopAutoRefresh()
|
||||||
autoRefresh.interval = props.interval
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(() => stopAutoRefresh())
|
watch(
|
||||||
|
() => props.ttl,
|
||||||
|
(seconds) => syncTTL(seconds),
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncTTL(props.ttl)
|
||||||
|
ttl.intervalID = setInterval(() => {
|
||||||
|
if (ttl.expire > 0) {
|
||||||
|
const nowSeconds = Math.floor(Date.now() / 1000)
|
||||||
|
ttl.value = Math.max(0, ttl.expire - nowSeconds)
|
||||||
|
} else {
|
||||||
|
ttl.value = -1
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
if (ttl.intervalID > 0) {
|
||||||
|
clearInterval(ttl.intervalID)
|
||||||
|
ttl.intervalID = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const onToggleRefresh = (on) => {
|
const onToggleRefresh = (on) => {
|
||||||
if (on) {
|
if (on) {
|
||||||
@ -108,15 +139,8 @@ const onToggleRefresh = (on) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCopyKey = () => {
|
const onCopyKey = () => {
|
||||||
ClipboardSetText(props.keyPath)
|
copy(props.keyPath)
|
||||||
.then((succ) => {
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
if (succ) {
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
$message.error(e.message)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTTL = () => {
|
const onTTL = () => {
|
||||||
@ -124,7 +148,7 @@ const onTTL = () => {
|
|||||||
server: props.server,
|
server: props.server,
|
||||||
db: props.db,
|
db: props.db,
|
||||||
key: binaryKey.value ? props.keyCode : props.keyPath,
|
key: binaryKey.value ? props.keyCode : props.keyPath,
|
||||||
ttl: props.ttl,
|
ttl: ttl.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -137,7 +161,11 @@ const onTTL = () => {
|
|||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-popover :delay="500" keep-alive-on-hover placement="bottom" trigger="hover">
|
<n-popover :delay="500" keep-alive-on-hover placement="bottom" trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<icon-button :loading="props.loading" size="18" @click="emit('reload')">
|
<icon-button
|
||||||
|
:loading="props.loading"
|
||||||
|
size="18"
|
||||||
|
@click="emit('reload')"
|
||||||
|
@dblclick.stop="() => {}">
|
||||||
<n-icon :size="props.size">
|
<n-icon :size="props.size">
|
||||||
<refresh
|
<refresh
|
||||||
:class="{ 'auto-rotate': autoRefresh.on }"
|
:class="{ 'auto-rotate': autoRefresh.on }"
|
||||||
@ -164,7 +192,7 @@ const onTTL = () => {
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="Timer" size="18" />
|
<n-icon :component="Timer" size="18" />
|
||||||
</template>
|
</template>
|
||||||
{{ ttlString }}
|
<span style="font-variant-numeric: tabular-nums">{{ ttlString }}</span>
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }}
|
TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }}
|
||||||
|
@ -15,10 +15,14 @@ import IconButton from '@/components/common/IconButton.vue'
|
|||||||
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
||||||
import Edit from '@/components/icons/Edit.vue'
|
import Edit from '@/components/icons/Edit.vue'
|
||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
|
||||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import { formatBytes } from '@/utils/byte_convert.js'
|
import { formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||||
|
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||||
|
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||||
|
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
@ -36,24 +40,19 @@ const props = defineProps({
|
|||||||
default: -1,
|
default: -1,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: Array,
|
type: [String, Array],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
size: Number,
|
size: Number,
|
||||||
length: Number,
|
length: Number,
|
||||||
format: {
|
format: String,
|
||||||
type: String,
|
decode: String,
|
||||||
default: formatTypes.RAW,
|
|
||||||
},
|
|
||||||
decode: {
|
|
||||||
type: String,
|
|
||||||
default: decodeTypes.NONE,
|
|
||||||
},
|
|
||||||
end: Boolean,
|
end: Boolean,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
|
textAlign: Number,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -84,7 +83,7 @@ const fieldFilterOption = ref(null)
|
|||||||
const fieldColumn = computed(() => ({
|
const fieldColumn = computed(() => ({
|
||||||
key: 'key',
|
key: 'key',
|
||||||
title: () => i18n.t('common.field'),
|
title: () => i18n.t('common.field'),
|
||||||
align: 'center',
|
align: props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
resizable: true,
|
resizable: true,
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
@ -95,29 +94,32 @@ const fieldColumn = computed(() => ({
|
|||||||
},
|
},
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
},
|
},
|
||||||
lineClamp: 10,
|
lineClamp: 1,
|
||||||
},
|
},
|
||||||
filterOptionValue: fieldFilterOption.value,
|
filterOptionValue: fieldFilterOption.value,
|
||||||
className: inEdit.value ? 'clickable' : '',
|
className: inEdit.value ? 'clickable wordline' : 'wordline',
|
||||||
filter: (value, row) => {
|
filter: (value, row) => {
|
||||||
return !!~row.k.indexOf(value.toString())
|
return !!~row.k.indexOf(value.toString())
|
||||||
},
|
},
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
|
if (row.rm === true) {
|
||||||
|
return h('s', {}, decodeRedisKey(row.k))
|
||||||
|
}
|
||||||
return decodeRedisKey(row.k)
|
return decodeRedisKey(row.k)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const displayCode = computed(() => {
|
const isCode = computed(() => {
|
||||||
return props.format === formatTypes.JSON
|
return props.format === formatTypes.JSON || props.format === formatTypes.UNICODE_JSON
|
||||||
})
|
})
|
||||||
// const valueFilterOption = ref(null)
|
// const valueFilterOption = ref(null)
|
||||||
const valueColumn = computed(() => ({
|
const valueColumn = computed(() => ({
|
||||||
key: 'value',
|
key: 'value',
|
||||||
title: () => i18n.t('common.value'),
|
title: () => i18n.t('common.value'),
|
||||||
align: displayCode.value ? 'left' : 'center',
|
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
resizable: true,
|
resizable: true,
|
||||||
ellipsis: displayCode.value
|
ellipsis: isCode.value
|
||||||
? false
|
? false
|
||||||
: {
|
: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -127,6 +129,7 @@ const valueColumn = computed(() => ({
|
|||||||
},
|
},
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
},
|
},
|
||||||
|
lineClamp: 1,
|
||||||
},
|
},
|
||||||
// filterOptionValue: valueFilterOption.value,
|
// filterOptionValue: valueFilterOption.value,
|
||||||
className: inEdit.value ? 'clickable' : '',
|
className: inEdit.value ? 'clickable' : '',
|
||||||
@ -137,10 +140,14 @@ const valueColumn = computed(() => ({
|
|||||||
// return !!~row.v.indexOf(value.toString())
|
// return !!~row.v.indexOf(value.toString())
|
||||||
// },
|
// },
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
if (displayCode.value) {
|
const val = row.dv || nativeRedisKey(row.v)
|
||||||
return h('pre', {}, row.dv || row.v)
|
if (isCode.value) {
|
||||||
|
return h('pre', { class: 'pre-wrap' }, val)
|
||||||
}
|
}
|
||||||
return row.dv || row.v
|
if (row.rm === true) {
|
||||||
|
return h('s', {}, val)
|
||||||
|
}
|
||||||
|
return val
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -148,6 +155,8 @@ const startEdit = async (no, key, value) => {
|
|||||||
currentEditRow.no = no
|
currentEditRow.no = no
|
||||||
currentEditRow.key = key
|
currentEditRow.key = key
|
||||||
currentEditRow.value = value
|
currentEditRow.value = value
|
||||||
|
currentEditRow.decode = props.decode
|
||||||
|
currentEditRow.format = props.format
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEdit = async (field, value, decode, format) => {
|
const saveEdit = async (field, value, decode, format) => {
|
||||||
@ -189,8 +198,11 @@ const resetEdit = () => {
|
|||||||
currentEditRow.no = 0
|
currentEditRow.no = 0
|
||||||
currentEditRow.key = ''
|
currentEditRow.key = ''
|
||||||
currentEditRow.value = null
|
currentEditRow.value = null
|
||||||
currentEditRow.format = formatTypes.RAW
|
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||||
currentEditRow.decode = decodeTypes.NONE
|
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||||
|
// }
|
||||||
|
// currentEditRow.format = formatTypes.RAW
|
||||||
|
// currentEditRow.decode = decodeTypes.NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionColumn = {
|
const actionColumn = {
|
||||||
@ -204,16 +216,29 @@ const actionColumn = {
|
|||||||
return h(EditableTableColumn, {
|
return h(EditableTableColumn, {
|
||||||
editing: false,
|
editing: false,
|
||||||
bindKey: row.k,
|
bindKey: row.k,
|
||||||
onCopy: async () => {
|
canRefresh: true,
|
||||||
try {
|
onRefresh: async () => {
|
||||||
const succ = await ClipboardSetText(row.v)
|
const { updated, success, msg } = await browserStore.getHashField({
|
||||||
if (succ) {
|
server: props.name,
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
db: props.db,
|
||||||
}
|
key: keyName.value,
|
||||||
} catch (e) {
|
field: row.k,
|
||||||
$message.error(e.message)
|
decode: props.decode,
|
||||||
|
format: props.format,
|
||||||
|
})
|
||||||
|
if (success) {
|
||||||
|
delete props.value[index]['rm']
|
||||||
|
$message.success(i18n.t('dialogue.reload_succ'))
|
||||||
|
} else {
|
||||||
|
// update fail, the key may have been deleted
|
||||||
|
$message.error(msg)
|
||||||
|
props.value[index]['rm'] = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onCopy: async () => {
|
||||||
|
copy(row.v)
|
||||||
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
|
},
|
||||||
onEdit: () => startEdit(index + 1, row.k, row.v),
|
onEdit: () => startEdit(index + 1, row.k, row.v),
|
||||||
onDelete: async () => {
|
onDelete: async () => {
|
||||||
try {
|
try {
|
||||||
@ -336,13 +361,22 @@ defineExpose({
|
|||||||
<div class="content-wrapper flex-box-v">
|
<div class="content-wrapper flex-box-v">
|
||||||
<slot name="toolbar" />
|
<slot name="toolbar" />
|
||||||
<div class="tb2 value-item-part flex-box-h">
|
<div class="tb2 value-item-part flex-box-h">
|
||||||
<div class="flex-box-h">
|
<div class="flex-box-h" style="max-width: 50%">
|
||||||
<content-search-input
|
<content-search-input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
@filter-changed="onFilterInput"
|
@filter-changed="onFilterInput"
|
||||||
@match-changed="onMatchInput" />
|
@match-changed="onMatchInput" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-expand"></div>
|
<div class="flex-item-expand"></div>
|
||||||
|
<switch-button
|
||||||
|
:icons="[AlignCenter, AlignLeft]"
|
||||||
|
:stroke-width="3.5"
|
||||||
|
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||||
|
:value="props.textAlign"
|
||||||
|
size="medium"
|
||||||
|
unselect-stroke-width="3"
|
||||||
|
@update:value="(val) => emit('update:textAlign', val)" />
|
||||||
|
<n-divider vertical />
|
||||||
<n-button-group>
|
<n-button-group>
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="props.end || props.loading"
|
:disabled="props.end || props.loading"
|
||||||
@ -403,11 +437,12 @@ defineExpose({
|
|||||||
class="entry-editor-container flex-item-expand"
|
class="entry-editor-container flex-item-expand"
|
||||||
style="width: 100%">
|
style="width: 100%">
|
||||||
<content-entry-editor
|
<content-entry-editor
|
||||||
|
v-model:decode="currentEditRow.decode"
|
||||||
|
v-model:format="currentEditRow.format"
|
||||||
v-model:fullscreen="fullEdit"
|
v-model:fullscreen="fullEdit"
|
||||||
:decode="currentEditRow.decode"
|
|
||||||
:field="currentEditRow.key"
|
:field="currentEditRow.key"
|
||||||
:field-label="$t('common.field')"
|
:field-label="$t('common.field')"
|
||||||
:format="currentEditRow.format"
|
:key-path="props.keyPath"
|
||||||
:show="inEdit"
|
:show="inEdit"
|
||||||
:value="currentEditRow.value"
|
:value="currentEditRow.value"
|
||||||
:value-label="$t('common.value')"
|
:value-label="$t('common.value')"
|
||||||
|
168
frontend/src/components/content_value/ContentValueJson.vue
Normal file
168
frontend/src/components/content_value/ContentValueJson.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Copy from '@/components/icons/Copy.vue'
|
||||||
|
import Save from '@/components/icons/Save.vue'
|
||||||
|
import { useThemeVars } from 'naive-ui'
|
||||||
|
import { types as redisTypes } from '@/consts/support_redis_type.js'
|
||||||
|
import { isEmpty, toLower } from 'lodash'
|
||||||
|
import useBrowserStore from 'stores/browser.js'
|
||||||
|
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||||
|
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
||||||
|
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
||||||
|
import { formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
db: Number,
|
||||||
|
keyPath: String,
|
||||||
|
keyCode: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
ttl: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
value: String,
|
||||||
|
size: Number,
|
||||||
|
length: Number,
|
||||||
|
loading: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const themeVars = useThemeVars()
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {ComputedRef<string|number[]>}
|
||||||
|
*/
|
||||||
|
const keyName = computed(() => {
|
||||||
|
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
|
||||||
|
})
|
||||||
|
|
||||||
|
const keyType = redisTypes.JSON
|
||||||
|
|
||||||
|
const editingContent = ref('')
|
||||||
|
|
||||||
|
const displayValue = computed(() => {
|
||||||
|
return decodeRedisKey(props.value) || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const enableSave = computed(() => {
|
||||||
|
return editingContent.value !== displayValue.value && !props.loading
|
||||||
|
})
|
||||||
|
|
||||||
|
const showMemoryUsage = computed(() => {
|
||||||
|
return !isNaN(props.size) && props.size > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy value
|
||||||
|
*/
|
||||||
|
const onCopyValue = () => {
|
||||||
|
copy(displayValue.value)
|
||||||
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save value
|
||||||
|
*/
|
||||||
|
const browserStore = useBrowserStore()
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const onInput = (content) => {
|
||||||
|
editingContent.value = content
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSave = async () => {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const { success, msg } = await browserStore.setKey({
|
||||||
|
server: props.name,
|
||||||
|
db: props.db,
|
||||||
|
key: keyName.value,
|
||||||
|
keyType: toLower(keyType),
|
||||||
|
value: editingContent.value,
|
||||||
|
ttl: -1,
|
||||||
|
format: formatTypes.JSON,
|
||||||
|
decode: decodeTypes.NONE,
|
||||||
|
})
|
||||||
|
if (success) {
|
||||||
|
$message.success(i18n.t('interface.save_value_succ'))
|
||||||
|
} else {
|
||||||
|
$message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reset: () => {
|
||||||
|
editingContent.value = ''
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-wrapper flex-box-v">
|
||||||
|
<slot name="toolbar" />
|
||||||
|
<div class="tb2 value-item-part flex-box-h">
|
||||||
|
<div class="flex-item-expand"></div>
|
||||||
|
<n-button-group>
|
||||||
|
<n-button :disabled="saving" :focusable="false" @click="onCopyValue">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Copy" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('interface.copy_value') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!enableSave"
|
||||||
|
:loading="saving"
|
||||||
|
:secondary="enableSave"
|
||||||
|
:type="enableSave ? 'primary' : ''"
|
||||||
|
@click="onSave">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Save" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-button-group>
|
||||||
|
</div>
|
||||||
|
<div class="value-wrapper value-item-part flex-item-expand flex-box-v">
|
||||||
|
<content-editor
|
||||||
|
:content="displayValue"
|
||||||
|
:loading="props.loading"
|
||||||
|
:offset-key="props.keyPath"
|
||||||
|
class="flex-item-expand"
|
||||||
|
keep-offset
|
||||||
|
language="json"
|
||||||
|
style="height: 100%"
|
||||||
|
@input="onInput"
|
||||||
|
@reset="onInput"
|
||||||
|
@save="onSave" />
|
||||||
|
<n-spin v-show="props.loading" />
|
||||||
|
</div>
|
||||||
|
<div class="value-footer flex-box-h">
|
||||||
|
<n-text v-if="showMemoryUsage">{{ $t('interface.memory_usage') }}: {{ formatBytes(props.size) }}</n-text>
|
||||||
|
<div class="flex-item-expand" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.value-wrapper {
|
||||||
|
//overflow: hidden;
|
||||||
|
border-top: v-bind('themeVars.borderColor') 1px solid;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-footer {
|
||||||
|
border-top: v-bind('themeVars.borderColor') 1px solid;
|
||||||
|
background-color: v-bind('themeVars.tableHeaderColor');
|
||||||
|
}
|
||||||
|
</style>
|
@ -16,8 +16,13 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
|
|||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||||
import Edit from '@/components/icons/Edit.vue'
|
import Edit from '@/components/icons/Edit.vue'
|
||||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import { formatBytes } from '@/utils/byte_convert.js'
|
import { formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||||
|
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||||
|
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||||
|
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||||
|
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
@ -51,9 +56,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
end: Boolean,
|
end: Boolean,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
|
textAlign: Number,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -77,16 +83,16 @@ const inEdit = computed(() => {
|
|||||||
})
|
})
|
||||||
const fullEdit = ref(false)
|
const fullEdit = ref(false)
|
||||||
|
|
||||||
const displayCode = computed(() => {
|
const isCode = computed(() => {
|
||||||
return props.format === formatTypes.JSON
|
return props.format === formatTypes.JSON || props.format === formatTypes.UNICODE_JSON
|
||||||
})
|
})
|
||||||
const valueFilterOption = ref(null)
|
const valueFilterOption = ref(null)
|
||||||
const valueColumn = computed(() => ({
|
const valueColumn = computed(() => ({
|
||||||
key: 'value',
|
key: 'value',
|
||||||
title: () => i18n.t('common.value'),
|
title: () => i18n.t('common.value'),
|
||||||
align: displayCode.value ? 'left' : 'center',
|
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
ellipsis: displayCode.value
|
ellipsis: isCode.value
|
||||||
? false
|
? false
|
||||||
: {
|
: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -96,26 +102,28 @@ const valueColumn = computed(() => ({
|
|||||||
},
|
},
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
},
|
},
|
||||||
|
lineClamp: 1,
|
||||||
},
|
},
|
||||||
filterOptionValue: valueFilterOption.value,
|
filterOptionValue: valueFilterOption.value,
|
||||||
className: inEdit.value ? 'clickable' : '',
|
className: inEdit.value ? 'clickable' : '',
|
||||||
filter: (value, row) => {
|
filter: (filterValue, row) => {
|
||||||
if (row.dv) {
|
const val = row.dv || nativeRedisKey(row.v)
|
||||||
return !!~row.dv.indexOf(value.toString())
|
return !!~val.indexOf(filterValue.toString())
|
||||||
}
|
|
||||||
return !!~row.v.indexOf(value.toString())
|
|
||||||
},
|
},
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
if (displayCode.value) {
|
const val = row.dv || nativeRedisKey(row.v)
|
||||||
return h('pre', {}, row.dv || row.v)
|
if (isCode.value) {
|
||||||
|
return h('pre', { class: 'pre-wrap' }, val)
|
||||||
}
|
}
|
||||||
return row.dv || row.v
|
return val
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const startEdit = async (no, value) => {
|
const startEdit = async (no, value) => {
|
||||||
currentEditRow.no = no
|
currentEditRow.no = no
|
||||||
currentEditRow.value = value
|
currentEditRow.value = value
|
||||||
|
currentEditRow.decode = props.decode
|
||||||
|
currentEditRow.format = props.format
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -142,7 +150,7 @@ const saveEdit = async (pos, value, decode, format) => {
|
|||||||
server: props.name,
|
server: props.name,
|
||||||
db: props.db,
|
db: props.db,
|
||||||
key: keyName.value,
|
key: keyName.value,
|
||||||
index,
|
index: row.index,
|
||||||
value,
|
value,
|
||||||
decode,
|
decode,
|
||||||
format,
|
format,
|
||||||
@ -160,6 +168,9 @@ const saveEdit = async (pos, value, decode, format) => {
|
|||||||
const resetEdit = () => {
|
const resetEdit = () => {
|
||||||
currentEditRow.no = 0
|
currentEditRow.no = 0
|
||||||
currentEditRow.value = null
|
currentEditRow.value = null
|
||||||
|
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||||
|
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionColumn = {
|
const actionColumn = {
|
||||||
@ -169,22 +180,16 @@ const actionColumn = {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (row, index) => {
|
render: ({ index, v }, _) => {
|
||||||
return h(EditableTableColumn, {
|
return h(EditableTableColumn, {
|
||||||
editing: false,
|
editing: false,
|
||||||
bindKey: `#${index + 1}`,
|
bindKey: `#${index + 1}`,
|
||||||
onCopy: async () => {
|
onCopy: async () => {
|
||||||
try {
|
copy(v)
|
||||||
const succ = await ClipboardSetText(row.v)
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
if (succ) {
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$message.error(e.message)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onEdit: () => {
|
onEdit: () => {
|
||||||
startEdit(index + 1, row.v)
|
startEdit(index + 1, v)
|
||||||
},
|
},
|
||||||
onDelete: async () => {
|
onDelete: async () => {
|
||||||
try {
|
try {
|
||||||
@ -216,7 +221,7 @@ const columns = computed(() => {
|
|||||||
width: 80,
|
width: 80,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
render: (row, index) => {
|
render: ({ index }, _) => {
|
||||||
return index + 1
|
return index + 1
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -231,7 +236,7 @@ const columns = computed(() => {
|
|||||||
width: 80,
|
width: 80,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
render: (row, index) => {
|
render: ({ index }, _) => {
|
||||||
if (index + 1 === currentEditRow.no) {
|
if (index + 1 === currentEditRow.no) {
|
||||||
// editing row, show edit state
|
// editing row, show edit state
|
||||||
return h(NIcon, { size: 16, color: 'red' }, () => h(Edit, { strokeWidth: 5 }))
|
return h(NIcon, { size: 16, color: 'red' }, () => h(Edit, { strokeWidth: 5 }))
|
||||||
@ -245,12 +250,12 @@ const columns = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const rowProps = (row, index) => {
|
const rowProps = ({ index, v }, _) => {
|
||||||
return {
|
return {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// in edit mode, switch edit row by click
|
// in edit mode, switch edit row by click
|
||||||
if (inEdit.value) {
|
if (inEdit.value) {
|
||||||
startEdit(index + 1, row.v)
|
startEdit(index + 1, v)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -304,13 +309,22 @@ defineExpose({
|
|||||||
<div class="content-wrapper flex-box-v">
|
<div class="content-wrapper flex-box-v">
|
||||||
<slot name="toolbar" />
|
<slot name="toolbar" />
|
||||||
<div class="tb2 value-item-part flex-box-h">
|
<div class="tb2 value-item-part flex-box-h">
|
||||||
<div class="flex-box-h">
|
<div class="flex-box-h" style="max-width: 50%">
|
||||||
<content-search-input
|
<content-search-input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
@filter-changed="onFilterInput"
|
@filter-changed="onFilterInput"
|
||||||
@match-changed="onMatchInput" />
|
@match-changed="onMatchInput" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-expand"></div>
|
<div class="flex-item-expand"></div>
|
||||||
|
<switch-button
|
||||||
|
:icons="[AlignCenter, AlignLeft]"
|
||||||
|
:stroke-width="3.5"
|
||||||
|
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||||
|
:value="props.textAlign"
|
||||||
|
size="medium"
|
||||||
|
unselect-stroke-width="3"
|
||||||
|
@update:value="(val) => emit('update:textAlign', val)" />
|
||||||
|
<n-divider vertical />
|
||||||
<n-button-group>
|
<n-button-group>
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="props.end || props.loading"
|
:disabled="props.end || props.loading"
|
||||||
@ -370,12 +384,13 @@ defineExpose({
|
|||||||
class="entry-editor-container flex-item-expand"
|
class="entry-editor-container flex-item-expand"
|
||||||
style="width: 100%">
|
style="width: 100%">
|
||||||
<content-entry-editor
|
<content-entry-editor
|
||||||
|
v-model:decode="currentEditRow.decode"
|
||||||
|
v-model:format="currentEditRow.format"
|
||||||
v-model:fullscreen="fullEdit"
|
v-model:fullscreen="fullEdit"
|
||||||
:decode="currentEditRow.decode"
|
|
||||||
:field="currentEditRow.no"
|
:field="currentEditRow.no"
|
||||||
:field-label="$t('common.index')"
|
:field-label="$t('common.index')"
|
||||||
:field-readonly="true"
|
:field-readonly="true"
|
||||||
:format="currentEditRow.format"
|
:key-path="props.keyPath"
|
||||||
:show="inEdit"
|
:show="inEdit"
|
||||||
:value="currentEditRow.value"
|
:value="currentEditRow.value"
|
||||||
:value-label="$t('common.value')"
|
:value-label="$t('common.value')"
|
||||||
|
@ -16,8 +16,13 @@ import Edit from '@/components/icons/Edit.vue'
|
|||||||
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import { formatBytes } from '@/utils/byte_convert.js'
|
import { formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||||
|
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||||
|
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||||
|
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||||
|
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
@ -50,9 +55,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
end: Boolean,
|
end: Boolean,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
|
textAlign: Number,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -76,16 +82,16 @@ const inEdit = computed(() => {
|
|||||||
})
|
})
|
||||||
const fullEdit = ref(false)
|
const fullEdit = ref(false)
|
||||||
|
|
||||||
const displayCode = computed(() => {
|
const isCode = computed(() => {
|
||||||
return props.format === formatTypes.JSON
|
return props.format === formatTypes.JSON || props.format === formatTypes.UNICODE_JSON
|
||||||
})
|
})
|
||||||
const valueFilterOption = ref(null)
|
const valueFilterOption = ref(null)
|
||||||
const valueColumn = computed(() => ({
|
const valueColumn = computed(() => ({
|
||||||
key: 'value',
|
key: 'value',
|
||||||
title: () => i18n.t('common.value'),
|
title: () => i18n.t('common.value'),
|
||||||
align: displayCode.value ? 'left' : 'center',
|
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
ellipsis: displayCode.value
|
ellipsis: isCode.value
|
||||||
? false
|
? false
|
||||||
: {
|
: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -95,26 +101,28 @@ const valueColumn = computed(() => ({
|
|||||||
},
|
},
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
},
|
},
|
||||||
|
lineClamp: 1,
|
||||||
},
|
},
|
||||||
filterOptionValue: valueFilterOption.value,
|
filterOptionValue: valueFilterOption.value,
|
||||||
className: inEdit.value ? 'clickable' : '',
|
className: inEdit.value ? 'clickable' : '',
|
||||||
filter: (value, row) => {
|
filter: (filterValue, row) => {
|
||||||
if (row.dv) {
|
const val = row.dv || nativeRedisKey(row.v)
|
||||||
return !!~row.dv.indexOf(value.toString())
|
return !!~val.indexOf(filterValue.toString())
|
||||||
}
|
|
||||||
return !!~row.v.indexOf(value.toString())
|
|
||||||
},
|
},
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
if (displayCode.value) {
|
const val = row.dv || nativeRedisKey(row.v)
|
||||||
return h('pre', {}, row.dv || row.v)
|
if (isCode.value) {
|
||||||
|
return h('pre', { class: 'pre-wrap' }, val)
|
||||||
}
|
}
|
||||||
return row.dv || row.v
|
return val
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const startEdit = async (no, value) => {
|
const startEdit = async (no, value) => {
|
||||||
currentEditRow.no = no
|
currentEditRow.no = no
|
||||||
currentEditRow.value = value
|
currentEditRow.value = value
|
||||||
|
currentEditRow.decode = props.decode
|
||||||
|
currentEditRow.format = props.format
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,6 +165,9 @@ const saveEdit = async (pos, value, decode, format) => {
|
|||||||
const resetEdit = () => {
|
const resetEdit = () => {
|
||||||
currentEditRow.no = 0
|
currentEditRow.no = 0
|
||||||
currentEditRow.value = null
|
currentEditRow.value = null
|
||||||
|
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||||
|
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionColumn = {
|
const actionColumn = {
|
||||||
@ -171,14 +182,8 @@ const actionColumn = {
|
|||||||
editing: false,
|
editing: false,
|
||||||
bindKey: `#${index + 1}`,
|
bindKey: `#${index + 1}`,
|
||||||
onCopy: async () => {
|
onCopy: async () => {
|
||||||
try {
|
copy(row.v)
|
||||||
const succ = await ClipboardSetText(row.v)
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
if (succ) {
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$message.error(e.message)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onEdit: () => {
|
onEdit: () => {
|
||||||
startEdit(index + 1, row.v)
|
startEdit(index + 1, row.v)
|
||||||
@ -301,13 +306,22 @@ defineExpose({
|
|||||||
<div class="content-wrapper flex-box-v">
|
<div class="content-wrapper flex-box-v">
|
||||||
<slot name="toolbar" />
|
<slot name="toolbar" />
|
||||||
<div class="tb2 value-item-part flex-box-h">
|
<div class="tb2 value-item-part flex-box-h">
|
||||||
<div class="flex-box-h">
|
<div class="flex-box-h" style="max-width: 50%">
|
||||||
<content-search-input
|
<content-search-input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
@filter-changed="onFilterInput"
|
@filter-changed="onFilterInput"
|
||||||
@match-changed="onMatchInput" />
|
@match-changed="onMatchInput" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-expand"></div>
|
<div class="flex-item-expand"></div>
|
||||||
|
<switch-button
|
||||||
|
:icons="[AlignCenter, AlignLeft]"
|
||||||
|
:stroke-width="3.5"
|
||||||
|
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||||
|
:value="props.textAlign"
|
||||||
|
size="medium"
|
||||||
|
unselect-stroke-width="3"
|
||||||
|
@update:value="(val) => emit('update:textAlign', val)" />
|
||||||
|
<n-divider vertical />
|
||||||
<n-button-group>
|
<n-button-group>
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="props.end || props.loading"
|
:disabled="props.end || props.loading"
|
||||||
@ -367,12 +381,13 @@ defineExpose({
|
|||||||
class="entry-editor-container flex-item-expand"
|
class="entry-editor-container flex-item-expand"
|
||||||
style="width: 100%">
|
style="width: 100%">
|
||||||
<content-entry-editor
|
<content-entry-editor
|
||||||
|
v-model:decode="currentEditRow.decode"
|
||||||
|
v-model:format="currentEditRow.format"
|
||||||
v-model:fullscreen="fullEdit"
|
v-model:fullscreen="fullEdit"
|
||||||
:decode="currentEditRow.decode"
|
|
||||||
:field="currentEditRow.no"
|
:field="currentEditRow.no"
|
||||||
:field-label="$t('common.index')"
|
:field-label="$t('common.index')"
|
||||||
:field-readonly="true"
|
:field-readonly="true"
|
||||||
:format="currentEditRow.format"
|
:key-path="props.keyPath"
|
||||||
:show="inEdit"
|
:show="inEdit"
|
||||||
:value="currentEditRow.value"
|
:value="currentEditRow.value"
|
||||||
:value-label="$t('common.value')"
|
:value-label="$t('common.value')"
|
||||||
|
@ -13,8 +13,8 @@ import LoadList from '@/components/icons/LoadList.vue'
|
|||||||
import LoadAll from '@/components/icons/LoadAll.vue'
|
import LoadAll from '@/components/icons/LoadAll.vue'
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
import IconButton from '@/components/common/IconButton.vue'
|
||||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import { formatBytes } from '@/utils/byte_convert.js'
|
import { formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
@ -93,7 +93,7 @@ const valueColumn = computed(() => ({
|
|||||||
},
|
},
|
||||||
// sorter: (row1, row2) => row1.value - row2.value,
|
// sorter: (row1, row2) => row1.value - row2.value,
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return h('pre', {}, row.dv)
|
return h('pre', { class: 'pre-wrap' }, row.dv)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
const actionColumn = {
|
const actionColumn = {
|
||||||
@ -108,14 +108,8 @@ const actionColumn = {
|
|||||||
bindKey: row.id,
|
bindKey: row.id,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
onCopy: async () => {
|
onCopy: async () => {
|
||||||
try {
|
copy(JSON.stringify(row.v))
|
||||||
const succ = await ClipboardSetText(JSON.stringify(row.v))
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
if (succ) {
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$message.error(e.message)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDelete: async () => {
|
onDelete: async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watchEffect } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import Copy from '@/components/icons/Copy.vue'
|
import Copy from '@/components/icons/Copy.vue'
|
||||||
import Save from '@/components/icons/Save.vue'
|
import Save from '@/components/icons/Save.vue'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import { formatTypes } from '@/consts/value_view_type.js'
|
import { formatTypes } from '@/consts/value_view_type.js'
|
||||||
import { types as redisTypes } from '@/consts/support_redis_type.js'
|
import { types as redisTypes } from '@/consts/support_redis_type.js'
|
||||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import { isEmpty, toLower } from 'lodash'
|
import { isEmpty, toLower } from 'lodash'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||||
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
|
||||||
import { formatBytes } from '@/utils/byte_convert.js'
|
import { formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: String,
|
name: String,
|
||||||
@ -28,6 +27,12 @@ const props = defineProps({
|
|||||||
default: -1,
|
default: -1,
|
||||||
},
|
},
|
||||||
value: [String, Array],
|
value: [String, Array],
|
||||||
|
format: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
decode: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
size: Number,
|
size: Number,
|
||||||
length: Number,
|
length: Number,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
@ -35,7 +40,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const prefStore = usePreferencesStore()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -49,6 +53,7 @@ const keyType = redisTypes.STRING
|
|||||||
const viewLanguage = computed(() => {
|
const viewLanguage = computed(() => {
|
||||||
switch (viewAs.format) {
|
switch (viewAs.format) {
|
||||||
case formatTypes.JSON:
|
case formatTypes.JSON:
|
||||||
|
case formatTypes.UNICODE_JSON:
|
||||||
return 'json'
|
return 'json'
|
||||||
case formatTypes.YAML:
|
case formatTypes.YAML:
|
||||||
return 'yaml'
|
return 'yaml'
|
||||||
@ -66,29 +71,27 @@ const viewAs = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const editingContent = ref('')
|
const editingContent = ref('')
|
||||||
|
const resetKey = ref('')
|
||||||
|
|
||||||
const enableSave = computed(() => {
|
const enableSave = computed(() => {
|
||||||
return editingContent.value !== viewAs.value && !props.loading
|
return editingContent.value !== viewAs.value && !props.loading
|
||||||
})
|
})
|
||||||
|
|
||||||
const displayValue = computed(() => {
|
const displayValue = computed(() => {
|
||||||
if (props.loading) {
|
return viewAs.value || decodeRedisKey(props.value) || ''
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return viewAs.value || decodeRedisKey(props.value)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const showMemoryUsage = computed(() => {
|
const showMemoryUsage = computed(() => {
|
||||||
return !isNaN(props.size) && props.size > 0
|
return !isNaN(props.size) && props.size > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(
|
watch(
|
||||||
() => {
|
() => props.value,
|
||||||
if (props.value !== undefined) {
|
(val) => {
|
||||||
|
if (!isEmpty(val)) {
|
||||||
onFormatChanged(viewAs.decode, viewAs.format)
|
onFormatChanged(viewAs.decode, viewAs.format)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ flush: 'post' },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const converting = ref(false)
|
const converting = ref(false)
|
||||||
@ -101,12 +104,14 @@ const onFormatChanged = async (decode = '', format = '') => {
|
|||||||
format: retFormat,
|
format: retFormat,
|
||||||
} = await browserStore.convertValue({
|
} = await browserStore.convertValue({
|
||||||
value: props.value,
|
value: props.value,
|
||||||
decode,
|
decode: decode || props.decode,
|
||||||
format,
|
format: format || props.format,
|
||||||
})
|
})
|
||||||
editingContent.value = viewAs.value = value
|
editingContent.value = viewAs.value = value
|
||||||
viewAs.decode = decode || retDecode
|
viewAs.decode = decode || retDecode
|
||||||
viewAs.format = format || retFormat
|
viewAs.format = format || retFormat
|
||||||
|
browserStore.setSelectedFormat(props.name, props.keyPath, props.db, viewAs.format, viewAs.decode)
|
||||||
|
resetKey.value = Date.now().toString()
|
||||||
} finally {
|
} finally {
|
||||||
converting.value = false
|
converting.value = false
|
||||||
}
|
}
|
||||||
@ -116,15 +121,8 @@ const onFormatChanged = async (decode = '', format = '') => {
|
|||||||
* Copy value
|
* Copy value
|
||||||
*/
|
*/
|
||||||
const onCopyValue = () => {
|
const onCopyValue = () => {
|
||||||
ClipboardSetText(displayValue.value)
|
copy(displayValue.value)
|
||||||
.then((succ) => {
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
if (succ) {
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
$message.error(e.message)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,6 +149,7 @@ const onSave = async () => {
|
|||||||
decode: viewAs.decode,
|
decode: viewAs.decode,
|
||||||
})
|
})
|
||||||
if (success) {
|
if (success) {
|
||||||
|
viewAs.value = editingContent.value
|
||||||
$message.success(i18n.t('interface.save_value_succ'))
|
$message.success(i18n.t('interface.save_value_succ'))
|
||||||
} else {
|
} else {
|
||||||
$message.error(msg)
|
$message.error(msg)
|
||||||
@ -198,19 +197,19 @@ defineExpose({
|
|||||||
</n-button-group>
|
</n-button-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="value-wrapper value-item-part flex-item-expand flex-box-v">
|
<div class="value-wrapper value-item-part flex-item-expand flex-box-v">
|
||||||
<n-spin :show="props.loading || converting" />
|
|
||||||
<content-editor
|
<content-editor
|
||||||
v-show="!(props.loading || converting)"
|
|
||||||
:content="displayValue"
|
:content="displayValue"
|
||||||
:language="viewLanguage"
|
:language="viewLanguage"
|
||||||
:loading="props.loading"
|
:loading="props.loading"
|
||||||
:show-folding="prefStore.showFolding"
|
:offset-key="props.keyPath"
|
||||||
:show-line-num="prefStore.showLineNum"
|
:reset-key="resetKey"
|
||||||
class="flex-item-expand"
|
class="flex-item-expand"
|
||||||
|
keep-offset
|
||||||
style="height: 100%"
|
style="height: 100%"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@reset="onInput"
|
@reset="onInput"
|
||||||
@save="onSave" />
|
@save="onSave" />
|
||||||
|
<n-spin v-show="props.loading || converting" />
|
||||||
</div>
|
</div>
|
||||||
<div class="value-footer flex-box-h">
|
<div class="value-footer flex-box-h">
|
||||||
<n-text v-if="!isNaN(props.length)">{{ $t('interface.length') }}: {{ props.length }}</n-text>
|
<n-text v-if="!isNaN(props.length)">{{ $t('interface.length') }}: {{ props.length }}</n-text>
|
||||||
|
@ -11,13 +11,17 @@ import useBrowserStore from 'stores/browser.js'
|
|||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import useDialogStore from 'stores/dialog.js'
|
import useDialogStore from 'stores/dialog.js'
|
||||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ContentToolbar from '@/components/content_value/ContentToolbar.vue'
|
import ContentToolbar from '@/components/content_value/ContentToolbar.vue'
|
||||||
|
import ContentValueJson from '@/components/content_value/ContentValueJson.vue'
|
||||||
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
|
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||||
|
import { isMacOS } from '@/utils/platform.js'
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const browserStore = useBrowserStore()
|
const browserStore = useBrowserStore()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
const prefStore = usePreferencesStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
blank: Boolean,
|
blank: Boolean,
|
||||||
@ -66,6 +70,7 @@ const valueComponents = {
|
|||||||
[redisTypes.SET]: ContentValueSet,
|
[redisTypes.SET]: ContentValueSet,
|
||||||
[redisTypes.ZSET]: ContentValueZset,
|
[redisTypes.ZSET]: ContentValueZset,
|
||||||
[redisTypes.STREAM]: ContentValueStream,
|
[redisTypes.STREAM]: ContentValueStream,
|
||||||
|
[redisTypes.JSON]: ContentValueJson,
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyName = computed(() => {
|
const keyName = computed(() => {
|
||||||
@ -84,15 +89,15 @@ const loadData = async (reset, full, selMatch) => {
|
|||||||
if (!!props.blank) {
|
if (!!props.blank) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { name, db, matchPattern, decode, format } = data.value
|
const { name, db, matchPattern } = data.value
|
||||||
reset = reset === true
|
reset = reset === true
|
||||||
await browserStore.loadKeyDetail({
|
await browserStore.loadKeyDetail({
|
||||||
server: name,
|
server: name,
|
||||||
db: db,
|
db: db,
|
||||||
key: keyName.value,
|
key: keyName.value,
|
||||||
matchPattern: selMatch === undefined ? matchPattern : selMatch,
|
matchPattern: selMatch === undefined ? matchPattern : selMatch,
|
||||||
decode: reset ? decodeTypes.NONE : decode,
|
decode: '',
|
||||||
format: reset ? formatTypes.RAW : format,
|
format: '',
|
||||||
reset,
|
reset,
|
||||||
full: full === true,
|
full: full === true,
|
||||||
})
|
})
|
||||||
@ -109,18 +114,42 @@ const loadData = async (reset, full, selMatch) => {
|
|||||||
const onReload = async (selDecode, selFormat) => {
|
const onReload = async (selDecode, selFormat) => {
|
||||||
try {
|
try {
|
||||||
const { name, db, keyCode, keyPath, decode, format, matchPattern } = data.value
|
const { name, db, keyCode, keyPath, decode, format, matchPattern } = data.value
|
||||||
|
const targetFormat = selFormat || format
|
||||||
|
const targetDecode = selDecode || decode
|
||||||
|
browserStore.setSelectedFormat(name, keyPath, db, targetFormat, targetDecode)
|
||||||
await browserStore.reloadKey({
|
await browserStore.reloadKey({
|
||||||
server: name,
|
server: name,
|
||||||
db,
|
db,
|
||||||
key: keyCode || keyPath,
|
key: keyCode || keyPath,
|
||||||
decode: selDecode || decode,
|
decode: targetDecode,
|
||||||
format: selFormat || format,
|
format: targetFormat,
|
||||||
matchPattern,
|
matchPattern,
|
||||||
|
showLoading: false,
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onKeyShortcut = (e) => {
|
||||||
|
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Delete':
|
||||||
|
onDelete()
|
||||||
|
return
|
||||||
|
case 'F5':
|
||||||
|
onReload()
|
||||||
|
return
|
||||||
|
case 'r':
|
||||||
|
if (isCtrlOn) {
|
||||||
|
onReload()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case 'F2':
|
||||||
|
onRename()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onRename = () => {
|
const onRename = () => {
|
||||||
const { name, db, keyPath } = data.value
|
const { name, db, keyPath } = data.value
|
||||||
if (binaryKey.value) {
|
if (binaryKey.value) {
|
||||||
@ -153,6 +182,11 @@ const onMatch = (match) => {
|
|||||||
loadData(true, false, match || '')
|
loadData(true, false, match || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onEntryTextAlignChanged = (align) => {
|
||||||
|
prefStore.editor.entryTextAlign = align !== TextAlignType.Left ? TextAlignType.Center : TextAlignType.Left
|
||||||
|
prefStore.savePreferences()
|
||||||
|
}
|
||||||
|
|
||||||
const contentRef = ref(null)
|
const contentRef = ref(null)
|
||||||
const initContent = async () => {
|
const initContent = async () => {
|
||||||
// onReload()
|
// onReload()
|
||||||
@ -199,11 +233,15 @@ watch(() => data.value?.keyPath, initContent)
|
|||||||
:size="data.size"
|
:size="data.size"
|
||||||
:ttl="data.ttl"
|
:ttl="data.ttl"
|
||||||
:value="data.value"
|
:value="data.value"
|
||||||
|
tabindex="0"
|
||||||
|
:text-align="prefStore.entryTextAlign"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
|
@keydown="onKeyShortcut"
|
||||||
@loadall="onLoadAll"
|
@loadall="onLoadAll"
|
||||||
@loadmore="onLoadMore"
|
@loadmore="onLoadMore"
|
||||||
@match="onMatch"
|
@match="onMatch"
|
||||||
@reload="onReload">
|
@reload="onReload"
|
||||||
|
@update:text-align="onEntryTextAlignChanged">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<content-toolbar
|
<content-toolbar
|
||||||
:db="data.db"
|
:db="data.db"
|
||||||
|
@ -16,8 +16,13 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
|
|||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||||
import Edit from '@/components/icons/Edit.vue'
|
import Edit from '@/components/icons/Edit.vue'
|
||||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import { formatBytes } from '@/utils/byte_convert.js'
|
import { formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import copy from 'copy-text-to-clipboard'
|
||||||
|
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||||
|
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||||
|
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||||
|
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||||
|
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
@ -50,9 +55,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
end: Boolean,
|
end: Boolean,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
|
textAlign: Number,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -82,7 +88,7 @@ const fullEdit = ref(false)
|
|||||||
const scoreColumn = computed(() => ({
|
const scoreColumn = computed(() => ({
|
||||||
key: 'score',
|
key: 'score',
|
||||||
title: () => i18n.t('common.score'),
|
title: () => i18n.t('common.score'),
|
||||||
align: 'center',
|
align: props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
resizable: true,
|
resizable: true,
|
||||||
sorter: (row1, row2) => row1.s - row2.s,
|
sorter: (row1, row2) => row1.s - row2.s,
|
||||||
@ -120,21 +126,21 @@ const scoreColumn = computed(() => ({
|
|||||||
// return true
|
// return true
|
||||||
// },
|
// },
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return row.s
|
return row.ss || row.s
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const displayCode = computed(() => {
|
const isCode = computed(() => {
|
||||||
return props.format === formatTypes.JSON
|
return props.format === formatTypes.JSON || props.format === formatTypes.UNICODE_JSON
|
||||||
})
|
})
|
||||||
const valueFilterOption = ref(null)
|
const valueFilterOption = ref(null)
|
||||||
const valueColumn = computed(() => ({
|
const valueColumn = computed(() => ({
|
||||||
key: 'value',
|
key: 'value',
|
||||||
title: () => i18n.t('common.value'),
|
title: () => i18n.t('common.value'),
|
||||||
align: displayCode.value ? 'left' : 'center',
|
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
resizable: true,
|
resizable: true,
|
||||||
ellipsis: displayCode.value
|
ellipsis: isCode.value
|
||||||
? false
|
? false
|
||||||
: {
|
: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -144,21 +150,21 @@ const valueColumn = computed(() => ({
|
|||||||
},
|
},
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
},
|
},
|
||||||
|
lineClamp: 1,
|
||||||
},
|
},
|
||||||
filterOptionValue: valueFilterOption.value,
|
filterOptionValue: valueFilterOption.value,
|
||||||
className: inEdit.value ? 'clickable' : '',
|
className: inEdit.value ? 'clickable' : '',
|
||||||
filter(value, row) {
|
filter(filterValue, row) {
|
||||||
if (row.dv) {
|
const val = row.dv || nativeRedisKey(row.v)
|
||||||
return !!~row.dv.indexOf(value.toString())
|
return !!~val.indexOf(filterValue.toString())
|
||||||
}
|
|
||||||
return !!~row.v.indexOf(value.toString())
|
|
||||||
},
|
},
|
||||||
// sorter: (row1, row2) => row1.value - row2.value,
|
// sorter: (row1, row2) => row1.value - row2.value,
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
if (displayCode.value) {
|
const val = row.dv || nativeRedisKey(row.v)
|
||||||
return h('pre', {}, row.dv || row.v)
|
if (isCode.value) {
|
||||||
|
return h('pre', { class: 'pre-wrap' }, val)
|
||||||
}
|
}
|
||||||
return row.dv || row.v
|
return val
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -166,6 +172,8 @@ const startEdit = async (no, score, value) => {
|
|||||||
currentEditRow.no = no
|
currentEditRow.no = no
|
||||||
currentEditRow.score = score
|
currentEditRow.score = score
|
||||||
currentEditRow.value = value
|
currentEditRow.value = value
|
||||||
|
currentEditRow.decode = props.decode
|
||||||
|
currentEditRow.format = props.format
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEdit = async (field, value, decode, format) => {
|
const saveEdit = async (field, value, decode, format) => {
|
||||||
@ -204,8 +212,9 @@ const resetEdit = () => {
|
|||||||
currentEditRow.no = 0
|
currentEditRow.no = 0
|
||||||
currentEditRow.score = 0
|
currentEditRow.score = 0
|
||||||
currentEditRow.value = null
|
currentEditRow.value = null
|
||||||
currentEditRow.format = formatTypes.RAW
|
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||||
currentEditRow.decode = decodeTypes.NONE
|
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionColumn = {
|
const actionColumn = {
|
||||||
@ -220,14 +229,8 @@ const actionColumn = {
|
|||||||
editing: false,
|
editing: false,
|
||||||
bindKey: row.v,
|
bindKey: row.v,
|
||||||
onCopy: async () => {
|
onCopy: async () => {
|
||||||
try {
|
copy(row.v)
|
||||||
const succ = await ClipboardSetText(row.v)
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
if (succ) {
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$message.error(e.message)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onEdit: () => startEdit(index + 1, row.s, row.v),
|
onEdit: () => startEdit(index + 1, row.s, row.v),
|
||||||
onDelete: async () => {
|
onDelete: async () => {
|
||||||
@ -338,13 +341,22 @@ defineExpose({
|
|||||||
<div class="content-wrapper flex-box-v">
|
<div class="content-wrapper flex-box-v">
|
||||||
<slot name="toolbar" />
|
<slot name="toolbar" />
|
||||||
<div class="tb2 value-item-part flex-box-h">
|
<div class="tb2 value-item-part flex-box-h">
|
||||||
<div class="flex-box-h">
|
<div class="flex-box-h" style="max-width: 50%">
|
||||||
<content-search-input
|
<content-search-input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
@filter-changed="onFilterInput"
|
@filter-changed="onFilterInput"
|
||||||
@match-changed="onMatchInput" />
|
@match-changed="onMatchInput" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-expand"></div>
|
<div class="flex-item-expand"></div>
|
||||||
|
<switch-button
|
||||||
|
:icons="[AlignCenter, AlignLeft]"
|
||||||
|
:stroke-width="3.5"
|
||||||
|
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||||
|
:value="props.textAlign"
|
||||||
|
size="medium"
|
||||||
|
unselect-stroke-width="3"
|
||||||
|
@update:value="(val) => emit('update:textAlign', val)" />
|
||||||
|
<n-divider vertical />
|
||||||
<n-button-group>
|
<n-button-group>
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="props.end || props.loading"
|
:disabled="props.end || props.loading"
|
||||||
@ -403,11 +415,12 @@ defineExpose({
|
|||||||
class="entry-editor-container flex-item-expand"
|
class="entry-editor-container flex-item-expand"
|
||||||
style="width: 100%">
|
style="width: 100%">
|
||||||
<content-entry-editor
|
<content-entry-editor
|
||||||
|
v-model:decode="currentEditRow.decode"
|
||||||
|
v-model:format="currentEditRow.format"
|
||||||
v-model:fullscreen="fullEdit"
|
v-model:fullscreen="fullEdit"
|
||||||
:decode="currentEditRow.decode"
|
|
||||||
:field="currentEditRow.score"
|
:field="currentEditRow.score"
|
||||||
:field-label="$t('common.score')"
|
:field-label="$t('common.score')"
|
||||||
:format="currentEditRow.format"
|
:key-path="props.keyPath"
|
||||||
:show="inEdit"
|
:show="inEdit"
|
||||||
:value="currentEditRow.value"
|
:value="currentEditRow.value"
|
||||||
:value-label="$t('common.value')"
|
:value-label="$t('common.value')"
|
||||||
|
@ -3,7 +3,10 @@ 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 { includes, isEmpty, map, pull, some, values } from 'lodash'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
|
import useDialogStore from 'stores/dialog.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
decode: {
|
decode: {
|
||||||
@ -17,13 +20,52 @@ const props = defineProps({
|
|||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const prefStore = usePreferencesStore()
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
|
const formatTypeOption = computed(() => {
|
||||||
|
return map(formatTypes, (t) => t)
|
||||||
|
})
|
||||||
|
|
||||||
|
const decodeTypeOption = computed(() => {
|
||||||
|
const buildinTypes = [decodeTypes.NONE],
|
||||||
|
customTypes = []
|
||||||
|
const typs = values(decodeTypes)
|
||||||
|
// build-in decoder
|
||||||
|
for (const typ of typs) {
|
||||||
|
if (includes(prefStore.buildInDecoder, typ)) {
|
||||||
|
buildinTypes.push(typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// custom decoder
|
||||||
|
if (!isEmpty(prefStore.decoder)) {
|
||||||
|
for (const decoder of prefStore.decoder) {
|
||||||
|
// replace build-in decoder if name conflicted
|
||||||
|
pull(buildinTypes, decoder.name)
|
||||||
|
customTypes.push(decoder.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [buildinTypes, customTypes]
|
||||||
|
})
|
||||||
|
|
||||||
|
const decodeMenuOption = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'new_rdm_decoder',
|
||||||
|
label: 'interface.custom_decoder',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
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) {
|
||||||
@ -33,6 +75,14 @@ const onFormatChanged = (selDecode, selFormat) => {
|
|||||||
emit('update:format', selFormat)
|
emit('update:format', selFormat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onDecodeMenu = (key) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'new_rdm_decoder':
|
||||||
|
dialogStore.openPreferencesDialog('decoder')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -41,19 +91,21 @@ 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 || formatTypes.RAW"
|
||||||
@update:value="(f) => onFormatChanged(props.decode, f)" />
|
@update:value="(f) => onFormatChanged(props.decode, f)" />
|
||||||
<n-divider vertical />
|
<n-divider vertical />
|
||||||
<dropdown-selector
|
<dropdown-selector
|
||||||
:default="decodeTypes.NONE"
|
:default="decodeTypes.NONE"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:icon="Conversion"
|
:icon="Conversion"
|
||||||
:options="decodeTypes"
|
:menu-option="decodeMenuOption"
|
||||||
|
:options="decodeTypeOption"
|
||||||
:tooltip="$t('interface.decode_with')"
|
:tooltip="$t('interface.decode_with')"
|
||||||
:value="props.decode"
|
:value="props.decode || decodeTypes.NONE"
|
||||||
@update:value="(d) => onFormatChanged(d, props.format)" />
|
@menu="onDecodeMenu"
|
||||||
|
@update:value="(d) => onFormatChanged(d, '')" />
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ const onOpenWebsite = () => {
|
|||||||
<n-text class="about-link" @click="onOpenWebsite">{{ $t('dialogue.about.website') }}</n-text>
|
<n-text class="about-link" @click="onOpenWebsite">{{ $t('dialogue.about.website') }}</n-text>
|
||||||
</n-space>
|
</n-space>
|
||||||
<div :style="{ color: themeVars.textColor3 }" class="about-copyright">
|
<div :style="{ color: themeVars.textColor3 }" class="about-copyright">
|
||||||
Copyright © 2023 Tinycraft.cc All rights reserved
|
Copyright © 2024 Tinycraft.cc All rights reserved
|
||||||
</div>
|
</div>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
@ -197,7 +197,6 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.addFieldsDialogVisible"
|
v-model:show="dialogStore.addFieldsDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:negative-button-props="{ size: 'medium' }"
|
:negative-button-props="{ size: 'medium' }"
|
||||||
:negative-text="$t('common.cancel')"
|
:negative-text="$t('common.cancel')"
|
||||||
@ -205,9 +204,11 @@ const onClose = () => {
|
|||||||
:positive-text="$t('common.confirm')"
|
:positive-text="$t('common.confirm')"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="title ? $t(title) : ''"
|
:title="title ? $t(title) : ''"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
style="width: 600px"
|
style="width: 600px"
|
||||||
transform-origin="center"
|
transform-origin="center"
|
||||||
|
@esc="onClose"
|
||||||
@positive-click="onAdd"
|
@positive-click="onAdd"
|
||||||
@negative-click="onClose">
|
@negative-click="onClose">
|
||||||
<n-scrollbar style="max-height: 500px">
|
<n-scrollbar style="max-height: 500px">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { every, get, includes, isEmpty, map, reject, sortBy, toNumber } from 'lodash'
|
import { every, get, includes, isEmpty, map, reject, sortBy, toNumber, trim } from 'lodash'
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ListSentinelMasters, TestConnection } from 'wailsjs/go/services/connectionService.js'
|
import { ListSentinelMasters, TestConnection } from 'wailsjs/go/services/connectionService.js'
|
||||||
@ -164,6 +164,18 @@ const onSaveConnection = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// trim addr by network type
|
||||||
|
if (get(generalForm.value, 'network', 'tcp') === 'unix') {
|
||||||
|
generalForm.value.network = 'unix'
|
||||||
|
generalForm.value.addr = ''
|
||||||
|
generalForm.value.port = 0
|
||||||
|
generalForm.value.sock = trim(generalForm.value.sock)
|
||||||
|
} else {
|
||||||
|
generalForm.value.network = 'tcp'
|
||||||
|
generalForm.value.sock = ''
|
||||||
|
generalForm.value.addr = trim(generalForm.value.addr)
|
||||||
|
}
|
||||||
|
|
||||||
// trim advance data
|
// trim advance data
|
||||||
if (get(generalForm.value, 'dbFilterType', 'none') === 'none') {
|
if (get(generalForm.value, 'dbFilterType', 'none') === 'none') {
|
||||||
generalForm.value.dbFilterList = []
|
generalForm.value.dbFilterList = []
|
||||||
@ -195,10 +207,24 @@ const onSaveConnection = async () => {
|
|||||||
generalForm.value.sentinel = {}
|
generalForm.value.sentinel = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trim cluster data
|
||||||
if (!!!generalForm.value.cluster.enable) {
|
if (!!!generalForm.value.cluster.enable) {
|
||||||
generalForm.value.cluster = {}
|
generalForm.value.cluster = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trim proxy data
|
||||||
|
if (generalForm.value.proxy.type !== 2) {
|
||||||
|
generalForm.value.proxy.schema = ''
|
||||||
|
generalForm.value.proxy.addr = ''
|
||||||
|
generalForm.value.proxy.port = 0
|
||||||
|
generalForm.value.proxy.auth = false
|
||||||
|
generalForm.value.proxy.username = ''
|
||||||
|
generalForm.value.proxy.password = ''
|
||||||
|
} else if (!generalForm.value.proxy.auth) {
|
||||||
|
generalForm.value.proxy.username = ''
|
||||||
|
generalForm.value.proxy.password = ''
|
||||||
|
}
|
||||||
|
|
||||||
// store new connection
|
// store new connection
|
||||||
const { success, msg } = await connectionStore.saveConnection(
|
const { success, msg } = await connectionStore.saveConnection(
|
||||||
isEditMode.value ? editName.value : null,
|
isEditMode.value ? editName.value : null,
|
||||||
@ -238,6 +264,7 @@ watch(
|
|||||||
pairs.push({ db: parseInt(db), alias: alias[db] })
|
pairs.push({ db: parseInt(db), alias: alias[db] })
|
||||||
}
|
}
|
||||||
aliasPair.value = pairs
|
aliasPair.value = pairs
|
||||||
|
generalForm.value.proxy.auth = !isEmpty(generalForm.value.proxy.username)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -267,27 +294,59 @@ const onTestConnection = async () => {
|
|||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
dialogStore.closeConnDialog()
|
dialogStore.closeConnDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pasteFromClipboard = async () => {
|
||||||
|
// url example:
|
||||||
|
// rediss://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
|
||||||
|
let opt = {}
|
||||||
|
try {
|
||||||
|
opt = await connectionStore.parseUrlFromClipboard()
|
||||||
|
} catch (e) {
|
||||||
|
$message.error(i18n.t('dialogue.connection.parse_fail', { reason: e.message }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
generalForm.value.network = opt.network || 'tcp'
|
||||||
|
generalForm.value.name = generalForm.value.addr = opt.addr
|
||||||
|
generalForm.value.port = opt.port
|
||||||
|
generalForm.value.username = opt.username
|
||||||
|
generalForm.value.password = opt.password
|
||||||
|
if (opt.connTimeout > 0) {
|
||||||
|
generalForm.value.connTimeout = opt.connTimeout
|
||||||
|
}
|
||||||
|
if (opt.execTimeout > 0) {
|
||||||
|
generalForm.value.execTimeout = opt.execTimeout
|
||||||
|
}
|
||||||
|
const { sslServerName = null } = opt
|
||||||
|
if (sslServerName != null) {
|
||||||
|
generalForm.value.ssl.enable = true
|
||||||
|
if (!isEmpty(sslServerName)) {
|
||||||
|
generalForm.value.ssl.sni = sslServerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$message.success(i18n.t('dialogue.connection.parse_pass', { url: opt.url }))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.connDialogVisible"
|
v-model:show="dialogStore.connDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:on-after-leave="resetForm"
|
:on-after-leave="resetForm"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="isEditMode ? $t('dialogue.connection.edit_title') : $t('dialogue.connection.new_title')"
|
:title="isEditMode ? $t('dialogue.connection.edit_title') : $t('dialogue.connection.new_title')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
style="width: 600px"
|
style="width: 600px"
|
||||||
transform-origin="center">
|
transform-origin="center"
|
||||||
|
@esc="onClose">
|
||||||
<n-spin :show="closingConnection">
|
<n-spin :show="closingConnection">
|
||||||
<n-tabs
|
<n-tabs
|
||||||
v-model:value="tab"
|
v-model:value="tab"
|
||||||
animated
|
animated
|
||||||
pane-style="min-height: 50vh;"
|
pane-style="min-height: 50vh;"
|
||||||
placement="left"
|
placement="left"
|
||||||
tab-style="justify-content: right;"
|
tab-style="justify-content: right; font-weight: 420;"
|
||||||
type="line">
|
type="line">
|
||||||
<!-- General pane -->
|
<!-- General pane -->
|
||||||
<n-tab-pane :tab="$t('dialogue.connection.general')" display-directive="show:lazy" name="general">
|
<n-tab-pane :tab="$t('dialogue.connection.general')" display-directive="show:lazy" name="general">
|
||||||
@ -318,16 +377,33 @@ const onClose = () => {
|
|||||||
:render-label="({ label, value }) => (value === '' ? $t(label) : label)" />
|
:render-label="({ label, value }) => (value === '' ? $t(label) : label)" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('dialogue.connection.addr')" :span="24" path="addr" required>
|
<n-form-item-gi :label="$t('dialogue.connection.addr')" :span="24" path="addr" required>
|
||||||
<n-input
|
<n-input-group>
|
||||||
v-model:value="generalForm.addr"
|
<n-select
|
||||||
:placeholder="$t('dialogue.connection.addr_tip')" />
|
v-model:value="generalForm.network"
|
||||||
<n-text style="width: 40px; text-align: center">:</n-text>
|
:options="[
|
||||||
<n-input-number
|
{ value: 'tcp', label: 'TCP' },
|
||||||
v-model:value="generalForm.port"
|
{ value: 'unix', label: 'UNIX' },
|
||||||
:max="65535"
|
]"
|
||||||
:min="1"
|
style="max-width: 100px" />
|
||||||
:show-button="false"
|
<template v-if="generalForm.network === 'unix'">
|
||||||
style="width: 200px" />
|
<n-input
|
||||||
|
v-model:value="generalForm.sock"
|
||||||
|
:placeholder="$t('dialogue.connection.sock_tip')" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.addr"
|
||||||
|
:placeholder="$t('dialogue.connection.addr_tip')" />
|
||||||
|
<n-text style="width: 40px; text-align: center">:</n-text>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="generalForm.port"
|
||||||
|
:max="65535"
|
||||||
|
:min="1"
|
||||||
|
:show-button="false"
|
||||||
|
placeholder="6379"
|
||||||
|
style="width: 200px" />
|
||||||
|
</template>
|
||||||
|
</n-input-group>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('dialogue.connection.pwd')" :span="12" path="password">
|
<n-form-item-gi :label="$t('dialogue.connection.pwd')" :span="12" path="password">
|
||||||
<n-input
|
<n-input
|
||||||
@ -431,7 +507,7 @@ const onClose = () => {
|
|||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi
|
<n-form-item-gi
|
||||||
v-show="generalForm.dbFilterType !== 'none'"
|
v-if="generalForm.dbFilterType !== 'none'"
|
||||||
:label="$t('dialogue.connection.advn.dbfilter_input')"
|
:label="$t('dialogue.connection.advn.dbfilter_input')"
|
||||||
:span="24">
|
:span="24">
|
||||||
<n-select
|
<n-select
|
||||||
@ -468,6 +544,7 @@ const onClose = () => {
|
|||||||
</n-form>
|
</n-form>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- Alias pane -->
|
||||||
<n-tab-pane :tab="$t('dialogue.connection.alias.title')" display-directive="show:lazy" name="alias">
|
<n-tab-pane :tab="$t('dialogue.connection.alias.title')" display-directive="show:lazy" name="alias">
|
||||||
<n-form
|
<n-form
|
||||||
:model="generalForm.alias"
|
:model="generalForm.alias"
|
||||||
@ -526,7 +603,7 @@ const onClose = () => {
|
|||||||
:disabled="!generalForm.ssl.enable"
|
:disabled="!generalForm.ssl.enable"
|
||||||
:placeholder="$t('dialogue.connection.ssl.key_file_tip')" />
|
:placeholder="$t('dialogue.connection.ssl.key_file_tip')" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item :label="$t('dialogue.connection.ssl.ca_file')">
|
<n-form-item :label="$t('dialogue.connection.ssl.ca_file')" :show-feedback="false">
|
||||||
<file-open-input
|
<file-open-input
|
||||||
v-model:value="generalForm.ssl.caFile"
|
v-model:value="generalForm.ssl.caFile"
|
||||||
:disabled="!generalForm.ssl.enable"
|
:disabled="!generalForm.ssl.enable"
|
||||||
@ -664,6 +741,69 @@ const onClose = () => {
|
|||||||
<!-- label-placement="top">-->
|
<!-- label-placement="top">-->
|
||||||
<!-- </n-form>-->
|
<!-- </n-form>-->
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- Proxy pane -->
|
||||||
|
<n-tab-pane :tab="$t('dialogue.connection.proxy.title')" display-directive="show:lazy" name="proxy">
|
||||||
|
<n-radio-group v-model:value="generalForm.proxy.type" name="radiogroup">
|
||||||
|
<n-space size="large" vertical>
|
||||||
|
<n-radio :label="$t('dialogue.connection.proxy.type_none')" :value="0" />
|
||||||
|
<n-radio :label="$t('dialogue.connection.proxy.type_system')" :value="1" />
|
||||||
|
<n-radio :label="$t('dialogue.connection.proxy.type_custom')" :value="2" />
|
||||||
|
<n-form
|
||||||
|
:disabled="generalForm.proxy.type !== 2"
|
||||||
|
:model="generalForm.proxy"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-placement="top">
|
||||||
|
<n-grid :x-gap="10">
|
||||||
|
<n-form-item-gi :show-label="false" :span="24" path="addr" required>
|
||||||
|
<n-input-group>
|
||||||
|
<n-select
|
||||||
|
v-model:value="generalForm.proxy.schema"
|
||||||
|
:consistent-menu-width="false"
|
||||||
|
:options="[
|
||||||
|
{ value: 'http', label: 'HTTP' },
|
||||||
|
{ value: 'https', label: 'HTTPS' },
|
||||||
|
{ value: 'socks5', label: 'SOCKS5' },
|
||||||
|
{ value: 'socks5h', label: 'SOCKS5H' },
|
||||||
|
]"
|
||||||
|
default-value="http"
|
||||||
|
style="max-width: 100px" />
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.proxy.addr"
|
||||||
|
:placeholder="$t('dialogue.connection.proxy.host')" />
|
||||||
|
<n-text style="width: 40px; text-align: center">:</n-text>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="generalForm.proxy.port"
|
||||||
|
:max="65535"
|
||||||
|
:min="0"
|
||||||
|
:show-button="false"
|
||||||
|
style="width: 200px" />
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :show-label="false" :span="24" path="auth">
|
||||||
|
<n-checkbox v-model:checked="generalForm.proxy.auth" size="medium">
|
||||||
|
{{ $t('dialogue.connection.proxy.auth') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('dialogue.connection.usr')" :span="12" path="username">
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.proxy.username"
|
||||||
|
:disabled="!!!generalForm.proxy.auth"
|
||||||
|
:placeholder="$t('dialogue.connection.proxy.usr_tip')" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('dialogue.connection.pwd')" :span="12" path="password">
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.proxy.password"
|
||||||
|
:disabled="!!!generalForm.proxy.auth"
|
||||||
|
:placeholder="$t('dialogue.connection.proxy.pwd_tip')"
|
||||||
|
show-password-on="click"
|
||||||
|
type="password" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
|
|
||||||
<!-- test result alert-->
|
<!-- test result alert-->
|
||||||
@ -685,6 +825,9 @@ const onClose = () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item n-dialog__action">
|
<div class="flex-item n-dialog__action">
|
||||||
|
<n-button :disabled="closingConnection" :focusable="false" @click="pasteFromClipboard">
|
||||||
|
{{ $t('dialogue.connection.parse_url_clipboard') }}
|
||||||
|
</n-button>
|
||||||
<n-button :disabled="closingConnection" :focusable="false" @click="onClose">
|
<n-button :disabled="closingConnection" :focusable="false" @click="onClose">
|
||||||
{{ $t('common.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
207
frontend/src/components/dialogs/DecoderDialog.vue
Normal file
207
frontend/src/components/dialogs/DecoderDialog.vue
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
<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"
|
||||||
|
: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')"
|
||||||
|
close-on-esc
|
||||||
|
preset="dialog"
|
||||||
|
transform-origin="center"
|
||||||
|
@esc="onClose"
|
||||||
|
@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>
|
||||||
|
@use '@/styles/content';
|
||||||
|
</style>
|
@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, reactive, ref, watchEffect } from 'vue'
|
import { computed, nextTick, reactive, ref, watchEffect } from 'vue'
|
||||||
import useDialog from 'stores/dialog'
|
import useDialog from 'stores/dialog'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { isEmpty, map, size } from 'lodash'
|
import { isEmpty, map, size } from 'lodash'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||||
@ -14,6 +13,7 @@ const deleteForm = reactive({
|
|||||||
loadingAffected: false,
|
loadingAffected: false,
|
||||||
affectedKeys: [],
|
affectedKeys: [],
|
||||||
async: true,
|
async: true,
|
||||||
|
direct: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const dialogStore = useDialog()
|
const dialogStore = useDialog()
|
||||||
@ -68,7 +68,6 @@ const keyLines = computed(() => {
|
|||||||
return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k))
|
return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k))
|
||||||
})
|
})
|
||||||
|
|
||||||
const i18n = useI18n()
|
|
||||||
const onConfirmDelete = async () => {
|
const onConfirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
deleting.value = true
|
deleting.value = true
|
||||||
@ -84,6 +83,21 @@ const onConfirmDelete = async () => {
|
|||||||
dialogStore.closeDeleteKeyDialog()
|
dialogStore.closeDeleteKeyDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onConfirmDirectDelete = async () => {
|
||||||
|
try {
|
||||||
|
deleting.value = true
|
||||||
|
const { server, db, key } = deleteForm
|
||||||
|
await nextTick()
|
||||||
|
browserStore.deleteByPattern(server, db, key).catch((e) => {})
|
||||||
|
} catch (e) {
|
||||||
|
$message.error(e.message)
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
deleting.value = false
|
||||||
|
}
|
||||||
|
dialogStore.closeDeleteKeyDialog()
|
||||||
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
dialogStore.closeDeleteKeyDialog()
|
dialogStore.closeDeleteKeyDialog()
|
||||||
}
|
}
|
||||||
@ -93,12 +107,13 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.deleteKeyDialogVisible"
|
v-model:show="dialogStore.deleteKeyDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('interface.batch_delete_key')"
|
:title="$t('interface.batch_delete_key')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center">
|
transform-origin="center"
|
||||||
|
@esc="onClose">
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-grid :x-gap="10">
|
<n-grid :x-gap="10">
|
||||||
@ -115,11 +130,9 @@ const onClose = () => {
|
|||||||
required>
|
required>
|
||||||
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<!-- <n-form-item :label="$t('dialogue.key.async_delete')" required>-->
|
<n-checkbox v-if="!deleteForm.showAffected" v-model:checked="deleteForm.direct">
|
||||||
<!-- <n-checkbox v-model:checked="deleteForm.async">-->
|
{{ $t('dialogue.key.direct_delete') }}
|
||||||
<!-- {{ $t('dialogue.key.async_delete_title') }}-->
|
</n-checkbox>
|
||||||
<!-- </n-checkbox>-->
|
|
||||||
<!-- </n-form-item>-->
|
|
||||||
<n-card
|
<n-card
|
||||||
v-if="deleteForm.showAffected"
|
v-if="deleteForm.showAffected"
|
||||||
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
||||||
@ -141,22 +154,32 @@ const onClose = () => {
|
|||||||
<div class="flex-item n-dialog__action">
|
<div class="flex-item n-dialog__action">
|
||||||
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="!deleteForm.showAffected"
|
v-if="deleteForm.direct"
|
||||||
:focusable="false"
|
:focusable="false"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="scanAffectedKey">
|
@click="onConfirmDirectDelete">
|
||||||
{{ $t('dialogue.key.show_affected_key') }}
|
{{ $t('dialogue.key.confirm_delete') }}
|
||||||
</n-button>
|
|
||||||
<n-button
|
|
||||||
v-else
|
|
||||||
:disabled="isEmpty(deleteForm.affectedKeys)"
|
|
||||||
:focusable="false"
|
|
||||||
:loading="loading"
|
|
||||||
type="primary"
|
|
||||||
@click="onConfirmDelete">
|
|
||||||
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<template v-else>
|
||||||
|
<n-button
|
||||||
|
v-if="!deleteForm.showAffected"
|
||||||
|
:focusable="false"
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
@click="scanAffectedKey">
|
||||||
|
{{ $t('dialogue.key.show_affected_key') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
v-else
|
||||||
|
:disabled="isEmpty(deleteForm.affectedKeys)"
|
||||||
|
:focusable="false"
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
@click="onConfirmDelete">
|
||||||
|
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watchEffect } from 'vue'
|
import { computed, reactive, ref, watchEffect } from 'vue'
|
||||||
import useDialog from 'stores/dialog'
|
import useDialog from 'stores/dialog'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
import FileSaveInput from '@/components/common/FileSaveInput.vue'
|
import FileSaveInput from '@/components/common/FileSaveInput.vue'
|
||||||
import { isEmpty, map, size } from 'lodash'
|
import { isEmpty, map, size } from 'lodash'
|
||||||
@ -40,7 +39,6 @@ const exportEnable = computed(() => {
|
|||||||
return !isEmpty(exportKeyForm.keys) && !isEmpty(exportKeyForm.file)
|
return !isEmpty(exportKeyForm.keys) && !isEmpty(exportKeyForm.file)
|
||||||
})
|
})
|
||||||
|
|
||||||
const i18n = useI18n()
|
|
||||||
const onConfirmExport = async () => {
|
const onConfirmExport = async () => {
|
||||||
try {
|
try {
|
||||||
exporting.value = true
|
exporting.value = true
|
||||||
@ -64,12 +62,13 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.exportKeyDialogVisible"
|
v-model:show="dialogStore.exportKeyDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('dialogue.export.name')"
|
:title="$t('dialogue.export.name')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center">
|
transform-origin="center"
|
||||||
|
@esc="onClose">
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<n-form :model="exportKeyForm" :show-require-mark="false" label-placement="top">
|
<n-form :model="exportKeyForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-grid :x-gap="10">
|
<n-grid :x-gap="10">
|
||||||
|
@ -54,12 +54,13 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.flushDBDialogVisible"
|
v-model:show="dialogStore.flushDBDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('interface.flush_db')"
|
:title="$t('interface.flush_db')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center">
|
transform-origin="center"
|
||||||
|
@esc="onClose">
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<n-form :model="flushForm" :show-require-mark="false" label-placement="top">
|
<n-form :model="flushForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-form-item :label="$t('dialogue.key.server')">
|
<n-form-item :label="$t('dialogue.key.server')">
|
||||||
|
@ -89,7 +89,6 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.groupDialogVisible"
|
v-model:show="dialogStore.groupDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:negative-button-props="{ size: 'medium' }"
|
:negative-button-props="{ size: 'medium' }"
|
||||||
:negative-text="$t('common.cancel')"
|
:negative-text="$t('common.cancel')"
|
||||||
@ -97,8 +96,10 @@ const onClose = () => {
|
|||||||
:positive-text="$t('common.confirm')"
|
:positive-text="$t('common.confirm')"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="isRenameMode ? $t('dialogue.group.rename') : $t('dialogue.group.new')"
|
:title="isRenameMode ? $t('dialogue.group.rename') : $t('dialogue.group.new')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center"
|
transform-origin="center"
|
||||||
|
@esc="onClose"
|
||||||
@positive-click="onConfirm"
|
@positive-click="onConfirm"
|
||||||
@negative-click="onClose">
|
@negative-click="onClose">
|
||||||
<n-form
|
<n-form
|
||||||
|
@ -103,12 +103,13 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.importKeyDialogVisible"
|
v-model:show="dialogStore.importKeyDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('dialogue.import.name')"
|
:title="$t('dialogue.import.name')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center">
|
transform-origin="center"
|
||||||
|
@esc="onClose">
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<n-form :model="importKeyForm" :show-require-mark="false" label-placement="top">
|
<n-form :model="importKeyForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-grid :x-gap="10">
|
<n-grid :x-gap="10">
|
||||||
|
@ -47,7 +47,6 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.keyFilterDialogVisible"
|
v-model:show="dialogStore.keyFilterDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:negative-button-props="{ size: 'medium' }"
|
:negative-button-props="{ size: 'medium' }"
|
||||||
:negative-text="$t('common.cancel')"
|
:negative-text="$t('common.cancel')"
|
||||||
@ -55,9 +54,11 @@ const onClose = () => {
|
|||||||
:positive-text="$t('common.confirm')"
|
:positive-text="$t('common.confirm')"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('dialogue.filter.set_key_filter')"
|
:title="$t('dialogue.filter.set_key_filter')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
style="width: 450px"
|
style="width: 450px"
|
||||||
transform-origin="center"
|
transform-origin="center"
|
||||||
|
@esc="onClose"
|
||||||
@positive-click="onConfirm"
|
@positive-click="onConfirm"
|
||||||
@negative-click="onClose">
|
@negative-click="onClose">
|
||||||
<n-form
|
<n-form
|
||||||
|
@ -14,6 +14,7 @@ import useTabStore from 'stores/tab.js'
|
|||||||
import NewStreamValue from '@/components/new_value/NewStreamValue.vue'
|
import NewStreamValue from '@/components/new_value/NewStreamValue.vue'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
import Import from '@/components/icons/Import.vue'
|
import Import from '@/components/icons/Import.vue'
|
||||||
|
import NewJsonValue from '@/components/new_value/NewJsonValue.vue'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const newForm = reactive({
|
const newForm = reactive({
|
||||||
@ -54,6 +55,7 @@ const newValueComponent = {
|
|||||||
[types.SET]: NewSetValue,
|
[types.SET]: NewSetValue,
|
||||||
[types.ZSET]: NewZSetValue,
|
[types.ZSET]: NewZSetValue,
|
||||||
[types.STREAM]: NewStreamValue,
|
[types.STREAM]: NewStreamValue,
|
||||||
|
[types.JSON]: NewJsonValue,
|
||||||
}
|
}
|
||||||
const defaultValue = {
|
const defaultValue = {
|
||||||
[types.STRING]: '',
|
[types.STRING]: '',
|
||||||
@ -62,6 +64,7 @@ const defaultValue = {
|
|||||||
[types.SET]: [],
|
[types.SET]: [],
|
||||||
[types.ZSET]: [],
|
[types.ZSET]: [],
|
||||||
[types.STREAM]: [],
|
[types.STREAM]: [],
|
||||||
|
[types.JSON]: '{}',
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogStore = useDialog()
|
const dialogStore = useDialog()
|
||||||
@ -90,7 +93,14 @@ watchEffect(() => {
|
|||||||
const renderTypeLabel = (option) => {
|
const renderTypeLabel = (option) => {
|
||||||
return h(
|
return h(
|
||||||
NSpace,
|
NSpace,
|
||||||
{ align: 'center', inline: true, size: 3 },
|
{
|
||||||
|
align: 'center',
|
||||||
|
inline: true,
|
||||||
|
size: 3,
|
||||||
|
itemStyle: {
|
||||||
|
lineHeight: 'var(--n-blank-height)',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
default: () => [
|
default: () => [
|
||||||
h('div', {
|
h('div', {
|
||||||
@ -144,7 +154,11 @@ const onAdd = async () => {
|
|||||||
value = defaultValue[type]
|
value = defaultValue[type]
|
||||||
}
|
}
|
||||||
// await browserStore.reloadKey({server, db, key: trim(key)})
|
// await browserStore.reloadKey({server, db, key: trim(key)})
|
||||||
const { success, msg, nodeKey } = await browserStore.setKey({
|
const {
|
||||||
|
success,
|
||||||
|
msg,
|
||||||
|
nodeKey = '',
|
||||||
|
} = await browserStore.setKey({
|
||||||
server,
|
server,
|
||||||
db,
|
db,
|
||||||
key: trim(key),
|
key: trim(key),
|
||||||
@ -155,12 +169,15 @@ const onAdd = async () => {
|
|||||||
if (success) {
|
if (success) {
|
||||||
// select current key
|
// select current key
|
||||||
await nextTick()
|
await nextTick()
|
||||||
tabStore.setSelectedKeys(server, nodeKey)
|
const selectedDB = browserStore.getSelectedDB(server)
|
||||||
browserStore.reloadKey({ server, db, key })
|
if (selectedDB === db) {
|
||||||
|
tabStore.setSelectedKeys(server, nodeKey)
|
||||||
|
browserStore.reloadKey({ server, db, key })
|
||||||
|
}
|
||||||
|
dialogStore.closeNewKeyDialog()
|
||||||
} else if (!isEmpty(msg)) {
|
} else if (!isEmpty(msg)) {
|
||||||
$message.error(msg)
|
$message.error(msg)
|
||||||
}
|
}
|
||||||
dialogStore.closeNewKeyDialog()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -181,13 +198,14 @@ const onImport = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.newKeyDialogVisible"
|
v-model:show="dialogStore.newKeyDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('dialogue.key.new')"
|
:title="$t('dialogue.key.new')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
style="width: 600px"
|
style="width: 600px"
|
||||||
transform-origin="center">
|
transform-origin="center"
|
||||||
|
@esc="onClose">
|
||||||
<n-scrollbar ref="scrollRef" style="max-height: 500px">
|
<n-scrollbar ref="scrollRef" style="max-height: 500px">
|
||||||
<n-form
|
<n-form
|
||||||
ref="newFormRef"
|
ref="newFormRef"
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
<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 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'
|
||||||
|
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
|
||||||
|
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
|
|
||||||
@ -17,11 +26,13 @@ const loading = ref(false)
|
|||||||
const initPreferences = async () => {
|
const initPreferences = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
tab.value = 'general'
|
tab.value = dialogStore.preferencesTag || 'general'
|
||||||
await prefStore.loadPreferences()
|
await prefStore.loadPreferences()
|
||||||
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
|
||||||
@ -42,6 +53,125 @@ 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 onOpenPrivacy = () => {
|
||||||
|
let helpUrl = ''
|
||||||
|
switch (prefStore.currentLanguage) {
|
||||||
|
case 'zh':
|
||||||
|
helpUrl = 'https://redis.tinycraft.cc/zh/guide/privacy.html'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
helpUrl = 'https://redis.tinycraft.cc/guide/privacy.html'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
BrowserOpenURL(helpUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDecodeHelp = () => {
|
||||||
|
let helpUrl = ''
|
||||||
|
switch (prefStore.currentLanguage) {
|
||||||
|
case 'zh':
|
||||||
|
helpUrl = 'https://redis.tinycraft.cc/zh/guide/custom-decoder.html'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
helpUrl = 'https://redis.tinycraft.cc/guide/custom-decoder.html'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
BrowserOpenURL(helpUrl)
|
||||||
|
}
|
||||||
|
|
||||||
const onSavePreferences = async () => {
|
const onSavePreferences = async () => {
|
||||||
const success = await prefStore.savePreferences()
|
const success = await prefStore.savePreferences()
|
||||||
if (success) {
|
if (success) {
|
||||||
@ -62,16 +192,24 @@ const onClose = () => {
|
|||||||
v-model:show="dialogStore.preferencesDialogVisible"
|
v-model:show="dialogStore.preferencesDialogVisible"
|
||||||
:auto-focus="false"
|
:auto-focus="false"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('preferences.name')"
|
:title="$t('preferences.name')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
style="width: 500px"
|
style="width: 640px"
|
||||||
transform-origin="center">
|
transform-origin="center"
|
||||||
|
@esc="onClose">
|
||||||
<!-- 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">
|
||||||
|
<!-- general pane -->
|
||||||
<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">
|
||||||
@ -92,17 +230,42 @@ const onClose = () => {
|
|||||||
:render-label="({ label, value }) => (value === 'auto' ? $t(label) : label)"
|
:render-label="({ label, value }) => (value === 'auto' ? $t(label) : label)"
|
||||||
filterable />
|
filterable />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('preferences.general.font')" :span="12" required>
|
<n-form-item-gi :span="24" required>
|
||||||
|
<template #label>
|
||||||
|
{{ $t('preferences.general.font') }}
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="Help" />
|
||||||
|
</template>
|
||||||
|
<div class="text-block">
|
||||||
|
{{ $t('preferences.font_tip') }}
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="prefStore.general.font"
|
v-model:value="prefStore.general.fontFamily"
|
||||||
:options="prefStore.fontOption"
|
:options="prefStore.fontOption"
|
||||||
|
:placeholder="$t('preferences.general.font_tip')"
|
||||||
:render-label="({ label, value }) => (value === '' ? $t(label) : label)"
|
:render-label="({ label, value }) => (value === '' ? $t(label) : label)"
|
||||||
filterable />
|
filterable
|
||||||
|
multiple
|
||||||
|
tag />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('preferences.general.font_size')" :span="12">
|
<n-form-item-gi :label="$t('preferences.general.font_size')" :span="24">
|
||||||
<n-input-number v-model:value="prefStore.general.fontSize" :max="65535" :min="1" />
|
<n-input-number v-model:value="prefStore.general.fontSize" :max="65535" :min="1" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('preferences.general.scan_size')" :span="12">
|
<n-form-item-gi :span="12">
|
||||||
|
<template #label>
|
||||||
|
{{ $t('preferences.general.scan_size') }}
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="Help" />
|
||||||
|
</template>
|
||||||
|
<div class="text-block">
|
||||||
|
{{ $t('preferences.general.scan_size_tip') }}
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
<n-input-number
|
<n-input-number
|
||||||
v-model:value="prefStore.general.scanSize"
|
v-model:value="prefStore.general.scanSize"
|
||||||
:min="1"
|
:min="1"
|
||||||
@ -115,36 +278,49 @@ const onClose = () => {
|
|||||||
:options="keyOptions"
|
:options="keyOptions"
|
||||||
:render-label="({ label }) => $t(label)" />
|
:render-label="({ label }) => $t(label)" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('preferences.general.proxy')" :span="24">
|
|
||||||
<n-space>
|
|
||||||
<n-checkbox v-model:checked="prefStore.general.useSysProxy">
|
|
||||||
{{ $t('preferences.general.use_system_proxy') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-checkbox v-model:checked="prefStore.general.useSysProxyHttp">
|
|
||||||
{{ $t('preferences.general.use_system_proxy_http') }}
|
|
||||||
</n-checkbox>
|
|
||||||
</n-space>
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi :label="$t('preferences.general.update')" :span="24">
|
<n-form-item-gi :label="$t('preferences.general.update')" :span="24">
|
||||||
<n-checkbox v-model:checked="prefStore.general.checkUpdate">
|
<n-checkbox v-model:checked="prefStore.general.checkUpdate">
|
||||||
{{ $t('preferences.general.auto_check_update') }}
|
{{ $t('preferences.general.auto_check_update') }}
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('preferences.general.privacy')" :span="24">
|
||||||
|
<n-checkbox v-model:checked="prefStore.general.allowTrack">
|
||||||
|
{{ $t('preferences.general.allow_track') }}
|
||||||
|
<n-button style="text-decoration: underline" text type="primary" @click="onOpenPrivacy">
|
||||||
|
{{ $t('preferences.general.privacy') }}
|
||||||
|
</n-button>
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- editor pane -->
|
||||||
<n-tab-pane :tab="$t('preferences.editor.name')" display-directive="show" name="editor">
|
<n-tab-pane :tab="$t('preferences.editor.name')" display-directive="show" name="editor">
|
||||||
<n-form :disabled="loading" :model="prefStore.editor" :show-require-mark="false" label-placement="top">
|
<n-form :disabled="loading" :model="prefStore.editor" :show-require-mark="false" label-placement="top">
|
||||||
<n-grid :x-gap="10">
|
<n-grid :x-gap="10">
|
||||||
<n-form-item-gi :label="$t('preferences.general.font')" :span="12" required>
|
<n-form-item-gi :span="24" required>
|
||||||
|
<template #label>
|
||||||
|
{{ $t('preferences.general.font') }}
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="Help" />
|
||||||
|
</template>
|
||||||
|
<div class="text-block">
|
||||||
|
{{ $t('preferences.font_tip') }}
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="prefStore.editor.font"
|
v-model:value="prefStore.editor.fontFamily"
|
||||||
:options="prefStore.fontOption"
|
:options="prefStore.fontOption"
|
||||||
|
:placeholder="$t('preferences.general.font_tip')"
|
||||||
:render-label="({ label, value }) => value || $t(label)"
|
:render-label="({ label, value }) => value || $t(label)"
|
||||||
filterable />
|
filterable
|
||||||
|
multiple
|
||||||
|
tag />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('preferences.general.font_size')" :span="12">
|
<n-form-item-gi :label="$t('preferences.general.font_size')" :span="24">
|
||||||
<n-input-number v-model:value="prefStore.editor.fontSize" :max="65535" :min="1" />
|
<n-input-number v-model:value="prefStore.editor.fontSize" :max="65535" :min="1" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :show-feedback="false" :show-label="false" :span="24">
|
<n-form-item-gi :show-feedback="false" :show-label="false" :span="24">
|
||||||
@ -157,9 +333,86 @@ const onClose = () => {
|
|||||||
{{ $t('preferences.editor.show_folding') }}
|
{{ $t('preferences.editor.show_folding') }}
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :show-feedback="false" :show-label="false" :span="24">
|
||||||
|
<n-checkbox v-model:checked="prefStore.editor.dropText">
|
||||||
|
{{ $t('preferences.editor.drop_text') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :show-feedback="false" :show-label="false" :span="24">
|
||||||
|
<n-checkbox v-model:checked="prefStore.editor.links">
|
||||||
|
{{ $t('preferences.editor.links') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- cli pane -->
|
||||||
|
<n-tab-pane :tab="$t('preferences.cli.name')" display-directive="show" name="cli">
|
||||||
|
<n-form :disabled="loading" :model="prefStore.cli" :show-require-mark="false" label-placement="top">
|
||||||
|
<n-grid :x-gap="10">
|
||||||
|
<n-form-item-gi :span="24" required>
|
||||||
|
<template #label>
|
||||||
|
{{ $t('preferences.general.font') }}
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="Help" />
|
||||||
|
</template>
|
||||||
|
<div class="text-block">
|
||||||
|
{{ $t('preferences.font_tip') }}
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
<n-select
|
||||||
|
v-model:value="prefStore.cli.fontFamily"
|
||||||
|
:options="prefStore.fontOption"
|
||||||
|
:placeholder="$t('preferences.general.font_tip')"
|
||||||
|
:render-label="({ label, value }) => value || $t(label)"
|
||||||
|
filterable
|
||||||
|
multiple
|
||||||
|
tag />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('preferences.general.font_size')" :span="24">
|
||||||
|
<n-input-number v-model:value="prefStore.cli.fontSize" :max="65535" :min="1" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('preferences.cli.cursor_style')" :span="24">
|
||||||
|
<n-radio-group v-model:value="prefStore.cli.cursorStyle" name="theme" size="medium">
|
||||||
|
<n-radio-button
|
||||||
|
v-for="opt in prefStore.cliCursorStyleOption"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value">
|
||||||
|
{{ $t(opt.label) }}
|
||||||
|
</n-radio-button>
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item-gi>
|
||||||
|
</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 vertical>
|
||||||
|
<n-space justify="space-between">
|
||||||
|
<n-button @click="dialogStore.openDecoderDialog()">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="AddLink" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('preferences.decoder.new') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="openDecodeHelp">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Help" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('preferences.decoder.help') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<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> -->
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ const onClose = () => {
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dialogStore.renameDialogVisible"
|
v-model:show="dialogStore.renameDialogVisible"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:close-on-esc="false"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:negative-button-props="{ focusable: false, size: 'medium' }"
|
:negative-button-props="{ focusable: false, size: 'medium' }"
|
||||||
:negative-text="$t('common.cancel')"
|
:negative-text="$t('common.cancel')"
|
||||||
@ -61,8 +60,10 @@ const onClose = () => {
|
|||||||
:positive-text="$t('common.confirm')"
|
:positive-text="$t('common.confirm')"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
:title="$t('interface.rename_key')"
|
:title="$t('interface.rename_key')"
|
||||||
|
close-on-esc
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center"
|
transform-origin="center"
|
||||||
|
@esc="onClose"
|
||||||
@positive-click="onRename"
|
@positive-click="onRename"
|
||||||
@negative-click="onClose">
|
@negative-click="onClose">
|
||||||
<n-form
|
<n-form
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user