mirror of
https://github.com/tiny-craft/tiny-rdm.git
synced 2025-04-27 18:28:05 +08:00
Compare commits
No commits in common. "main" and "v1.0.3" have entirely different histories.
.github
.gitignoreREADME.mdREADME_ja.mdREADME_zh.mdapp.gobackend
consts
services
browser_service.gocli_service.goconnection_service.goga_service.gomonitor_service.gopreferences_service.gopubsub_service.gostorage_service.gosystem_service.go
storage
types
utils
convert
base64_convert.gobinary_convert.gobrotli_convert.gocmd_convert.gocommon.gocommon_nonwindows.gocommon_windows.goconvert.godeflate_convert.gogzip_convert.gohex_convert.gojson_convert.golz4_convert.gomsgpack_convert.gophp_convert.gopickle_convert.gounicode_json_convert.goxml_convert.goyaml_convert.gozstd_convert.go
map
proxy
redis
slice
string
build
darwin
dmg
linux/tiny-rdm_0.0.0_amd64/DEBIAN
docs/images
frontend
README.mdindex.htmlpackage-lock.jsonpackage.jsonpackage.json.md5
src
App.vueAppContent.vue
assets/images
components
43
.github/CONTRIBUTING.md
vendored
43
.github/CONTRIBUTING.md
vendored
@ -1,43 +0,0 @@
|
|||||||
## Tiny RDM Contribute Guide
|
|
||||||
|
|
||||||
### Multi-language Contributions
|
|
||||||
|
|
||||||
#### Adding New Language
|
|
||||||
|
|
||||||
1. New file: Add a new JSON file in the [frontend/src/langs](../frontend/src/langs/), with the file naming format is "
|
|
||||||
{language}-{region}.json", e.g. English is "en-us.json", simplified Chinese is "zh-cn.json". Highly recommended to duplicate the [en-us.json](../frontend/src/langs/en-us.json) file and rename it.
|
|
||||||
2. Fill content: Refer to [en-us.json](../frontend/src/langs/en-us.json), or duplicate the file and modify the language content.
|
|
||||||
3. Update codes: Edit[frontend/src/langs/index.js](.../frontend/src/langs/index.js), import the new language data inside.
|
|
||||||
```javascript
|
|
||||||
import en from './en-us'
|
|
||||||
// import your new localize file 'zh-cn' here
|
|
||||||
import zh from './zh-cn'
|
|
||||||
|
|
||||||
export const lang = {
|
|
||||||
en,
|
|
||||||
// export new language data 'zh' here
|
|
||||||
zh,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
4. Submit review once there are no issues with the translation context in the application. (learn how to submit)
|
|
||||||
|
|
||||||
### Code Submission`(To be completed)`
|
|
||||||
|
|
||||||
#### Pull Request Title
|
|
||||||
The format of PR's title like "<type>: <description>"
|
|
||||||
- type: PR type
|
|
||||||
- description: PR description
|
|
||||||
|
|
||||||
PR type list below:
|
|
||||||
|
|
||||||
| type | description |
|
|
||||||
|----------|----------------------------------------------------|
|
|
||||||
| revert | Revert a commit |
|
|
||||||
| feat | New features |
|
|
||||||
| perf | Performance improvements |
|
|
||||||
| fix | Fix any bugs |
|
|
||||||
| style | Style updates |
|
|
||||||
| docs | Document updates |
|
|
||||||
| refactor | Code refactors |
|
|
||||||
| chore | Some chores |
|
|
||||||
| ci | Automation process configuration or script updates |
|
|
42
.github/CONTRIBUTING_zh.md
vendored
42
.github/CONTRIBUTING_zh.md
vendored
@ -1,42 +0,0 @@
|
|||||||
## Tiny RDM 代码贡献指南
|
|
||||||
|
|
||||||
### 多国语言贡献
|
|
||||||
|
|
||||||
#### 增加新的语言
|
|
||||||
1. 创建文件:在[frontend/src/langs](../frontend/src/langs/)目录下新增语言配置JSON文件,文件名格式为“{语言}-{地区}.json”,如英文为“en-us.json”,简体中文为“zh-cn.json”,建议直接复制[en-us.json](../frontend/src/langs/en-us.json)文件进行改名。
|
|
||||||
2. 填充内容:参考[en-us.json](../frontend/src/langs/en-us.json),或者直接克隆一份文件,对语言部分内容进行修改。
|
|
||||||
3. 代码修改:在[frontend/src/langs/index.js](.../frontend/src/langs/index.js)文件内导入新增的语言数据
|
|
||||||
```javascript
|
|
||||||
import en from './en-us'
|
|
||||||
// import your new localize file 'zh-cn' here
|
|
||||||
import zh from './zh-cn'
|
|
||||||
|
|
||||||
export const lang = {
|
|
||||||
en,
|
|
||||||
// export new language data 'zh' here
|
|
||||||
zh,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
4. 检查应用中对应翻译语境无问题后,可提交审核([查看如何提交](#pull_request))
|
|
||||||
|
|
||||||
### 代码提交`(待完善)`
|
|
||||||
|
|
||||||
#### PR提交规范
|
|
||||||
PR提交格式为“<type>: <description>”
|
|
||||||
- type: 提交类型
|
|
||||||
- description: 提交内容描述
|
|
||||||
|
|
||||||
其中提交类型如下:
|
|
||||||
|
|
||||||
| 提交类型 | 类型描述 |
|
|
||||||
|----------|--------------|
|
|
||||||
| revert | 回退某个commit提交 |
|
|
||||||
| feat | 新功能/新特性 |
|
|
||||||
| perf | 功能、体验等方面的优化 |
|
|
||||||
| fix | 修复问题 |
|
|
||||||
| style | 样式相关修改 |
|
|
||||||
| docs | 文档更新 |
|
|
||||||
| refactor | 代码重构 |
|
|
||||||
| chore | 杂项修改 |
|
|
||||||
| ci | 自动化流程配置或脚本修改 |
|
|
||||||
|
|
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: '[BUG]'
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**Tiny RDM Version**
|
|
||||||
What version of Tiny RDM are you using?
|
|
||||||
|
|
||||||
**OS Version**
|
|
||||||
Which OS and version you launch? (Mac/Windows/Linux)
|
|
||||||
|
|
||||||
**Redis Version**
|
|
||||||
Which version of Redis are you using?
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
Steps to Reproduce:
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: '[FEATURE]'
|
|
||||||
---
|
|
134
.github/workflows/release-linux-webkit2-41.yaml
vendored
134
.github/workflows/release-linux-webkit2-41.yaml
vendored
@ -1,134 +0,0 @@
|
|||||||
name: Release Linux App
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Version tag'
|
|
||||||
required: true
|
|
||||||
default: '1.0.0'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release Linux App
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- linux/amd64
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Normalise platform tag
|
|
||||||
id: normalise_platform
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g')
|
|
||||||
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
|
|
||||||
id: normalise_version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event.release.tag_name }}" == "" ]; then
|
|
||||||
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
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: stable
|
|
||||||
|
|
||||||
- name: Install wails
|
|
||||||
shell: bash
|
|
||||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
|
||||||
|
|
||||||
- name: Install Ubuntu prerequisites
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libfuse-dev libfuse2
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
|
|
||||||
- name: Build frontend assets
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
npm install -g npm@9
|
|
||||||
jq '.info.productVersion = "${{ steps.normalise_version.outputs.version }}"' wails.json > tmp.json
|
|
||||||
mv tmp.json wails.json
|
|
||||||
cd frontend
|
|
||||||
jq '.version = "${{ steps.normalise_version.outputs.version }}"' package.json > tmp.json
|
|
||||||
mv tmp.json package.json
|
|
||||||
npm install
|
|
||||||
|
|
||||||
- name: Build wails app for Linux
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
|
||||||
-ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.LINUX_GA_SECRET }}" \
|
|
||||||
-tags webkit2_41 \
|
|
||||||
-o tiny-rdm
|
|
||||||
|
|
||||||
- name: Setup control template
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
content=$(cat build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control)
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.Name}}/$(jq -r '.name' wails.json)/g")
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.Info.ProductVersion}}/$(jq -r '.info.productVersion' wails.json)/g")
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.Author.Name}}/$(jq -r '.author.name' wails.json)/g")
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.Author.Email}}/$(jq -r '.author.email' wails.json)/g")
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.libwebkit2gtk.PackageName}}/libwebkit2gtk-4.1-0/g")
|
|
||||||
echo $content
|
|
||||||
echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control
|
|
||||||
|
|
||||||
- name: Setup app template
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
content=$(cat build/linux/tiny-rdm_0.0.0_amd64/usr/share/applications/tiny-rdm.desktop)
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.Info.ProductName}}/$(jq -r '.info.productName' wails.json)/g")
|
|
||||||
content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
|
|
||||||
echo $content
|
|
||||||
echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/usr/share/applications/tiny-rdm.desktop
|
|
||||||
|
|
||||||
- name: Package up deb file
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mv build/bin/tiny-rdm build/linux/tiny-rdm_0.0.0_amd64/usr/local/bin/
|
|
||||||
cd build/linux
|
|
||||||
mv tiny-rdm_0.0.0_amd64 "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"
|
|
||||||
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"
|
|
||||||
|
|
||||||
- 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 }}_webkit2_41.deb"
|
|
||||||
|
|
||||||
- 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 }}_webkit2_41.deb
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
91
.github/workflows/release-linux.yaml
vendored
91
.github/workflows/release-linux.yaml
vendored
@ -3,12 +3,6 @@ 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:
|
||||||
@ -18,7 +12,6 @@ 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
|
||||||
@ -30,26 +23,12 @@ 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: |
|
||||||
if [ "${{ github.event.release.tag_name }}" == "" ]; then
|
|
||||||
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')
|
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@ -64,12 +43,12 @@ 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 libfuse-dev libfuse2
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
|
||||||
- name: Build frontend assets
|
- name: Build frontend assets
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -84,10 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build wails app for Linux
|
- name: Build wails app for Linux
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} -ldflags "-X main.version=${{ github.event.release.tag_name }}" -o tiny-rdm
|
||||||
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
|
||||||
-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
|
|
||||||
|
|
||||||
- name: Setup control template
|
- name: Setup control template
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -98,7 +74,6 @@ jobs:
|
|||||||
content=$(echo "$content" | sed -e "s/{{.Author.Name}}/$(jq -r '.author.name' wails.json)/g")
|
content=$(echo "$content" | sed -e "s/{{.Author.Name}}/$(jq -r '.author.name' wails.json)/g")
|
||||||
content=$(echo "$content" | sed -e "s/{{.Author.Email}}/$(jq -r '.author.email' wails.json)/g")
|
content=$(echo "$content" | sed -e "s/{{.Author.Email}}/$(jq -r '.author.email' wails.json)/g")
|
||||||
content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
|
content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
|
||||||
content=$(echo "$content" | sed -e "s/{{.libwebkit2gtk.PackageName}}/libwebkit2gtk-4.0-37/g")
|
|
||||||
echo $content
|
echo $content
|
||||||
echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control
|
echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control
|
||||||
|
|
||||||
@ -120,57 +95,11 @@ 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: Package up appimage file
|
|
||||||
run: |
|
|
||||||
curl https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-${{ steps.normalise_platform_arch.outputs.arch }}.AppImage \
|
|
||||||
-o linuxdeploy \
|
|
||||||
-L
|
|
||||||
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
|
- name: Upload release asset
|
||||||
uses: softprops/action-gh-release@v1
|
shell: bash
|
||||||
with:
|
working-directory: ./build/linux/
|
||||||
tag_name: v${{ steps.normalise_version.outputs.version }}
|
run: |
|
||||||
files: |
|
filepath="tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64.deb"
|
||||||
./build/linux/tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.deb
|
filename="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
|
upload_url="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${{ github.event.release.id }}/assets"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary @$filepath "$upload_url?name=$filename"
|
||||||
|
78
.github/workflows/release-macos.yaml
vendored
78
.github/workflows/release-macos.yaml
vendored
@ -3,12 +3,6 @@ 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:
|
||||||
@ -28,20 +22,15 @@ jobs:
|
|||||||
id: normalise_platform
|
id: normalise_platform
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g' -e 's/darwin/mac/g' -e 's/amd64/intel/g')
|
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g' -e 's/darwin/mac/g')
|
||||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Normalise version tag
|
- name: Normalise version tag
|
||||||
id: normalise_version
|
id: normalise_version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event.release.tag_name }}" == "" ]; then
|
|
||||||
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')
|
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@ -65,7 +54,7 @@ jobs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
|
||||||
- name: Build frontend assets
|
- name: Build frontend assets
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -80,9 +69,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build wails app for macOS
|
- name: Build wails app for macOS
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} -ldflags "-X main.version=${{ github.event.release.tag_name }}"
|
||||||
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
|
||||||
-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
|
||||||
@ -91,42 +78,35 @@ jobs:
|
|||||||
# AC_USERNAME: ${{ secrets.AC_USERNAME }}
|
# AC_USERNAME: ${{ secrets.AC_USERNAME }}
|
||||||
# AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
|
# AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
|
||||||
|
|
||||||
- name: Checkout create-image
|
- name: Compress macOS app
|
||||||
uses: actions/checkout@v2
|
shell: bash
|
||||||
with:
|
working-directory: ./build/bin
|
||||||
repository: create-dmg/create-dmg
|
run: |
|
||||||
path: ./build/create-dmg
|
mv tinyrdm.app "Tiny RDM.app"
|
||||||
ref: master
|
zip -r TinyRDM-${{ steps.normalise_platform.outputs.tag }}.zip "Tiny RDM.app"
|
||||||
|
|
||||||
|
- name: Upload release asset (ZIP Package)
|
||||||
|
shell: bash
|
||||||
|
working-directory: ./build/bin/
|
||||||
|
run: |
|
||||||
|
filepath="TinyRDM-${{ steps.normalise_platform.outputs.tag }}.zip"
|
||||||
|
filename="TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip"
|
||||||
|
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 macOS DMG
|
- name: Build macOS DMG
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: ./build
|
|
||||||
run: |
|
|
||||||
mv bin/tinyrdm.app "bin/Tiny RDM.app"
|
|
||||||
./create-dmg/create-dmg \
|
|
||||||
--no-internet-enable \
|
|
||||||
--volname "Tiny RDM" \
|
|
||||||
--volicon "bin/Tiny RDM.app/Contents/Resources/iconfile.icns" \
|
|
||||||
--background "dmg/background.tiff" \
|
|
||||||
--text-size 12 \
|
|
||||||
--window-pos 400 400 \
|
|
||||||
--window-size 660 450 \
|
|
||||||
--icon-size 80 \
|
|
||||||
--icon "Tiny RDM.app" 180 180 \
|
|
||||||
--hide-extension "Tiny RDM.app" \
|
|
||||||
--app-drop-link 480 180 \
|
|
||||||
--add-file "Repair" "dmg/fix-app" 230 290 \
|
|
||||||
--add-file "损坏修复" "dmg/fix-app_zh" 430 290 \
|
|
||||||
"bin/TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg" \
|
|
||||||
"bin"
|
|
||||||
|
|
||||||
- name: Rename dmg
|
|
||||||
working-directory: ./build/bin
|
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"
|
run: |
|
||||||
|
rm TinyRDM-${{ steps.normalise_platform.outputs.tag }}.zip
|
||||||
|
ln -s /Applications Applications
|
||||||
|
hdiutil create -volname "Tiny RDM" -srcfolder . -ov -format UDBZ TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg
|
||||||
|
|
||||||
- name: Upload release asset (DMG Package)
|
- name: Upload release asset (DMG Package)
|
||||||
uses: softprops/action-gh-release@v1
|
shell: bash
|
||||||
with:
|
working-directory: ./build/bin/
|
||||||
tag_name: v${{ steps.normalise_version.outputs.version }}
|
run: |
|
||||||
files: ./build/bin/TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.dmg
|
filepath="TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
filename="TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.dmg"
|
||||||
|
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"
|
||||||
|
68
.github/workflows/release-windows.yaml
vendored
68
.github/workflows/release-windows.yaml
vendored
@ -3,12 +3,6 @@ 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:
|
||||||
@ -18,7 +12,6 @@ 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
|
||||||
@ -27,27 +20,15 @@ jobs:
|
|||||||
id: normalise_platform
|
id: normalise_platform
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g' -e 's/amd64/x64/g')
|
tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/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: |
|
||||||
if [ "${{ github.event.release.tag_name }}" == "" ]; then
|
|
||||||
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')
|
version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@ -57,7 +38,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
|
args: install nsis jq upx
|
||||||
|
|
||||||
- name: Install wails
|
- name: Install wails
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -66,7 +47,7 @@ jobs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
|
||||||
- name: Build frontend assets
|
- name: Build frontend assets
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -81,31 +62,26 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Windows portable app
|
- name: Build Windows portable app
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} -upx -webview2 embed -ldflags "-X main.version=${{ github.event.release.tag_name }}"
|
||||||
CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} \
|
|
||||||
-webview2 embed \
|
|
||||||
-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" "TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip"
|
run: Compress-Archive "Tiny RDM.exe" tiny-rdm.zip
|
||||||
|
|
||||||
- name: Upload release asset (Portable)
|
- name: Upload release asset (Portable)
|
||||||
uses: softprops/action-gh-release@v1
|
shell: bash
|
||||||
with:
|
working-directory: ./build/bin
|
||||||
tag_name: v${{ steps.normalise_version.outputs.version }}
|
run: |
|
||||||
files: ./build/bin/TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip
|
filepath="tiny-rdm.zip"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
filename="TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip"
|
||||||
|
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: |
|
run: CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} -nsis -upx -webview2 embed -ldflags "-X main.version=${{ github.event.release.tag_name }}"
|
||||||
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"
|
||||||
@ -113,15 +89,13 @@ 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://timestamp.digicert.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' TinyRDM-${{ steps.normalise_platform_name.outputs.pname }}-installer.exe
|
& '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
|
||||||
|
|
||||||
- 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)
|
||||||
uses: softprops/action-gh-release@v1
|
shell: bash
|
||||||
with:
|
working-directory: ./build/bin/
|
||||||
tag_name: v${{ steps.normalise_version.outputs.version }}
|
run: |
|
||||||
files: ./build/bin/TinyRDM_Setup_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.exe
|
filepath="TinyRDM-amd64-installer.exe"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
filename="TinyRDM_Setup_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.exe"
|
||||||
|
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"
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,8 +2,6 @@ build/bin
|
|||||||
node_modules
|
node_modules
|
||||||
frontend/dist
|
frontend/dist
|
||||||
frontend/wailsjs
|
frontend/wailsjs
|
||||||
frontend/package.json.md5
|
|
||||||
design/
|
design/
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
test
|
|
||||||
|
84
README.md
84
README.md
@ -1,60 +1,32 @@
|
|||||||
|
<h4 align="right"><strong>English</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a></h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<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>English</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">
|
|
||||||
简体中文</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)
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/releases)
|
[](https://github.com/tiny-craft/tiny-rdm/releases)
|
||||||

|
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/stargazers)
|
[](https://github.com/tiny-craft/tiny-rdm/stargazers)
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/fork)
|
[](https://github.com/tiny-craft/tiny-rdm/fork)
|
||||||
[](https://discord.gg/VTFbBMGjWh)
|
|
||||||
[](https://twitter.com/Lykin53448)
|
|
||||||
|
|
||||||
<strong>Tiny RDM is a modern lightweight cross-platform Redis desktop manager available for Mac, Windows, and
|
<strong>Tiny RDM is a modern lightweight cross-platform Redis desktop manager available for Mac, Windows, and Linux.</strong>
|
||||||
Linux.</strong>
|
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
## Feature
|
## Feature
|
||||||
|
|
||||||
* Super lightweight, built on Webview2, without embedded browsers (Thanks
|
* Built on Webview, no embedded browsers (Thanks to [Wails](https://github.com/wailsapp/wails)).
|
||||||
to [Wails](https://github.com/wailsapp/wails)).
|
* More elegant UI and visualized layout (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 (Click here to contribute and support more languages).
|
||||||
* Better connection management: supports SSH Tunnel/SSL/Sentinel Mode/Cluster Mode/HTTP proxy/SOCKS5 proxy.
|
* Convenient data viewing and editing.
|
||||||
* Visualize key value operations, CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets, and Streams.
|
* More features under continuous development...
|
||||||
* Support multiple data viewing format and decode/decompression methods.
|
|
||||||
* Use SCAN for segmented loading, making it easy to list millions of keys.
|
|
||||||
* Logs list for command operation history.
|
|
||||||
* Provides command-line mode.
|
|
||||||
* Provides slow logs list.
|
|
||||||
* Segmented loading and querying for List/Hash/Set/Sorted Set.
|
|
||||||
* Provide value decode/decompression for List/Hash/Set/Sorted Set.
|
|
||||||
* Integrate with Monaco Editor
|
|
||||||
* Support real-time commands monitoring.
|
|
||||||
* Support import/export data.
|
|
||||||
* Support publish/subscribe.
|
|
||||||
* Support import/export connection profile.
|
|
||||||
* Custom data encoder and decoder for value display ([Here are the instructions](https://redis.tinycraft.cc/guide/custom-decoder/)).
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
We publish binaries for Mac, Windows, and Linux.
|
||||||
Available to download for free from [here](https://github.com/tiny-craft/tiny-rdm/releases).
|
Available to download for free from [here](https://github.com/tiny-craft/tiny-rdm/releases).
|
||||||
|
|
||||||
> If you can't open it after installation on macOS, exec the following command then reopen:
|
> If you can't open it after installation on macOS, exec the following command then reopen:
|
||||||
@ -62,54 +34,32 @@ Available to download for free from [here](https://github.com/tiny-craft/tiny-rd
|
|||||||
> sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
|
> sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
## Build Guidelines
|
## Build
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
* Go >= 1.21
|
||||||
* Go (latest version)
|
|
||||||
* 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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pull the Code
|
### Clone 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
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
### Compile and run
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Run
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wails dev
|
wails dev
|
||||||
```
|
```
|
||||||
## About
|
|
||||||
|
|
||||||
### Wechat Official Account
|
## License
|
||||||
|
|
||||||
<img src="docs/images/wechat_official.png" alt="wechat" width="360" />
|
Tiny RDM is licensed under [GNU General Public](/LICENSE) license.
|
||||||
|
|
||||||
### Sponsor
|
|
||||||
|
|
||||||
If this project helpful for you, feel free to buy me a cup of coffee ☕️.
|
|
||||||
|
|
||||||
* Wechat Sponsor
|
|
||||||
|
|
||||||
<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />
|
|
||||||
|
111
README_ja.md
111
README_ja.md
@ -1,111 +0,0 @@
|
|||||||
<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" />
|
|
84
README_zh.md
84
README_zh.md
@ -1,56 +1,32 @@
|
|||||||
|
<h4 align="right"><strong><a href="/">English</a></strong> | 简体中文</h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<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> | 简体中文 | <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)
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/releases)
|
[](https://github.com/tiny-craft/tiny-rdm/releases)
|
||||||

|
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/stargazers)
|
[](https://github.com/tiny-craft/tiny-rdm/stargazers)
|
||||||
[](https://github.com/tiny-craft/tiny-rdm/fork)
|
[](https://github.com/tiny-craft/tiny-rdm/fork)
|
||||||
|
|
||||||
<strong>一个现代化轻量级的跨平台Redis桌面客户端,支持Mac、Windows和Linux</strong>
|
<strong>一个现代化轻量级的跨平台Redis桌面客户端,支持Mac、Windows和Linux</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<picture>
|

|
||||||
<source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_zh.png">
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="screenshots/light_zh.png">
|
|
||||||
<img alt="screenshot" src="screenshots/dark_zh.png">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_zh2.png">
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="screenshots/light_zh2.png">
|
|
||||||
<img alt="screenshot" src="screenshots/dark_zh2.png">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
* 极度轻量,基于Webview2,无内嵌浏览器(感谢[Wails](https://github.com/wailsapp/wails))
|
* 基于Webview,无内嵌浏览器(感谢[Wails](https://github.com/wailsapp/wails))
|
||||||
* 界面精美易用,提供浅色/深色主题(感谢[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))
|
* 多国语言支持(点我贡献和完善多国语言支持)
|
||||||
* 更好用的连接管理:支持SSH隧道/SSL/哨兵模式/集群模式/HTTP代理/SOCKS5代理
|
* 便捷的数据查看和编辑修改
|
||||||
* 可视化键值操作,增删查改一应俱全
|
* 更多功能持续开发中…
|
||||||
* 支持多种数据查看格式以及转码/解压方式
|
|
||||||
* 采用SCAN分段加载,可轻松处理数百万键列表
|
|
||||||
* 操作命令执行日志展示
|
|
||||||
* 提供命令行操作
|
|
||||||
* 提供慢日志展示
|
|
||||||
* List/Hash/Set/Sorted Set的分段加载和查询
|
|
||||||
* List/Hash/Set/Sorted Set值的转码显示
|
|
||||||
* 内置高级编辑器Monaco Editor
|
|
||||||
* 支持命令实时监控
|
|
||||||
* 支持导入/导出数据
|
|
||||||
* 支持发布订阅
|
|
||||||
* 支持导入/导出连接配置
|
|
||||||
* 自定义数据展示编码/解码([这是操作指引](https://redis.tinycraft.cc/zh/guide/custom-decoder/))
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
提供Mac、Windows和Linux安装包,可[免费下载](https://github.com/tiny-craft/tiny-rdm/releases)。
|
提供Mac、Windows和Linux下载安装,可[免费下载](https://github.com/tiny-craft/tiny-rdm/releases)。
|
||||||
|
|
||||||
> 如果在macOS上安装后无法打开,报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可:
|
> 如果在macOS上安装后无法打开,报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可:
|
||||||
> ``` shell
|
> ``` shell
|
||||||
@ -58,68 +34,36 @@
|
|||||||
> ```
|
> ```
|
||||||
|
|
||||||
## 构建项目
|
## 构建项目
|
||||||
|
|
||||||
### 运行环境要求
|
### 运行环境要求
|
||||||
|
* Go >= 1.21
|
||||||
* Go(最新版本)
|
|
||||||
* Node.js >= 16
|
* Node.js >= 16
|
||||||
* NPM >= 9
|
* NPM >= 9
|
||||||
|
|
||||||
### 安装wails
|
### 安装wails
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### 拉取代码
|
### 拉取代码
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
|
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
|
||||||
```
|
```
|
||||||
|
|
||||||
### 构建前端代码
|
### 构建前端代码
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --prefix ./frontend
|
npm install --prefix ./frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
或者
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 编译运行开发版本
|
### 编译运行开发版本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wails dev
|
wails dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 关于
|
## 关于
|
||||||
|
此APP由我个人开发,也作为本人第一个开源项目的尝试,由于精力有限,可能会存在BUG或者使用体验上的问题,欢迎提交issue和PR。
|
||||||
|
同时本人也在探索开源代码、独立开发和盈利性商业应用之间的平衡关系,欢迎有共同意向的小伙伴加入群聊探讨和交换想法。
|
||||||
|
* QQ群:831077639
|
||||||
|
|
||||||
如果你也同为独立开发者(团队),喜欢开源,或者对Tiny Craft的相关产品感兴趣,可以关注微信公众号或者加入QQ群,探讨心得,反馈意见,交个朋友。
|
## 开源许可
|
||||||
|
|
||||||
### 微信公众号(用户交流微信群)
|
Tiny RDM 基于 [GNU General Public](/LICENSE) 开源协议.
|
||||||
|
|
||||||
我会不定期更新一些关于独立开发的思考和感悟,以及独立产品的介绍,欢迎扫码关注~👏
|
|
||||||
|
|
||||||
<img src="docs/images/wechat_official.png" alt="wechat" width="360" />
|
|
||||||
|
|
||||||
### B站官方账号
|
|
||||||
|
|
||||||
<img src="docs/images/bilibili_official.png" alt="bilibili" width="360" />
|
|
||||||
|
|
||||||
### 独立开发互助QQ群
|
|
||||||
|
|
||||||
```
|
|
||||||
831077639
|
|
||||||
```
|
|
||||||
|
|
||||||
### 赞助
|
|
||||||
|
|
||||||
该项目完全为爱发电,如果对你有所帮助,可以请作者喝杯咖啡 ☕️
|
|
||||||
|
|
||||||
* 微信赞赏
|
|
||||||
|
|
||||||
<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />
|
|
||||||
|
27
app.go
Normal file
27
app.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App struct
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp creates a new App application struct
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startup is called when the app starts. The context is saved
|
||||||
|
// so we can call the runtime methods
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greet returns a greeting for the given name
|
||||||
|
func (a *App) Greet(name string) string {
|
||||||
|
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||||
|
}
|
@ -4,7 +4,3 @@ const DEFAULT_FONT_SIZE = 14
|
|||||||
const DEFAULT_ASIDE_WIDTH = 300
|
const DEFAULT_ASIDE_WIDTH = 300
|
||||||
const DEFAULT_WINDOW_WIDTH = 1024
|
const DEFAULT_WINDOW_WIDTH = 1024
|
||||||
const DEFAULT_WINDOW_HEIGHT = 768
|
const DEFAULT_WINDOW_HEIGHT = 768
|
||||||
const MIN_WINDOW_WIDTH = 960
|
|
||||||
const MIN_WINDOW_HEIGHT = 640
|
|
||||||
const DEFAULT_LOAD_SIZE = 10000
|
|
||||||
const DEFAULT_SCAN_SIZE = 3000
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,161 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"tinyrdm/backend/types"
|
|
||||||
sliceutil "tinyrdm/backend/utils/slice"
|
|
||||||
strutil "tinyrdm/backend/utils/string"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cliService struct {
|
|
||||||
ctx context.Context
|
|
||||||
ctxCancel context.CancelFunc
|
|
||||||
mutex sync.Mutex
|
|
||||||
clients map[string]redis.UniversalClient
|
|
||||||
selectedDB map[string]int
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliOutput struct {
|
|
||||||
Content []string `json:"content"` // output content
|
|
||||||
Prompt string `json:"prompt,omitempty"` // new line prompt, empty if not ready to input
|
|
||||||
}
|
|
||||||
|
|
||||||
var cli *cliService
|
|
||||||
var onceCli sync.Once
|
|
||||||
|
|
||||||
func Cli() *cliService {
|
|
||||||
if cli == nil {
|
|
||||||
onceCli.Do(func() {
|
|
||||||
cli = &cliService{
|
|
||||||
clients: map[string]redis.UniversalClient{},
|
|
||||||
selectedDB: map[string]int{},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return cli
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliService) runCommand(server, data string) {
|
|
||||||
if cmds := strutil.SplitCmd(data); len(cmds) > 0 && len(cmds[0]) > 0 {
|
|
||||||
if client, err := c.getRedisClient(server); err == nil {
|
|
||||||
args := sliceutil.Map(cmds, func(i int) any {
|
|
||||||
return cmds[i]
|
|
||||||
})
|
|
||||||
if result, err := client.Do(c.ctx, args...).Result(); err == nil || errors.Is(err, redis.Nil) {
|
|
||||||
if strings.ToLower(cmds[0]) == "select" {
|
|
||||||
// switch database
|
|
||||||
if db, ok := strutil.AnyToInt(cmds[1]); ok {
|
|
||||||
c.selectedDB[server] = db
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.echo(server, strutil.AnyToString(result, "", 0), true)
|
|
||||||
} else {
|
|
||||||
c.echoError(server, err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.echoReady(server)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliService) echo(server, data string, newLineReady bool) {
|
|
||||||
output := cliOutput{
|
|
||||||
Content: strings.Split(data, "\n"),
|
|
||||||
}
|
|
||||||
if newLineReady {
|
|
||||||
output.Prompt = fmt.Sprintf("%s:db%d> ", server, c.selectedDB[server])
|
|
||||||
}
|
|
||||||
runtime.EventsEmit(c.ctx, "cmd:output:"+server, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliService) echoReady(server string) {
|
|
||||||
c.echo(server, "", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliService) echoError(server, data string) {
|
|
||||||
c.echo(server, "\x1b[31m"+data+"\x1b[0m", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliService) getRedisClient(server string) (redis.UniversalClient, error) {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
client, ok := c.clients[server]
|
|
||||||
if !ok {
|
|
||||||
var err error
|
|
||||||
conf := Connection().getConnection(server)
|
|
||||||
if conf == nil {
|
|
||||||
return nil, fmt.Errorf("no connection profile named: %s", server)
|
|
||||||
}
|
|
||||||
if client, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.clients[server] = client
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliService) Start(ctx context.Context) {
|
|
||||||
c.ctx, c.ctxCancel = context.WithCancel(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartCli start a cli session
|
|
||||||
func (c *cliService) StartCli(server string, db int) (resp types.JSResp) {
|
|
||||||
client, err := c.getRedisClient(server)
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client.Do(c.ctx, "select", db)
|
|
||||||
c.selectedDB[server] = db
|
|
||||||
|
|
||||||
// monitor input
|
|
||||||
runtime.EventsOn(c.ctx, "cmd:input:"+server, func(data ...interface{}) {
|
|
||||||
if len(data) > 0 {
|
|
||||||
if str, ok := data[0].(string); ok {
|
|
||||||
c.runCommand(server, str)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.echoReady(server)
|
|
||||||
})
|
|
||||||
|
|
||||||
// echo prefix
|
|
||||||
c.echoReady(server)
|
|
||||||
resp.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseCli close cli session
|
|
||||||
func (c *cliService) CloseCli(server string) (resp types.JSResp) {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
if client, ok := c.clients[server]; ok {
|
|
||||||
client.Close()
|
|
||||||
delete(c.clients, server)
|
|
||||||
delete(c.selectedDB, server)
|
|
||||||
}
|
|
||||||
runtime.EventsOff(c.ctx, "cmd:input:"+server)
|
|
||||||
resp.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseAll close all cli sessions
|
|
||||||
func (c *cliService) CloseAll() {
|
|
||||||
if c.ctxCancel != nil {
|
|
||||||
c.ctxCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
for server := range c.clients {
|
|
||||||
c.CloseCli(server)
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,110 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"tinyrdm/backend/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// google analytics service
|
|
||||||
type gaService struct {
|
|
||||||
measurementID string
|
|
||||||
secretKey string
|
|
||||||
clientID string
|
|
||||||
}
|
|
||||||
|
|
||||||
type GaDataItem struct {
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
Events []GaEventItem `json:"events"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GaEventItem struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Params map[string]any `json:"params"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var ga *gaService
|
|
||||||
var onceGA sync.Once
|
|
||||||
|
|
||||||
func GA() *gaService {
|
|
||||||
if ga == nil {
|
|
||||||
onceGA.Do(func() {
|
|
||||||
// get or create an unique user id
|
|
||||||
st := storage.NewLocalStore("device.txt")
|
|
||||||
uidByte, err := st.Load()
|
|
||||||
if err != nil {
|
|
||||||
uidByte = []byte(strings.ReplaceAll(uuid.NewString(), "-", ""))
|
|
||||||
st.Store(uidByte)
|
|
||||||
}
|
|
||||||
|
|
||||||
ga = &gaService{
|
|
||||||
clientID: string(uidByte),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ga
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *gaService) SetSecretKey(measurementID, secretKey string) {
|
|
||||||
a.measurementID = measurementID
|
|
||||||
a.secretKey = secretKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *gaService) isValid() bool {
|
|
||||||
return len(a.measurementID) > 0 && len(a.secretKey) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *gaService) sendEvent(events ...GaEventItem) error {
|
|
||||||
body, err := json.Marshal(GaDataItem{
|
|
||||||
ClientID: a.clientID,
|
|
||||||
Events: events,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
//url := "https://www.google-analytics.com/debug/mp/collect"
|
|
||||||
url := "https://www.google-analytics.com/mp/collect"
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
q := req.URL.Query()
|
|
||||||
q.Add("measurement_id", a.measurementID)
|
|
||||||
q.Add("api_secret", a.secretKey)
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
//if dump, err := httputil.DumpResponse(response, true); err == nil {
|
|
||||||
// log.Println(string(dump))
|
|
||||||
//}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Startup sends application startup event
|
|
||||||
func (a *gaService) Startup(version string) {
|
|
||||||
if !a.isValid() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go a.sendEvent(GaEventItem{
|
|
||||||
Name: "startup",
|
|
||||||
Params: map[string]any{
|
|
||||||
"os": runtime.GOOS,
|
|
||||||
"arch": runtime.GOARCH,
|
|
||||||
"version": version,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,198 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
"tinyrdm/backend/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type monitorItem struct {
|
|
||||||
client *redis.Client
|
|
||||||
cmd *redis.MonitorCmd
|
|
||||||
mutex sync.Mutex
|
|
||||||
ch chan string
|
|
||||||
closeCh chan struct{}
|
|
||||||
eventName string
|
|
||||||
}
|
|
||||||
|
|
||||||
type monitorService struct {
|
|
||||||
ctx context.Context
|
|
||||||
ctxCancel context.CancelFunc
|
|
||||||
mutex sync.Mutex
|
|
||||||
items map[string]*monitorItem
|
|
||||||
}
|
|
||||||
|
|
||||||
var monitor *monitorService
|
|
||||||
var onceMonitor sync.Once
|
|
||||||
|
|
||||||
func Monitor() *monitorService {
|
|
||||||
if monitor == nil {
|
|
||||||
onceMonitor.Do(func() {
|
|
||||||
monitor = &monitorService{
|
|
||||||
items: map[string]*monitorItem{},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return monitor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *monitorService) getItem(server string) (*monitorItem, error) {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
item, ok := c.items[server]
|
|
||||||
if !ok {
|
|
||||||
var err error
|
|
||||||
conf := Connection().getConnection(server)
|
|
||||||
if conf == nil {
|
|
||||||
return nil, fmt.Errorf("no connection profile named: %s", server)
|
|
||||||
}
|
|
||||||
var uniClient redis.UniversalClient
|
|
||||||
if uniClient, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var client *redis.Client
|
|
||||||
if client, ok = uniClient.(*redis.Client); !ok {
|
|
||||||
return nil, errors.New("create redis client fail")
|
|
||||||
}
|
|
||||||
item = &monitorItem{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
c.items[server] = item
|
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *monitorService) Start(ctx context.Context) {
|
|
||||||
c.ctx, c.ctxCancel = context.WithCancel(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartMonitor start a monitor by server name
|
|
||||||
func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
|
|
||||||
item, err := c.getItem(server)
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
item.ch = make(chan string)
|
|
||||||
item.closeCh = make(chan struct{})
|
|
||||||
item.eventName = "monitor:" + strconv.Itoa(int(time.Now().Unix()))
|
|
||||||
item.cmd = item.client.Monitor(c.ctx, item.ch)
|
|
||||||
item.cmd.Start()
|
|
||||||
|
|
||||||
go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.cmd, item.eventName)
|
|
||||||
resp.Success = true
|
|
||||||
resp.Data = struct {
|
|
||||||
EventName string `json:"eventName"`
|
|
||||||
}{
|
|
||||||
EventName: item.eventName,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, cmd *redis.MonitorCmd, eventName string) {
|
|
||||||
cache := make([]string, 0, 1000)
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case data := <-ch:
|
|
||||||
if data != "OK" {
|
|
||||||
go func() {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
cache = append(cache, data)
|
|
||||||
if len(cache) > 300 {
|
|
||||||
runtime.EventsEmit(c.ctx, eventName, cache)
|
|
||||||
cache = cache[:0:cap(cache)]
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-ticker.C:
|
|
||||||
func() {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
if len(cache) > 0 {
|
|
||||||
runtime.EventsEmit(c.ctx, eventName, cache)
|
|
||||||
cache = cache[:0:cap(cache)]
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case <-closeCh:
|
|
||||||
// monitor stopped
|
|
||||||
cmd.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopMonitor stop monitor by server name
|
|
||||||
func (c *monitorService) StopMonitor(server string) (resp types.JSResp) {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
item, ok := c.items[server]
|
|
||||||
if !ok || item.cmd == nil {
|
|
||||||
resp.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//close(item.ch)
|
|
||||||
item.client.Close()
|
|
||||||
close(item.closeCh)
|
|
||||||
delete(c.items, server)
|
|
||||||
resp.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopAll stop all monitor
|
|
||||||
func (c *monitorService) StopAll() {
|
|
||||||
if c.ctxCancel != nil {
|
|
||||||
c.ctxCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
for server := range c.items {
|
|
||||||
c.StopMonitor(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *monitorService) ExportLog(logs []string) (resp types.JSResp) {
|
|
||||||
filepath, err := runtime.SaveFileDialog(c.ctx, runtime.SaveDialogOptions{
|
|
||||||
ShowHiddenFiles: false,
|
|
||||||
DefaultFilename: fmt.Sprintf("monitor_log_%s.txt", time.Now().Format("20060102150405")),
|
|
||||||
Filters: []runtime.FileFilter{
|
|
||||||
{Pattern: "*.txt"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Create(filepath)
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
writer := bufio.NewWriter(file)
|
|
||||||
for _, line := range logs {
|
|
||||||
_, _ = writer.WriteString(line + "\n")
|
|
||||||
}
|
|
||||||
writer.Flush()
|
|
||||||
|
|
||||||
resp.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,12 +1,9 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/adrg/sysfont"
|
"github.com/adrg/sysfont"
|
||||||
runtime2 "github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -14,8 +11,6 @@ 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 {
|
||||||
@ -51,7 +46,6 @@ func (p *preferencesService) SetPreferences(pf types.Preferences) (resp types.JS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.UpdateEnv()
|
|
||||||
resp.Success = true
|
resp.Success = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -102,25 +96,6 @@ 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
|
||||||
@ -137,24 +112,16 @@ func (p *preferencesService) GetAppVersion() (resp types.JSResp) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *preferencesService) SaveWindowSize(width, height int, maximised bool) {
|
func (p *preferencesService) SaveWindowSize(width, height int) {
|
||||||
if maximised {
|
|
||||||
// do not update window size if maximised state
|
|
||||||
p.UpdatePreferences(map[string]any{
|
|
||||||
"behavior.windowMaximised": true,
|
|
||||||
})
|
|
||||||
} else if width >= consts.MIN_WINDOW_WIDTH && height >= consts.MIN_WINDOW_HEIGHT {
|
|
||||||
p.UpdatePreferences(map[string]any{
|
p.UpdatePreferences(map[string]any{
|
||||||
"behavior.windowWidth": width,
|
"behavior.windowWidth": width,
|
||||||
"behavior.windowHeight": height,
|
"behavior.windowHeight": height,
|
||||||
"behavior.windowMaximised": false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (p *preferencesService) GetWindowSize() (width, height int, maximised bool) {
|
func (p *preferencesService) GetWindowSize() (width, height int) {
|
||||||
data := p.pref.GetPreferences()
|
data := p.pref.GetPreferences()
|
||||||
width, height, maximised = data.Behavior.WindowWidth, data.Behavior.WindowHeight, data.Behavior.WindowMaximised
|
width, height = data.Behavior.WindowWidth, data.Behavior.WindowHeight
|
||||||
if width <= 0 {
|
if width <= 0 {
|
||||||
width = consts.DEFAULT_WINDOW_WIDTH
|
width = consts.DEFAULT_WINDOW_WIDTH
|
||||||
}
|
}
|
||||||
@ -164,89 +131,22 @@ func (p *preferencesService) GetWindowSize() (width, height int, maximised bool)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *preferencesService) GetWindowPosition(ctx context.Context) (x, y int) {
|
type latestRelease struct {
|
||||||
data := p.pref.GetPreferences()
|
|
||||||
x, y = data.Behavior.WindowPosX, data.Behavior.WindowPosY
|
|
||||||
width, height := data.Behavior.WindowWidth, data.Behavior.WindowHeight
|
|
||||||
var screenWidth, screenHeight int
|
|
||||||
if screens, err := runtime2.ScreenGetAll(ctx); err == nil {
|
|
||||||
for _, screen := range screens {
|
|
||||||
if screen.IsCurrent {
|
|
||||||
screenWidth, screenHeight = screen.Size.Width, screen.Size.Height
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if screenWidth <= 0 || screenHeight <= 0 {
|
|
||||||
screenWidth, screenHeight = consts.DEFAULT_WINDOW_WIDTH, consts.DEFAULT_WINDOW_HEIGHT
|
|
||||||
}
|
|
||||||
if x <= 0 || x+width > screenWidth || y <= 0 || y+height > screenHeight {
|
|
||||||
// out of screen, reset to center
|
|
||||||
x, y = (screenWidth-width)/2, (screenHeight-height)/2
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *preferencesService) SaveWindowPosition(x, y int) {
|
|
||||||
if x > 0 || y > 0 {
|
|
||||||
p.UpdatePreferences(map[string]any{
|
|
||||||
"behavior.windowPosX": x,
|
|
||||||
"behavior.windowPosY": y,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *preferencesService) GetScanSize() int {
|
|
||||||
data := p.pref.GetPreferences()
|
|
||||||
size := data.General.ScanSize
|
|
||||||
if size <= 0 {
|
|
||||||
size = consts.DEFAULT_SCAN_SIZE
|
|
||||||
}
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *preferencesService) GetDecoder() []convutil.CmdConvert {
|
|
||||||
data := p.pref.GetPreferences()
|
|
||||||
return sliceutil.FilterMap(data.Decoder, func(i int) (convutil.CmdConvert, bool) {
|
|
||||||
//if !data.Decoder[i].Enable {
|
|
||||||
// return convutil.CmdConvert{}, false
|
|
||||||
//}
|
|
||||||
return convutil.CmdConvert{
|
|
||||||
Name: data.Decoder[i].Name,
|
|
||||||
Auto: data.Decoder[i].Auto,
|
|
||||||
DecodePath: data.Decoder[i].DecodePath,
|
|
||||||
DecodeArgs: data.Decoder[i].DecodeArgs,
|
|
||||||
EncodePath: data.Decoder[i].EncodePath,
|
|
||||||
EncodeArgs: data.Decoder[i].EncodeArgs,
|
|
||||||
}, true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type sponsorItem struct {
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Link string `json:"link"`
|
TagName string `json:"tag_name"`
|
||||||
Region []string `json:"region"`
|
Url string `json:"url"`
|
||||||
}
|
HtmlUrl string `json:"html_url"`
|
||||||
|
|
||||||
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 upgradeInfo
|
var respObj latestRelease
|
||||||
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"
|
||||||
@ -257,19 +157,8 @@ func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
|
|||||||
resp.Success = true
|
resp.Success = true
|
||||||
resp.Data = map[string]any{
|
resp.Data = map[string]any{
|
||||||
"version": p.clientVersion,
|
"version": p.clientVersion,
|
||||||
"latest": respObj.Version,
|
"latest": respObj.TagName,
|
||||||
"description": respObj.Description,
|
"page_url": respObj.HtmlUrl,
|
||||||
"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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,190 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
"tinyrdm/backend/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pubsubItem struct {
|
|
||||||
client redis.UniversalClient
|
|
||||||
pubsub *redis.PubSub
|
|
||||||
mutex sync.Mutex
|
|
||||||
closeCh chan struct{}
|
|
||||||
eventName string
|
|
||||||
}
|
|
||||||
|
|
||||||
type subMessage struct {
|
|
||||||
Timestamp int64 `json:"timestamp"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type pubsubService struct {
|
|
||||||
ctx context.Context
|
|
||||||
ctxCancel context.CancelFunc
|
|
||||||
mutex sync.Mutex
|
|
||||||
items map[string]*pubsubItem
|
|
||||||
}
|
|
||||||
|
|
||||||
var pubsub *pubsubService
|
|
||||||
var oncePubsub sync.Once
|
|
||||||
|
|
||||||
func Pubsub() *pubsubService {
|
|
||||||
if pubsub == nil {
|
|
||||||
oncePubsub.Do(func() {
|
|
||||||
pubsub = &pubsubService{
|
|
||||||
items: map[string]*pubsubItem{},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pubsubService) getItem(server string) (*pubsubItem, error) {
|
|
||||||
p.mutex.Lock()
|
|
||||||
defer p.mutex.Unlock()
|
|
||||||
|
|
||||||
item, ok := p.items[server]
|
|
||||||
if !ok {
|
|
||||||
var err error
|
|
||||||
conf := Connection().getConnection(server)
|
|
||||||
if conf == nil {
|
|
||||||
return nil, fmt.Errorf("no connection profile named: %s", server)
|
|
||||||
}
|
|
||||||
var uniClient redis.UniversalClient
|
|
||||||
if uniClient, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item = &pubsubItem{
|
|
||||||
client: uniClient,
|
|
||||||
}
|
|
||||||
p.items[server] = item
|
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pubsubService) Start(ctx context.Context) {
|
|
||||||
p.ctx, p.ctxCancel = context.WithCancel(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish publish message to channel
|
|
||||||
func (p *pubsubService) Publish(server, channel, payload string) (resp types.JSResp) {
|
|
||||||
rdb, err := Browser().getRedisClient(server, -1)
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var received int64
|
|
||||||
received, err = rdb.client.Publish(p.ctx, channel, payload).Result()
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Success = true
|
|
||||||
resp.Data = struct {
|
|
||||||
Received int64 `json:"received"`
|
|
||||||
}{
|
|
||||||
Received: received,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartSubscribe start to subscribe a channel
|
|
||||||
func (p *pubsubService) StartSubscribe(server string) (resp types.JSResp) {
|
|
||||||
item, err := p.getItem(server)
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
item.closeCh = make(chan struct{})
|
|
||||||
item.eventName = "sub:" + strconv.Itoa(int(time.Now().Unix()))
|
|
||||||
item.pubsub = item.client.PSubscribe(p.ctx, "*")
|
|
||||||
|
|
||||||
go p.processSubscribe(&item.mutex, item.pubsub.Channel(), item.closeCh, item.eventName)
|
|
||||||
resp.Success = true
|
|
||||||
resp.Data = struct {
|
|
||||||
EventName string `json:"eventName"`
|
|
||||||
}{
|
|
||||||
EventName: item.eventName,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pubsubService) processSubscribe(mutex *sync.Mutex, ch <-chan *redis.Message, closeCh <-chan struct{}, eventName string) {
|
|
||||||
cache := make([]subMessage, 0, 1000)
|
|
||||||
ticker := time.NewTicker(300 * time.Millisecond)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case data := <-ch:
|
|
||||||
go func() {
|
|
||||||
timestamp := time.Now().UnixMilli()
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
cache = append(cache, subMessage{
|
|
||||||
Timestamp: timestamp,
|
|
||||||
Channel: data.Channel,
|
|
||||||
Message: data.Payload,
|
|
||||||
})
|
|
||||||
if len(cache) > 300 {
|
|
||||||
runtime.EventsEmit(p.ctx, eventName, cache)
|
|
||||||
cache = cache[:0:cap(cache)]
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case <-ticker.C:
|
|
||||||
func() {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
if len(cache) > 0 {
|
|
||||||
runtime.EventsEmit(p.ctx, eventName, cache)
|
|
||||||
cache = cache[:0:cap(cache)]
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case <-closeCh:
|
|
||||||
// subscribe stopped
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopSubscribe stop subscribe by server name
|
|
||||||
func (p *pubsubService) StopSubscribe(server string) (resp types.JSResp) {
|
|
||||||
p.mutex.Lock()
|
|
||||||
defer p.mutex.Unlock()
|
|
||||||
|
|
||||||
item, ok := p.items[server]
|
|
||||||
if !ok || item.pubsub == nil {
|
|
||||||
resp.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//item.pubsub.Unsubscribe(p.ctx, "*")
|
|
||||||
item.pubsub.Close()
|
|
||||||
close(item.closeCh)
|
|
||||||
delete(p.items, server)
|
|
||||||
resp.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopAll stop all subscribe
|
|
||||||
func (p *pubsubService) StopAll() {
|
|
||||||
if p.ctxCancel != nil {
|
|
||||||
p.ctxCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
for server := range p.items {
|
|
||||||
p.StopSubscribe(server)
|
|
||||||
}
|
|
||||||
}
|
|
18
backend/services/storage_service.go
Normal file
18
backend/services/storage_service.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type storageService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var storage *storageService
|
||||||
|
var onceStorage sync.Once
|
||||||
|
|
||||||
|
func Storage() *storageService {
|
||||||
|
if storage == nil {
|
||||||
|
onceStorage.Do(func() {
|
||||||
|
storage = &storageService{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return storage
|
||||||
|
}
|
@ -1,166 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
runtime2 "runtime"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
"tinyrdm/backend/consts"
|
|
||||||
"tinyrdm/backend/types"
|
|
||||||
sliceutil "tinyrdm/backend/utils/slice"
|
|
||||||
)
|
|
||||||
|
|
||||||
type systemService struct {
|
|
||||||
ctx context.Context
|
|
||||||
appVersion string
|
|
||||||
}
|
|
||||||
|
|
||||||
var system *systemService
|
|
||||||
var onceSystem sync.Once
|
|
||||||
|
|
||||||
func System() *systemService {
|
|
||||||
if system == nil {
|
|
||||||
onceSystem.Do(func() {
|
|
||||||
system = &systemService{
|
|
||||||
appVersion: "0.0.0",
|
|
||||||
}
|
|
||||||
go system.loopWindowEvent()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return system
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *systemService) Start(ctx context.Context, version string) {
|
|
||||||
s.ctx = ctx
|
|
||||||
s.appVersion = version
|
|
||||||
|
|
||||||
// maximize the window if screen size is lower than the minimum window size
|
|
||||||
if screen, err := runtime.ScreenGetAll(ctx); err == nil && len(screen) > 0 {
|
|
||||||
for _, sc := range screen {
|
|
||||||
if sc.IsCurrent {
|
|
||||||
if sc.Size.Width < consts.MIN_WINDOW_WIDTH || sc.Size.Height < consts.MIN_WINDOW_HEIGHT {
|
|
||||||
runtime.WindowMaximise(ctx)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
func (s *systemService) SelectFile(title string, extensions []string) (resp types.JSResp) {
|
|
||||||
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
|
|
||||||
return runtime.FileFilter{
|
|
||||||
Pattern: "*." + extensions[i],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
filepath, err := runtime.OpenFileDialog(s.ctx, runtime.OpenDialogOptions{
|
|
||||||
Title: title,
|
|
||||||
ShowHiddenFiles: true,
|
|
||||||
Filters: filters,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.Success = true
|
|
||||||
resp.Data = map[string]any{
|
|
||||||
"path": filepath,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveFile open file dialog to save a file
|
|
||||||
func (s *systemService) SaveFile(title string, defaultName string, extensions []string) (resp types.JSResp) {
|
|
||||||
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
|
|
||||||
return runtime.FileFilter{
|
|
||||||
Pattern: "*." + extensions[i],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
filepath, err := runtime.SaveFileDialog(s.ctx, runtime.SaveDialogOptions{
|
|
||||||
Title: title,
|
|
||||||
ShowHiddenFiles: true,
|
|
||||||
DefaultFilename: defaultName,
|
|
||||||
Filters: filters,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
resp.Msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.Success = true
|
|
||||||
resp.Data = map[string]any{
|
|
||||||
"path": filepath,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *systemService) loopWindowEvent() {
|
|
||||||
var fullscreen, maximised, minimised, normal bool
|
|
||||||
var width, height int
|
|
||||||
var dirty bool
|
|
||||||
for {
|
|
||||||
time.Sleep(300 * time.Millisecond)
|
|
||||||
if s.ctx == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dirty = false
|
|
||||||
if f := runtime.WindowIsFullscreen(s.ctx); f != fullscreen {
|
|
||||||
// full-screen switched
|
|
||||||
fullscreen = f
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if w, h := runtime.WindowGetSize(s.ctx); w != width || h != height {
|
|
||||||
// window size changed
|
|
||||||
width, height = w, h
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if m := runtime.WindowIsMaximised(s.ctx); m != maximised {
|
|
||||||
maximised = m
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if m := runtime.WindowIsMinimised(s.ctx); m != minimised {
|
|
||||||
minimised = m
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := runtime.WindowIsNormal(s.ctx); n != normal {
|
|
||||||
normal = n
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if dirty {
|
|
||||||
runtime.EventsEmit(s.ctx, "window_changed", map[string]any{
|
|
||||||
"fullscreen": fullscreen,
|
|
||||||
"width": width,
|
|
||||||
"height": height,
|
|
||||||
"maximised": maximised,
|
|
||||||
"minimised": minimised,
|
|
||||||
"normal": normal,
|
|
||||||
})
|
|
||||||
|
|
||||||
if !fullscreen && !minimised {
|
|
||||||
// save window size and position
|
|
||||||
Preferences().SaveWindowSize(width, height, maximised)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,10 +3,9 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"slices"
|
|
||||||
"sync"
|
"sync"
|
||||||
"tinyrdm/backend/consts"
|
|
||||||
"tinyrdm/backend/types"
|
"tinyrdm/backend/types"
|
||||||
|
sliceutil "tinyrdm/backend/utils/slice"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnectionsStorage struct {
|
type ConnectionsStorage struct {
|
||||||
@ -27,7 +26,6 @@ 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: "",
|
||||||
@ -36,21 +34,14 @@ func (c *ConnectionsStorage) defaultConnectionItem() types.ConnectionConfig {
|
|||||||
KeySeparator: ":",
|
KeySeparator: ":",
|
||||||
ConnTimeout: 60,
|
ConnTimeout: 60,
|
||||||
ExecTimeout: 60,
|
ExecTimeout: 60,
|
||||||
DBFilterType: "none",
|
|
||||||
DBFilterList: []int{},
|
|
||||||
LoadSize: consts.DEFAULT_LOAD_SIZE,
|
|
||||||
MarkColor: "",
|
MarkColor: "",
|
||||||
RefreshInterval: 5,
|
|
||||||
Sentinel: types.ConnectionSentinel{
|
|
||||||
Master: "mymaster",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectionsStorage) getConnections() (ret types.Connections) {
|
func (c *ConnectionsStorage) getConnections() (ret types.Connections) {
|
||||||
b, err := c.storage.Load()
|
b, err := c.storage.Load()
|
||||||
ret = c.defaultConnections()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ret = c.defaultConnections()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +190,8 @@ func (c *ConnectionsStorage) UpdateConnection(name string, param types.Connectio
|
|||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := retrieve(conn.Connections, name, param); err != nil {
|
err := retrieve(conn.Connections, name, param)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,10 +248,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 := slices.IndexFunc(conns, func(connection types.Connection) bool {
|
idx, ok := sliceutil.Find(conns, func(i int) bool {
|
||||||
return connection.Name == name
|
return conns[i].Name == name
|
||||||
})
|
})
|
||||||
if idx >= 0 {
|
if ok {
|
||||||
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
|
||||||
@ -290,7 +282,7 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
|
|||||||
return c.saveConnections(conns)
|
return c.saveConnections(conns)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateGroup create a new group
|
// CreateGroup create new group
|
||||||
func (c *ConnectionsStorage) CreateGroup(name string) error {
|
func (c *ConnectionsStorage) CreateGroup(name string) error {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
@ -336,7 +328,7 @@ func (c *ConnectionsStorage) RenameGroup(name, newName string) error {
|
|||||||
return c.saveConnections(conns)
|
return c.saveConnections(conns)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteGroup remove specified group, include all connections under it
|
// DeleteGroup remove special group, include all connections under it
|
||||||
func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error {
|
func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"tinyrdm/backend/consts"
|
|
||||||
"tinyrdm/backend/types"
|
"tinyrdm/backend/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,10 +16,8 @@ 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: storage,
|
storage: NewLocalStore("preferences.yaml"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,9 +26,9 @@ func (p *PreferencesStorage) DefaultPreferences() types.Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *PreferencesStorage) getPreferences() (ret types.Preferences) {
|
func (p *PreferencesStorage) getPreferences() (ret types.Preferences) {
|
||||||
ret = p.DefaultPreferences()
|
|
||||||
b, err := p.storage.Load()
|
b, err := p.storage.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ret = p.DefaultPreferences()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,12 +45,6 @@ func (p *PreferencesStorage) GetPreferences() (ret types.Preferences) {
|
|||||||
defer p.mutex.Unlock()
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
ret = p.getPreferences()
|
ret = p.getPreferences()
|
||||||
if ret.General.ScanSize <= 0 {
|
|
||||||
ret.General.ScanSize = consts.DEFAULT_SCAN_SIZE
|
|
||||||
}
|
|
||||||
ret.Behavior.AsideWidth = max(ret.Behavior.AsideWidth, consts.DEFAULT_ASIDE_WIDTH)
|
|
||||||
ret.Behavior.WindowWidth = max(ret.Behavior.WindowWidth, consts.MIN_WINDOW_WIDTH)
|
|
||||||
ret.Behavior.WindowHeight = max(ret.Behavior.WindowHeight, consts.MIN_WINDOW_HEIGHT)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,6 @@ type ConnectionCategory int
|
|||||||
type ConnectionConfig struct {
|
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"`
|
|
||||||
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"`
|
||||||
@ -16,18 +13,8 @@ type ConnectionConfig struct {
|
|||||||
KeySeparator string `json:"keySeparator,omitempty" yaml:"key_separator,omitempty"`
|
KeySeparator string `json:"keySeparator,omitempty" yaml:"key_separator,omitempty"`
|
||||||
ConnTimeout int `json:"connTimeout,omitempty" yaml:"conn_timeout,omitempty"`
|
ConnTimeout int `json:"connTimeout,omitempty" yaml:"conn_timeout,omitempty"`
|
||||||
ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"`
|
ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"`
|
||||||
DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"`
|
|
||||||
DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"`
|
|
||||||
KeyView int `json:"keyView,omitempty" yaml:"key_view,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:"refresh_interval,omitempty"`
|
|
||||||
Alias map[int]string `json:"alias,omitempty" yaml:"alias,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"`
|
|
||||||
Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"`
|
|
||||||
Proxy ConnectionProxy `json:"proxy,omitempty" yaml:"proxy,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
@ -38,51 +25,25 @@ type Connection struct {
|
|||||||
|
|
||||||
type Connections []Connection
|
type Connections []Connection
|
||||||
|
|
||||||
|
type ConnectionGroup struct {
|
||||||
|
GroupName string `json:"groupName" yaml:"group_name"`
|
||||||
|
Connections []Connection `json:"connections" yaml:"connections"`
|
||||||
|
}
|
||||||
|
|
||||||
type ConnectionDB struct {
|
type ConnectionDB struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Alias string `json:"alias,omitempty"`
|
Keys int `json:"keys"`
|
||||||
Index int `json:"index"`
|
|
||||||
MaxKeys int `json:"maxKeys"`
|
|
||||||
Expires int `json:"expires,omitempty"`
|
Expires int `json:"expires,omitempty"`
|
||||||
AvgTTL int `json:"avgTtl,omitempty"`
|
AvgTTL int `json:"avgTtl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionSSL struct {
|
|
||||||
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
|
||||||
KeyFile string `json:"keyFile,omitempty" yaml:"keyfile,omitempty"`
|
|
||||||
CertFile string `json:"certFile,omitempty" yaml:"certfile,omitempty"`
|
|
||||||
CAFile string `json:"caFile,omitempty" yaml:"cafile,omitempty"`
|
|
||||||
AllowInsecure bool `json:"allowInsecure,omitempty" yaml:"allow_insecure,omitempty"`
|
|
||||||
SNI string `json:"sni,omitempty" yaml:"sni,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionSSH struct {
|
type ConnectionSSH struct {
|
||||||
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
Enable bool `json:"enable" yaml:"enable"`
|
||||||
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"`
|
||||||
LoginType string `json:"loginType,omitempty" yaml:"login_type"`
|
LoginType string `json:"loginType" yaml:"login_type"`
|
||||||
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
Username string `json:"username" yaml:"username"`
|
||||||
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||||
PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"`
|
PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"`
|
||||||
Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"`
|
Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionSentinel struct {
|
|
||||||
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
|
||||||
Master string `json:"master,omitempty" yaml:"master,omitempty"`
|
|
||||||
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
|
||||||
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionCluster struct {
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
@ -5,108 +5,3 @@ type JSResp struct {
|
|||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
Data any `json:"data,omitempty"`
|
Data any `json:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeySummaryParam struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
DB int `json:"db"`
|
|
||||||
Key any `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeySummary struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
TTL int64 `json:"ttl,omitempty"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
Length int64 `json:"length,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeyDetailParam struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
DB int `json:"db"`
|
|
||||||
Key any `json:"key"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
Decode string `json:"decode,omitempty"`
|
|
||||||
MatchPattern string `json:"matchPattern,omitempty"`
|
|
||||||
Reset bool `json:"reset"`
|
|
||||||
Full bool `json:"full"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeyDetail struct {
|
|
||||||
Value any `json:"value"`
|
|
||||||
KeyType string `json:"key_type"`
|
|
||||||
Length int64 `json:"length,omitempty"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
Decode string `json:"decode,omitempty"`
|
|
||||||
Match string `json:"match,omitempty"`
|
|
||||||
Reset bool `json:"reset"`
|
|
||||||
End bool `json:"end"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetKeyParam struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
DB int `json:"db"`
|
|
||||||
Key any `json:"key"`
|
|
||||||
KeyType string `json:"keyType"`
|
|
||||||
Value any `json:"value"`
|
|
||||||
TTL int64 `json:"ttl"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
Decode string `json:"decode,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetListParam struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
DB int `json:"db"`
|
|
||||||
Key any `json:"key"`
|
|
||||||
Index int `json:"index"`
|
|
||||||
Value any `json:"value"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
Decode string `json:"decode,omitempty"`
|
|
||||||
RetFormat string `json:"retFormat,omitempty"`
|
|
||||||
RetDecode string `json:"retDecode,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetHashParam struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
DB int `json:"db"`
|
|
||||||
Key any `json:"key"`
|
|
||||||
Field string `json:"field,omitempty"`
|
|
||||||
NewField string `json:"newField,omitempty"`
|
|
||||||
Value any `json:"value"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
Decode string `json:"decode,omitempty"`
|
|
||||||
RetFormat string `json:"retFormat,omitempty"`
|
|
||||||
RetDecode string `json:"retDecode,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetSetParam struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
DB int `json:"db"`
|
|
||||||
Key any `json:"key"`
|
|
||||||
Value any `json:"value"`
|
|
||||||
NewValue any `json:"newValue"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
Decode string `json:"decode,omitempty"`
|
|
||||||
RetFormat string `json:"retFormat,omitempty"`
|
|
||||||
RetDecode string `json:"retDecode,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetZSetParam struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
DB int `json:"db"`
|
|
||||||
Key any `json:"key"`
|
|
||||||
Value any `json:"value"`
|
|
||||||
NewValue any `json:"newValue"`
|
|
||||||
Score float64 `json:"score"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
Decode string `json:"decode,omitempty"`
|
|
||||||
RetFormat string `json:"retFormat,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"`
|
|
||||||
}
|
|
||||||
|
@ -6,8 +6,6 @@ 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 {
|
||||||
@ -21,75 +19,32 @@ func NewPreferences() Preferences {
|
|||||||
Theme: "auto",
|
Theme: "auto",
|
||||||
Language: "auto",
|
Language: "auto",
|
||||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||||
ScanSize: consts.DEFAULT_SCAN_SIZE,
|
|
||||||
KeyIconStyle: 0,
|
|
||||||
CheckUpdate: true,
|
CheckUpdate: true,
|
||||||
AllowTrack: true,
|
|
||||||
},
|
},
|
||||||
Editor: PreferencesEditor{
|
Editor: PreferencesEditor{
|
||||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||||
ShowLineNum: 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"`
|
||||||
WindowMaximised bool `json:"windowMaximised" yaml:"window_maximised"`
|
|
||||||
WindowPosX int `json:"windowPosX" yaml:"window_pos_x"`
|
|
||||||
WindowPosY int `json:"windowPosY" yaml:"window_pos_y"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
|
|
||||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||||
ScanSize int `json:"scanSize" yaml:"scan_size"`
|
|
||||||
KeyIconStyle int `json:"keyIconStyle" yaml:"key_icon_style"`
|
|
||||||
UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"`
|
UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"`
|
||||||
UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"`
|
UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"`
|
||||||
CheckUpdate bool `json:"checkUpdate" yaml:"check_update"`
|
CheckUpdate bool `json:"checkUpdate" yaml:"check_update"`
|
||||||
SkipVersion string `json:"skipVersion" yaml:"skip_version,omitempty"`
|
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"`
|
||||||
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
|
|
||||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||||
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,51 +1,11 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
type ListEntryItem struct {
|
type ZSetItem struct {
|
||||||
Index int `json:"index"`
|
Value string `json:"value"`
|
||||||
Value any `json:"v"`
|
Score float64 `json:"score"`
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListReplaceItem struct {
|
type StreamItem struct {
|
||||||
Index int `json:"index"`
|
|
||||||
Value any `json:"v,omitempty"`
|
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HashEntryItem struct {
|
|
||||||
Key string `json:"k"`
|
|
||||||
Value any `json:"v"`
|
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HashReplaceItem struct {
|
|
||||||
Key any `json:"k"`
|
|
||||||
NewKey any `json:"nk"`
|
|
||||||
Value any `json:"v"`
|
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetEntryItem struct {
|
|
||||||
Value any `json:"v"`
|
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ZSetEntryItem struct {
|
|
||||||
Score float64 `json:"s"`
|
|
||||||
ScoreStr string `json:"ss,omitempty"`
|
|
||||||
Value any `json:"v"`
|
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ZSetReplaceItem struct {
|
|
||||||
Score float64 `json:"s"`
|
|
||||||
Value string `json:"v"`
|
|
||||||
NewValue string `json:"nv"`
|
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamEntryItem struct {
|
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Value map[string]any `json:"v"`
|
Value map[string]any `json:"value"`
|
||||||
DisplayValue string `json:"dv,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,11 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
const FORMAT_RAW = "Raw"
|
const PLAIN_TEXT = "Plain Text"
|
||||||
const FORMAT_JSON = "JSON"
|
const JSON = "JSON"
|
||||||
const FORMAT_UNICODE_JSON = "Unicode JSON"
|
const BASE64_TEXT = "Base64 Text"
|
||||||
const FORMAT_YAML = "YAML"
|
const BASE64_JSON = "Base64 JSON"
|
||||||
const FORMAT_XML = "XML"
|
const HEX = "Hex"
|
||||||
const FORMAT_HEX = "Hex"
|
const BINARY = "Binary"
|
||||||
const FORMAT_BINARY = "Binary"
|
const GZIP = "GZip"
|
||||||
|
const GZIP_JSON = "GZip JSON"
|
||||||
const DECODE_NONE = "None"
|
const DEFLATE = "Deflate"
|
||||||
const DECODE_BASE64 = "Base64"
|
|
||||||
const DECODE_GZIP = "GZip"
|
|
||||||
const DECODE_DEFLATE = "Deflate"
|
|
||||||
const DECODE_ZSTD = "ZStd"
|
|
||||||
const DECODE_LZ4 = "LZ4"
|
|
||||||
const DECODE_BROTLI = "Brotli"
|
|
||||||
const DECODE_MSGPACK = "Msgpack"
|
|
||||||
const DECODE_PHP = "PHP"
|
|
||||||
const DECODE_PICKLE = "Pickle"
|
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package convutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func runCommand(name string, arg ...string) ([]byte, error) {
|
|
||||||
cmd := exec.Command(name, arg...)
|
|
||||||
return cmd.Output()
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
//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()
|
|
||||||
}
|
|
@ -1,264 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,200 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
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,6 +107,27 @@ 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{}
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -76,8 +76,10 @@ func (l *LogHook) DialHook(next redis.DialHook) redis.DialHook {
|
|||||||
|
|
||||||
func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||||
|
log.Println(cmd)
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
err := next(ctx, cmd)
|
err := next(ctx, cmd)
|
||||||
|
if l.cmdExec != nil {
|
||||||
b := make([]byte, 0, 64)
|
b := make([]byte, 0, 64)
|
||||||
for i, arg := range cmd.Args() {
|
for i, arg := range cmd.Args() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@ -85,8 +87,6 @@ func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
|||||||
}
|
}
|
||||||
b = appendArg(b, arg)
|
b = appendArg(b, arg)
|
||||||
}
|
}
|
||||||
log.Println(string(b))
|
|
||||||
if l.cmdExec != nil {
|
|
||||||
l.cmdExec(string(b), time.Since(t).Milliseconds())
|
l.cmdExec(string(b), time.Since(t).Milliseconds())
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@ -98,24 +98,19 @@ func (l *LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.Proc
|
|||||||
t := time.Now()
|
t := time.Now()
|
||||||
err := next(ctx, cmds)
|
err := next(ctx, cmds)
|
||||||
cost := time.Since(t).Milliseconds()
|
cost := time.Since(t).Milliseconds()
|
||||||
b := make([]byte, 0, 64)
|
for _, cmd := range cmds {
|
||||||
for i, cmd := range cmds {
|
|
||||||
log.Println("pipeline: ", cmd)
|
log.Println("pipeline: ", cmd)
|
||||||
if l.cmdExec != nil {
|
if l.cmdExec != nil {
|
||||||
|
b := make([]byte, 0, 64)
|
||||||
for i, arg := range cmd.Args() {
|
for i, arg := range cmd.Args() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
b = append(b, ' ')
|
b = append(b, ' ')
|
||||||
}
|
}
|
||||||
b = appendArg(b, arg)
|
b = appendArg(b, arg)
|
||||||
}
|
}
|
||||||
if i != len(cmds) {
|
|
||||||
b = append(b, '\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if l.cmdExec != nil {
|
|
||||||
l.cmdExec(string(b), cost)
|
l.cmdExec(string(b), cost)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,137 @@
|
|||||||
package sliceutil
|
package sliceutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
. "tinyrdm/backend/utils"
|
. "tinyrdm/backend/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Map map items to new array
|
// Get 获取指定索引的值, 如果不存在则返回默认值
|
||||||
|
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)
|
||||||
@ -15,7 +141,7 @@ func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterMap filter and map items to new array
|
// FilterMap 数组过滤和映射转换
|
||||||
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)
|
||||||
@ -29,7 +155,68 @@ func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join join any array to a single string by custom function
|
// ToMap 数组转键值对
|
||||||
|
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 {
|
||||||
@ -49,14 +236,21 @@ func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) strin
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinString join string array to a single string
|
// JoinString 字符串数组拼接成字符串
|
||||||
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]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unique filter unique item
|
// JoinInt 整形数组拼接转字符串
|
||||||
|
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{}{}
|
||||||
@ -69,3 +263,136 @@ 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
|
||||||
|
}
|
||||||
|
@ -1,172 +0,0 @@
|
|||||||
package strutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
sliceutil "tinyrdm/backend/utils/slice"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AnyToString(value interface{}, prefix string, layer int) (s string) {
|
|
||||||
if value == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch value.(type) {
|
|
||||||
case float64:
|
|
||||||
ft := value.(float64)
|
|
||||||
s = strconv.FormatFloat(ft, 'f', -1, 64)
|
|
||||||
case float32:
|
|
||||||
ft := value.(float32)
|
|
||||||
s = strconv.FormatFloat(float64(ft), 'f', -1, 64)
|
|
||||||
case int:
|
|
||||||
it := value.(int)
|
|
||||||
s = strconv.Itoa(it)
|
|
||||||
case uint:
|
|
||||||
it := value.(uint)
|
|
||||||
s = strconv.Itoa(int(it))
|
|
||||||
case int8:
|
|
||||||
it := value.(int8)
|
|
||||||
s = strconv.Itoa(int(it))
|
|
||||||
case uint8:
|
|
||||||
it := value.(uint8)
|
|
||||||
s = strconv.Itoa(int(it))
|
|
||||||
case int16:
|
|
||||||
it := value.(int16)
|
|
||||||
s = strconv.Itoa(int(it))
|
|
||||||
case uint16:
|
|
||||||
it := value.(uint16)
|
|
||||||
s = strconv.Itoa(int(it))
|
|
||||||
case int32:
|
|
||||||
it := value.(int32)
|
|
||||||
s = strconv.Itoa(int(it))
|
|
||||||
case uint32:
|
|
||||||
it := value.(uint32)
|
|
||||||
s = strconv.Itoa(int(it))
|
|
||||||
case int64:
|
|
||||||
it := value.(int64)
|
|
||||||
s = strconv.FormatInt(it, 10)
|
|
||||||
case uint64:
|
|
||||||
it := value.(uint64)
|
|
||||||
s = strconv.FormatUint(it, 10)
|
|
||||||
case string:
|
|
||||||
if layer > 0 {
|
|
||||||
s = "\"" + value.(string) + "\""
|
|
||||||
} else {
|
|
||||||
s = value.(string)
|
|
||||||
}
|
|
||||||
case bool:
|
|
||||||
val, _ := value.(bool)
|
|
||||||
if val {
|
|
||||||
s = "True"
|
|
||||||
} else {
|
|
||||||
s = "False"
|
|
||||||
}
|
|
||||||
case []byte:
|
|
||||||
s = prefix + string(value.([]byte))
|
|
||||||
case []string:
|
|
||||||
ss := value.([]string)
|
|
||||||
anyStr := sliceutil.Map(ss, func(i int) string {
|
|
||||||
str := AnyToString(ss[i], prefix, layer+1)
|
|
||||||
return prefix + strconv.Itoa(i+1) + ") " + str
|
|
||||||
})
|
|
||||||
s = prefix + sliceutil.JoinString(anyStr, "\r\n")
|
|
||||||
case []any:
|
|
||||||
as := value.([]any)
|
|
||||||
anyItems := sliceutil.Map(as, func(i int) string {
|
|
||||||
str := AnyToString(as[i], prefix, layer+1)
|
|
||||||
return prefix + strconv.Itoa(i+1) + ") " + str
|
|
||||||
})
|
|
||||||
s = sliceutil.JoinString(anyItems, "\r\n")
|
|
||||||
case map[any]any:
|
|
||||||
am := value.(map[any]any)
|
|
||||||
var items []string
|
|
||||||
index := 0
|
|
||||||
for k, v := range am {
|
|
||||||
kk := prefix + strconv.Itoa(index+1) + ") " + AnyToString(k, prefix, layer+1)
|
|
||||||
vv := prefix + strconv.Itoa(index+2) + ") " + AnyToString(v, "\t", layer+1)
|
|
||||||
if layer > 0 {
|
|
||||||
indent := layer
|
|
||||||
if index == 0 {
|
|
||||||
indent -= 1
|
|
||||||
}
|
|
||||||
for i := 0; i < indent; i++ {
|
|
||||||
vv = " " + vv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index += 2
|
|
||||||
items = append(items, kk, vv)
|
|
||||||
}
|
|
||||||
s = sliceutil.JoinString(items, "\r\n")
|
|
||||||
default:
|
|
||||||
b, _ := json.Marshal(value)
|
|
||||||
s = prefix + string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//func AnyToHex(val any) (string, bool) {
|
|
||||||
// var src string
|
|
||||||
// switch val.(type) {
|
|
||||||
// case string:
|
|
||||||
// src = val.(string)
|
|
||||||
// case []byte:
|
|
||||||
// src = string(val.([]byte))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if len(src) <= 0 {
|
|
||||||
// return "", false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var output strings.Builder
|
|
||||||
// for i := range src {
|
|
||||||
// if !utf8.ValidString(src[i : i+1]) {
|
|
||||||
// output.WriteString(fmt.Sprintf("\\x%02x", src[i:i+1]))
|
|
||||||
// } else {
|
|
||||||
// output.WriteString(src[i : i+1])
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return output.String(), true
|
|
||||||
//}
|
|
||||||
|
|
||||||
func SplitCmd(cmd string) []string {
|
|
||||||
var result []string
|
|
||||||
var curStr strings.Builder
|
|
||||||
var preChar int32
|
|
||||||
var quotesChar int32
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if strings.Contains(part, "\\") {
|
|
||||||
if unquotePart, e := strconv.Unquote(`"` + part + `"`); e == nil {
|
|
||||||
return unquotePart, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return part, true
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
package strutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ContainsBinary(str string) bool {
|
|
||||||
//buf := []byte(str)
|
|
||||||
//size := 0
|
|
||||||
//for start := 0; start < len(buf); start += size {
|
|
||||||
// var r rune
|
|
||||||
// if r, size = utf8.DecodeRune(buf[start:]); r == utf8.RuneError {
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
rs := []rune(str)
|
|
||||||
for _, r := range rs {
|
|
||||||
if r == unicode.ReplacementChar {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if !unicode.IsPrint(r) && !unicode.IsSpace(r) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsSameChar(str string) bool {
|
|
||||||
if len(str) <= 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
rs := []rune(str)
|
|
||||||
first := rs[0]
|
|
||||||
for _, r := range rs {
|
|
||||||
if r != first {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
275
backend/utils/string/convert.go
Normal file
275
backend/utils/string/convert.go
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package strutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/flate"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"tinyrdm/backend/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConvertTo convert string to specified type
|
||||||
|
// @param targetType empty string indicates automatic detection of the string type
|
||||||
|
func ConvertTo(str, targetType string) (value, resultType string) {
|
||||||
|
if len(str) <= 0 {
|
||||||
|
// empty content
|
||||||
|
if len(targetType) <= 0 {
|
||||||
|
resultType = types.PLAIN_TEXT
|
||||||
|
} else {
|
||||||
|
resultType = targetType
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch targetType {
|
||||||
|
case types.PLAIN_TEXT:
|
||||||
|
value = str
|
||||||
|
resultType = targetType
|
||||||
|
return
|
||||||
|
|
||||||
|
case types.JSON:
|
||||||
|
value, _ = decodeJson(str)
|
||||||
|
resultType = targetType
|
||||||
|
return
|
||||||
|
|
||||||
|
case types.BASE64_TEXT, types.BASE64_JSON:
|
||||||
|
if base64Str, ok := decodeBase64(str); ok {
|
||||||
|
if targetType == types.BASE64_JSON {
|
||||||
|
value, _ = decodeJson(base64Str)
|
||||||
|
} else {
|
||||||
|
value = base64Str
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = str
|
||||||
|
}
|
||||||
|
resultType = targetType
|
||||||
|
return
|
||||||
|
|
||||||
|
case types.HEX:
|
||||||
|
if hexStr, ok := decodeHex(str); ok {
|
||||||
|
log.Print(hexStr)
|
||||||
|
value = hexStr
|
||||||
|
} else {
|
||||||
|
value = str
|
||||||
|
}
|
||||||
|
resultType = targetType
|
||||||
|
return
|
||||||
|
|
||||||
|
case types.BINARY:
|
||||||
|
var binary strings.Builder
|
||||||
|
for _, char := range str {
|
||||||
|
binary.WriteString(fmt.Sprintf("%08b", int(char)))
|
||||||
|
}
|
||||||
|
value = binary.String()
|
||||||
|
resultType = targetType
|
||||||
|
return
|
||||||
|
|
||||||
|
case types.GZIP, types.GZIP_JSON:
|
||||||
|
if gzipStr, ok := decodeGZip(str); ok {
|
||||||
|
if targetType == types.BASE64_JSON {
|
||||||
|
value, _ = decodeJson(gzipStr)
|
||||||
|
} else {
|
||||||
|
value = gzipStr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = str
|
||||||
|
}
|
||||||
|
resultType = targetType
|
||||||
|
return
|
||||||
|
|
||||||
|
case types.DEFLATE:
|
||||||
|
value, _ = decodeDeflate(str)
|
||||||
|
resultType = targetType
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// type isn't specified or unknown, try to automatically detect and return converted value
|
||||||
|
return autoToType(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt automatic convert to possible types
|
||||||
|
// if no conversion is possible, it will return the origin string value and "plain text" type
|
||||||
|
func autoToType(str string) (value, resultType string) {
|
||||||
|
if len(str) > 0 {
|
||||||
|
var ok bool
|
||||||
|
if value, ok = decodeJson(str); ok {
|
||||||
|
resultType = types.JSON
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = decodeBase64(str); ok {
|
||||||
|
if value, ok = decodeJson(value); ok {
|
||||||
|
resultType = types.BASE64_JSON
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultType = types.BASE64_TEXT
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = decodeGZip(str); ok {
|
||||||
|
resultType = types.GZIP
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok = decodeDeflate(str); ok {
|
||||||
|
resultType = types.DEFLATE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = str
|
||||||
|
resultType = types.PLAIN_TEXT
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJson(str string) (string, bool) {
|
||||||
|
var data any
|
||||||
|
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
|
||||||
|
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
|
||||||
|
if err := json.Unmarshal([]byte(str), &data); err == nil {
|
||||||
|
var jsonByte []byte
|
||||||
|
if jsonByte, err = json.MarshalIndent(data, "", " "); err == nil {
|
||||||
|
return string(jsonByte), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBase64(str string) (string, bool) {
|
||||||
|
if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil {
|
||||||
|
return string(decodedStr), true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHex(str string) (string, bool) {
|
||||||
|
encodeStr := hex.EncodeToString([]byte(str))
|
||||||
|
var resultStr strings.Builder
|
||||||
|
for i := 0; i < len(encodeStr); i += 2 {
|
||||||
|
resultStr.WriteString("\\x")
|
||||||
|
resultStr.WriteString(encodeStr[i : i+2])
|
||||||
|
}
|
||||||
|
return resultStr.String(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeGZip(str string) (string, bool) {
|
||||||
|
if reader, err := gzip.NewReader(strings.NewReader(str)); err == nil {
|
||||||
|
defer reader.Close()
|
||||||
|
var decompressed []byte
|
||||||
|
if decompressed, err = io.ReadAll(reader); err == nil {
|
||||||
|
return string(decompressed), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDeflate(str string) (string, bool) {
|
||||||
|
reader := flate.NewReader(strings.NewReader(str))
|
||||||
|
defer reader.Close()
|
||||||
|
if decompressed, err := io.ReadAll(reader); err == nil {
|
||||||
|
return string(decompressed), true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveAs(str, targetType string) (value string, err error) {
|
||||||
|
switch targetType {
|
||||||
|
case types.PLAIN_TEXT:
|
||||||
|
return str, nil
|
||||||
|
|
||||||
|
case types.BASE64_TEXT:
|
||||||
|
base64Str, _ := encodeBase64(str)
|
||||||
|
return base64Str, nil
|
||||||
|
|
||||||
|
case types.JSON, types.BASE64_JSON, types.GZIP_JSON:
|
||||||
|
if jsonStr, ok := encodeJson(str); ok {
|
||||||
|
if targetType == types.BASE64_JSON {
|
||||||
|
base64Str, _ := encodeBase64(jsonStr)
|
||||||
|
return base64Str, nil
|
||||||
|
} else {
|
||||||
|
return jsonStr, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return str, errors.New("invalid json")
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.GZIP:
|
||||||
|
if gzipStr, ok := encodeGZip(str); ok {
|
||||||
|
return gzipStr, nil
|
||||||
|
} else {
|
||||||
|
return str, errors.New("fail to build gzip data")
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.DEFLATE:
|
||||||
|
if deflateStr, ok := encodeDeflate(str); ok {
|
||||||
|
return deflateStr, nil
|
||||||
|
} else {
|
||||||
|
return str, errors.New("fail to build deflate data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, errors.New("fail to save with unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeJson(str string) (string, bool) {
|
||||||
|
var data any
|
||||||
|
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
|
||||||
|
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
|
||||||
|
if err := json.Unmarshal([]byte(str), &data); err == nil {
|
||||||
|
var jsonByte []byte
|
||||||
|
if jsonByte, err = json.Marshal(data); err == nil {
|
||||||
|
return string(jsonByte), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeBase64(str string) (string, bool) {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(str)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeGZip(str string) (string, bool) {
|
||||||
|
var compress = func(b []byte) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := gzip.NewWriter(&buf)
|
||||||
|
if _, err := writer.Write([]byte(str)); err != nil {
|
||||||
|
writer.Close()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
return string(buf.Bytes()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if gzipStr, err := compress([]byte(str)); err == nil {
|
||||||
|
return gzipStr, true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDeflate(str string) (string, bool) {
|
||||||
|
var compress = func(b []byte) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer, err := flate.NewWriter(&buf, flate.DefaultCompression)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if _, err = writer.Write([]byte(str)); err != nil {
|
||||||
|
writer.Close()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
return string(buf.Bytes()), nil
|
||||||
|
}
|
||||||
|
if deflateStr, err := compress([]byte(str)); err == nil {
|
||||||
|
return deflateStr, true
|
||||||
|
}
|
||||||
|
return str, false
|
||||||
|
}
|
@ -1,158 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package strutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
sliceutil "tinyrdm/backend/utils/slice"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EncodeRedisKey encode the redis key to integer array
|
|
||||||
// if key contains binary which could not display on ui, convert the key to char array
|
|
||||||
func EncodeRedisKey(key string) any {
|
|
||||||
if ContainsBinary(key) {
|
|
||||||
b := []byte(key)
|
|
||||||
arr := make([]int, len(b))
|
|
||||||
for i, bb := range b {
|
|
||||||
arr[i] = int(bb)
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeRedisKey decode redis key to readable string
|
|
||||||
func DecodeRedisKey(key any) string {
|
|
||||||
switch key.(type) {
|
|
||||||
case string:
|
|
||||||
return key.(string)
|
|
||||||
|
|
||||||
case []any:
|
|
||||||
arr := key.([]any)
|
|
||||||
bytes := sliceutil.Map(arr, func(i int) byte {
|
|
||||||
if c, ok := AnyToInt(arr[i]); ok {
|
|
||||||
return byte(c)
|
|
||||||
}
|
|
||||||
return '0'
|
|
||||||
})
|
|
||||||
return string(bytes)
|
|
||||||
|
|
||||||
case []int:
|
|
||||||
arr := key.([]int)
|
|
||||||
b := make([]byte, len(arr))
|
|
||||||
for i, bb := range arr {
|
|
||||||
b[i] = byte(bb)
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnyToInt convert any value to int
|
|
||||||
func AnyToInt(val any) (int, bool) {
|
|
||||||
switch val.(type) {
|
|
||||||
case string:
|
|
||||||
num, err := strconv.Atoi(val.(string))
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return num, true
|
|
||||||
case float64:
|
|
||||||
return int(val.(float64)), true
|
|
||||||
case float32:
|
|
||||||
return int(val.(float32)), true
|
|
||||||
case int64:
|
|
||||||
return int(val.(int64)), true
|
|
||||||
case int32:
|
|
||||||
return int(val.(int32)), true
|
|
||||||
case int:
|
|
||||||
return val.(int), true
|
|
||||||
case bool:
|
|
||||||
if val.(bool) {
|
|
||||||
return 1, true
|
|
||||||
} else {
|
|
||||||
return 0, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
@ -18,7 +18,7 @@
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>iconfile</string>
|
<string>iconfile</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>11.7.0</string>
|
<string>10.13.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>11.7.0</string>
|
<string>10.13.0</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
Binary file not shown.
@ -1,40 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
clear
|
|
||||||
BLACK="\033[0;30m"
|
|
||||||
DARK_GRAY="\033[1;30m"
|
|
||||||
BLUE="\033[0;34m"
|
|
||||||
LIGHT_BLUE="\033[1;34m"
|
|
||||||
GREEN="\033[0;32m"
|
|
||||||
LIGHT_GREEN="\033[1;32m"
|
|
||||||
CYAN="\033[0;36m"
|
|
||||||
LIGHT_CYAN="\033[1;36m"
|
|
||||||
RED="\033[0;31m"
|
|
||||||
LIGHT_RED="\033[1;31m"
|
|
||||||
PURPLE="\033[0;35m"
|
|
||||||
LIGHT_PURPLE="\033[1;35m"
|
|
||||||
BROWN="\033[0;33m"
|
|
||||||
YELLOW="\033[0;33m"
|
|
||||||
LIGHT_GRAY="\033[0;37m"
|
|
||||||
WHITE="\033[1;37m"
|
|
||||||
NC="\033[0m"
|
|
||||||
|
|
||||||
parentPath=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
|
|
||||||
cd "$parentPath"
|
|
||||||
appPath=$( find "$parentPath" -name '*.app' -maxdepth 1)
|
|
||||||
appName=${appPath##*/}
|
|
||||||
appBashName=${appName// /\ }
|
|
||||||
appDIR="/Applications/${appBashName}"
|
|
||||||
echo -e "This tool fix these situations: \"${appBashName}\" is damaged and can't not be opened."
|
|
||||||
echo ""
|
|
||||||
if [ ! -d "$appDIR" ];then
|
|
||||||
echo ""
|
|
||||||
echo -e "Execution result: ${RED}You haven't installed ${appBashName} yet, please install it first.${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}Please enter your login password, and then press enter. (The password is invisible during input)${NC}"
|
|
||||||
sudo spctl --master-disable
|
|
||||||
sudo xattr -rd com.apple.quarantine /Applications/"$appBashName"
|
|
||||||
sudo xattr -rc /Applications/"$appBashName"
|
|
||||||
sudo codesign --sign - --force --deep /Applications/"$appBashName"
|
|
||||||
echo -e "Execution result: ${GREEN}Already fixed! ${NC} ${appBashName} will work correctly.${NC}"
|
|
||||||
fi
|
|
||||||
echo -e "You can close this window now"
|
|
@ -1,42 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
clear
|
|
||||||
BLACK="\033[0;30m"
|
|
||||||
DARK_GRAY="\033[1;30m"
|
|
||||||
BLUE="\033[0;34m"
|
|
||||||
LIGHT_BLUE="\033[1;34m"
|
|
||||||
GREEN="\033[0;32m"
|
|
||||||
LIGHT_GREEN="\033[1;32m"
|
|
||||||
CYAN="\033[0;36m"
|
|
||||||
LIGHT_CYAN="\033[1;36m"
|
|
||||||
RED="\033[0;31m"
|
|
||||||
LIGHT_RED="\033[1;31m"
|
|
||||||
PURPLE="\033[0;35m"
|
|
||||||
LIGHT_PURPLE="\033[1;35m"
|
|
||||||
BROWN="\033[0;33m"
|
|
||||||
YELLOW="\033[0;33m"
|
|
||||||
LIGHT_GRAY="\033[0;37m"
|
|
||||||
WHITE="\033[1;37m"
|
|
||||||
NC="\033[0m"
|
|
||||||
|
|
||||||
parentPath=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
|
|
||||||
cd "$parentPath"
|
|
||||||
appPath=$( find "$parentPath" -name '*.app' -maxdepth 1)
|
|
||||||
appName=${appPath##*/}
|
|
||||||
appBashName=${appName// /\ }
|
|
||||||
appDIR="/Applications/${appBashName}"
|
|
||||||
echo -e "『${appBashName} 提示已损坏,无法打开/ 来自身份不明的开发者』等问题修复工具"
|
|
||||||
echo ""
|
|
||||||
# 未安装APP时提醒安装,已安装绕过公证
|
|
||||||
if [ ! -d "$appDIR" ];then
|
|
||||||
echo ""
|
|
||||||
echo -e "执行结果:${RED}您还未安装 ${appBashName} ,请先安装${NC}"
|
|
||||||
else
|
|
||||||
# 绕过公证
|
|
||||||
echo -e "${YELLOW}请输入开机密码,输入完成后按下回车键(输入过程中密码是看不见的)${NC}"
|
|
||||||
sudo spctl --master-disable
|
|
||||||
sudo xattr -rd com.apple.quarantine /Applications/"$appBashName"
|
|
||||||
sudo xattr -rc /Applications/"$appBashName"
|
|
||||||
sudo codesign --sign - --force --deep /Applications/"$appBashName"
|
|
||||||
echo -e "执行结果:${GREEN}修复成功!${NC}您现在可以正常运行 ${appBashName} 了。${NC}"
|
|
||||||
fi
|
|
||||||
echo -e "本窗口可以关闭啦!"
|
|
@ -3,7 +3,6 @@ Version: {{.Info.ProductVersion}}
|
|||||||
Section: base
|
Section: base
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
Depends: {{.libwebkit2gtk.PackageName}}
|
|
||||||
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}}
|
||||||
|
Binary file not shown.
Before ![]() (image error) Size: 66 KiB |
Binary file not shown.
Before ![]() (image error) Size: 60 KiB |
Binary file not shown.
Before ![]() (image error) Size: 46 KiB |
@ -1,3 +1,8 @@
|
|||||||
# Frontend of Tiny RDM
|
# Vue 3 + Vite
|
||||||
|
|
||||||
Use Vue3 + Vite
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs,
|
||||||
|
check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||||
|
@ -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>
|
||||||
<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>
|
||||||
|
|
||||||
|
4283
frontend/package-lock.json
generated
4283
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,26 +9,21 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^4.4.8",
|
"dayjs": "^1.11.10",
|
||||||
"copy-text-to-clipboard": "^3.2.0",
|
"highlight.js": "^11.8.0",
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"monaco-editor": "^0.47.0",
|
"pinia": "^2.1.6",
|
||||||
"pinia": "^3.0.1",
|
"sass": "^1.68.0",
|
||||||
"sass": "^1.86.3",
|
"vue": "^3.3.4",
|
||||||
"vue": "^3.5.13",
|
"vue-i18n": "^9.4.1"
|
||||||
"vue-chartjs": "^5.3.2",
|
|
||||||
"vue-i18n": "^11.1.3",
|
|
||||||
"xterm": "^5.3.0",
|
|
||||||
"xterm-addon-fit": "^0.8.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
"naive-ui": "^2.41.0",
|
"naive-ui": "^2.34.4",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.0.3",
|
||||||
"unplugin-auto-import": "^19.1.2",
|
"unplugin-auto-import": "^0.16.6",
|
||||||
"unplugin-icons": "^22.1.0",
|
"unplugin-icons": "^0.17.0",
|
||||||
"unplugin-vue-components": "^28.4.1",
|
"unplugin-vue-components": "^0.25.2",
|
||||||
"vite": "^6.2.5"
|
"vite": "^4.4.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
0e5f5193b1fa5522d70d8a71c4487dd4
|
a65375421b9b10cadef51ed8edc1c6f1
|
@ -4,25 +4,25 @@ import NewKeyDialog from './components/dialogs/NewKeyDialog.vue'
|
|||||||
import PreferencesDialog from './components/dialogs/PreferencesDialog.vue'
|
import PreferencesDialog from './components/dialogs/PreferencesDialog.vue'
|
||||||
import RenameKeyDialog from './components/dialogs/RenameKeyDialog.vue'
|
import RenameKeyDialog from './components/dialogs/RenameKeyDialog.vue'
|
||||||
import SetTtlDialog from './components/dialogs/SetTtlDialog.vue'
|
import SetTtlDialog from './components/dialogs/SetTtlDialog.vue'
|
||||||
|
import hljs from 'highlight.js/lib/core'
|
||||||
|
import json from 'highlight.js/lib/languages/json'
|
||||||
|
import plaintext from 'highlight.js/lib/languages/plaintext'
|
||||||
import AddFieldsDialog from './components/dialogs/AddFieldsDialog.vue'
|
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 { h, onMounted, ref, watch } from 'vue'
|
import { 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, NButton, NSpace } from 'naive-ui'
|
import { darkTheme } from 'naive-ui'
|
||||||
import KeyFilterDialog from './components/dialogs/KeyFilterDialog.vue'
|
import KeyFilterDialog from './components/dialogs/KeyFilterDialog.vue'
|
||||||
import { Environment, WindowSetDarkTheme, WindowSetLightTheme } from 'wailsjs/runtime/runtime.js'
|
import { WindowSetDarkTheme, WindowSetLightTheme } from 'wailsjs/runtime/runtime.js'
|
||||||
import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
|
import { 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 ExportKeyDialog from '@/components/dialogs/ExportKeyDialog.vue'
|
hljs.registerLanguage('json', json)
|
||||||
import ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue'
|
hljs.registerLanguage('plaintext', plaintext)
|
||||||
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()
|
||||||
@ -32,66 +32,10 @@ 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
|
||||||
}
|
}
|
||||||
@ -112,10 +56,11 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider
|
<n-config-provider
|
||||||
|
:hljs="hljs"
|
||||||
:inline-theme-disabled="true"
|
:inline-theme-disabled="true"
|
||||||
:locale="prefStore.themeLocale"
|
:locale="prefStore.themeLocale"
|
||||||
:theme="prefStore.isDark ? darkTheme : undefined"
|
:theme="prefStore.isDark ? darkTheme : undefined"
|
||||||
:theme-overrides="prefStore.isDark ? darkThemeOverrides : themeOverrides"
|
:theme-overrides="themeOverrides"
|
||||||
class="fill-height">
|
class="fill-height">
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<app-content :loading="initializing" />
|
<app-content :loading="initializing" />
|
||||||
@ -128,12 +73,8 @@ watch(
|
|||||||
<add-fields-dialog />
|
<add-fields-dialog />
|
||||||
<rename-key-dialog />
|
<rename-key-dialog />
|
||||||
<delete-key-dialog />
|
<delete-key-dialog />
|
||||||
<export-key-dialog />
|
|
||||||
<import-key-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,22 +1,21 @@
|
|||||||
<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, onUnmounted, reactive, ref, watchEffect } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { debounce } from 'lodash'
|
import { debounce, get } from 'lodash'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import Ribbon from './components/sidebar/Ribbon.vue'
|
import NavMenu from './components/sidebar/NavMenu.vue'
|
||||||
import ConnectionPane from './components/sidebar/ConnectionPane.vue'
|
import ConnectionPane from './components/sidebar/ConnectionPane.vue'
|
||||||
import ContentServerPane from './components/content/ContentServerPane.vue'
|
import ContentServerPane from './components/content/ContentServerPane.vue'
|
||||||
import useTabStore from './stores/tab.js'
|
import useTabStore from './stores/tab.js'
|
||||||
import usePreferencesStore from './stores/preferences.js'
|
import usePreferencesStore from './stores/preferences.js'
|
||||||
|
import useConnectionStore from './stores/connections.js'
|
||||||
import ContentLogPane from './components/content/ContentLogPane.vue'
|
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 { WindowIsFullscreen, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
|
||||||
import { isMacOS, isWindows } from '@/utils/platform.js'
|
import { isMacOS } 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 { extraTheme } from '@/utils/extra_theme.js'
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
|
|
||||||
@ -25,210 +24,175 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
navMenuWidth: 50,
|
navMenuWidth: 60,
|
||||||
toolbarHeight: 38,
|
hoverResize: false,
|
||||||
|
resizing: false,
|
||||||
|
toolbarHeight: 45,
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
const logPaneRef = ref(null)
|
const logPaneRef = ref(null)
|
||||||
const exThemeVars = computed(() => {
|
|
||||||
return extraTheme(prefStore.isDark)
|
|
||||||
})
|
|
||||||
// const preferences = ref({})
|
// const preferences = ref({})
|
||||||
// provide('preferences', preferences)
|
// provide('preferences', preferences)
|
||||||
|
|
||||||
const saveSidebarWidth = debounce(prefStore.savePreferences, 1000, { trailing: true })
|
const saveWidth = debounce(prefStore.savePreferences, 1000, { trailing: true })
|
||||||
const handleResize = () => {
|
const handleResize = (evt) => {
|
||||||
saveSidebarWidth()
|
if (data.resizing) {
|
||||||
|
prefStore.setAsideWidth(Math.max(evt.clientX - data.navMenuWidth, 300))
|
||||||
|
saveWidth()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
const stopResize = () => {
|
||||||
if (tabStore.nav === 'log') {
|
data.resizing = false
|
||||||
|
document.removeEventListener('mousemove', handleResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
// TODO: Save sidebar x-position
|
||||||
|
}
|
||||||
|
|
||||||
|
const startResize = () => {
|
||||||
|
data.resizing = true
|
||||||
|
document.addEventListener('mousemove', handleResize)
|
||||||
|
document.addEventListener('mouseup', stopResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const asideWidthVal = computed(() => {
|
||||||
|
return prefStore.behavior.asideWidth + 'px'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dragging = computed(() => {
|
||||||
|
return data.hoverResize || data.resizing
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tabStore.nav,
|
||||||
|
(nav) => {
|
||||||
|
if (nav === 'log') {
|
||||||
logPaneRef.value?.refresh()
|
logPaneRef.value?.refresh()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const borderRadius = computed(() => {
|
||||||
|
// FIXME: cannot get full screen status sync?
|
||||||
|
// if (isMacOS()) {
|
||||||
|
// return WindowIsFullscreen().then((full) => {
|
||||||
|
// return full ? '0' : '10px'
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
return '10px'
|
||||||
})
|
})
|
||||||
|
|
||||||
const logoWrapperWidth = computed(() => {
|
const border = computed(() => {
|
||||||
return `${data.navMenuWidth + prefStore.behavior.asideWidth - 4}px`
|
const color = isMacOS() && false ? '#0000' : themeVars.value.borderColor
|
||||||
|
return `1px solid ${color}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const logoPaddingLeft = ref(10)
|
|
||||||
const maximised = ref(false)
|
|
||||||
const hideRadius = ref(false)
|
|
||||||
const wrapperStyle = computed(() => {
|
|
||||||
if (isWindows()) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
return hideRadius.value
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
border: `1px solid ${themeVars.value.borderColor}`,
|
|
||||||
borderRadius: '10px',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const spinStyle = computed(() => {
|
|
||||||
if (isWindows()) {
|
|
||||||
return {
|
|
||||||
backgroundColor: themeVars.value.bodyColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hideRadius.value
|
|
||||||
? {
|
|
||||||
backgroundColor: themeVars.value.bodyColor,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
backgroundColor: themeVars.value.bodyColor,
|
|
||||||
borderRadius: '10px',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onToggleFullscreen = (fullscreen) => {
|
|
||||||
hideRadius.value = fullscreen
|
|
||||||
if (fullscreen) {
|
|
||||||
logoPaddingLeft.value = 10
|
|
||||||
} else {
|
|
||||||
logoPaddingLeft.value = isMacOS() ? 70 : 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onToggleMaximize = (isMaximised) => {
|
|
||||||
if (isMaximised) {
|
|
||||||
maximised.value = true
|
|
||||||
if (!isMacOS()) {
|
|
||||||
hideRadius.value = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
maximised.value = false
|
|
||||||
if (!isMacOS()) {
|
|
||||||
hideRadius.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventsOn('window_changed', (info) => {
|
|
||||||
const { fullscreen, maximised } = info
|
|
||||||
onToggleFullscreen(fullscreen === true)
|
|
||||||
onToggleMaximize(maximised)
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const fullscreen = await WindowIsFullscreen()
|
|
||||||
onToggleFullscreen(fullscreen === true)
|
|
||||||
const maximised = await WindowIsMaximised()
|
|
||||||
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>
|
||||||
<!-- app content-->
|
<!-- app content-->
|
||||||
<n-spin :show="props.loading" :style="spinStyle" :theme-overrides="{ opacitySpinning: 0 }">
|
<n-spin
|
||||||
<div id="app-content-wrapper" :style="wrapperStyle" class="flex-box-v">
|
:show="props.loading"
|
||||||
|
:theme-overrides="{ opacitySpinning: 0 }"
|
||||||
|
:style="{ backgroundColor: themeVars.bodyColor, borderRadius, border }">
|
||||||
|
<div
|
||||||
|
id="app-content-wrapper"
|
||||||
|
class="flex-box-v"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: themeVars.bodyColor,
|
||||||
|
color: themeVars.textColorBase,
|
||||||
|
}">
|
||||||
<!-- title bar -->
|
<!-- title bar -->
|
||||||
<div
|
<div
|
||||||
id="app-toolbar"
|
id="app-toolbar"
|
||||||
:style="{ height: data.toolbarHeight + 'px' }"
|
|
||||||
class="flex-box-h"
|
class="flex-box-h"
|
||||||
style="--wails-draggable: drag"
|
style="--wails-draggable: drag"
|
||||||
|
:style="{ height: data.toolbarHeight + 'px' }"
|
||||||
@dblclick="WindowToggleMaximise">
|
@dblclick="WindowToggleMaximise">
|
||||||
<!-- title -->
|
<!-- title -->
|
||||||
<div
|
<div
|
||||||
id="app-toolbar-title"
|
id="app-toolbar-title"
|
||||||
:style="{
|
:style="{
|
||||||
width: logoWrapperWidth,
|
width: `${data.navMenuWidth + prefStore.behavior.asideWidth - 4}px`,
|
||||||
minWidth: logoWrapperWidth,
|
paddingLeft: isMacOS() ? '70px' : '10px',
|
||||||
paddingLeft: `${logoPaddingLeft}px`,
|
|
||||||
}">
|
}">
|
||||||
<n-space :size="3" :wrap="false" :wrap-item="false" align="center">
|
<n-space align="center" :wrap-item="false" :wrap="false" :size="3">
|
||||||
<n-avatar :size="32" :src="iconUrl" color="#0000" style="min-width: 32px" />
|
<n-avatar :src="iconUrl" color="#0000" :size="35" style="min-width: 35px" />
|
||||||
<div style="min-width: 68px; white-space: nowrap; font-weight: 800">Tiny RDM</div>
|
<div style="min-width: 68px; 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'" strong class="ellipsis" style="font-size: 13px">
|
||||||
- {{ tabStore.currentTabName }}
|
- {{ get(tabStore.currentTab, 'name') }}
|
||||||
</n-text>
|
</n-text>
|
||||||
</transition>
|
</transition>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'resize-divider-hover': data.hoverResize,
|
||||||
|
'resize-divider-drag': data.resizing,
|
||||||
|
}"
|
||||||
|
class="resize-divider resize-divider-hide"
|
||||||
|
@mousedown="startResize"
|
||||||
|
@mouseout="data.hoverResize = false"
|
||||||
|
@mouseover="data.hoverResize = true" />
|
||||||
<!-- browser tabs -->
|
<!-- browser tabs -->
|
||||||
<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" style="min-width: 15px"></div>
|
<div class="flex-item-expand"></div>
|
||||||
<!-- simulate window control buttons -->
|
<!-- simulate window control buttons -->
|
||||||
<toolbar-control-widget
|
<toolbar-control-widget v-if="!isMacOS()" :size="data.toolbarHeight" style="align-self: flex-start" />
|
||||||
v-if="!isMacOS()"
|
|
||||||
:maximised="maximised"
|
|
||||||
:size="data.toolbarHeight"
|
|
||||||
style="align-self: flex-start" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- content -->
|
<!-- content -->
|
||||||
<div
|
<div
|
||||||
id="app-content"
|
id="app-content"
|
||||||
:style="prefStore.generalFont"
|
:style="prefStore.generalFont"
|
||||||
class="flex-box-h flex-item-expand"
|
style="--wails-draggable: none"
|
||||||
style="--wails-draggable: none">
|
class="flex-box-h flex-item-expand">
|
||||||
<ribbon v-model:value="tabStore.nav" :width="data.navMenuWidth" />
|
<nav-menu v-model:value="tabStore.nav" :width="data.navMenuWidth" />
|
||||||
<!-- browser page -->
|
<!-- browser page -->
|
||||||
<div v-show="tabStore.nav === 'browser'" class="content-area flex-box-h flex-item-expand">
|
<div v-show="tabStore.nav === 'browser'" :class="{ dragging }" class="flex-box-h flex-item-expand">
|
||||||
<resizeable-wrapper
|
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
|
||||||
v-model:size="prefStore.behavior.asideWidth"
|
|
||||||
:min-size="300"
|
|
||||||
:offset="data.navMenuWidth"
|
|
||||||
class="flex-item"
|
|
||||||
@update:size="handleResize">
|
|
||||||
<browser-pane
|
<browser-pane
|
||||||
v-for="t in tabStore.tabs"
|
v-for="t in tabStore.tabs"
|
||||||
v-show="tabStore.currentTabName === t.name"
|
v-show="get(tabStore.currentTab, 'name') === t.name"
|
||||||
:key="t.name"
|
:key="t.name"
|
||||||
:db="t.db"
|
|
||||||
:server="t.name"
|
|
||||||
class="app-side flex-item-expand" />
|
|
||||||
</resizeable-wrapper>
|
|
||||||
<content-pane
|
|
||||||
v-for="t in tabStore.tabs"
|
|
||||||
v-show="tabStore.currentTabName === t.name"
|
|
||||||
:key="t.name"
|
|
||||||
:server="t.name"
|
|
||||||
class="flex-item-expand" />
|
class="flex-item-expand" />
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'resize-divider-hover': data.hoverResize,
|
||||||
|
'resize-divider-drag': data.resizing,
|
||||||
|
}"
|
||||||
|
class="resize-divider"
|
||||||
|
@mousedown="startResize"
|
||||||
|
@mouseout="data.hoverResize = false"
|
||||||
|
@mouseover="data.hoverResize = true" />
|
||||||
|
</div>
|
||||||
|
<content-pane class="flex-item-expand" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- server list page -->
|
<!-- server list page -->
|
||||||
<div v-show="tabStore.nav === 'server'" class="content-area flex-box-h flex-item-expand">
|
<div v-show="tabStore.nav === 'server'" :class="{ dragging }" class="flex-box-h flex-item-expand">
|
||||||
<resizeable-wrapper
|
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
|
||||||
v-model:size="prefStore.behavior.asideWidth"
|
<connection-pane class="flex-item-expand" />
|
||||||
:min-size="300"
|
<div
|
||||||
:offset="data.navMenuWidth"
|
:class="{
|
||||||
class="flex-item"
|
'resize-divider-hover': data.hoverResize,
|
||||||
@update:size="handleResize">
|
'resize-divider-drag': data.resizing,
|
||||||
<connection-pane class="app-side flex-item-expand" />
|
}"
|
||||||
</resizeable-wrapper>
|
class="resize-divider"
|
||||||
|
@mousedown="startResize"
|
||||||
|
@mouseout="data.hoverResize = false"
|
||||||
|
@mouseover="data.hoverResize = true" />
|
||||||
|
</div>
|
||||||
<content-server-pane class="flex-item-expand" />
|
<content-server-pane class="flex-item-expand" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- log page -->
|
<!-- log page -->
|
||||||
<div v-show="tabStore.nav === 'log'" class="content-area flex-box-h flex-item-expand">
|
<div v-show="tabStore.nav === 'log'" class="flex-box-h flex-item-expand">
|
||||||
<content-log-pane ref="logPaneRef" class="flex-item-expand" />
|
<content-log-pane ref="logPaneRef" class="flex-item-expand" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -238,16 +202,15 @@ const onKeyShortcut = (e) => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
#app-content-wrapper {
|
#app-content-wrapper {
|
||||||
width: 100vw;
|
width: calc(100vw - 2px);
|
||||||
height: 100vh;
|
height: calc(100vh - 2px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: v-bind('themeVars.bodyColor');
|
border-radius: 10px;
|
||||||
color: v-bind('themeVars.textColorBase');
|
|
||||||
|
|
||||||
#app-toolbar {
|
#app-toolbar {
|
||||||
background-color: v-bind('exThemeVars.titleColor');
|
background-color: v-bind('themeVars.tabColor');
|
||||||
border-bottom: 1px solid v-bind('exThemeVars.splitColor');
|
border-bottom: 1px solid v-bind('themeVars.borderColor');
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
@ -262,25 +225,43 @@ const onKeyShortcut = (e) => {
|
|||||||
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 {
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
|
|
||||||
.content-area {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-side {
|
#app-side {
|
||||||
//overflow: hidden;
|
//overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: v-bind('exThemeVars.sidebarColor');
|
background-color: v-bind('themeVars.tabColor');
|
||||||
border-right: 1px solid v-bind('exThemeVars.splitColor');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resize-divider {
|
||||||
|
width: 3px;
|
||||||
|
border-right: 1px solid v-bind('themeVars.borderColor');
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-divider-hide {
|
||||||
|
background-color: #0000;
|
||||||
|
border-right-color: #0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-divider-hover {
|
||||||
|
background-color: v-bind('themeVars.borderColor');
|
||||||
|
border-right-color: v-bind('themeVars.borderColor');
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-divider-drag {
|
||||||
|
background-color: v-bind('themeVars.primaryColor');
|
||||||
|
border-right-color: v-bind('themeVars.primaryColor');
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging {
|
||||||
|
cursor: col-resize !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
Binary file not shown.
Before ![]() (image error) Size: 66 KiB |
Binary file not shown.
Before ![]() (image error) Size: 60 KiB |
@ -1,69 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { isNumber } from 'lodash'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
on: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
defaultValue: {
|
|
||||||
type: Number,
|
|
||||||
default: 2,
|
|
||||||
},
|
|
||||||
interval: {
|
|
||||||
type: Number,
|
|
||||||
default: 2,
|
|
||||||
},
|
|
||||||
onRefresh: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['toggle', 'update:on', 'update:interval'])
|
|
||||||
|
|
||||||
const onToggle = (on) => {
|
|
||||||
emit('update:on', on === true)
|
|
||||||
if (on) {
|
|
||||||
let interval = props.interval
|
|
||||||
if (!isNumber(interval)) {
|
|
||||||
interval = props.defaultValue
|
|
||||||
}
|
|
||||||
interval = Math.max(1, interval)
|
|
||||||
emit('update:interval', interval)
|
|
||||||
emit('toggle', true)
|
|
||||||
} else {
|
|
||||||
emit('toggle', false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-form :show-feedback="false" label-align="right" label-placement="left" label-width="auto" size="small">
|
|
||||||
<n-form-item :label="$t('interface.auto_refresh')">
|
|
||||||
<n-switch :loading="props.loading" :value="props.on" @update:value="onToggle" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="$t('interface.refresh_interval')">
|
|
||||||
<n-input-number
|
|
||||||
:autofocus="false"
|
|
||||||
:default-value="props.defaultValue"
|
|
||||||
:disabled="props.on"
|
|
||||||
:max="9999"
|
|
||||||
:min="1"
|
|
||||||
:show-button="false"
|
|
||||||
:value="props.interval"
|
|
||||||
style="max-width: 100px"
|
|
||||||
@update:value="(val) => emit('update:interval', val)">
|
|
||||||
<template #suffix>
|
|
||||||
{{ $t('common.unit_second') }}
|
|
||||||
</template>
|
|
||||||
</n-input-number>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
@ -1,148 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, h, ref } from 'vue'
|
|
||||||
import { get, isEmpty, some } from 'lodash'
|
|
||||||
import { NIcon, NText } from 'naive-ui'
|
|
||||||
import { useRender } from '@/utils/render.js'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
type: Array,
|
|
||||||
value: () => [],
|
|
||||||
},
|
|
||||||
menuOption: {
|
|
||||||
type: Array,
|
|
||||||
value: () => [],
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
icon: [String, Object],
|
|
||||||
default: String,
|
|
||||||
disabled: Boolean,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:value', 'menu'])
|
|
||||||
const i18n = useI18n()
|
|
||||||
const render = useRender()
|
|
||||||
|
|
||||||
const renderHeader = () => {
|
|
||||||
return h('div', { class: 'type-selector-header' }, [h(NText, null, () => props.tooltip)])
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropdownOption = computed(() => {
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
key: 'header',
|
|
||||||
type: 'render',
|
|
||||||
render: renderHeader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'header-divider',
|
|
||||||
type: 'divider',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if (get(props.options, 0) instanceof Array) {
|
|
||||||
// multiple group
|
|
||||||
for (let i = 0; i < props.options.length; i++) {
|
|
||||||
if (i !== 0 && !isEmpty(props.options[i])) {
|
|
||||||
// add divider
|
|
||||||
options.push({
|
|
||||||
key: 'header-divider' + (i + 1),
|
|
||||||
type: 'divider',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for (const option of props.options[i]) {
|
|
||||||
options.push({
|
|
||||||
key: option,
|
|
||||||
label: option,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const option of props.options) {
|
|
||||||
options.push({
|
|
||||||
key: option,
|
|
||||||
label: option,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
if (some(props.menuOption, { key })) {
|
|
||||||
emit('menu', key)
|
|
||||||
} else {
|
|
||||||
emit('update:value', key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
|
||||||
return props.value || get(dropdownOption.value, [1, 'label'], props.default)
|
|
||||||
})
|
|
||||||
|
|
||||||
const showDropdown = ref(false)
|
|
||||||
const onDropdownShow = (show) => {
|
|
||||||
showDropdown.value = show === true
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-dropdown
|
|
||||||
:disabled="props.disabled"
|
|
||||||
:options="dropdownOption"
|
|
||||||
:render-label="({ label }) => render.renderLabel(label, { class: 'type-selector-item' })"
|
|
||||||
:show-arrow="true"
|
|
||||||
:value="props.value"
|
|
||||||
trigger="click"
|
|
||||||
@select="onDropdownSelect"
|
|
||||||
@update:show="onDropdownShow">
|
|
||||||
<n-tooltip :disabled="showDropdown" :show-arrow="false">
|
|
||||||
{{ props.tooltip }}
|
|
||||||
<template #trigger>
|
|
||||||
<n-button :disabled="disabled" :focusable="false" quaternary>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon>
|
|
||||||
<component :is="icon" />
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
{{ buttonText }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-tooltip>
|
|
||||||
</n-dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.type-selector-header {
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-selector-item {
|
|
||||||
min-width: 100px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -4,34 +4,30 @@ import Delete from '@/components/icons/Delete.vue'
|
|||||||
import Edit from '@/components/icons/Edit.vue'
|
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 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', 'refresh', 'save', 'cancel'])
|
const emit = defineEmits(['edit', 'delete', 'save', 'cancel'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- TODO: support multiple save -->
|
||||||
<div v-if="props.editing" class="flex-box-h edit-column-func">
|
<div v-if="props.editing" class="flex-box-h edit-column-func">
|
||||||
<icon-button :icon="Save" @click="emit('save')" />
|
<icon-button :icon="Save" @click="emit('save')" />
|
||||||
<icon-button :icon="Close" @click="emit('cancel')" />
|
<icon-button :icon="Close" @click="emit('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 v-if="!props.readonly" :icon="Edit" @click="emit('edit')" />
|
||||||
<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')" />
|
|
||||||
<n-popconfirm
|
<n-popconfirm
|
||||||
:negative-text="$t('common.cancel')"
|
:negative-text="$t('common.cancel')"
|
||||||
:positive-text="$t('common.confirm')"
|
:positive-text="$t('common.confirm')"
|
||||||
@positive-click="emit('delete')">
|
@positive-click="emit('delete')">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<icon-button :icon="Delete" :title="$t('interface.delete_row')" />
|
<icon-button :icon="Delete" />
|
||||||
</template>
|
</template>
|
||||||
{{ $t('dialogue.remove_tip', { name: props.bindKey }) }}
|
{{ $t('dialogue.remove_tip', { name: props.bindKey }) }}
|
||||||
</n-popconfirm>
|
</n-popconfirm>
|
||||||
|
@ -17,6 +17,7 @@ const handleUpdateValue = (val) => {
|
|||||||
<template>
|
<template>
|
||||||
<div style="min-height: 22px">
|
<div style="min-height: 22px">
|
||||||
<template v-if="props.isEdit">
|
<template v-if="props.isEdit">
|
||||||
|
<!-- TODO: ADD FULL SCREEN EDIT SUPPORT -->
|
||||||
<n-input :value="props.value" @update:value="handleUpdateValue" />
|
<n-input :value="props.value" @update:value="handleUpdateValue" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { SelectFile } from 'wailsjs/go/services/systemService.js'
|
|
||||||
import { get, isEmpty } from 'lodash'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
value: String,
|
|
||||||
placeholder: String,
|
|
||||||
disabled: Boolean,
|
|
||||||
ext: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:value'])
|
|
||||||
|
|
||||||
const onInput = (val) => {
|
|
||||||
emit('update:value', val)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClear = () => {
|
|
||||||
emit('update:value', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectFile = async () => {
|
|
||||||
const { success, data } = await SelectFile('', isEmpty(props.ext) ? null : [props.ext])
|
|
||||||
if (success) {
|
|
||||||
const path = get(data, 'path', '')
|
|
||||||
emit('update:value', path)
|
|
||||||
} else {
|
|
||||||
// emit('update:value', '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input
|
|
||||||
:disabled="props.disabled"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:title="props.value"
|
|
||||||
:value="props.value"
|
|
||||||
clearable
|
|
||||||
@clear="onClear"
|
|
||||||
@input="onInput" />
|
|
||||||
<n-button :disabled="props.disabled" :focusable="false" @click="handleSelectFile">...</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
@ -1,46 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { SaveFile } from 'wailsjs/go/services/systemService.js'
|
|
||||||
import { get } from 'lodash'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
value: String,
|
|
||||||
placeholder: String,
|
|
||||||
disabled: Boolean,
|
|
||||||
defaultPath: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:value'])
|
|
||||||
|
|
||||||
const onInput = (val) => {
|
|
||||||
emit('update:value', val)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClear = () => {
|
|
||||||
emit('update:value', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveFile = async () => {
|
|
||||||
const { success, data } = await SaveFile(null, props.defaultPath, ['csv'])
|
|
||||||
if (success) {
|
|
||||||
const path = get(data, 'path', '')
|
|
||||||
emit('update:value', path)
|
|
||||||
} else {
|
|
||||||
emit('update:value', '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input
|
|
||||||
:disabled="props.disabled"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:value="props.value"
|
|
||||||
clearable
|
|
||||||
@clear="onClear"
|
|
||||||
@input="onInput" />
|
|
||||||
<n-button :disabled="props.disabled" :focusable="false" @click="handleSaveFile">...</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
@ -1,15 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, useSlots } from 'vue'
|
import { computed } 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,
|
||||||
tooltipDelay: {
|
|
||||||
type: Number,
|
|
||||||
default: 800,
|
|
||||||
},
|
|
||||||
type: String,
|
|
||||||
icon: [String, Object],
|
icon: [String, Object],
|
||||||
size: {
|
size: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@ -17,81 +14,36 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: 'currentColor',
|
||||||
},
|
},
|
||||||
strokeWidth: {
|
strokeWidth: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: 3,
|
default: 3,
|
||||||
},
|
},
|
||||||
loading: Boolean,
|
|
||||||
border: Boolean,
|
border: Boolean,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
buttonStyle: [String, Object],
|
|
||||||
buttonClass: [String, Object],
|
|
||||||
small: Boolean,
|
|
||||||
secondary: Boolean,
|
|
||||||
tertiary: Boolean,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['click'])
|
|
||||||
|
|
||||||
const slots = useSlots()
|
|
||||||
|
|
||||||
const hasTooltip = computed(() => {
|
const hasTooltip = computed(() => {
|
||||||
return props.tooltip || props.tTooltip || slots.tooltip
|
return props.tooltip || props.tTooltip
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tooltip v-if="hasTooltip" :delay="tooltipDelay" :keep-alive-on-hover="false" :show-arrow="false">
|
<n-tooltip v-if="hasTooltip">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button
|
<n-button :disabled="disabled" :text="!border" :focusable="false" @click.prevent="emit('click')">
|
||||||
:class="props.buttonClass"
|
<n-icon :color="props.color" :size="props.size">
|
||||||
:color="props.color"
|
|
||||||
:disabled="props.disabled"
|
|
||||||
:focusable="false"
|
|
||||||
:loading="loading"
|
|
||||||
:secondary="props.secondary"
|
|
||||||
:size="props.small ? 'small' : ''"
|
|
||||||
:style="props.buttonStyle"
|
|
||||||
:tertiary="props.tertiary"
|
|
||||||
:text="!props.border"
|
|
||||||
:type="props.type"
|
|
||||||
@click.prevent="emit('click')">
|
|
||||||
<template #icon>
|
|
||||||
<slot>
|
|
||||||
<n-icon :color="props.color || 'currentColor'" :size="props.size">
|
|
||||||
<component :is="props.icon" :stroke-width="props.strokeWidth" />
|
<component :is="props.icon" :stroke-width="props.strokeWidth" />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<slot name="tooltip">
|
|
||||||
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
|
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
|
||||||
</slot>
|
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
<n-button
|
<n-button v-else :disabled="disabled" :text="!border" :focusable="false" @click.prevent="emit('click')">
|
||||||
v-else
|
<n-icon :color="props.color" :size="props.size">
|
||||||
:class="props.buttonClass"
|
|
||||||
:color="props.color"
|
|
||||||
:disabled="props.disabled"
|
|
||||||
:focusable="false"
|
|
||||||
:loading="loading"
|
|
||||||
:secondary="props.secondary"
|
|
||||||
:size="props.small ? 'small' : ''"
|
|
||||||
:style="props.buttonStyle"
|
|
||||||
:tertiary="props.tertiary"
|
|
||||||
:text="!props.border"
|
|
||||||
:type="props.type"
|
|
||||||
@click.prevent="emit('click')">
|
|
||||||
<template #icon>
|
|
||||||
<slot>
|
|
||||||
<n-icon :color="props.color || 'currentColor'" :size="props.size">
|
|
||||||
<component :is="props.icon" :stroke-width="props.strokeWidth" />
|
<component :is="props.icon" :stroke-width="props.strokeWidth" />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,139 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, h } from 'vue'
|
|
||||||
import { NSpace, useThemeVars } from 'naive-ui'
|
|
||||||
import { types, typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js'
|
|
||||||
import { get, isEmpty, map, toUpper } from 'lodash'
|
|
||||||
import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
default: 'ALL',
|
|
||||||
},
|
|
||||||
placement: {
|
|
||||||
type: String,
|
|
||||||
default: 'bottom-start',
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
disableTip: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:value', 'select'])
|
|
||||||
|
|
||||||
const options = computed(() => {
|
|
||||||
const opts = map(types, (v) => ({
|
|
||||||
label: v,
|
|
||||||
key: v,
|
|
||||||
}))
|
|
||||||
return [{ label: 'ALL', key: 'ALL' }, ...opts]
|
|
||||||
})
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
|
||||||
const renderIcon = (option) => {
|
|
||||||
return h(RedisTypeTag, {
|
|
||||||
type: option.key,
|
|
||||||
defaultLabel: 'A',
|
|
||||||
short: true,
|
|
||||||
size: 'small',
|
|
||||||
inverse: option.key === props.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderLabel = (option) => {
|
|
||||||
const children = [
|
|
||||||
h(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
fontWeight: option.key === props.value ? 'bold' : 'normal',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
option.label,
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
'div',
|
|
||||||
{ style: { width: '16px' } },
|
|
||||||
h(RedisTypeTag, {
|
|
||||||
type: toUpper(option.key),
|
|
||||||
point: true,
|
|
||||||
style: { display: option.key === props.value ? 'block' : 'none' },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
return h(NSpace, { align: 'center', wrapItem: false }, () => children)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fontColor = computed(() => {
|
|
||||||
return get(typesColor, props.value, '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const backgroundColor = computed(() => {
|
|
||||||
return get(typesBgColor, props.value, '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayValue = computed(() => {
|
|
||||||
return get(typesShortName, toUpper(props.value), 'A')
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSelect = (select) => {
|
|
||||||
if (props.value !== select) {
|
|
||||||
emit('update:value', select)
|
|
||||||
emit('select', select)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<template v-if="props.disabled">
|
|
||||||
<n-tooltip :disabled="isEmpty(props.disableTip)">
|
|
||||||
<div>{{ props.disableTip }}</div>
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag
|
|
||||||
:bordered="true"
|
|
||||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
|
||||||
class="redis-tag"
|
|
||||||
disabled
|
|
||||||
size="medium"
|
|
||||||
strong>
|
|
||||||
{{ displayValue }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-tooltip>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.redis-tag {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.dropdown-type-item) {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,125 +1,46 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js'
|
import { typesBgColor, typesColor, validType } from '@/consts/support_redis_type.js'
|
||||||
import Binary from '@/components/icons/Binary.vue'
|
|
||||||
import { get, toUpper } from 'lodash'
|
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import Loading from '@/components/icons/Loading.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
|
validator(value) {
|
||||||
|
return validType(value)
|
||||||
|
},
|
||||||
default: 'STRING',
|
default: 'STRING',
|
||||||
},
|
},
|
||||||
defaultLabel: String,
|
bordered: Boolean,
|
||||||
binaryKey: Boolean,
|
|
||||||
size: String,
|
size: String,
|
||||||
short: Boolean,
|
|
||||||
point: Boolean,
|
|
||||||
pointSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 14,
|
|
||||||
},
|
|
||||||
round: Boolean,
|
|
||||||
inverse: Boolean,
|
|
||||||
loading: Boolean,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
|
||||||
|
|
||||||
const fontColor = computed(() => {
|
const fontColor = computed(() => {
|
||||||
if (props.inverse) {
|
return typesColor[props.type]
|
||||||
return props.loading ? themeVars.value.tagColor : typesBgColor[props.type]
|
|
||||||
} else {
|
|
||||||
return props.loading ? themeVars.value.textColorBase : typesColor[props.type]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const backgroundColor = computed(() => {
|
const backgroundColor = computed(() => {
|
||||||
if (props.inverse) {
|
return typesBgColor[props.type]
|
||||||
return props.loading ? themeVars.value.textColorBase : typesColor[props.type]
|
|
||||||
} else {
|
|
||||||
return props.loading ? themeVars.value.tagColor : typesBgColor[props.type]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const label = computed(() => {
|
|
||||||
if (props.short) {
|
|
||||||
return get(typesShortName, toUpper(props.type), props.defaultLabel || 'N')
|
|
||||||
}
|
|
||||||
return toUpper(props.type)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
|
||||||
v-if="props.point"
|
|
||||||
:class="{ 'redis-type-tag-loading': props.loading }"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: fontColor,
|
|
||||||
width: Math.max(props.pointSize, 5) + 'px',
|
|
||||||
height: Math.max(props.pointSize, 5) + 'px',
|
|
||||||
}"
|
|
||||||
class="redis-type-tag-round redis-type-tag-point" />
|
|
||||||
<n-tag
|
<n-tag
|
||||||
v-else
|
:bordered="props.bordered"
|
||||||
:class="{
|
:class="[props.size === 'small' ? 'redis-type-tag-small' : 'redis-type-tag']"
|
||||||
'redis-type-tag-normal': !props.short && props.size !== 'small',
|
:color="{ color: backgroundColor, borderColor: fontColor, textColor: fontColor }"
|
||||||
'redis-type-tag-small': !props.short && props.size === 'small',
|
|
||||||
'redis-type-tag-round': props.round,
|
|
||||||
'redis-type-tag-loading': props.loading,
|
|
||||||
'redis-type-tag': props.short,
|
|
||||||
}"
|
|
||||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
|
||||||
:size="props.size"
|
:size="props.size"
|
||||||
bordered
|
|
||||||
strong>
|
strong>
|
||||||
<b v-if="!props.loading">{{ label }}</b>
|
{{ props.type }}
|
||||||
<n-icon v-else-if="props.short" size="14">
|
|
||||||
<loading stroke-width="4" />
|
|
||||||
</n-icon>
|
|
||||||
<b v-else>LOADING</b>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon v-if="binaryKey" :component="Binary" size="18" />
|
|
||||||
</template>
|
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
<!-- <div class="redis-type-tag flex-box-h" :style="{backgroundColor: backgroundColor}">{{ props.type }}</div>-->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.redis-type-tag-round {
|
.redis-type-tag {
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.redis-type-tag-normal {
|
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.redis-type-tag-small {
|
.redis-type-tag-small {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.redis-type-tag-loading {
|
|
||||||
animation: fadeInOut 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInOut {
|
|
||||||
0% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.redis-type-tag {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizeable component wrapper
|
|
||||||
*/
|
|
||||||
const themeVars = useThemeVars()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
size: {
|
|
||||||
type: Number,
|
|
||||||
default: 100,
|
|
||||||
},
|
|
||||||
minSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 300,
|
|
||||||
},
|
|
||||||
maxSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
borderWidth: {
|
|
||||||
type: Number,
|
|
||||||
default: 4,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:size'])
|
|
||||||
|
|
||||||
const resizing = ref(false)
|
|
||||||
const hover = ref(false)
|
|
||||||
|
|
||||||
const handleResize = (evt) => {
|
|
||||||
if (resizing.value) {
|
|
||||||
let size = evt.clientX - props.offset
|
|
||||||
if (size < props.minSize) {
|
|
||||||
size = props.minSize
|
|
||||||
}
|
|
||||||
if (props.maxSize > 0 && size > props.maxSize) {
|
|
||||||
size = props.maxSize
|
|
||||||
}
|
|
||||||
emit('update:size', size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopResize = () => {
|
|
||||||
resizing.value = false
|
|
||||||
document.removeEventListener('mousemove', handleResize)
|
|
||||||
document.removeEventListener('mouseup', stopResize)
|
|
||||||
}
|
|
||||||
|
|
||||||
const startResize = () => {
|
|
||||||
if (props.disabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resizing.value = true
|
|
||||||
document.addEventListener('mousemove', handleResize)
|
|
||||||
document.addEventListener('mouseup', stopResize)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseOver = () => {
|
|
||||||
if (props.disabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hover.value = true
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :style="{ width: props.size + 'px' }" class="resize-wrapper flex-box-h">
|
|
||||||
<slot></slot>
|
|
||||||
<div
|
|
||||||
:class="{
|
|
||||||
'resize-divider-hover': hover,
|
|
||||||
'resize-divider-drag': resizing,
|
|
||||||
dragging: hover || resizing,
|
|
||||||
}"
|
|
||||||
:style="{ width: props.borderWidth + 'px', right: Math.floor(-props.borderWidth / 2) + 'px' }"
|
|
||||||
class="resize-divider"
|
|
||||||
@mousedown="startResize"
|
|
||||||
@mouseout="hover = false"
|
|
||||||
@mouseover="handleMouseOver" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.resize-wrapper {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.resize-divider {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
transition: background-color 0.3s ease-in;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-divider-hide {
|
|
||||||
background-color: #0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-divider-hover {
|
|
||||||
background-color: v-bind('themeVars.borderColor');
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-divider-drag {
|
|
||||||
background-color: v-bind('themeVars.primaryColor');
|
|
||||||
}
|
|
||||||
|
|
||||||
.dragging {
|
|
||||||
cursor: col-resize !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,70 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { NIcon } from 'naive-ui'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
value: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: 'small',
|
|
||||||
},
|
|
||||||
icons: Array,
|
|
||||||
tTooltips: Array,
|
|
||||||
tTooltipPlacement: {
|
|
||||||
type: String,
|
|
||||||
default: 'bottom',
|
|
||||||
},
|
|
||||||
iconSize: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: 20,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: 'currentColor',
|
|
||||||
},
|
|
||||||
strokeWidth: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: 3,
|
|
||||||
},
|
|
||||||
unselectStrokeWidth: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: 3,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:value'])
|
|
||||||
|
|
||||||
const handleSwitch = (idx) => {
|
|
||||||
if (idx !== props.value) {
|
|
||||||
emit('update:value', idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-button-group>
|
|
||||||
<n-tooltip
|
|
||||||
v-for="(icon, i) in props.icons"
|
|
||||||
:key="i"
|
|
||||||
:disabled="!(props.tTooltips && props.tTooltips[i])"
|
|
||||||
:placement="props.tTooltipPlacement"
|
|
||||||
:show-arrow="false">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button :focusable="false" :size="props.size" :tertiary="i !== props.value" @click="handleSwitch(i)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :size="props.iconSize">
|
|
||||||
<component
|
|
||||||
:is="icon"
|
|
||||||
:stroke-width="i !== props.value ? props.unselectStrokeWidth : props.strokeWidth" />
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
{{ props.tTooltips ? $t(props.tTooltips[i]) : '' }}
|
|
||||||
</n-tooltip>
|
|
||||||
</n-button-group>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
@ -4,8 +4,7 @@ import WindowMax from '@/components/icons/WindowMax.vue'
|
|||||||
import WindowClose from '@/components/icons/WindowClose.vue'
|
import WindowClose from '@/components/icons/WindowClose.vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import { Quit, WindowMinimise, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
|
import { Quit, WindowIsMaximised, WindowMinimise, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
|
||||||
import WindowRestore from '@/components/icons/WindowRestore.vue'
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -13,9 +12,6 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 35,
|
default: 35,
|
||||||
},
|
},
|
||||||
maximised: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const buttonSize = computed(() => {
|
const buttonSize = computed(() => {
|
||||||
@ -36,8 +32,8 @@ const handleClose = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-space :size="0" :wrap-item="false" align="center" justify="center">
|
<n-space :wrap-item="false" align="center" justify="center" :size="0">
|
||||||
<n-tooltip :delay="1000" :show-arrow="false">
|
<n-tooltip :show-arrow="false">
|
||||||
{{ $t('menu.minimise') }}
|
{{ $t('menu.minimise') }}
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="btn-wrapper" @click="handleMinimise">
|
<div class="btn-wrapper" @click="handleMinimise">
|
||||||
@ -45,23 +41,15 @@ const handleClose = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
<n-tooltip v-if="maximised" :delay="1000" :show-arrow="false">
|
<n-tooltip :show-arrow="false">
|
||||||
{{ $t('menu.restore') }}
|
{{ WindowIsMaximised() ? $t('menu.restore') : $t('menu.maximise') }}
|
||||||
<template #trigger>
|
|
||||||
<div class="btn-wrapper" @click="handleMaximise">
|
|
||||||
<window-restore />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-tooltip>
|
|
||||||
<n-tooltip v-else :delay="1000" :show-arrow="false">
|
|
||||||
{{ $t('menu.maximise') }}
|
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="btn-wrapper" @click="handleMaximise">
|
<div class="btn-wrapper" @click="handleMaximise">
|
||||||
<window-max />
|
<window-max />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
<n-tooltip :delay="1000" :show-arrow="false">
|
<n-tooltip :show-arrow="false">
|
||||||
{{ $t('menu.close') }}
|
{{ $t('menu.close') }}
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="btn-wrapper" @click="handleClose">
|
<div class="btn-wrapper" @click="handleClose">
|
||||||
@ -72,7 +60,7 @@ const handleClose = () => {
|
|||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped lang="scss">
|
||||||
.btn-wrapper {
|
.btn-wrapper {
|
||||||
width: v-bind('buttonSize');
|
width: v-bind('buttonSize');
|
||||||
height: v-bind('buttonSize');
|
height: v-bind('buttonSize');
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
value: {
|
|
||||||
type: Number,
|
|
||||||
default: -1,
|
|
||||||
},
|
|
||||||
unit: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:value', 'update:unit'])
|
|
||||||
|
|
||||||
const unit = [
|
|
||||||
{
|
|
||||||
value: 1,
|
|
||||||
label: 'common.second',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 60,
|
|
||||||
label: 'common.minute',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 3600,
|
|
||||||
label: 'common.hour',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 86400,
|
|
||||||
label: 'common.day',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const unitValue = computed(() => {
|
|
||||||
switch (props.unit) {
|
|
||||||
case 60:
|
|
||||||
return 60
|
|
||||||
case 3600:
|
|
||||||
return 3600
|
|
||||||
case 86400:
|
|
||||||
return 86400
|
|
||||||
default:
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-number
|
|
||||||
:max="Number.MAX_SAFE_INTEGER"
|
|
||||||
:min="-1"
|
|
||||||
:show-button="false"
|
|
||||||
:value="props.value"
|
|
||||||
class="flex-item-expand"
|
|
||||||
@update:value="(val) => emit('update:value', val)" />
|
|
||||||
<n-select
|
|
||||||
:options="unit"
|
|
||||||
:render-label="({ label }) => $t(label)"
|
|
||||||
:value="unitValue"
|
|
||||||
style="max-width: 150px"
|
|
||||||
@update:value="(val) => emit('update:unit', val)" />
|
|
||||||
</n-input-group>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
@ -1,17 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h, nextTick, reactive, ref } from 'vue'
|
import { computed, nextTick, reactive, ref } from 'vue'
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
import IconButton from '@/components/common/IconButton.vue'
|
||||||
import Refresh from '@/components/icons/Refresh.vue'
|
import Refresh from '@/components/icons/Refresh.vue'
|
||||||
import { map, size, split, uniqBy } from 'lodash'
|
import useConnectionStore from 'stores/connections.js'
|
||||||
|
import { map, uniqBy } from 'lodash'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import Delete from '@/components/icons/Delete.vue'
|
import Delete from '@/components/icons/Delete.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import useBrowserStore from 'stores/browser.js'
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const connectionStore = useConnectionStore()
|
||||||
|
|
||||||
const browserStore = useBrowserStore()
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
loading: false,
|
loading: false,
|
||||||
@ -26,7 +23,7 @@ const filterServerOption = computed(() => {
|
|||||||
value: server,
|
value: server,
|
||||||
}))
|
}))
|
||||||
options.splice(0, 0, {
|
options.splice(0, 0, {
|
||||||
label: 'common.all',
|
label: i18n.t('common.all'),
|
||||||
value: '',
|
value: '',
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
@ -34,116 +31,54 @@ const filterServerOption = computed(() => {
|
|||||||
|
|
||||||
const tableRef = ref(null)
|
const tableRef = ref(null)
|
||||||
|
|
||||||
const columns = computed(() => [
|
const loadHistory = () => {
|
||||||
{
|
|
||||||
title: () => i18n.t('log.exec_time'),
|
|
||||||
key: 'timestamp',
|
|
||||||
defaultSortOrder: 'ascend',
|
|
||||||
sorter: 'default',
|
|
||||||
width: 180,
|
|
||||||
align: 'center',
|
|
||||||
titleAlign: 'center',
|
|
||||||
render: ({ timestamp }, index) => {
|
|
||||||
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: () => i18n.t('log.server'),
|
|
||||||
key: 'server',
|
|
||||||
filterOptionValue: data.server,
|
|
||||||
filter: (value, row) => {
|
|
||||||
return value === '' || row.server === value.toString()
|
|
||||||
},
|
|
||||||
width: 150,
|
|
||||||
align: 'center',
|
|
||||||
titleAlign: 'center',
|
|
||||||
ellipsis: {
|
|
||||||
tooltip: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: () => i18n.t('log.cmd'),
|
|
||||||
key: 'cmd',
|
|
||||||
titleAlign: 'center',
|
|
||||||
filterOptionValue: data.keyword,
|
|
||||||
resizable: true,
|
|
||||||
filter: (value, row) => {
|
|
||||||
return value === '' || !!~row.cmd.indexOf(value.toString())
|
|
||||||
},
|
|
||||||
render: ({ cmd }, index) => {
|
|
||||||
const cmdList = split(cmd, '\n')
|
|
||||||
if (size(cmdList) > 1) {
|
|
||||||
return h(
|
|
||||||
'div',
|
|
||||||
null,
|
|
||||||
map(cmdList, (c) => h('div', { class: 'cmd-line' }, c)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return h('div', { class: 'cmd-line' }, cmd)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: () => i18n.t('log.cost_time'),
|
|
||||||
key: 'cost',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
titleAlign: 'center',
|
|
||||||
render: ({ cost }, index) => {
|
|
||||||
const ms = dayjs.duration(cost).asMilliseconds()
|
|
||||||
if (ms < 1000) {
|
|
||||||
return `${ms} ms`
|
|
||||||
} else {
|
|
||||||
return `${Math.floor(ms / 1000)} s`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const loadHistory = async () => {
|
|
||||||
try {
|
|
||||||
await nextTick()
|
|
||||||
data.loading = true
|
data.loading = true
|
||||||
const list = await browserStore.getCmdHistory()
|
connectionStore
|
||||||
|
.getCmdHistory()
|
||||||
|
.then((list) => {
|
||||||
data.history = list || []
|
data.history = list || []
|
||||||
} finally {
|
})
|
||||||
|
.finally(() => {
|
||||||
data.loading = false
|
data.loading = false
|
||||||
await nextTick()
|
tableRef.value?.scrollTo({ top: 999999 })
|
||||||
tableRef.value?.scrollTo({ position: 'bottom' })
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanHistory = async () => {
|
const cleanHistory = async () => {
|
||||||
$dialog.warning(i18n.t('log.confirm_clean_log'), async () => {
|
$dialog.warning(i18n.t('log.confirm_clean_log'), () => {
|
||||||
try {
|
|
||||||
data.loading = true
|
data.loading = true
|
||||||
const success = await browserStore.cleanCmdHistory()
|
connectionStore
|
||||||
|
.cleanCmdHistory()
|
||||||
|
.then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
data.history = []
|
data.history = []
|
||||||
await nextTick()
|
tableRef.value?.scrollTo({ top: 0 })
|
||||||
tableRef.value?.scrollTo({ position: 'top' })
|
$message.success(i18n.t('common.success'))
|
||||||
$message.success(i18n.t('dialogue.handle_succ'))
|
|
||||||
}
|
}
|
||||||
} finally {
|
})
|
||||||
|
.finally(() => {
|
||||||
data.loading = false
|
data.loading = false
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refresh: loadHistory,
|
refresh: () => nextTick().then(loadHistory),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="content-log content-container content-value fill-height flex-box-v">
|
<n-card
|
||||||
<n-h3>{{ $t('log.title') }}</n-h3>
|
:title="$t('log.launch_log')"
|
||||||
|
:bordered="false"
|
||||||
|
class="content-container flex-box-v"
|
||||||
|
content-style="display: flex;flex-direction: column; overflow: hidden;">
|
||||||
<n-form :disabled="data.loading" class="flex-item" inline>
|
<n-form :disabled="data.loading" class="flex-item" inline>
|
||||||
<n-form-item :label="$t('log.filter_server')">
|
<n-form-item :label="$t('log.filter_server')">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="data.server"
|
v-model:value="data.server"
|
||||||
:consistent-menu-width="false"
|
:consistent-menu-width="false"
|
||||||
:options="filterServerOption"
|
:options="filterServerOption"
|
||||||
:render-label="({ label, value }) => (value === '' ? $t(label) : label)"
|
|
||||||
style="min-width: 100px" />
|
style="min-width: 100px" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item :label="$t('log.filter_keyword')">
|
<n-form-item :label="$t('log.filter_keyword')">
|
||||||
@ -156,17 +91,72 @@ defineExpose({
|
|||||||
<icon-button :icon="Delete" border t-tooltip="log.clean_log" @click="cleanHistory" />
|
<icon-button :icon="Delete" border t-tooltip="log.clean_log" @click="cleanHistory" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
<div class="content-value fill-height flex-box-h">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
:columns="columns"
|
:columns="[
|
||||||
|
{
|
||||||
|
title: $t('log.exec_time'),
|
||||||
|
key: 'timestamp',
|
||||||
|
defaultSortOrder: 'ascend',
|
||||||
|
sorter: 'default',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
render({ timestamp }, index) {
|
||||||
|
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('log.server'),
|
||||||
|
key: 'server',
|
||||||
|
filterOptionValue: data.server,
|
||||||
|
filter(value, row) {
|
||||||
|
return value === '' || row.server === value.toString()
|
||||||
|
},
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('log.cmd'),
|
||||||
|
key: 'cmd',
|
||||||
|
titleAlign: 'center',
|
||||||
|
filterOptionValue: data.keyword,
|
||||||
|
resizable: true,
|
||||||
|
filter(value, row) {
|
||||||
|
return value === '' || !!~row.cmd.indexOf(value.toString())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('log.cost_time'),
|
||||||
|
key: 'cost',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
render({ cost }, index) {
|
||||||
|
const ms = dayjs.duration(cost).asMilliseconds()
|
||||||
|
if (ms < 1000) {
|
||||||
|
return `${ms} ms`
|
||||||
|
} else {
|
||||||
|
return `${Math.floor(ms / 1000)} s`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
:data="data.history"
|
:data="data.history"
|
||||||
:loading="data.loading"
|
|
||||||
class="flex-item-expand"
|
class="flex-item-expand"
|
||||||
flex-height
|
flex-height />
|
||||||
virtual-scroll />
|
|
||||||
</div>
|
</div>
|
||||||
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '@/styles/content';
|
@import '@/styles/content';
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
padding: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,38 +1,73 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { find, map, toUpper } from 'lodash'
|
import { types } from '@/consts/support_redis_type.js'
|
||||||
|
import ContentValueHash from '@/components/content_value/ContentValueHash.vue'
|
||||||
|
import ContentValueList from '@/components/content_value/ContentValueList.vue'
|
||||||
|
import ContentValueString from '@/components/content_value/ContentValueString.vue'
|
||||||
|
import ContentValueSet from '@/components/content_value/ContentValueSet.vue'
|
||||||
|
import ContentValueZset from '@/components/content_value/ContentValueZSet.vue'
|
||||||
|
import { isEmpty, map, toUpper } from 'lodash'
|
||||||
import useTabStore from 'stores/tab.js'
|
import useTabStore from 'stores/tab.js'
|
||||||
|
import useConnectionStore from 'stores/connections.js'
|
||||||
import ContentServerStatus from '@/components/content_value/ContentServerStatus.vue'
|
import ContentServerStatus from '@/components/content_value/ContentServerStatus.vue'
|
||||||
import Status from '@/components/icons/Status.vue'
|
import ContentValueStream from '@/components/content_value/ContentValueStream.vue'
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import { BrowserTabType } from '@/consts/browser_tab_type.js'
|
|
||||||
import Terminal from '@/components/icons/Terminal.vue'
|
|
||||||
import Log from '@/components/icons/Log.vue'
|
|
||||||
import Detail from '@/components/icons/Detail.vue'
|
|
||||||
import ContentValueWrapper from '@/components/content_value/ContentValueWrapper.vue'
|
|
||||||
import ContentCli from '@/components/content_value/ContentCli.vue'
|
|
||||||
import Monitor from '@/components/icons/Monitor.vue'
|
|
||||||
import ContentSlog from '@/components/content_value/ContentSlog.vue'
|
|
||||||
import ContentMonitor from '@/components/content_value/ContentMonitor.vue'
|
|
||||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
|
||||||
import ContentPubsub from '@/components/content_value/ContentPubsub.vue'
|
|
||||||
import Subscribe from '@/components/icons/Subscribe.vue'
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const serverInfo = ref({})
|
||||||
|
const autoRefresh = ref(false)
|
||||||
|
const serverName = computed(() => {
|
||||||
|
if (tabContent.value != null) {
|
||||||
|
return tabContent.value.name
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
const loadingServerInfo = ref(false)
|
||||||
|
const autoLoadingServerInfo = ref(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ServerStatusItem
|
* refresh server status info
|
||||||
* @property {string} name
|
* @param {boolean} [force] force refresh will show loading indicator
|
||||||
* @property {Object} info
|
* @returns {Promise<void>}
|
||||||
* @property {boolean} autoRefresh
|
|
||||||
* @property {boolean} loading loading status for refresh
|
|
||||||
* @property {boolean} autoLoading loading status for auto refresh
|
|
||||||
*/
|
*/
|
||||||
|
const refreshInfo = async (force) => {
|
||||||
|
if (force) {
|
||||||
|
loadingServerInfo.value = true
|
||||||
|
} else {
|
||||||
|
autoLoadingServerInfo.value = true
|
||||||
|
}
|
||||||
|
if (!isEmpty(serverName.value) && connectionStore.isConnected(serverName.value)) {
|
||||||
|
try {
|
||||||
|
serverInfo.value = await connectionStore.getServerInfo(serverName.value)
|
||||||
|
} finally {
|
||||||
|
loadingServerInfo.value = false
|
||||||
|
autoLoadingServerInfo.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
let intervalId
|
||||||
server: String,
|
onMounted(() => {
|
||||||
|
refreshInfo(true)
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
if (autoRefresh.value) {
|
||||||
|
refreshInfo()
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const valueComponents = {
|
||||||
|
[types.STRING]: ContentValueString,
|
||||||
|
[types.HASH]: ContentValueHash,
|
||||||
|
[types.LIST]: ContentValueList,
|
||||||
|
[types.SET]: ContentValueSet,
|
||||||
|
[types.ZSET]: ContentValueZset,
|
||||||
|
[types.STREAM]: ContentValueStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const tab = computed(() =>
|
const tab = computed(() =>
|
||||||
map(tabStore.tabs, (item) => ({
|
map(tabStore.tabs, (item) => ({
|
||||||
@ -41,201 +76,114 @@ const tab = computed(() =>
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
const tabContent = computed(() => {
|
|
||||||
const tab = find(tabStore.tabs, { name: props.server })
|
|
||||||
if (tab == null) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: tab.name,
|
|
||||||
subTab: tab.subTab,
|
|
||||||
type: toUpper(tab.type),
|
|
||||||
db: tab.db,
|
|
||||||
keyPath: tab.keyCode != null ? decodeRedisKey(tab.keyCode) : tab.key,
|
|
||||||
keyCode: tab.keyCode,
|
|
||||||
ttl: tab.ttl,
|
|
||||||
value: tab.value,
|
|
||||||
size: tab.size || 0,
|
|
||||||
length: tab.length || 0,
|
|
||||||
decode: tab.decode,
|
|
||||||
format: tab.format,
|
|
||||||
matchPattern: tab.matchPattern || '',
|
|
||||||
end: tab.end === true,
|
|
||||||
loading: tab.loading === true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isBlankValue = computed(() => {
|
|
||||||
return tabContent.value?.keyPath == null
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedSubTab = computed(() => {
|
|
||||||
const { subTab = BrowserTabType.Status } = tabStore.currentTab || {}
|
|
||||||
return subTab
|
|
||||||
})
|
|
||||||
|
|
||||||
// BUG: naive-ui tabs will set the bottom line to '0px' after switch to another page and back again
|
|
||||||
// watch parent tabs' changing and call 'syncBarPosition' manually
|
|
||||||
const tabsRef = ref(null)
|
|
||||||
const cliRef = ref(null)
|
|
||||||
watch(
|
watch(
|
||||||
() => tabContent.value?.name,
|
() => tabStore.nav,
|
||||||
(name) => {
|
(nav) => {
|
||||||
if (name === props.server) {
|
if (nav === 'browser') {
|
||||||
nextTick().then(() => {
|
refreshInfo()
|
||||||
tabsRef.value?.syncBarPosition()
|
|
||||||
cliRef.value?.resizeTerm()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tabContent = computed(() => {
|
||||||
|
const tab = tabStore.currentTab
|
||||||
|
if (tab == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: tab.name,
|
||||||
|
type: toUpper(tab.type),
|
||||||
|
db: tab.db,
|
||||||
|
keyPath: tab.key,
|
||||||
|
ttl: tab.ttl,
|
||||||
|
value: tab.value,
|
||||||
|
size: tab.size || 0,
|
||||||
|
viewAs: tab.viewAs,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const showServerStatus = computed(() => {
|
||||||
|
return tabContent.value == null || isEmpty(tabContent.value.keyPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showNonexists = computed(() => {
|
||||||
|
return tabContent.value.value == null
|
||||||
|
})
|
||||||
|
|
||||||
|
const onUpdateValue = (tabIndex) => {
|
||||||
|
tabStore.switchTab(tabIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reload current selection key
|
||||||
|
* @returns {Promise<null>}
|
||||||
|
*/
|
||||||
|
const onReloadKey = async () => {
|
||||||
|
const tab = tabStore.currentTab
|
||||||
|
if (tab == null || isEmpty(tab.key)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key, tab.viewAs)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="content-container flex-box-v">
|
<div class="content-container flex-box-v">
|
||||||
<n-tabs
|
<div v-if="showServerStatus" class="content-container flex-item-expand flex-box-v">
|
||||||
ref="tabsRef"
|
<!-- select nothing or select server node, display server status -->
|
||||||
:tabs-padding="5"
|
|
||||||
:theme-overrides="{
|
|
||||||
tabFontWeightActive: 'normal',
|
|
||||||
tabGapSmallLine: '10px',
|
|
||||||
tabGapMediumLine: '10px',
|
|
||||||
tabGapLargeLine: '10px',
|
|
||||||
}"
|
|
||||||
:value="selectedSubTab"
|
|
||||||
class="content-sub-tab"
|
|
||||||
:default-value="BrowserTabType.Status.toString()"
|
|
||||||
pane-class="content-sub-tab-pane"
|
|
||||||
placement="top"
|
|
||||||
tab-style="padding-left: 10px; padding-right: 10px;"
|
|
||||||
type="line"
|
|
||||||
@update:value="tabStore.switchSubTab">
|
|
||||||
<!-- server status pane -->
|
|
||||||
<n-tab-pane :name="BrowserTabType.Status.toString()" display-directive="show:lazy">
|
|
||||||
<template #tab>
|
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
|
||||||
<n-icon size="16">
|
|
||||||
<status
|
|
||||||
:inverse="selectedSubTab === BrowserTabType.Status.toString()"
|
|
||||||
:stroke-color="themeVars.tabColor"
|
|
||||||
stroke-width="4" />
|
|
||||||
</n-icon>
|
|
||||||
<span>{{ $t('interface.sub_tab.status') }}</span>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<content-server-status
|
<content-server-status
|
||||||
:pause="selectedSubTab !== BrowserTabType.Status.toString()"
|
v-model:auto-refresh="autoRefresh"
|
||||||
:server="props.server" />
|
:info="serverInfo"
|
||||||
</n-tab-pane>
|
:loading="loadingServerInfo"
|
||||||
|
:auto-loading="autoLoadingServerInfo"
|
||||||
<!-- key detail pane -->
|
:server="serverName"
|
||||||
<n-tab-pane :name="BrowserTabType.KeyDetail.toString()" display-directive="show:lazy">
|
@refresh="refreshInfo(true)" />
|
||||||
<template #tab>
|
</div>
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
<div v-else-if="showNonexists" class="content-container flex-item-expand flex-box-v">
|
||||||
<n-icon size="16">
|
<n-empty :description="$t('interface.nonexist_tab_content')" class="empty-content">
|
||||||
<detail
|
<template #extra>
|
||||||
:inverse="selectedSubTab === BrowserTabType.KeyDetail.toString()"
|
<n-button :focusable="false" @click="onReloadKey">{{ $t('interface.reload') }}</n-button>
|
||||||
:stroke-color="themeVars.tabColor"
|
|
||||||
stroke-width="4" />
|
|
||||||
</n-icon>
|
|
||||||
<span>{{ $t('interface.sub_tab.key_detail') }}</span>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
</template>
|
||||||
<content-value-wrapper :blank="isBlankValue" :content="tabContent" />
|
</n-empty>
|
||||||
</n-tab-pane>
|
</div>
|
||||||
|
<component
|
||||||
<!-- cli pane -->
|
:is="valueComponents[tabContent.type]"
|
||||||
<n-tab-pane :name="BrowserTabType.Cli.toString()" display-directive="show:lazy">
|
v-else
|
||||||
<template #tab>
|
:db="tabContent.db"
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
:key-path="tabContent.keyPath"
|
||||||
<n-icon size="16">
|
:name="tabContent.name"
|
||||||
<terminal
|
:ttl="tabContent.ttl"
|
||||||
:inverse="selectedSubTab === BrowserTabType.Cli.toString()"
|
:value="tabContent.value"
|
||||||
:stroke-color="themeVars.tabColor"
|
:size="tabContent.size"
|
||||||
stroke-width="4" />
|
:view-as="tabContent.viewAs" />
|
||||||
</n-icon>
|
|
||||||
<span>{{ $t('interface.sub_tab.cli') }}</span>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<content-cli ref="cliRef" :name="props.server" />
|
|
||||||
</n-tab-pane>
|
|
||||||
|
|
||||||
<!-- slow log pane -->
|
|
||||||
<n-tab-pane :name="BrowserTabType.SlowLog.toString()" display-directive="show:lazy">
|
|
||||||
<template #tab>
|
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
|
||||||
<n-icon size="16">
|
|
||||||
<log
|
|
||||||
:inverse="selectedSubTab === BrowserTabType.SlowLog.toString()"
|
|
||||||
:stroke-color="themeVars.tabColor"
|
|
||||||
stroke-width="4" />
|
|
||||||
</n-icon>
|
|
||||||
<span>{{ $t('interface.sub_tab.slow_log') }}</span>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<content-slog :server="props.server" />
|
|
||||||
</n-tab-pane>
|
|
||||||
|
|
||||||
<!-- command monitor pane -->
|
|
||||||
<n-tab-pane :name="BrowserTabType.CmdMonitor.toString()" display-directive="show:lazy">
|
|
||||||
<template #tab>
|
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
|
||||||
<n-icon size="16">
|
|
||||||
<monitor
|
|
||||||
:inverse="selectedSubTab === BrowserTabType.CmdMonitor.toString()"
|
|
||||||
:stroke-color="themeVars.tabColor"
|
|
||||||
stroke-width="4" />
|
|
||||||
</n-icon>
|
|
||||||
<span>{{ $t('interface.sub_tab.cmd_monitor') }}</span>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<content-monitor :server="props.server" />
|
|
||||||
</n-tab-pane>
|
|
||||||
|
|
||||||
<!-- pub/sub message pane -->
|
|
||||||
<n-tab-pane :name="BrowserTabType.PubMessage.toString()" display-directive="show:lazy">
|
|
||||||
<template #tab>
|
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
|
||||||
<n-icon size="16">
|
|
||||||
<subscribe
|
|
||||||
:inverse="selectedSubTab === BrowserTabType.PubMessage.toString()"
|
|
||||||
:stroke-color="themeVars.tabColor"
|
|
||||||
stroke-width="4" />
|
|
||||||
</n-icon>
|
|
||||||
<span>{{ $t('interface.sub_tab.pub_message') }}</span>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<content-pubsub :server="props.server" />
|
|
||||||
</n-tab-pane>
|
|
||||||
</n-tabs>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '@/styles/content';
|
@import '@/styles/content';
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
//padding: 5px 5px 0;
|
padding: 5px;
|
||||||
//padding-top: 0;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: v-bind('themeVars.tabColor');
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.content-sub-tab {
|
|
||||||
background-color: v-bind('themeVars.tabColor');
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-sub-tab-pane {
|
|
||||||
padding: 0 !important;
|
|
||||||
height: 100%;
|
|
||||||
background-color: v-bind('themeVars.bodyColor');
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-tabs .n-tabs-bar {
|
|
||||||
transition: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//.tab-item {
|
||||||
|
// gap: 5px;
|
||||||
|
// padding: 0 5px 0 10px;
|
||||||
|
// align-items: center;
|
||||||
|
// max-width: 150px;
|
||||||
|
//
|
||||||
|
// transition: all var(--transition-duration-fast) var(--transition-function-ease-in-out-bezier);
|
||||||
|
//
|
||||||
|
// &-label {
|
||||||
|
// font-size: 15px;
|
||||||
|
// text-align: center;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// &-close {
|
||||||
|
// &:hover {
|
||||||
|
// background-color: rgb(176, 177, 182, 0.4);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,32 +1,8 @@
|
|||||||
<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>
|
||||||
@ -42,27 +18,16 @@ const sponsorAd = computed(() => {
|
|||||||
</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>
|
||||||
@use '@/styles/content';
|
@import '@/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 {
|
||||||
|
@ -1,42 +1,44 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Server from '@/components/icons/Server.vue'
|
import ToggleServer from '@/components/icons/ToggleServer.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 usePreferencesStore from 'stores/preferences.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 prefStore = usePreferencesStore()
|
|
||||||
|
|
||||||
const onCloseTab = (tabIndex) => {
|
const onCloseTab = (tabIndex) => {
|
||||||
|
$dialog.warning(i18n.t('dialogue.close_confirm'), () => {
|
||||||
const tab = get(tabStore.tabs, tabIndex)
|
const tab = get(tabStore.tabs, tabIndex)
|
||||||
tabStore.closeTab(tab.name)
|
if (tab != null) {
|
||||||
|
connectionStore.closeConnection(tab.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabMarkColor = computed(() => {
|
|
||||||
const { name } = tabStore?.currentTab || {}
|
|
||||||
const { markColor = '' } = connectionStore.serverProfile[name] || {}
|
|
||||||
return markColor
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const tabClass = (idx) => {
|
const activeTabStyle = computed(() => {
|
||||||
if (tabStore.activatedIndex === idx) {
|
const { name } = tabStore.currentTab
|
||||||
return ['value-tab', 'value-tab-active', tabMarkColor.value ? 'value-tab-active_mark' : '']
|
const { markColor = '' } = connectionStore.serverProfile[name] || {}
|
||||||
} else if (tabStore.activatedIndex - 1 === idx) {
|
return {
|
||||||
return ['value-tab', 'value-tab-inactive']
|
backgroundColor: themeVars.value.bodyColor,
|
||||||
} else {
|
borderTopWidth: markColor ? '3px' : '1px',
|
||||||
return ['value-tab', 'value-tab-inactive', 'value-tab-inactive2']
|
borderTopColor: markColor || themeVars.value.borderColor,
|
||||||
}
|
borderBottomColor: themeVars.value.bodyColor,
|
||||||
|
borderTopLeftRadius: themeVars.value.borderRadius,
|
||||||
|
borderTopRightRadius: themeVars.value.borderRadius,
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
const inactiveTabStyle = computed(() => ({
|
||||||
|
borderWidth: '0 0 1px',
|
||||||
|
// borderBottomColor: themeVars.value.borderColor,
|
||||||
|
borderTopLeftRadius: themeVars.value.borderRadius,
|
||||||
|
borderTopRightRadius: themeVars.value.borderRadius,
|
||||||
|
}))
|
||||||
|
|
||||||
const tab = computed(() =>
|
const tab = computed(() =>
|
||||||
map(tabStore.tabs, (item) => ({
|
map(tabStore.tabs, (item) => ({
|
||||||
@ -44,78 +46,47 @@ const tab = computed(() =>
|
|||||||
label: item.title,
|
label: item.title,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
const exThemeVars = computed(() => {
|
|
||||||
return extraTheme(prefStore.isDark)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tabs
|
<n-tabs
|
||||||
v-model:value="tabStore.activatedIndex"
|
v-model:value="tabStore.activatedIndex"
|
||||||
:closable="true"
|
:closable="true"
|
||||||
:tabs-padding="3"
|
:tab-style="{
|
||||||
:theme-overrides="{
|
borderStyle: 'solid',
|
||||||
tabFontWeightActive: 800,
|
borderWidth: '1px',
|
||||||
tabGapSmallCard: 0,
|
borderLeftColor: themeVars.borderColor,
|
||||||
tabGapMediumCard: 0,
|
borderRightColor: themeVars.borderColor,
|
||||||
tabGapLargeCard: 0,
|
|
||||||
tabColor: '#0000',
|
|
||||||
tabBorderColor: '#0000',
|
|
||||||
tabTextColorCard: themeVars.closeIconColor,
|
|
||||||
}"
|
}"
|
||||||
size="small"
|
size="small"
|
||||||
type="card"
|
type="card"
|
||||||
@close="onCloseTab"
|
@close="onCloseTab"
|
||||||
@update:value="(tabIndex) => tabStore.switchTab(tabIndex)">
|
@update:value="(tabIndex) => tabStore.switchTab(tabIndex)"
|
||||||
<n-tab v-for="(t, i) in tab" :key="i" :class="tabClass(i)" :closable="true" :name="i" @dblclick.stop="() => {}">
|
:theme-overrides="{
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
tabFontWeightActive: 800,
|
||||||
<n-icon size="18">
|
tabBorderRadius: 0,
|
||||||
<server stroke-width="4" />
|
tabGapSmallCard: 0,
|
||||||
</n-icon>
|
tabGapMediumCard: 0,
|
||||||
|
tabGapLargeCard: 0,
|
||||||
|
tabColor: '#0000',
|
||||||
|
// tabBorderColor: themeVars.borderColor,
|
||||||
|
tabBorderColor: '#0000',
|
||||||
|
tabTextColorCard: themeVars.closeIconColor,
|
||||||
|
}">
|
||||||
|
<n-tab
|
||||||
|
v-for="(t, i) in tab"
|
||||||
|
:key="i"
|
||||||
|
:name="i"
|
||||||
|
:closable="tabStore.activatedIndex === i"
|
||||||
|
:style="tabStore.activatedIndex === i ? activeTabStyle : inactiveTabStyle"
|
||||||
|
style="--wails-draggable: none"
|
||||||
|
@dblclick.stop="() => {}">
|
||||||
|
<n-space align="center" justify="center" :wrap-item="false" :size="5" inline>
|
||||||
|
<n-icon :component="ToggleServer" size="18" />
|
||||||
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
|
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-tab>
|
</n-tab>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style scoped lang="scss"></style>
|
||||||
.value-tab {
|
|
||||||
--wails-draggable: none;
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid v-bind('exThemeVars.splitColor') !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-tab-active {
|
|
||||||
background-color: v-bind('themeVars.tabColor') !important;
|
|
||||||
border-bottom-color: v-bind('themeVars.tabColor') !important;
|
|
||||||
|
|
||||||
&_mark {
|
|
||||||
border-top: 3px solid v-bind('tabMarkColor') !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-tab-inactive {
|
|
||||||
border-color: #0000 !important;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: v-bind('exThemeVars.splitColor') !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-tab-inactive2 {
|
|
||||||
&:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 25%;
|
|
||||||
height: 50%;
|
|
||||||
width: 1px;
|
|
||||||
background-color: v-bind('themeVars.borderColor');
|
|
||||||
right: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover::after {
|
|
||||||
background-color: #0000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,589 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { Terminal } from 'xterm'
|
|
||||||
import { FitAddon } from 'xterm-addon-fit'
|
|
||||||
import { computed, defineExpose, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import 'xterm/css/xterm.css'
|
|
||||||
import { EventsEmit, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import { get, isEmpty, set } from 'lodash'
|
|
||||||
import { CloseCli, StartCli } from 'wailsjs/go/services/cliService.js'
|
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
|
||||||
import { i18nGlobal } from '@/utils/i18n.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
name: String,
|
|
||||||
activated: Boolean,
|
|
||||||
})
|
|
||||||
|
|
||||||
const prefStore = usePreferencesStore()
|
|
||||||
const termRef = ref(null)
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {xterm.Terminal|null}
|
|
||||||
*/
|
|
||||||
let termInst = null
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {xterm-addon-fit.FitAddon|null}
|
|
||||||
*/
|
|
||||||
let fitAddonInst = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @return {{fitAddon: xterm-addon-fit.FitAddon, term: Terminal}}
|
|
||||||
*/
|
|
||||||
const newTerm = () => {
|
|
||||||
const { fontSize = 14, fontFamily = 'Courier New' } = prefStore.cliFont
|
|
||||||
const term = new Terminal({
|
|
||||||
allowProposedApi: true,
|
|
||||||
fontFamily,
|
|
||||||
fontSize,
|
|
||||||
cursorStyle: prefStore.cli.cursorStyle || 'block',
|
|
||||||
cursorBlink: true,
|
|
||||||
disableStdin: false,
|
|
||||||
screenReaderMode: true,
|
|
||||||
// LogLevel: 'debug',
|
|
||||||
theme: {
|
|
||||||
// foreground: '#ECECEC',
|
|
||||||
background: '#000000',
|
|
||||||
// cursor: 'help',
|
|
||||||
// lineHeight: 20,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const fitAddon = new FitAddon()
|
|
||||||
term.open(termRef.value)
|
|
||||||
term.loadAddon(fitAddon)
|
|
||||||
|
|
||||||
term.onData(onTermData)
|
|
||||||
term.attachCustomKeyEventHandler(onTermKey)
|
|
||||||
return { term, fitAddon }
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const { term, fitAddon } = newTerm()
|
|
||||||
termInst = term
|
|
||||||
fitAddonInst = fitAddon
|
|
||||||
window.addEventListener('resize', resizeTerm)
|
|
||||||
|
|
||||||
term.writeln('\r\n' + i18nGlobal.t('interface.cli_welcome'))
|
|
||||||
// term.write('\x1b[4h') // insert mode
|
|
||||||
CloseCli(props.name)
|
|
||||||
StartCli(props.name, 0)
|
|
||||||
|
|
||||||
EventsOn(`cmd:output:${props.name}`, receiveTermOutput)
|
|
||||||
fitAddon.fit()
|
|
||||||
term.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', resizeTerm)
|
|
||||||
EventsOff(`cmd:output:${props.name}`)
|
|
||||||
termInst.dispose()
|
|
||||||
termInst = null
|
|
||||||
console.warn('destroy term')
|
|
||||||
})
|
|
||||||
|
|
||||||
const resizeTerm = () => {
|
|
||||||
if (fitAddonInst != null) {
|
|
||||||
fitAddonInst.fit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
resizeTerm,
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => prefStore.cliFont,
|
|
||||||
({ fontSize = 14, fontFamily = 'Courier New' }) => {
|
|
||||||
if (termInst != null) {
|
|
||||||
termInst.options.fontSize = fontSize
|
|
||||||
termInst.options.fontFamily = fontFamily
|
|
||||||
}
|
|
||||||
resizeTerm()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => prefStore.cli.cursorStyle,
|
|
||||||
(style) => {
|
|
||||||
if (termInst != null) {
|
|
||||||
termInst.options.cursorStyle = style || 'block'
|
|
||||||
}
|
|
||||||
resizeTerm()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const prefixContent = computed(() => {
|
|
||||||
return '\x1b[33m' + promptPrefix.value + '\x1b[0m'
|
|
||||||
})
|
|
||||||
|
|
||||||
const prefixLen = computed(() => {
|
|
||||||
let len = 0
|
|
||||||
for (let i = 0; i < promptPrefix.value.length; i++) {
|
|
||||||
const char = promptPrefix.value.charCodeAt(i)
|
|
||||||
if (char >= 0x0000 && char <= 0x00ff) {
|
|
||||||
// single byte ASCII char
|
|
||||||
len += 1
|
|
||||||
} else {
|
|
||||||
// multibyte Unicode char
|
|
||||||
len += 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len
|
|
||||||
})
|
|
||||||
|
|
||||||
let promptPrefix = ref('')
|
|
||||||
let inputCursor = 0
|
|
||||||
const inputHistory = []
|
|
||||||
let historyIndex = 0
|
|
||||||
let waitForOutput = false
|
|
||||||
const onTermData = (data) => {
|
|
||||||
if (termInst == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const cc = data.charCodeAt(0)
|
|
||||||
switch (cc) {
|
|
||||||
case 127: // backspace
|
|
||||||
deleteInput(true)
|
|
||||||
return
|
|
||||||
|
|
||||||
case 13: // enter
|
|
||||||
// try to process local command first
|
|
||||||
switch (getCurrentInput()) {
|
|
||||||
case 'clear':
|
|
||||||
case 'clr':
|
|
||||||
termInst.clear()
|
|
||||||
replaceTermInput()
|
|
||||||
newInputLine()
|
|
||||||
return
|
|
||||||
|
|
||||||
default: // send command to server
|
|
||||||
flushTermInput()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case 27:
|
|
||||||
switch (data.substring(1)) {
|
|
||||||
case '[A': // arrow up
|
|
||||||
changeHistory(true)
|
|
||||||
return
|
|
||||||
case '[B': // arrow down
|
|
||||||
changeHistory(false)
|
|
||||||
return
|
|
||||||
case '[C': // arrow right ->
|
|
||||||
moveInputCursor(1)
|
|
||||||
return
|
|
||||||
case '[D': // arrow left <-
|
|
||||||
moveInputCursor(-1)
|
|
||||||
return
|
|
||||||
case '[3~': // del
|
|
||||||
deleteInput(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case 9: // tab
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateInput(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
|
|
||||||
* @param {number} step above 0 indicate move right; 0 indicate move to last
|
|
||||||
*/
|
|
||||||
const moveInputCursor = (step) => {
|
|
||||||
if (termInst == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let updateCursor = false
|
|
||||||
if (step > 0) {
|
|
||||||
// move right
|
|
||||||
const currentLine = getCurrentInput()
|
|
||||||
if (inputCursor + step <= currentLine.length) {
|
|
||||||
inputCursor += step
|
|
||||||
updateCursor = true
|
|
||||||
}
|
|
||||||
} else if (step < 0) {
|
|
||||||
// move left
|
|
||||||
if (inputCursor + step >= 0) {
|
|
||||||
inputCursor += step
|
|
||||||
updateCursor = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateCursor) {
|
|
||||||
moveInputCursorTo(inputCursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* move cursor to the end of current line
|
|
||||||
*/
|
|
||||||
const moveInputCursorToEnd = () => {
|
|
||||||
moveInputCursorTo(Number.MAX_SAFE_INTEGER)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* move cursor to pos
|
|
||||||
* @param {number} pos
|
|
||||||
*/
|
|
||||||
const moveInputCursorTo = (pos) => {
|
|
||||||
const currentLine = getCurrentInput()
|
|
||||||
inputCursor = Math.min(Math.max(0, pos), currentLine.length)
|
|
||||||
termInst.write(`\x1B[${prefixLen.value + inputCursor + 1}G`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update current input cache and refresh term
|
|
||||||
* @param {string} data
|
|
||||||
*/
|
|
||||||
const updateInput = (data) => {
|
|
||||||
if (data == null || data.length <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// replace (Non-Breaking Space) with normal blank space
|
|
||||||
data = data.replace(/\u00A0/g, ' ')
|
|
||||||
|
|
||||||
if (termInst == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentLine = getCurrentInput()
|
|
||||||
if (inputCursor < currentLine.length) {
|
|
||||||
// insert
|
|
||||||
currentLine = currentLine.substring(0, inputCursor) + data + currentLine.substring(inputCursor)
|
|
||||||
replaceTermInput(currentLine)
|
|
||||||
moveInputCursor(data.length)
|
|
||||||
} else {
|
|
||||||
// append
|
|
||||||
currentLine += data
|
|
||||||
termInst.write(data)
|
|
||||||
inputCursor += data.length
|
|
||||||
}
|
|
||||||
updateCurrentInput(currentLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {boolean} back backspace or not
|
|
||||||
*/
|
|
||||||
const deleteInput = (back = false) => {
|
|
||||||
if (termInst == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentLine = getCurrentInput()
|
|
||||||
if (inputCursor < currentLine.length) {
|
|
||||||
// delete middle part
|
|
||||||
if (back) {
|
|
||||||
currentLine = currentLine.substring(0, inputCursor - 1) + currentLine.substring(inputCursor)
|
|
||||||
inputCursor -= 1
|
|
||||||
} else {
|
|
||||||
currentLine = currentLine.substring(0, inputCursor) + currentLine.substring(inputCursor + 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (back) {
|
|
||||||
// delete last one
|
|
||||||
currentLine = currentLine.slice(0, -1)
|
|
||||||
inputCursor -= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceTermInput(currentLine)
|
|
||||||
updateCurrentInput(currentLine)
|
|
||||||
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 = () => {
|
|
||||||
return get(inputHistory, historyIndex, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCurrentInput = (input) => {
|
|
||||||
set(inputHistory, historyIndex, input || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const newInputLine = () => {
|
|
||||||
if (historyIndex >= 0 && historyIndex < inputHistory.length - 1) {
|
|
||||||
// edit prev history, move to last
|
|
||||||
const pop = inputHistory.splice(historyIndex, 1)
|
|
||||||
inputHistory[inputHistory.length - 1] = pop[0]
|
|
||||||
}
|
|
||||||
if (get(inputHistory, inputHistory.length - 1, '')) {
|
|
||||||
historyIndex = inputHistory.length
|
|
||||||
updateCurrentInput('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get prev or next history record
|
|
||||||
* @param prev
|
|
||||||
* @return {*|null}
|
|
||||||
*/
|
|
||||||
const changeHistory = (prev) => {
|
|
||||||
let currentLine = null
|
|
||||||
if (prev) {
|
|
||||||
if (historyIndex > 0) {
|
|
||||||
historyIndex -= 1
|
|
||||||
currentLine = inputHistory[historyIndex]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (historyIndex < inputHistory.length - 1) {
|
|
||||||
historyIndex += 1
|
|
||||||
currentLine = inputHistory[historyIndex]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLine != null) {
|
|
||||||
if (termInst == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceTermInput(currentLine)
|
|
||||||
moveInputCursorToEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* flush terminal input and send current prompt to server
|
|
||||||
* @param {boolean} flushCmd
|
|
||||||
*/
|
|
||||||
const flushTermInput = (flushCmd = false) => {
|
|
||||||
const currentLine = getCurrentInput()
|
|
||||||
EventsEmit(`cmd:input:${props.name}`, currentLine)
|
|
||||||
inputCursor = 0
|
|
||||||
// historyIndex = inputHistory.length
|
|
||||||
waitForOutput = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear current input line and replace with new content
|
|
||||||
* @param {string|null} [content]
|
|
||||||
*/
|
|
||||||
const replaceTermInput = (content = '') => {
|
|
||||||
if (termInst == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// erase current line and write new content
|
|
||||||
termInst.write('\r\x1B[K' + prefixContent.value + (content || ''))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* process receive output content
|
|
||||||
* @param {{content: string[], prompt: string}} data
|
|
||||||
*/
|
|
||||||
const receiveTermOutput = (data) => {
|
|
||||||
if (termInst == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { content = [], prompt } = data || {}
|
|
||||||
if (!isEmpty(content)) {
|
|
||||||
for (const line of content) {
|
|
||||||
termInst.write('\r\n' + line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isEmpty(prompt)) {
|
|
||||||
promptPrefix.value = prompt
|
|
||||||
termInst.write('\r\n' + prefixContent.value)
|
|
||||||
waitForOutput = false
|
|
||||||
inputCursor = 0
|
|
||||||
newInputLine()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="termRef" class="xterm" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.xterm {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #000000;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.xterm-screen {
|
|
||||||
padding: 0 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-viewport::-webkit-scrollbar {
|
|
||||||
background-color: #000000;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
||||||
background: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-decoration-overview-ruler {
|
|
||||||
right: 1px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,256 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import * as monaco from 'monaco-editor'
|
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import { isEmpty } from 'lodash'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
content: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
language: {
|
|
||||||
type: String,
|
|
||||||
default: 'json',
|
|
||||||
},
|
|
||||||
readonly: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
resetKey: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
offsetKey: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
keepOffset: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['reset', 'input', 'save'])
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
|
||||||
/** @type {HTMLElement|null} */
|
|
||||||
const editorRef = ref(null)
|
|
||||||
/** @type monaco.editor.IStandaloneCodeEditor */
|
|
||||||
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 = () => {
|
|
||||||
if (editorNode != null && editorNode.dispose != null) {
|
|
||||||
const model = editorNode.getModel()
|
|
||||||
if (model != null) {
|
|
||||||
model.dispose()
|
|
||||||
}
|
|
||||||
editorNode.dispose()
|
|
||||||
editorNode = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const readonlyValue = computed(() => {
|
|
||||||
return props.readonly || props.loading
|
|
||||||
})
|
|
||||||
|
|
||||||
const pref = usePreferencesStore()
|
|
||||||
onMounted(async () => {
|
|
||||||
if (editorRef.value != null) {
|
|
||||||
const { fontSize, fontFamily = ['monaco'] } = pref.editorFont
|
|
||||||
editorNode = monaco.editor.create(editorRef.value, {
|
|
||||||
// value: props.content,
|
|
||||||
theme: pref.isDark ? 'rdm-dark' : 'rdm-light',
|
|
||||||
language: props.language,
|
|
||||||
lineNumbers: pref.showLineNum ? 'on' : 'off',
|
|
||||||
links: pref.editorLinks,
|
|
||||||
readOnly: readonlyValue.value,
|
|
||||||
colorDecorators: true,
|
|
||||||
accessibilitySupport: 'off',
|
|
||||||
wordWrap: 'on',
|
|
||||||
tabSize: 2,
|
|
||||||
folding: pref.showFolding,
|
|
||||||
dragAndDrop: pref.dropText,
|
|
||||||
fontFamily,
|
|
||||||
fontSize,
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
scrollbar: {
|
|
||||||
useShadows: false,
|
|
||||||
verticalScrollbarSize: '10px',
|
|
||||||
},
|
|
||||||
// formatOnType: true,
|
|
||||||
contextmenu: false,
|
|
||||||
lineNumbersMinChars: 2,
|
|
||||||
lineDecorationsWidth: 0,
|
|
||||||
minimap: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
selectionHighlight: false,
|
|
||||||
renderLineHighlight: 'gutter',
|
|
||||||
})
|
|
||||||
|
|
||||||
// add shortcut for save
|
|
||||||
editorNode.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, (event) => {
|
|
||||||
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?.getAction('editor.action.formatDocument')?.run()
|
|
||||||
// })
|
|
||||||
|
|
||||||
if (editorNode.onDidChangeModelContent) {
|
|
||||||
editorNode.onDidChangeModelContent(() => {
|
|
||||||
emit('input', editorNode.getValue())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.content,
|
|
||||||
async (content) => {
|
|
||||||
if (editorNode != null) {
|
|
||||||
editorNode.setValue(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 })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => readonlyValue.value,
|
|
||||||
(readOnly) => {
|
|
||||||
if (editorNode != null) {
|
|
||||||
editorNode.updateOptions({
|
|
||||||
readOnly,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.language,
|
|
||||||
(language) => {
|
|
||||||
if (editorNode != null) {
|
|
||||||
const model = editorNode.getModel()
|
|
||||||
if (model != null) {
|
|
||||||
monaco.editor.setModelLanguage(model, language)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => pref.isDark,
|
|
||||||
(dark) => {
|
|
||||||
if (editorNode != null) {
|
|
||||||
editorNode.updateOptions({
|
|
||||||
theme: dark ? 'rdm-dark' : 'rdm-light',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
destroyEditor()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="{ 'editor-border': props.border === true }" style="position: relative">
|
|
||||||
<div ref="editorRef" class="editor-inst" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.editor-border {
|
|
||||||
border: 1px solid v-bind('themeVars.borderColor');
|
|
||||||
border-radius: v-bind('themeVars.borderRadius');
|
|
||||||
padding: 3px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-inst {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
bottom: 2px;
|
|
||||||
left: 2px;
|
|
||||||
right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.line-numbers) {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,292 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, defineEmits, defineProps, nextTick, reactive, ref, watchEffect } from 'vue'
|
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import Save from '@/components/icons/Save.vue'
|
|
||||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
|
||||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
|
||||||
import useBrowserStore from 'stores/browser.js'
|
|
||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
|
||||||
import FullScreen from '@/components/icons/FullScreen.vue'
|
|
||||||
import WindowClose from '@/components/icons/WindowClose.vue'
|
|
||||||
import Pin from '@/components/icons/Pin.vue'
|
|
||||||
import OffScreen from '@/components/icons/OffScreen.vue'
|
|
||||||
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
|
||||||
import { isEmpty, toString } from 'lodash'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
keyPath: String,
|
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
type: [String, Number],
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: [String, Array],
|
|
||||||
},
|
|
||||||
fieldLabel: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
valueLabel: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
decode: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
format: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
fieldReadonly: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
fullscreen: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
|
||||||
const browserStore = useBrowserStore()
|
|
||||||
const emit = defineEmits([
|
|
||||||
'update:field',
|
|
||||||
'update:value',
|
|
||||||
'update:decode',
|
|
||||||
'update:format',
|
|
||||||
'update:fullscreen',
|
|
||||||
'save',
|
|
||||||
'close',
|
|
||||||
])
|
|
||||||
|
|
||||||
watchEffect(
|
|
||||||
() => {
|
|
||||||
if (props.show && !isEmpty(props.keyPath)) {
|
|
||||||
onFormatChanged(props.decode, props.format)
|
|
||||||
} else {
|
|
||||||
viewAs.value = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
flush: 'post',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const isPin = ref(false)
|
|
||||||
const viewAs = reactive({
|
|
||||||
field: '',
|
|
||||||
value: '',
|
|
||||||
format: formatTypes.RAW,
|
|
||||||
decode: decodeTypes.NONE,
|
|
||||||
})
|
|
||||||
const displayValue = computed(() => {
|
|
||||||
if (loading.value) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
if (viewAs.value == null) {
|
|
||||||
return decodeRedisKey(props.value)
|
|
||||||
}
|
|
||||||
return viewAs.value
|
|
||||||
})
|
|
||||||
const editingContent = ref('')
|
|
||||||
const enableSave = computed(() => {
|
|
||||||
return toString(props.field) !== viewAs.field || editingContent.value !== viewAs.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const viewLanguage = computed(() => {
|
|
||||||
switch (viewAs.format) {
|
|
||||||
case formatTypes.JSON:
|
|
||||||
case formatTypes.UNICODE_JSON:
|
|
||||||
return 'json'
|
|
||||||
case formatTypes.YAML:
|
|
||||||
return 'yaml'
|
|
||||||
case formatTypes.XML:
|
|
||||||
return 'xml'
|
|
||||||
default:
|
|
||||||
return 'plaintext'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {decodeTypes|null} decode
|
|
||||||
* @param {formatTypes|null} format
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
|
||||||
const onFormatChanged = async (decode = null, format = null) => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
decode: retDecode,
|
|
||||||
format: retFormat,
|
|
||||||
} = await browserStore.convertValue({
|
|
||||||
value: props.value,
|
|
||||||
decode,
|
|
||||||
format,
|
|
||||||
})
|
|
||||||
viewAs.field = props.field + ''
|
|
||||||
editingContent.value = viewAs.value = value
|
|
||||||
viewAs.decode = decode || retDecode
|
|
||||||
viewAs.format = format || retFormat
|
|
||||||
emit('update:decode', viewAs.decode)
|
|
||||||
emit('update:format', viewAs.format)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onInput = (content) => {
|
|
||||||
editingContent.value = content
|
|
||||||
}
|
|
||||||
|
|
||||||
const onToggleFullscreen = () => {
|
|
||||||
emit('update:fullscreen', !!!props.fullscreen)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
isPin.value = false
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSave = () => {
|
|
||||||
emit('save', viewAs.field, editingContent.value, viewAs.decode, viewAs.format)
|
|
||||||
if (!isPin.value) {
|
|
||||||
nextTick().then(onClose)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-show="show" class="entry-editor flex-box-v">
|
|
||||||
<n-card :title="$t('interface.edit_row')" autofocus class="flex-item-expand" size="small">
|
|
||||||
<div class="editor-content flex-box-v flex-item-expand">
|
|
||||||
<!-- field -->
|
|
||||||
<div class="editor-content-item flex-box-v">
|
|
||||||
<div class="editor-content-item-label">{{ props.fieldLabel }}</div>
|
|
||||||
<n-input
|
|
||||||
v-model:value="viewAs.field"
|
|
||||||
:placeholder="props.field + ''"
|
|
||||||
:readonly="props.fieldReadonly"
|
|
||||||
class="editor-content-item-input"
|
|
||||||
type="text" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- value -->
|
|
||||||
<div class="editor-content-item flex-box-v flex-item-expand">
|
|
||||||
<div class="editor-content-item-label">{{ props.valueLabel }}</div>
|
|
||||||
<content-editor
|
|
||||||
:border="true"
|
|
||||||
:content="displayValue"
|
|
||||||
:key-path="viewAs.field"
|
|
||||||
:language="viewLanguage"
|
|
||||||
class="flex-item-expand"
|
|
||||||
@input="onInput"
|
|
||||||
@reset="onInput"
|
|
||||||
@save="onSave" />
|
|
||||||
<format-selector
|
|
||||||
:decode="viewAs.decode"
|
|
||||||
:format="viewAs.format"
|
|
||||||
style="margin-top: 5px"
|
|
||||||
@format-changed="(d, f) => onFormatChanged(d, f)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #header-extra>
|
|
||||||
<n-space :size="5">
|
|
||||||
<icon-button
|
|
||||||
:button-class="{ 'pinable-btn': true, 'unpin-btn': !isPin, 'pin-btn': isPin }"
|
|
||||||
:icon="Pin"
|
|
||||||
:size="19"
|
|
||||||
:t-tooltip="isPin ? 'interface.unpin_edit' : 'interface.pin_edit'"
|
|
||||||
stroke-width="4"
|
|
||||||
@click="isPin = !isPin" />
|
|
||||||
<icon-button
|
|
||||||
:button-class="['pinable-btn', 'unpin-btn']"
|
|
||||||
:icon="props.fullscreen ? OffScreen : FullScreen"
|
|
||||||
:size="18"
|
|
||||||
stroke-width="5"
|
|
||||||
t-tooltip="interface.fullscreen"
|
|
||||||
@click="onToggleFullscreen" />
|
|
||||||
<icon-button
|
|
||||||
:button-class="['pinable-btn', 'unpin-btn']"
|
|
||||||
:icon="WindowClose"
|
|
||||||
:size="18"
|
|
||||||
stroke-width="5"
|
|
||||||
t-tooltip="menu.close"
|
|
||||||
@click="onClose" />
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<template #action>
|
|
||||||
<n-space :wrap="false" :wrap-item="false" justify="end">
|
|
||||||
<n-button :disabled="!enableSave" :secondary="enableSave" type="primary" @click="onSave">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Save" />
|
|
||||||
</template>
|
|
||||||
{{ $t('common.update') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.entry-editor {
|
|
||||||
padding-left: 2px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
.editor-content {
|
|
||||||
&-item {
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-label {
|
|
||||||
height: 18px;
|
|
||||||
color: v-bind('themeVars.textColor3');
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-input {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-card__content) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-card__action) {
|
|
||||||
padding: 5px 10px;
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.pinable-btn) {
|
|
||||||
padding: 3px;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.unpin-btn) {
|
|
||||||
border-color: #0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.pin-btn) {
|
|
||||||
border-color: v-bind('themeVars.iconColorDisabled');
|
|
||||||
background-color: v-bind('themeVars.iconColorDisabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
//:deep(.n-card--bordered) {
|
|
||||||
// border-radius: 0;
|
|
||||||
//}
|
|
||||||
</style>
|
|
@ -1,197 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
||||||
import { debounce, filter, get, includes, isEmpty, join } from 'lodash'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import Play from '@/components/icons/Play.vue'
|
|
||||||
import Pause from '@/components/icons/Pause.vue'
|
|
||||||
import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js'
|
|
||||||
import { EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import Copy from '@/components/icons/Copy.vue'
|
|
||||||
import Export from '@/components/icons/Export.vue'
|
|
||||||
import Delete from '@/components/icons/Delete.vue'
|
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
|
||||||
import Bottom from '@/components/icons/Bottom.vue'
|
|
||||||
import copy from 'copy-text-to-clipboard'
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
|
||||||
|
|
||||||
const i18n = useI18n()
|
|
||||||
const props = defineProps({
|
|
||||||
server: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = reactive({
|
|
||||||
monitorEvent: '',
|
|
||||||
list: [],
|
|
||||||
listLimit: 20,
|
|
||||||
keyword: '',
|
|
||||||
autoShowLast: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const listRef = ref(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// try to stop prev monitor first
|
|
||||||
onStopMonitor()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
onStopMonitor()
|
|
||||||
})
|
|
||||||
|
|
||||||
const isMonitoring = computed(() => {
|
|
||||||
return !isEmpty(data.monitorEvent)
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayList = computed(() => {
|
|
||||||
if (!isEmpty(data.keyword)) {
|
|
||||||
return filter(data.list, (line) => includes(line, data.keyword))
|
|
||||||
}
|
|
||||||
return data.list
|
|
||||||
})
|
|
||||||
|
|
||||||
const _scrollToBottom = () => {
|
|
||||||
nextTick(() => {
|
|
||||||
listRef.value?.scrollTo({ position: 'bottom' })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const scrollToBottom = debounce(_scrollToBottom, 1000, { leading: true, trailing: true })
|
|
||||||
|
|
||||||
const onStartMonitor = async () => {
|
|
||||||
if (isMonitoring.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: ret, success, msg } = await StartMonitor(props.server)
|
|
||||||
if (!success) {
|
|
||||||
$message.error(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.monitorEvent = get(ret, 'eventName')
|
|
||||||
EventsOn(data.monitorEvent, (content) => {
|
|
||||||
if (content instanceof Array) {
|
|
||||||
data.list.push(...content)
|
|
||||||
} else {
|
|
||||||
data.list.push(content)
|
|
||||||
}
|
|
||||||
if (data.autoShowLast) {
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const onStopMonitor = async () => {
|
|
||||||
const { success, msg } = await StopMonitor(props.server)
|
|
||||||
if (!success) {
|
|
||||||
$message.error(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
EventsOff(data.monitorEvent)
|
|
||||||
data.monitorEvent = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCopyLog = async () => {
|
|
||||||
copy(join(data.list, '\n'))
|
|
||||||
$message.success(i18n.t('interface.copy_succ'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const onExportLog = () => {
|
|
||||||
ExportLog(data.list)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCleanLog = () => {
|
|
||||||
data.list = []
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="content-log content-container fill-height flex-box-v">
|
|
||||||
<n-form class="flex-item" label-align="left" label-placement="left" label-width="auto" size="small">
|
|
||||||
<n-form-item :feedback="$t('monitor.warning')" :label="$t('monitor.actions')">
|
|
||||||
<n-space :wrap="false" :wrap-item="false" style="width: 100%">
|
|
||||||
<n-button
|
|
||||||
v-if="!isMonitoring"
|
|
||||||
:focusable="false"
|
|
||||||
secondary
|
|
||||||
strong
|
|
||||||
type="success"
|
|
||||||
@click="onStartMonitor">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Play" size="18" />
|
|
||||||
</template>
|
|
||||||
{{ $t('monitor.start') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-else :focusable="false" secondary strong type="warning" @click="onStopMonitor">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Pause" size="18" />
|
|
||||||
</template>
|
|
||||||
{{ $t('monitor.stop') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button-group>
|
|
||||||
<icon-button
|
|
||||||
:icon="Copy"
|
|
||||||
border
|
|
||||||
size="18"
|
|
||||||
stroke-width="3.5"
|
|
||||||
t-tooltip="monitor.copy_log"
|
|
||||||
@click="onCopyLog" />
|
|
||||||
<icon-button
|
|
||||||
:icon="Export"
|
|
||||||
border
|
|
||||||
size="18"
|
|
||||||
stroke-width="3.5"
|
|
||||||
t-tooltip="monitor.save_log"
|
|
||||||
@click="onExportLog" />
|
|
||||||
</n-button-group>
|
|
||||||
<icon-button
|
|
||||||
:icon="Bottom"
|
|
||||||
:secondary="data.autoShowLast"
|
|
||||||
:type="data.autoShowLast ? 'primary' : 'default'"
|
|
||||||
border
|
|
||||||
size="18"
|
|
||||||
stroke-width="3.5"
|
|
||||||
t-tooltip="monitor.always_show_last"
|
|
||||||
@click="data.autoShowLast = !data.autoShowLast" />
|
|
||||||
<div class="flex-item-expand" />
|
|
||||||
<icon-button
|
|
||||||
:icon="Delete"
|
|
||||||
border
|
|
||||||
size="18"
|
|
||||||
stroke-width="3.5"
|
|
||||||
t-tooltip="monitor.clean_log"
|
|
||||||
@click="onCleanLog" />
|
|
||||||
</n-space>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="$t('monitor.search')">
|
|
||||||
<n-input v-model:value="data.keyword" clearable placeholder="" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
<n-virtual-list ref="listRef" :item-size="25" :items="displayList" class="list-wrapper">
|
|
||||||
<template #default="{ item }">
|
|
||||||
<div class="line-item content-value">
|
|
||||||
<b>></b>
|
|
||||||
{{ item }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-virtual-list>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@use '@/styles/content';
|
|
||||||
|
|
||||||
.line-item {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-wrapper {
|
|
||||||
background-color: v-bind('themeVars.codeColor');
|
|
||||||
border: solid 1px v-bind('themeVars.borderColor');
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,294 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
||||||
import { debounce, get, isEmpty, size, uniq } from 'lodash'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useThemeVars } from 'naive-ui'
|
|
||||||
import useBrowserStore from 'stores/browser.js'
|
|
||||||
import { EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import Publish from '@/components/icons/Publish.vue'
|
|
||||||
import Subscribe from '@/components/icons/Subscribe.vue'
|
|
||||||
import Pause from '@/components/icons/Pause.vue'
|
|
||||||
import Delete from '@/components/icons/Delete.vue'
|
|
||||||
import { Publish as PublishSend, StartSubscribe, StopSubscribe } from 'wailsjs/go/services/pubsubService.js'
|
|
||||||
import Checked from '@/components/icons/Checked.vue'
|
|
||||||
import Bottom from '@/components/icons/Bottom.vue'
|
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
|
||||||
|
|
||||||
const browserStore = useBrowserStore()
|
|
||||||
const i18n = useI18n()
|
|
||||||
const props = defineProps({
|
|
||||||
server: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = reactive({
|
|
||||||
subscribeEvent: '',
|
|
||||||
list: [],
|
|
||||||
keyword: '',
|
|
||||||
autoShowLast: true,
|
|
||||||
ellipsisMessage: false,
|
|
||||||
channelHistory: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const publishData = reactive({
|
|
||||||
channel: '',
|
|
||||||
message: '',
|
|
||||||
received: 0,
|
|
||||||
lastShowReceived: -1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const tableRef = ref(null)
|
|
||||||
|
|
||||||
const columns = computed(() => [
|
|
||||||
{
|
|
||||||
title: () => i18n.t('pubsub.time'),
|
|
||||||
key: 'timestamp',
|
|
||||||
width: 200,
|
|
||||||
align: 'center',
|
|
||||||
titleAlign: 'center',
|
|
||||||
render: ({ timestamp }, index) => {
|
|
||||||
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: () => i18n.t('pubsub.channel'),
|
|
||||||
key: 'channel',
|
|
||||||
filterOptionValue: data.client,
|
|
||||||
resizable: true,
|
|
||||||
filter: (value, row) => {
|
|
||||||
return value === '' || row.client === value.toString() || row.addr === value.toString()
|
|
||||||
},
|
|
||||||
width: 200,
|
|
||||||
align: 'center',
|
|
||||||
titleAlign: 'center',
|
|
||||||
ellipsis: {
|
|
||||||
tooltip: {
|
|
||||||
style: {
|
|
||||||
maxWidth: '50vw',
|
|
||||||
maxHeight: '50vh',
|
|
||||||
},
|
|
||||||
scrollable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: () => i18n.t('pubsub.message'),
|
|
||||||
key: 'message',
|
|
||||||
titleAlign: 'center',
|
|
||||||
filterOptionValue: data.keyword,
|
|
||||||
resizable: true,
|
|
||||||
className: 'content-value',
|
|
||||||
ellipsis: data.ellipsisMessage
|
|
||||||
? {
|
|
||||||
tooltip: {
|
|
||||||
style: {
|
|
||||||
maxWidth: '50vw',
|
|
||||||
maxHeight: '50vh',
|
|
||||||
},
|
|
||||||
scrollable: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
filter: (value, row) => {
|
|
||||||
return value === '' || !!~row.cmd.indexOf(value.toString())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// try to stop prev subscribe first
|
|
||||||
onStopSubscribe()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
onStopSubscribe()
|
|
||||||
})
|
|
||||||
|
|
||||||
const isSubscribing = computed(() => {
|
|
||||||
return !isEmpty(data.subscribeEvent)
|
|
||||||
})
|
|
||||||
|
|
||||||
const publishEnable = computed(() => {
|
|
||||||
return !isEmpty(publishData.channel)
|
|
||||||
})
|
|
||||||
|
|
||||||
const _scrollToBottom = () => {
|
|
||||||
nextTick(() => {
|
|
||||||
tableRef.value?.scrollTo({ position: 'bottom' })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const scrollToBottom = debounce(_scrollToBottom, 300, { leading: true, trailing: true })
|
|
||||||
|
|
||||||
const onStartSubscribe = async () => {
|
|
||||||
if (isSubscribing.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: ret, success, msg } = await StartSubscribe(props.server)
|
|
||||||
if (!success) {
|
|
||||||
$message.error(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.subscribeEvent = get(ret, 'eventName')
|
|
||||||
EventsOn(data.subscribeEvent, (content) => {
|
|
||||||
if (content instanceof Array) {
|
|
||||||
data.list.push(...content)
|
|
||||||
} else {
|
|
||||||
data.list.push(content)
|
|
||||||
}
|
|
||||||
if (data.autoShowLast) {
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const onStopSubscribe = async () => {
|
|
||||||
const { success, msg } = await StopSubscribe(props.server)
|
|
||||||
if (!success) {
|
|
||||||
$message.error(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
EventsOff(data.subscribeEvent)
|
|
||||||
data.subscribeEvent = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCleanLog = () => {
|
|
||||||
data.list = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPublish = async () => {
|
|
||||||
if (isEmpty(publishData.channel)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
success,
|
|
||||||
msg,
|
|
||||||
data: { received = 0 },
|
|
||||||
} = await PublishSend(props.server, publishData.channel, publishData.message || '')
|
|
||||||
if (!success) {
|
|
||||||
publishData.received = 0
|
|
||||||
if (!isEmpty(msg)) {
|
|
||||||
$message.error(msg)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
publishData.message = ''
|
|
||||||
publishData.received = received
|
|
||||||
publishData.lastShowReceived = Date.now()
|
|
||||||
// save channel history
|
|
||||||
data.channelHistory = uniq(data.channelHistory.concat(publishData.channel))
|
|
||||||
|
|
||||||
// hide send status after 2 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (publishData.lastShowReceived > 0 && Date.now() - publishData.lastShowReceived > 2000) {
|
|
||||||
publishData.lastShowReceived = -1
|
|
||||||
}
|
|
||||||
}, 2100)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="content-log content-container fill-height flex-box-v">
|
|
||||||
<n-form class="flex-item" label-align="left" label-placement="left" label-width="auto" size="small">
|
|
||||||
<n-form-item :show-label="false">
|
|
||||||
<n-space :wrap="false" :wrap-item="false" style="width: 100%">
|
|
||||||
<n-button
|
|
||||||
v-if="!isSubscribing"
|
|
||||||
:focusable="false"
|
|
||||||
secondary
|
|
||||||
strong
|
|
||||||
type="success"
|
|
||||||
@click="onStartSubscribe">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Subscribe" size="18" />
|
|
||||||
</template>
|
|
||||||
{{ $t('pubsub.subscribe') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-else :focusable="false" secondary strong type="warning" @click="onStopSubscribe">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Pause" size="18" />
|
|
||||||
</template>
|
|
||||||
{{ $t('pubsub.unsubscribe') }}
|
|
||||||
</n-button>
|
|
||||||
<icon-button
|
|
||||||
:icon="Bottom"
|
|
||||||
:secondary="data.autoShowLast"
|
|
||||||
:type="data.autoShowLast ? 'primary' : 'default'"
|
|
||||||
border
|
|
||||||
size="18"
|
|
||||||
stroke-width="3.5"
|
|
||||||
t-tooltip="monitor.always_show_last"
|
|
||||||
@click="data.autoShowLast = !data.autoShowLast" />
|
|
||||||
<div class="flex-item-expand" />
|
|
||||||
<icon-button
|
|
||||||
:icon="Delete"
|
|
||||||
border
|
|
||||||
size="18"
|
|
||||||
stroke-width="3.5"
|
|
||||||
t-tooltip="pubsub.clear"
|
|
||||||
@click="onCleanLog" />
|
|
||||||
</n-space>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
<n-data-table
|
|
||||||
ref="tableRef"
|
|
||||||
:columns="columns"
|
|
||||||
:data="data.list"
|
|
||||||
:loading="data.loading"
|
|
||||||
class="flex-item-expand"
|
|
||||||
flex-height
|
|
||||||
size="small"
|
|
||||||
virtual-scroll />
|
|
||||||
<div class="total-message">{{ $t('pubsub.receive_message', { total: size(data.list) }) }}</div>
|
|
||||||
<div class="flex-box-h publish-input">
|
|
||||||
<n-input-group>
|
|
||||||
<n-auto-complete
|
|
||||||
v-model:value="publishData.channel"
|
|
||||||
:get-show="() => true"
|
|
||||||
:options="data.channelHistory"
|
|
||||||
:placeholder="$t('pubsub.channel')"
|
|
||||||
style="width: 35%; max-width: 200px"
|
|
||||||
@keydown.enter="onPublish" />
|
|
||||||
<n-input
|
|
||||||
v-model:value="publishData.message"
|
|
||||||
:placeholder="$t('pubsub.message')"
|
|
||||||
@keydown.enter="onPublish">
|
|
||||||
<template #suffix>
|
|
||||||
<transition mode="out-in" name="fade">
|
|
||||||
<n-tag v-show="publishData.lastShowReceived > 0" bordered size="small" type="success">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Checked" size="16" />
|
|
||||||
</template>
|
|
||||||
{{ publishData.received }}
|
|
||||||
</n-tag>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</n-input-group>
|
|
||||||
<n-button :disabled="!publishEnable" type="info" @click="onPublish">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Publish" size="18" />
|
|
||||||
</template>
|
|
||||||
{{ $t('pubsub.publish') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@use '@/styles/content';
|
|
||||||
|
|
||||||
.total-message {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.publish-input {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,196 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, nextTick, reactive } from 'vue'
|
|
||||||
import { debounce, isEmpty, trim } from 'lodash'
|
|
||||||
import { NButton, NInput } from 'naive-ui'
|
|
||||||
import IconButton from '@/components/common/IconButton.vue'
|
|
||||||
import SpellCheck from '@/components/icons/SpellCheck.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
fullSearchIcon: {
|
|
||||||
type: [String, Object],
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
debounceWait: {
|
|
||||||
type: Number,
|
|
||||||
default: 500,
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
useGlob: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
exact: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['filterChanged', 'matchChanged', 'exactChanged'])
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {UnwrapNestedRefs<{filter: string, match: string, exact: boolean}>}
|
|
||||||
*/
|
|
||||||
const inputData = reactive({
|
|
||||||
match: '',
|
|
||||||
filter: '',
|
|
||||||
exact: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasMatch = computed(() => {
|
|
||||||
return !isEmpty(trim(inputData.match))
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasFilter = computed(() => {
|
|
||||||
return !isEmpty(trim(inputData.filter))
|
|
||||||
})
|
|
||||||
|
|
||||||
const onExactChecked = () => {
|
|
||||||
// update search search result
|
|
||||||
if (hasMatch.value) {
|
|
||||||
nextTick(() => onForceFullSearch())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFullSearch = () => {
|
|
||||||
inputData.filter = trim(inputData.filter)
|
|
||||||
if (!isEmpty(inputData.filter)) {
|
|
||||||
inputData.match = inputData.filter
|
|
||||||
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 = () => {
|
|
||||||
emit('filterChanged', inputData.filter, inputData.exact)
|
|
||||||
}
|
|
||||||
const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true })
|
|
||||||
|
|
||||||
const onClearFilter = () => {
|
|
||||||
inputData.filter = ''
|
|
||||||
onClearMatch()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onUpdateMatch = () => {
|
|
||||||
inputData.filter = inputData.match
|
|
||||||
onClearMatch()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClearMatch = () => {
|
|
||||||
const changed = !isEmpty(inputData.match)
|
|
||||||
inputData.match = ''
|
|
||||||
if (changed) {
|
|
||||||
emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
|
|
||||||
} else {
|
|
||||||
emit('filterChanged', inputData.filter, inputData.exact)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
reset: onClearFilter,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-input-group style="overflow: hidden">
|
|
||||||
<slot name="prepend" />
|
|
||||||
<n-input
|
|
||||||
v-model:value="inputData.filter"
|
|
||||||
:placeholder="$t('interface.filter')"
|
|
||||||
:size="props.small ? 'small' : ''"
|
|
||||||
:theme-overrides="{ paddingSmall: '0 3px', paddingMedium: '0 6px' }"
|
|
||||||
clearable
|
|
||||||
@clear="onClearFilter"
|
|
||||||
@input="onInput"
|
|
||||||
@keyup.enter="onFullSearch">
|
|
||||||
<template #prefix>
|
|
||||||
<slot name="prefix" />
|
|
||||||
<n-tooltip v-if="hasMatch" placement="bottom">
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag closable size="small" @close="onClearMatch" @dblclick="onUpdateMatch">
|
|
||||||
{{ inputData.match }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
|
||||||
{{
|
|
||||||
$t('interface.full_search_result', {
|
|
||||||
pattern: props.useGlob ? inputData.match : '*' + inputData.match + '*',
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
<template #suffix>
|
|
||||||
<template v-if="props.useGlob">
|
|
||||||
<n-tooltip placement="bottom" trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<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>
|
|
||||||
<div class="text-block" style="max-width: 600px">
|
|
||||||
{{ $t('dialogue.filter.exact_match_tip') }}
|
|
||||||
</div>
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
|
|
||||||
<icon-button
|
|
||||||
v-if="props.fullSearchIcon"
|
|
||||||
:disabled="hasMatch && !hasFilter"
|
|
||||||
:icon="props.fullSearchIcon"
|
|
||||||
:size="small ? 16 : 20"
|
|
||||||
:tooltip-delay="1"
|
|
||||||
border
|
|
||||||
small
|
|
||||||
stroke-width="4"
|
|
||||||
@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">
|
|
||||||
{{ $t('interface.full_search') }}
|
|
||||||
</n-button>
|
|
||||||
<slot name="append" />
|
|
||||||
</n-input-group>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<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>
|
|
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