Compare commits

...

61 Commits

Author SHA1 Message Date
Lykin 3860415429 chore: update dependencies 2025-03-14 17:17:30 +08:00
Lykin dec906a69b chore: update dependencies 2025-03-14 17:13:53 +08:00
Lykin 118b9b8870 chore: upgrade wails to the latest version 2025-02-15 18:43:23 +08:00
Lykin 864bd74029 chore: update release-windows action 2025-01-24 19:16:33 +08:00
Lykin aedf4d4222 chore: update release-windows action 2025-01-24 18:21:00 +08:00
Lykin a4d7c598f9 chore: update release-windows action 2025-01-24 18:12:43 +08:00
Lykin 480e4cc7fd chore: update release-windows action 2025-01-24 18:04:15 +08:00
Lykin 1016df4a25 chore: update dependencies 2025-01-24 15:39:37 +08:00
Lykin 46d7ba2a9f fix: correct text translation 2025-01-16 20:03:40 +08:00
Lykin 19c8368dff fix: add default value to format selector 2025-01-08 16:44:28 +08:00
Lykin 1c322fdac5 pref: correct display raw value for list/set/zset 2025-01-08 16:19:30 +08:00
Lykin 53e8c26380 fix: entry editor support byte array 2025-01-08 16:06:08 +08:00
Lykin 400a908cf9 fix: can not display hash value with unreadable char 2025-01-08 11:55:12 +08:00
Lykin edd77182a5 fix: can not switch to an open connection from connection tree view 2025-01-07 16:24:12 +08:00
Lykin 6e44c64441 chore: update dependencies 2025-01-07 16:20:22 +08:00
Lykin 51beb7249f fix: reset sub tab to `status` page 2024-12-25 11:04:36 +08:00
Lykin 79fd2a6d39 chore: update dependencies 2024-12-19 15:13:33 +08:00
todoList 30c3decd65
fix: incorrect function call argument 2024-12-19 15:11:35 +08:00
Lykin 23fc32e92f chore: update dependencies 2024-12-16 18:43:01 +08:00
Lykin f458a1a0e4 chore: update dependencies 2024-12-16 17:57:38 +08:00
Lykin 52aaad6339 pref: change new version checking and add sponsor ad 2024-12-16 17:57:28 +08:00
Lykin dd70d6b595 chore: update dependencies 2024-12-05 17:14:42 +08:00
Lykin 5d425aadb1 fix: close connection when monitor stopped 2024-12-05 17:12:58 +08:00
Lykin c02a24ee94 chore: compatible with new sass 2024-11-07 14:58:44 +08:00
Lykin e03fc8ad28 chore: update dependencies 2024-11-07 14:57:19 +08:00
Lykin 3367f13d80 fix: incorrect update index in list when filter on 2024-11-06 16:38:52 +08:00
Lykin b601ba255b pref: place the tooltip of switch bottom place at the bottom by default 2024-11-06 16:17:29 +08:00
Lykin ccb4bb85ae fix: type mismatch when encoding to msgpack (#380) 2024-11-04 14:26:17 +08:00
Lykin b0dfe348bd Merge remote-tracking branch 'origin/main' 2024-10-18 16:37:56 +08:00
Lykin a3a1a17af3 fix: force switch to key detail tab when `Auto Refresh` on 2024-10-18 16:37:03 +08:00
Joanne Tweed ca9f0a08e1
fix: reset `ReadTimeout`&`ReadTimeout` for sentinel mode through ssh 2024-10-14 16:20:49 +08:00
Lykin 3f5b63a36f revert: revert `wails` to `2.9.1` 2024-09-29 22:40:30 +08:00
Lykin ab3560fc2b chore: update dependencies 2024-09-28 15:31:52 +08:00
Lykin cb428747e2 fix: key deletion operation was incomplete (#348) 2024-09-28 11:55:38 +08:00
Lykin aa3383db43 chore: update dependencies 2024-09-25 14:54:49 +08:00
Lykin 026591c8d4 fix: no db loaded (#353) 2024-09-25 14:40:43 +08:00
Lykin a70b5b56ff pref: compatible with IPv6 address 2024-09-06 15:08:53 +08:00
Lykin eaa68df583 fix: can not remove argument from custom decoder (#347) 2024-09-03 14:34:40 +08:00
Lykin 2388f309d8 fix: parse cmd error with single quotes 2024-09-02 18:50:51 +08:00
Lykin a9c7cb1cd2 chore: trim svg icon 2024-08-31 23:54:18 +08:00
Lykin b506e8a6a4 fix: auto refresh should not force activation of owning tabs (#336) 2024-08-20 14:57:51 +08:00
Lykin c082a0c41f pref: does not close dialog when adding data error (#338) 2024-08-20 14:29:25 +08:00
Ikko Eltociear Ashimine 970ebcf902
docs: add Japanese README (#335) 2024-08-18 11:50:15 +08:00
Lykin b223feb441 chore: update the minimum compatible version for macOS 2024-08-16 23:39:59 +08:00
Lykin 469a62333f fix: error occurred when add SET key with the same name twice (#327) 2024-08-16 23:38:38 +08:00
Lykin c38944e948 fix: stop command monitor may block ui (#326) 2024-08-15 12:33:51 +08:00
Lykin 5efbd4d316 chore: update dependencies 2024-08-07 23:17:50 +08:00
Lykin c54567115e pref: remove transparent background on Windows 2024-08-07 23:12:01 +08:00
Lykin a14e7e947e fix: compatible when the `INFO` command is unavailable 2024-08-07 11:25:54 +08:00
Lykin 868b0c81b6 fix: compatible with non-macOS when the shortcut key contains `metaKey` 2024-08-07 10:14:59 +08:00
Lykin a3cb09863a fix: remove tabindex of div due to black window edge 2024-08-06 23:11:14 +08:00
Lykin b26f5d2bde fix: sync env config (#319) 2024-08-06 18:12:21 +08:00
Lykin 0038092193 fix: add window shadow on Windows 2024-07-31 23:25:14 +08:00
Lykin 0739cb8b68 feat: add `LZ4` encoder/decoder 2024-07-31 16:49:52 +08:00
Lykin 70c38d9aa7 fix: can not display correct content with decoder after modified 2024-07-31 16:46:50 +08:00
Lykin 237b25086c chore: update dependencies 2024-07-27 22:27:33 +08:00
Lykin 71dbda01da pref: support text alignment config for HASH/LIST/SET/ZSET (#264) 2024-07-27 22:20:27 +08:00
Lykin 5deb6ce443 pref: support text alignment config for HASH/LIST/SET/ZSET (#264) 2024-07-26 17:19:28 +08:00
Lykin ee398d4d98 pref: support `Home` and `End` input for cli (#313) 2024-07-26 11:09:13 +08:00
Lykin 29b51f836f pref: replace linear css animation due to it takes a lot of CPU 2024-07-24 11:30:06 +08:00
Lykin ea8ceba32a fix: reconnect will crash after connection fail first time (#301) 2024-07-11 16:03:13 +08:00
69 changed files with 2861 additions and 2098 deletions

View File

@ -105,6 +105,7 @@ jobs:
-ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }}"
- name: Codesign Windows NSIS installer
shell: powershell
working-directory: ./build/bin
run: |
echo "Creating certificate file"
@ -112,7 +113,7 @@ jobs:
Set-Content -Path certificate\certificate.txt -Value '${{ secrets.WIN_SIGNING_CERT }}'
certutil -decode certificate\certificate.txt certificate\certificate.pfx
echo "Signing TinyRDM installer"
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /fd sha256 /tr http://ts.ssl.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' TinyRDM-${{ 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://timestamp.digicert.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' TinyRDM-${{ steps.normalise_platform_name.outputs.pname }}-installer.exe
- name: Rename installer
working-directory: ./build/bin

View File

@ -3,7 +3,7 @@
</div>
<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></h4>
简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a></h4>
<div align="center">
[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)

111
README_ja.md Normal file
View File

@ -0,0 +1,111 @@
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><strong><a href="/">English</a></strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | 日本語</h4>
<div align="center">
[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)
[![Discord](https://img.shields.io/discord/1170373259133456434?label=Discord&color=5865F2)](https://discord.gg/VTFbBMGjWh)
[![X](https://img.shields.io/badge/Twitter-black?logo=x&logoColor=white)](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" />

View File

@ -2,7 +2,7 @@
<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> | 简体中文</h4>
<h4 align="center"><strong><a href="/">English</a></strong> | 简体中文 | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a></h4>
<div align="center">
[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)

View File

@ -156,12 +156,13 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
} else {
// get database info
var res string
res, err = client.Info(ctx, "keyspace").Result()
if err != nil {
resp.Msg = "get server info fail:" + err.Error()
return
info := map[string]map[string]string{}
if res, err = client.Info(ctx, "keyspace").Result(); err != nil {
//resp.Msg = "get server info fail:" + err.Error()
//return
} else {
info = b.parseInfo(res)
}
info := b.parseInfo(res)
if totaldb <= 0 {
// cannot retrieve the database count by "CONFIG GET databases", try to get max index from keyspace
@ -299,8 +300,8 @@ func (b *browserService) createRedisClient(ctx context.Context, selConn types.Co
return
}
// get a redis client from local cache or create a new open
// if db >= 0, will also switch to db index
// get a redis client from local cache or create a new one
// if db >= 0, it will also switch to target database index
func (b *browserService) getRedisClient(server string, db int) (item *connectionItem, err error) {
b.mutex.Lock()
defer b.mutex.Unlock()
@ -325,6 +326,7 @@ func (b *browserService) getRedisClient(server string, db int) (item *connection
selConn := Connection().getConnection(server)
if selConn == nil {
err = fmt.Errorf("no match connection \"%s\"", server)
delete(b.connMap, server)
return
}
@ -337,6 +339,7 @@ func (b *browserService) getRedisClient(server string, db int) (item *connection
connConfig.LastDB = db
client, err = b.createRedisClient(ctx, connConfig)
if err != nil {
delete(b.connMap, server)
return
}
item = &connectionItem{
@ -873,7 +876,8 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
continue
}
items = append(items, types.ListEntryItem{
Value: val,
Index: len(items),
Value: strutil.EncodeRedisKey(val),
})
if doConvert {
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
@ -990,7 +994,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
}
for _, val := range loadedKey {
items = append(items, types.SetEntryItem{
Value: val,
Value: strutil.EncodeRedisKey(val),
})
if doConvert {
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
@ -1011,7 +1015,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
items = make([]types.SetEntryItem, len(loadedKey))
for i, val := range loadedKey {
items[i].Value = val
items[i].Value = strutil.EncodeRedisKey(val)
if doConvert {
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
items[i].DisplayValue = dv
@ -1057,7 +1061,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
for i := 0; i < len(loadedVal); i += 2 {
if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
items = append(items, types.ZSetEntryItem{
Value: loadedVal[i],
Value: strutil.EncodeRedisKey(loadedVal[i]),
Score: score,
})
if doConvert {
@ -1091,7 +1095,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
continue
}
entry := types.ZSetEntryItem{
Value: val,
Value: strutil.EncodeRedisKey(val),
}
if math.IsInf(z.Score, 1) {
entry.ScoreStr = "+inf"
@ -1337,7 +1341,10 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
if err == nil && expiration > 0 {
client.Expire(ctx, key, expiration)
}
savedValue = param.Value
var ok bool
if savedValue, ok = param.Value.(string); !ok {
savedValue = ""
}
}
if err != nil {
@ -1345,9 +1352,11 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
return
}
resp.Success = true
resp.Data = map[string]any{
"value": savedValue,
respData := map[string]any{}
if val, ok := savedValue.(string); ok {
respData["value"] = strutil.EncodeRedisKey(val)
}
resp.Data = respData
return
}
@ -1606,10 +1615,11 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key)
str := strutil.DecodeRedisKey(param.Value)
index := int64(param.Index)
var replaced, removed []types.ListReplaceItem
if len(str) <= 0 {
// remove from list
err = client.LSet(ctx, key, param.Index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
err = client.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
if err != nil {
resp.Msg = err.Error()
return
@ -1631,7 +1641,7 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return
}
err = client.LSet(ctx, key, param.Index, saveStr).Err()
err = client.LSet(ctx, key, index, saveStr).Err()
if err != nil {
resp.Msg = err.Error()
return

View File

@ -5,7 +5,6 @@ import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"github.com/klauspost/compress/zip"
"github.com/redis/go-redis/v9"
"github.com/vrischmann/userdir"
@ -65,7 +64,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
} else if config.Proxy.Type == 2 {
// use custom proxy
proxyUrl := url.URL{
Host: fmt.Sprintf("%s:%d", config.Proxy.Addr, config.Proxy.Port),
Host: net.JoinHostPort(config.Proxy.Addr, strconv.Itoa(config.Proxy.Port)),
}
if len(config.Proxy.Username) > 0 {
proxyUrl.User = url.UserPassword(config.Proxy.Username, config.Proxy.Password)
@ -111,7 +110,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
return nil, errors.New("invalid login type")
}
sshAddr = fmt.Sprintf("%s:%d", config.SSH.Addr, config.SSH.Port)
sshAddr = net.JoinHostPort(config.SSH.Addr, strconv.Itoa(config.SSH.Port))
}
var tlsConfig *tls.Config
@ -168,9 +167,9 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
port = config.Port
}
if len(config.Addr) <= 0 {
option.Addr = fmt.Sprintf("127.0.0.1:%d", port)
option.Addr = net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
} else {
option.Addr = fmt.Sprintf("%s:%d", config.Addr, port)
option.Addr = net.JoinHostPort(config.Addr, strconv.Itoa(port))
}
}
@ -224,9 +223,13 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (re
if len(addr) < 2 {
return nil, errors.New("cannot get master address")
}
option.Addr = fmt.Sprintf("%s:%s", addr[0], addr[1])
option.Addr = net.JoinHostPort(addr[0], addr[1])
option.Username = config.Sentinel.Username
option.Password = config.Sentinel.Password
if option.Dialer != nil {
option.ReadTimeout = -2
option.WriteTimeout = -2
}
}
if config.LastDB > 0 {
@ -310,7 +313,7 @@ func (c *connectionService) ListSentinelMasters(config types.ConnectionConfig) (
if infoMap, ok := info.(map[any]any); ok {
retInfo = append(retInfo, map[string]string{
"name": infoMap["name"].(string),
"addr": fmt.Sprintf("%s:%s", infoMap["ip"].(string), infoMap["port"].(string)),
"addr": net.JoinHostPort(infoMap["ip"].(string), infoMap["port"].(string)),
})
}
}

View File

@ -89,7 +89,7 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
item.cmd = item.client.Monitor(c.ctx, item.ch)
item.cmd.Start()
go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.eventName)
go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.cmd, item.eventName)
resp.Success = true
resp.Data = struct {
EventName string `json:"eventName"`
@ -99,7 +99,7 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
return
}
func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, eventName string) {
func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, cmd *redis.MonitorCmd, eventName string) {
lastEmitTime := time.Now().Add(-1 * time.Minute)
cache := make([]string, 0, 1000)
for {
@ -120,6 +120,7 @@ func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, clo
case <-closeCh:
// monitor stopped
cmd.Stop()
return
}
}
@ -136,8 +137,8 @@ func (c *monitorService) StopMonitor(server string) (resp types.JSResp) {
return
}
item.cmd.Stop()
//close(item.ch)
item.client.Close()
close(item.closeCh)
delete(c.items, server)
resp.Success = true

View File

@ -6,6 +6,7 @@ import (
"github.com/adrg/sysfont"
runtime2 "github.com/wailsapp/wails/v2/pkg/runtime"
"net/http"
"os"
"sort"
"strings"
"sync"
@ -50,6 +51,7 @@ func (p *preferencesService) SetPreferences(pf types.Preferences) (resp types.JS
return
}
p.UpdateEnv()
resp.Success = true
return
}
@ -114,6 +116,11 @@ func (p *preferencesService) GetBuildInDecoder() (resp types.JSResp) {
return
}
func (p *preferencesService) GetLanguage() string {
pref := p.pref.GetPreferences()
return pref.General.Language
}
func (p *preferencesService) SetAppVersion(ver string) {
if !strings.HasPrefix(ver, "v") {
p.clientVersion = "v" + ver
@ -215,22 +222,31 @@ func (p *preferencesService) GetDecoder() []convutil.CmdConvert {
})
}
type latestRelease struct {
Name string `json:"name"`
TagName string `json:"tag_name"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
type sponsorItem struct {
Name string `json:"name"`
Link string `json:"link"`
Region []string `json:"region"`
}
type upgradeInfo struct {
Version string `json:"version"`
Changelog map[string]string `json:"changelog"`
Description map[string]string `json:"description"`
DownloadURl map[string]string `json:"download_url"`
DownloadPage map[string]string `json:"download_page"`
Sponsor []sponsorItem `json:"sponsor,omitempty"`
}
func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
// 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 {
resp.Msg = "network error"
return
}
var respObj latestRelease
var respObj upgradeInfo
err = json.NewDecoder(res.Body).Decode(&respObj)
if err != nil {
resp.Msg = "invalid content"
@ -240,9 +256,20 @@ func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
// compare with current version
resp.Success = true
resp.Data = map[string]any{
"version": p.clientVersion,
"latest": respObj.TagName,
"page_url": respObj.HtmlUrl,
"version": p.clientVersion,
"latest": respObj.Version,
"description": respObj.Description,
"download_page": respObj.DownloadPage,
"sponsor": respObj.Sponsor,
}
return
}
// UpdateEnv Update System Environment
func (p *preferencesService) UpdateEnv() {
if p.GetLanguage() == "zh" {
os.Setenv("LANG", "zh_CN.UTF-8")
} else {
os.Unsetenv("LANG")
}
}

View File

@ -56,7 +56,7 @@ type SetListParam struct {
Server string `json:"server"`
DB int `json:"db"`
Key any `json:"key"`
Index int64 `json:"index"`
Index int `json:"index"`
Value any `json:"value"`
Format string `json:"format,omitempty"`
Decode string `json:"decode,omitempty"`

View File

@ -27,11 +27,12 @@ func NewPreferences() Preferences {
AllowTrack: true,
},
Editor: PreferencesEditor{
FontSize: consts.DEFAULT_FONT_SIZE,
ShowLineNum: true,
ShowFolding: true,
DropText: true,
Links: true,
FontSize: consts.DEFAULT_FONT_SIZE,
ShowLineNum: true,
ShowFolding: true,
DropText: true,
Links: true,
EntryTextAlign: 0,
},
Cli: PreferencesCli{
FontSize: consts.DEFAULT_FONT_SIZE,
@ -67,13 +68,14 @@ type PreferencesGeneral struct {
}
type PreferencesEditor struct {
Font string `json:"font" yaml:"font,omitempty"`
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
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"`
Font string `json:"font" yaml:"font,omitempty"`
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
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 {

View File

@ -1,12 +1,13 @@
package types
type ListEntryItem struct {
Index int `json:"index"`
Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"`
}
type ListReplaceItem struct {
Index int64 `json:"index"`
Index int `json:"index"`
Value any `json:"v,omitempty"`
DisplayValue string `json:"dv,omitempty"`
}
@ -32,7 +33,7 @@ type SetEntryItem struct {
type ZSetEntryItem struct {
Score float64 `json:"s"`
ScoreStr string `json:"ss,omitempty"`
Value string `json:"v"`
Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"`
}

View File

@ -13,6 +13,7 @@ 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"

View File

@ -24,6 +24,7 @@ var (
gzipConv GZipConvert
deflateConv DeflateConvert
zstdConv ZStdConvert
lz4Conv LZ4Convert
brotliConv BrotliConvert
msgpackConv MsgpackConvert
phpConv = NewPhpConvert()
@ -44,6 +45,7 @@ var BuildInDecoders = map[string]DataConvert{
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,
@ -138,6 +140,11 @@ func autoDecode(str string, customDecoder []CmdConvert) (value, resultDecode str
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

View File

@ -0,0 +1,39 @@
package convutil
import (
"bytes"
"github.com/pierrec/lz4/v4"
"io"
)
type LZ4Convert struct{}
func (LZ4Convert) Enable() bool {
return true
}
func (LZ4Convert) Encode(str string) (string, bool) {
var compress = func(b []byte) (string, error) {
var buf bytes.Buffer
writer := lz4.NewWriter(&buf)
if _, err := writer.Write([]byte(str)); err != nil {
writer.Close()
return "", err
}
writer.Close()
return string(buf.Bytes()), nil
}
if gzipStr, err := compress([]byte(str)); err == nil {
return gzipStr, true
}
return str, false
}
func (LZ4Convert) Decode(str string) (string, bool) {
reader := lz4.NewReader(bytes.NewReader([]byte(str)))
if decompressed, err := io.ReadAll(reader); err == nil {
return string(decompressed), true
}
return str, false
}

View File

@ -11,9 +11,12 @@ func (MsgpackConvert) Enable() bool {
return true
}
func (MsgpackConvert) Encode(str string) (string, bool) {
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
}
@ -43,3 +46,25 @@ func (MsgpackConvert) Decode(str string) (string, bool) {
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
}
}

View File

@ -107,27 +107,6 @@ func Merge[M ~map[K]V, K Hashable, V any](mapArr ...M) M {
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 根据条件省略指定元素
func Omit[M ~map[K]V, K Hashable, V any](m M, omitFunc func(k K, v V) bool) (M, []K) {
result := M{}

View File

@ -135,29 +135,38 @@ func SplitCmd(cmd string) []string {
var result []string
var curStr strings.Builder
var preChar int32
inQuotes := false
var quotesChar int32
for _, char := range []rune(cmd) {
if char == '"' && preChar != '\\' {
inQuotes = !inQuotes
} else if char == ' ' && !inQuotes {
if curStr.Len() > 0 {
if part, e := strconv.Unquote(`"` + curStr.String() + `"`); e == nil {
result = append(result, part)
}
curStr.Reset()
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())
if curStr.Len() > 0 {
if part, e := strconv.Unquote(`"` + curStr.String() + `"`); e == nil {
result = append(result, part)
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
}

View File

@ -18,7 +18,7 @@
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<string>11.7.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>

View File

@ -18,7 +18,7 @@
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<string>11.7.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>

File diff suppressed because it is too large Load Diff

View File

@ -9,26 +9,26 @@
"preview": "vite preview"
},
"dependencies": {
"chart.js": "^4.4.3",
"chart.js": "^4.4.8",
"copy-text-to-clipboard": "^3.2.0",
"dayjs": "^1.11.11",
"dayjs": "^1.11.13",
"lodash": "^4.17.21",
"monaco-editor": "^0.47.0",
"pinia": "^2.1.7",
"sass": "^1.77.6",
"vue": "^3.4.31",
"vue-chartjs": "^5.3.1",
"vue-i18n": "^9.13.1",
"pinia": "^3.0.1",
"sass": "^1.85.1",
"vue": "^3.5.13",
"vue-chartjs": "^5.3.2",
"vue-i18n": "^11.1.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"naive-ui": "^2.38.2",
"prettier": "^3.3.2",
"unplugin-auto-import": "^0.17.6",
"unplugin-icons": "^0.19.0",
"unplugin-vue-components": "^0.27.2",
"vite": "^5.3.3"
"@vitejs/plugin-vue": "^5.2.1",
"naive-ui": "^2.41.0",
"prettier": "^3.5.3",
"unplugin-auto-import": "^19.1.1",
"unplugin-icons": "^22.1.0",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2"
}
}

View File

@ -1 +1 @@
5e836053179f25770daa9c7d32dc862e
47ebcfd89e9e219e5b4ccf43ca2aa197

View File

@ -1,7 +1,7 @@
<script setup>
import ContentPane from './components/content/ContentPane.vue'
import BrowserPane from './components/sidebar/BrowserPane.vue'
import { computed, onMounted, reactive, ref, watchEffect } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref, watchEffect } from 'vue'
import { debounce } from 'lodash'
import { useThemeVars } from 'naive-ui'
import Ribbon from './components/sidebar/Ribbon.vue'
@ -13,7 +13,7 @@ import ContentLogPane from './components/content/ContentLogPane.vue'
import ContentValueTab from '@/components/content/ContentValueTab.vue'
import ToolbarControlWidget from '@/components/common/ToolbarControlWidget.vue'
import { EventsOn, WindowIsFullscreen, WindowIsMaximised, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
import { isMacOS } from '@/utils/platform.js'
import { isMacOS, isWindows } from '@/utils/platform.js'
import iconUrl from '@/assets/images/icon.png'
import ResizeableWrapper from '@/components/common/ResizeableWrapper.vue'
import { extraTheme } from '@/utils/extra_theme.js'
@ -57,6 +57,9 @@ const logoPaddingLeft = ref(10)
const maximised = ref(false)
const hideRadius = ref(false)
const wrapperStyle = computed(() => {
if (isWindows()) {
return {}
}
return hideRadius.value
? {}
: {
@ -65,6 +68,11 @@ const wrapperStyle = computed(() => {
}
})
const spinStyle = computed(() => {
if (isWindows()) {
return {
backgroundColor: themeVars.value.bodyColor,
}
}
return hideRadius.value
? {
backgroundColor: themeVars.value.bodyColor,
@ -109,12 +117,18 @@ onMounted(async () => {
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 (e.metaKey) {
if (isCtrlOn) {
// close current tab
const tabStore = useTabStore()
const currentTab = tabStore.currentTab
@ -130,7 +144,7 @@ const onKeyShortcut = (e) => {
<template>
<!-- app content-->
<n-spin :show="props.loading" :style="spinStyle" :theme-overrides="{ opacitySpinning: 0 }">
<div id="app-content-wrapper" :style="wrapperStyle" class="flex-box-v" tabindex="0" @keydown="onKeyShortcut">
<div id="app-content-wrapper" :style="wrapperStyle" class="flex-box-v">
<!-- title bar -->
<div
id="app-toolbar"

View File

@ -12,6 +12,10 @@ const props = defineProps({
},
icons: Array,
tTooltips: Array,
tTooltipPlacement: {
type: String,
default: 'bottom',
},
iconSize: {
type: [Number, String],
default: 20,
@ -45,6 +49,7 @@ const handleSwitch = (idx) => {
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)">

View File

@ -168,5 +168,5 @@ defineExpose({
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
</style>

View File

@ -70,7 +70,7 @@ const isBlankValue = computed(() => {
})
const selectedSubTab = computed(() => {
const { subTab = 'status' } = tabStore.currentTab || {}
const { subTab = BrowserTabType.Status } = tabStore.currentTab || {}
return subTab
})
@ -104,7 +104,7 @@ watch(
}"
:value="selectedSubTab"
class="content-sub-tab"
default-value="status"
:default-value="BrowserTabType.Status.toString()"
pane-class="content-sub-tab-pane"
placement="top"
tab-style="padding-left: 10px; padding-right: 10px;"
@ -212,7 +212,7 @@ watch(
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
.content-container {
//padding: 5px 5px 0;

View File

@ -1,8 +1,32 @@
<script setup>
import { computed } from 'vue'
import AddLink from '@/components/icons/AddLink.vue'
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 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>
<template>
@ -18,16 +42,27 @@ const dialogStore = useDialogStore()
</n-button>
</template>
</n-empty>
<n-button v-if="sponsorAd != null" class="sponsor-ad" style="" text @click="onOpenSponsor(sponsorAd.link)">
{{ sponsorAd.name }}
</n-button>
</div>
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
.content-container {
justify-content: center;
padding: 5px;
box-sizing: border-box;
& > .sponsor-ad {
text-align: center;
margin-top: 20px;
vertical-align: bottom;
color: v-bind('themeVars.textColor3');
}
}
.color-preset-item {

View File

@ -253,6 +253,15 @@ const onTermKey = (e) => {
}
// 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

View File

@ -23,7 +23,7 @@ const props = defineProps({
type: [String, Number],
},
value: {
type: String,
type: [String, Array],
},
fieldLabel: {
type: String,

View File

@ -181,7 +181,7 @@ const onCleanLog = () => {
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
.line-item {
margin-bottom: 5px;

View File

@ -281,7 +281,7 @@ const onPublish = async () => {
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
.total-message {
margin: 10px 0 0;

View File

@ -776,7 +776,7 @@ const clientTableColumns = computed(() => {
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
.line-chart {
display: flex;

View File

@ -222,5 +222,5 @@ const onListLimitChanged = (limit) => {
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
</style>

View File

@ -15,10 +15,14 @@ import IconButton from '@/components/common/IconButton.vue'
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
import Edit from '@/components/icons/Edit.vue'
import FormatSelector from '@/components/content_value/FormatSelector.vue'
import { decodeRedisKey } from '@/utils/key_convert.js'
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
import SwitchButton from '@/components/common/SwitchButton.vue'
import AlignLeft from '@/components/icons/AlignLeft.vue'
import AlignCenter from '@/components/icons/AlignCenter.vue'
import { TextAlignType } from '@/consts/text_align_type.js'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -36,7 +40,7 @@ const props = defineProps({
default: -1,
},
value: {
type: Array,
type: [String, Array],
default: () => [],
},
size: Number,
@ -45,9 +49,10 @@ const props = defineProps({
decode: String,
end: Boolean,
loading: Boolean,
textAlign: Number,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
/**
*
@ -78,7 +83,7 @@ const fieldFilterOption = ref(null)
const fieldColumn = computed(() => ({
key: 'key',
title: () => i18n.t('common.field'),
align: 'center',
align: props.textAlign !== TextAlignType.Left ? 'center' : 'left',
titleAlign: 'center',
resizable: true,
ellipsis: {
@ -111,7 +116,7 @@ const isCode = computed(() => {
const valueColumn = computed(() => ({
key: 'value',
title: () => i18n.t('common.value'),
align: isCode.value ? 'left' : 'center',
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
titleAlign: 'center',
resizable: true,
ellipsis: isCode.value
@ -135,13 +140,14 @@ const valueColumn = computed(() => ({
// return !!~row.v.indexOf(value.toString())
// },
render: (row) => {
const val = row.dv || nativeRedisKey(row.v)
if (isCode.value) {
return h('pre', { class: 'pre-wrap' }, row.dv || row.v)
return h('pre', { class: 'pre-wrap' }, val)
}
if (row.rm === true) {
return h('s', {}, row.dv || row.v)
return h('s', {}, val)
}
return row.dv || row.v
return val
},
}))
@ -362,6 +368,15 @@ defineExpose({
@match-changed="onMatchInput" />
</div>
<div class="flex-item-expand"></div>
<switch-button
:icons="[AlignCenter, AlignLeft]"
:stroke-width="3.5"
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
:value="props.textAlign"
size="medium"
unselect-stroke-width="3"
@update:value="(val) => emit('update:textAlign', val)" />
<n-divider vertical />
<n-button-group>
<icon-button
:disabled="props.end || props.loading"

View File

@ -18,6 +18,11 @@ import Edit from '@/components/icons/Edit.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
import { TextAlignType } from '@/consts/text_align_type.js'
import AlignLeft from '@/components/icons/AlignLeft.vue'
import AlignCenter from '@/components/icons/AlignCenter.vue'
import SwitchButton from '@/components/common/SwitchButton.vue'
import { nativeRedisKey } from '@/utils/key_convert.js'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -51,9 +56,10 @@ const props = defineProps({
},
end: Boolean,
loading: Boolean,
textAlign: Number,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
/**
*
@ -84,7 +90,7 @@ const valueFilterOption = ref(null)
const valueColumn = computed(() => ({
key: 'value',
title: () => i18n.t('common.value'),
align: isCode.value ? 'left' : 'center',
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
titleAlign: 'center',
ellipsis: isCode.value
? false
@ -100,17 +106,16 @@ const valueColumn = computed(() => ({
},
filterOptionValue: valueFilterOption.value,
className: inEdit.value ? 'clickable' : '',
filter: (value, row) => {
if (row.dv) {
return !!~row.dv.indexOf(value.toString())
}
return !!~row.v.indexOf(value.toString())
filter: (filterValue, row) => {
const val = row.dv || nativeRedisKey(row.v)
return !!~val.indexOf(filterValue.toString())
},
render: (row) => {
const val = row.dv || nativeRedisKey(row.v)
if (isCode.value) {
return h('pre', { class: 'pre-wrap' }, row.dv || row.v)
return h('pre', { class: 'pre-wrap' }, val)
}
return row.dv || row.v
return val
},
}))
@ -145,7 +150,7 @@ const saveEdit = async (pos, value, decode, format) => {
server: props.name,
db: props.db,
key: keyName.value,
index,
index: row.index,
value,
decode,
format,
@ -175,16 +180,16 @@ const actionColumn = {
align: 'center',
titleAlign: 'center',
fixed: 'right',
render: (row, index) => {
render: ({ index, v }, _) => {
return h(EditableTableColumn, {
editing: false,
bindKey: `#${index + 1}`,
onCopy: async () => {
copy(row.v)
copy(v)
$message.success(i18n.t('interface.copy_succ'))
},
onEdit: () => {
startEdit(index + 1, row.v)
startEdit(index + 1, v)
},
onDelete: async () => {
try {
@ -216,7 +221,7 @@ const columns = computed(() => {
width: 80,
align: 'center',
titleAlign: 'center',
render: (row, index) => {
render: ({ index }, _) => {
return index + 1
},
},
@ -231,7 +236,7 @@ const columns = computed(() => {
width: 80,
align: 'center',
titleAlign: 'center',
render: (row, index) => {
render: ({ index }, _) => {
if (index + 1 === currentEditRow.no) {
// editing row, show edit state
return h(NIcon, { size: 16, color: 'red' }, () => h(Edit, { strokeWidth: 5 }))
@ -245,12 +250,12 @@ const columns = computed(() => {
}
})
const rowProps = (row, index) => {
const rowProps = ({ index, v }, _) => {
return {
onClick: () => {
// in edit mode, switch edit row by click
if (inEdit.value) {
startEdit(index + 1, row.v)
startEdit(index + 1, v)
}
},
}
@ -311,6 +316,15 @@ defineExpose({
@match-changed="onMatchInput" />
</div>
<div class="flex-item-expand"></div>
<switch-button
:icons="[AlignCenter, AlignLeft]"
:stroke-width="3.5"
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
:value="props.textAlign"
size="medium"
unselect-stroke-width="3"
@update:value="(val) => emit('update:textAlign', val)" />
<n-divider vertical />
<n-button-group>
<icon-button
:disabled="props.end || props.loading"

View File

@ -18,6 +18,11 @@ import FormatSelector from '@/components/content_value/FormatSelector.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
import AlignLeft from '@/components/icons/AlignLeft.vue'
import AlignCenter from '@/components/icons/AlignCenter.vue'
import SwitchButton from '@/components/common/SwitchButton.vue'
import { TextAlignType } from '@/consts/text_align_type.js'
import { nativeRedisKey } from '@/utils/key_convert.js'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -50,9 +55,10 @@ const props = defineProps({
},
end: Boolean,
loading: Boolean,
textAlign: Number,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
/**
*
@ -83,7 +89,7 @@ const valueFilterOption = ref(null)
const valueColumn = computed(() => ({
key: 'value',
title: () => i18n.t('common.value'),
align: isCode.value ? 'left' : 'center',
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
titleAlign: 'center',
ellipsis: isCode.value
? false
@ -99,17 +105,16 @@ const valueColumn = computed(() => ({
},
filterOptionValue: valueFilterOption.value,
className: inEdit.value ? 'clickable' : '',
filter: (value, row) => {
if (row.dv) {
return !!~row.dv.indexOf(value.toString())
}
return !!~row.v.indexOf(value.toString())
filter: (filterValue, row) => {
const val = row.dv || nativeRedisKey(row.v)
return !!~val.indexOf(filterValue.toString())
},
render: (row) => {
const val = row.dv || nativeRedisKey(row.v)
if (isCode.value) {
return h('pre', { class: 'pre-wrap' }, row.dv || row.v)
return h('pre', { class: 'pre-wrap' }, val)
}
return row.dv || row.v
return val
},
}))
@ -308,6 +313,15 @@ defineExpose({
@match-changed="onMatchInput" />
</div>
<div class="flex-item-expand"></div>
<switch-button
:icons="[AlignCenter, AlignLeft]"
:stroke-width="3.5"
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
:value="props.textAlign"
size="medium"
unselect-stroke-width="3"
@update:value="(val) => emit('update:textAlign', val)" />
<n-divider vertical />
<n-button-group>
<icon-button
:disabled="props.end || props.loading"

View File

@ -14,10 +14,14 @@ import useDialogStore from 'stores/dialog.js'
import { useI18n } from 'vue-i18n'
import ContentToolbar from '@/components/content_value/ContentToolbar.vue'
import ContentValueJson from '@/components/content_value/ContentValueJson.vue'
import usePreferencesStore from 'stores/preferences.js'
import { TextAlignType } from '@/consts/text_align_type.js'
import { isMacOS } from '@/utils/platform.js'
const themeVars = useThemeVars()
const browserStore = useBrowserStore()
const dialogStore = useDialogStore()
const prefStore = usePreferencesStore()
const props = defineProps({
blank: Boolean,
@ -127,7 +131,7 @@ const onReload = async (selDecode, selFormat) => {
}
const onKeyShortcut = (e) => {
// console.log(e)
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
switch (e.key) {
case 'Delete':
onDelete()
@ -136,7 +140,7 @@ const onKeyShortcut = (e) => {
onReload()
return
case 'r':
if (e.metaKey) {
if (isCtrlOn) {
onReload()
}
return
@ -178,6 +182,11 @@ const onMatch = (match) => {
loadData(true, false, match || '')
}
const onEntryTextAlignChanged = (align) => {
prefStore.editor.entryTextAlign = align !== TextAlignType.Left ? TextAlignType.Center : TextAlignType.Left
prefStore.savePreferences()
}
const contentRef = ref(null)
const initContent = async () => {
// onReload()
@ -225,12 +234,14 @@ watch(() => data.value?.keyPath, initContent)
:ttl="data.ttl"
:value="data.value"
tabindex="0"
:text-align="prefStore.entryTextAlign"
@delete="onDelete"
@keydown="onKeyShortcut"
@loadall="onLoadAll"
@loadmore="onLoadMore"
@match="onMatch"
@reload="onReload">
@reload="onReload"
@update:text-align="onEntryTextAlignChanged">
<template #toolbar>
<content-toolbar
:db="data.db"

View File

@ -18,6 +18,11 @@ import Edit from '@/components/icons/Edit.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
import { TextAlignType } from '@/consts/text_align_type.js'
import AlignLeft from '@/components/icons/AlignLeft.vue'
import AlignCenter from '@/components/icons/AlignCenter.vue'
import SwitchButton from '@/components/common/SwitchButton.vue'
import { nativeRedisKey } from '@/utils/key_convert.js'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -50,9 +55,10 @@ const props = defineProps({
},
end: Boolean,
loading: Boolean,
textAlign: Number,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
/**
*
@ -82,7 +88,7 @@ const fullEdit = ref(false)
const scoreColumn = computed(() => ({
key: 'score',
title: () => i18n.t('common.score'),
align: 'center',
align: props.textAlign !== TextAlignType.Left ? 'center' : 'left',
titleAlign: 'center',
resizable: true,
sorter: (row1, row2) => row1.s - row2.s,
@ -131,7 +137,7 @@ const valueFilterOption = ref(null)
const valueColumn = computed(() => ({
key: 'value',
title: () => i18n.t('common.value'),
align: isCode.value ? 'left' : 'center',
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
titleAlign: 'center',
resizable: true,
ellipsis: isCode.value
@ -148,18 +154,17 @@ const valueColumn = computed(() => ({
},
filterOptionValue: valueFilterOption.value,
className: inEdit.value ? 'clickable' : '',
filter(value, row) {
if (row.dv) {
return !!~row.dv.indexOf(value.toString())
}
return !!~row.v.indexOf(value.toString())
filter(filterValue, row) {
const val = row.dv || nativeRedisKey(row.v)
return !!~val.indexOf(filterValue.toString())
},
// sorter: (row1, row2) => row1.value - row2.value,
render: (row) => {
const val = row.dv || nativeRedisKey(row.v)
if (isCode.value) {
return h('pre', { class: 'pre-wrap' }, row.dv || row.v)
return h('pre', { class: 'pre-wrap' }, val)
}
return row.dv || row.v
return val
},
}))
@ -343,6 +348,15 @@ defineExpose({
@match-changed="onMatchInput" />
</div>
<div class="flex-item-expand"></div>
<switch-button
:icons="[AlignCenter, AlignLeft]"
:stroke-width="3.5"
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
:value="props.textAlign"
size="medium"
unselect-stroke-width="3"
@update:value="(val) => emit('update:textAlign', val)" />
<n-divider vertical />
<n-button-group>
<icon-button
:disabled="props.end || props.loading"

View File

@ -93,7 +93,7 @@ const onDecodeMenu = (key) => {
:icon="Code"
:options="formatTypeOption"
:tooltip="$t('interface.view_as')"
:value="props.format"
:value="props.format || formatTypes.RAW"
@update:value="(f) => onFormatChanged(props.decode, f)" />
<n-divider vertical />
<dropdown-selector
@ -103,7 +103,7 @@ const onDecodeMenu = (key) => {
:menu-option="decodeMenuOption"
:options="decodeTypeOption"
:tooltip="$t('interface.decode_with')"
:value="props.decode"
:value="props.decode || decodeTypes.NONE"
@menu="onDecodeMenu"
@update:value="(d) => onFormatChanged(d, '')" />
</n-space>

View File

@ -203,5 +203,5 @@ const onClose = () => {}
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
</style>

View File

@ -174,10 +174,10 @@ const onAdd = async () => {
tabStore.setSelectedKeys(server, nodeKey)
browserStore.reloadKey({ server, db, key })
}
dialogStore.closeNewKeyDialog()
} else if (!isEmpty(msg)) {
$message.error(msg)
}
dialogStore.closeNewKeyDialog()
} catch (e) {
return false
}

View File

@ -0,0 +1,39 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M36 19H12"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M42 9H6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M42 29H6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M36 39H12"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,39 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M42 9H6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M34 19H6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M42 29H6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M34 39H6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -39,18 +39,6 @@ const props = defineProps({
stroke="currentColor"
stroke-linejoin="round" />
<path :stroke-width="props.strokeWidth" d="M43 22H5" stroke="currentColor" stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M5 16V28"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M43 16V28"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>

View File

@ -37,18 +37,6 @@ const props = defineProps({
d="M26 38H38"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M44 37V27"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M4 37V27"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>

View File

@ -458,7 +458,7 @@ watch(
</template>
<style lang="scss" scoped>
@import '@/styles/style';
@use '@/styles/style' as style;
:deep(.toggle-btn) {
border-style: solid;
@ -483,7 +483,7 @@ watch(
}
.nav-pane-bottom {
@include top-shadow(0.1);
@include style.top-shadow(0.1);
color: v-bind('themeVars.iconColor');
border-top: v-bind('themeVars.borderColor') 1px solid;
}

View File

@ -26,6 +26,7 @@ import usePreferencesStore from 'stores/preferences.js'
import { typesIconStyle } from '@/consts/support_redis_type.js'
import { nativeRedisKey } from '@/utils/key_convert.js'
import copy from 'copy-text-to-clipboard'
import { isMacOS } from '@/utils/platform.js'
const props = defineProps({
server: String,
@ -326,6 +327,7 @@ const handleKeyCopy = () => {
}
const onKeyShortcut = (e) => {
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
switch (e.key) {
case 'ArrowUp':
handleKeyUp()
@ -340,7 +342,7 @@ const onKeyShortcut = (e) => {
handleKeyRight()
break
case 'c':
if (e.metaKey) {
if (isCtrlOn) {
handleKeyCopy()
}
break
@ -351,7 +353,7 @@ const onKeyShortcut = (e) => {
handleSelectContextMenu('value_reload')
break
case 'r':
if (e.metaKey) {
if (isCtrlOn) {
handleSelectContextMenu('value_reload')
}
break
@ -848,7 +850,7 @@ defineExpose({
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
.browser-tree-wrapper {
height: 100%;

View File

@ -295,6 +295,7 @@ const openConnection = async (name) => {
tabStore.upsertTab({
server: name,
db: browserStore.getSelectedDB(name),
forceSwitch: true,
})
}
} catch (e) {
@ -542,7 +543,7 @@ const onCancelOpen = () => {
</template>
<style lang="scss" scoped>
@import '@/styles/content';
@use '@/styles/content';
.connection-tree-wrapper {
height: 100%;

View File

@ -0,0 +1,8 @@
/**
* all types of text alignment
* @enum {number}
*/
export const TextAlignType = {
Center: 0,
Left: 1,
}

View File

@ -22,6 +22,7 @@ export const decodeTypes = {
GZIP: 'GZip',
DEFLATE: 'Deflate',
ZSTD: 'ZStd',
LZ4: 'LZ4',
BROTLI: 'Brotli',
MSGPACK: 'Msgpack',
PHP: 'PHP',

View File

@ -126,6 +126,8 @@
"length": "Length",
"entries": "Entries",
"memory_usage": "Memory Usage",
"text_align_left": "Text Align Left",
"text_align_center": "Text Align Center",
"view_as": "View As",
"decode_with": "Decode / Decompress",
"custom_decoder": "New Custom Decoder",

View File

@ -126,6 +126,8 @@
"length": "Longitud",
"entries": "Entradas",
"memory_usage": "Uso de memoria",
"text_align_left": "Alinear a la izquierda",
"text_align_center": "Centrar",
"view_as": "Ver como",
"decode_with": "Decodificar / Descomprimir",
"custom_decoder": "Nuevo decodificador personalizado",

View File

@ -126,6 +126,8 @@
"length": "Longueur",
"entries": "Entrées",
"memory_usage": "Utilisation de la mémoire",
"text_align_left": "Aligner à gauche",
"text_align_center": "Centrer",
"view_as": "Voir comme",
"decode_with": "Décoder / Décompresser",
"custom_decoder": "Nouveau décodeur personnalisé",

View File

@ -126,6 +126,8 @@
"length": "長さ",
"entries": "エントリ",
"memory_usage": "メモリ使用量",
"text_align_left": "左揃え",
"text_align_center": "中央揃え",
"view_as": "表示形式",
"decode_with": "デコード/解凍",
"custom_decoder": "新しいカスタムデコーダー",

View File

@ -126,6 +126,8 @@
"length": "길이",
"entries": "항목 수",
"memory_usage": "메모리 사용량",
"text_align_left": "텍스트 왼쪽 정렬",
"text_align_center": "텍스트 가운데 정렬",
"view_as": "보기",
"decode_with": "디코딩/압축 해제",
"custom_decoder": "새 사용자 정의 디코더",

View File

@ -126,6 +126,8 @@
"length": "Tamanho",
"entries": "Entradas",
"memory_usage": "Uso de Memória",
"text_align_left": "Alinhar à esquerda",
"text_align_center": "Centralizar",
"view_as": "Visualizar Como",
"decode_with": "Decodificar / Descompressão",
"custom_decoder": "Novo Decodificador Personalizado",

View File

@ -126,6 +126,8 @@
"length": "Длина",
"entries": "Записи",
"memory_usage": "Использование памяти",
"text_align_left": "Выравнивание по левому краю",
"text_align_center": "Выравнивание по центру",
"view_as": "Вид",
"decode_with": "Декодировать/Распаковать",
"custom_decoder": "Новый пользовательский декодер",

View File

@ -56,7 +56,7 @@
"show_linenum": "显示行号",
"show_folding": "启用代码折叠",
"drop_text": "允许拖放文本",
"links": "支持接跳转"
"links": "支持接跳转"
},
"cli": {
"name": "命令行",
@ -126,6 +126,8 @@
"length": "长度",
"entries": "条目",
"memory_usage": "内存占用",
"text_align_left": "文本居左",
"text_align_center": "文本居中",
"view_as": "查看方式",
"decode_with": "解码/解压方式",
"custom_decoder": "添加自定义解码",

View File

@ -126,6 +126,8 @@
"length": "長度",
"entries": "條目",
"memory_usage": "記憶體使用量",
"text_align_left": "文字靠左",
"text_align_center": "文字置中",
"view_as": "檢視方式",
"decode_with": "解碼/解壓方式",
"custom_decoder": "新增自定義解碼",

View File

@ -296,6 +296,9 @@ export class RedisServerState {
removeKeyNode(key, isLayer) {
if (isLayer === true) {
this.deleteChildrenKeyNodes(key)
} else {
const nodeKey = `${ConnectionType.RedisValue}/${key}`
this.nodeMap.delete(nodeKey)
}
const dbRoot = this.getRoot()

View File

@ -386,9 +386,10 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db
* @param {string|number[]} [key] null or blank indicate that update tab to display normal content (blank content or server status)
* @param {boolean} [clearValue]
* @param {boolean} [redirect] redirect to key detail tab
* @return {Promise<void>}
*/
async loadKeySummary({ server, db, key, clearValue }) {
async loadKeySummary({ server, db, key, clearValue, redirect = true }) {
try {
const tab = useTabStore()
if (!isEmpty(key)) {
@ -402,7 +403,7 @@ const useBrowserStore = defineStore('browser', {
const k = nativeRedisKey(key)
const binaryKey = k !== key
tab.upsertTab({
subTab: BrowserTabType.KeyDetail,
subTab: redirect === false ? null : BrowserTabType.KeyDetail,
server,
db,
type,
@ -492,7 +493,7 @@ const useBrowserStore = defineStore('browser', {
if (showLoading) {
tab.updateLoading({ server, db, loading: true })
}
await this.loadKeySummary({ server, db, key, clearValue: true })
await this.loadKeySummary({ server, db, key, clearValue: true, redirect: false })
await this.loadKeyDetail({
server,
db,
@ -708,17 +709,19 @@ const useBrowserStore = defineStore('browser', {
}
let match = prefix
const separator = this.getSeparator(server)
if (!endsWith(match, separator)) {
match += separator + '*'
} else {
match += '*'
if (!isEmpty(match)) {
if (!endsWith(match, separator)) {
match += separator + '*'
} else {
match += '*'
}
}
// FIXME: ignore original match pattern due to redis not support combination matching
const { match: originMatch, type: keyType, exact } = this.getKeyFilter(server)
const { keys, maxKeys, success } = await this._loadKeys({
server,
db,
match: originMatch,
match: match || originMatch,
exact: false,
matchType: keyType,
all: true,
@ -2090,7 +2093,7 @@ const useBrowserStore = defineStore('browser', {
if (serverInst == null) {
serverInst = new RedisServerState({
name: server,
separator: this.getSeparator(name),
separator: this.getSeparator(server),
})
}
return serverInst.getFilter()

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { lang } from '@/langs/index.js'
import { cloneDeep, findIndex, get, isEmpty, join, map, merge, pick, set, some, split } from 'lodash'
import { cloneDeep, findIndex, get, isEmpty, join, map, pick, set, some, split } from 'lodash'
import {
CheckForUpdate,
GetBuildInDecoder,
@ -15,6 +15,7 @@ import { enUS, NButton, NSpace, useOsTheme, zhCN } from 'naive-ui'
import { h, nextTick } from 'vue'
import { compareVersion } from '@/utils/version.js'
import { typesIconStyle } from '@/consts/support_redis_type.js'
import { TextAlignType } from '@/consts/text_align_type.js'
const osTheme = useOsTheme()
const usePreferencesStore = defineStore('preferences', {
@ -63,6 +64,7 @@ const usePreferencesStore = defineStore('preferences', {
showFolding: true,
dropText: true,
links: true,
entryTextAlign: TextAlignType.Center,
},
cli: {
fontFamily: [],
@ -271,6 +273,10 @@ const usePreferencesStore = defineStore('preferences', {
keyIconType() {
return get(this.general, 'keyIconStyle', typesIconStyle.SHORT)
},
entryTextAlign() {
return get(this.editor, 'entryTextAlign', TextAlignType.Center)
},
},
actions: {
_applyPreferences(data) {
@ -421,15 +427,15 @@ const usePreferencesStore = defineStore('preferences', {
return false
}
this.decoder[idx] = merge(this.decoder[idx], {
name: newName || name,
enable,
auto,
encodePath,
encodeArgs,
decodePath,
decodeArgs,
})
let selDecoder = this.decoder[idx]
selDecoder.name = newName || name
selDecoder.enable = enable
selDecoder.auto = auto
selDecoder.encodePath = encodePath
selDecoder.encodeArgs = encodeArgs
selDecoder.decodePath = decodePath
selDecoder.decodeArgs = decodeArgs
this.decoder[idx] = selDecoder
return true
},
@ -461,15 +467,27 @@ const usePreferencesStore = defineStore('preferences', {
try {
const { success, data = {} } = await CheckForUpdate()
if (success) {
const { version = 'v1.0.0', latest, page_url: pageUrl } = data
const {
version = 'v1.0.0',
latest,
download_page: pageUrl = {},
description = {},
sponsor = [],
} = data
const downUrl = pageUrl[this.currentLanguage] || pageUrl['en']
const descStr = description[this.currentLanguage] || description['en']
// save sponsor ad
if (!isEmpty(sponsor)) {
localStorage.setItem('sponsor_ad', JSON.stringify(sponsor))
}
if (
(manual || latest > this.general.skipVersion) &&
(manual || compareVersion(latest, this.general.skipVersion) !== 0) &&
compareVersion(latest, version) > 0 &&
!isEmpty(pageUrl)
!isEmpty(downUrl)
) {
const notiRef = $notification.show({
title: i18nGlobal.t('dialogue.upgrade.title'),
content: i18nGlobal.t('dialogue.upgrade.new_version_tip', { ver: latest }),
title: `${i18nGlobal.t('dialogue.upgrade.title')} - ${latest}`,
content: descStr || i18nGlobal.t('dialogue.upgrade.new_version_tip', { ver: latest }),
action: () =>
h('div', { class: 'flex-box-h flex-item-expand' }, [
h(NSpace, { wrapItem: false }, () => [
@ -502,13 +520,13 @@ const usePreferencesStore = defineStore('preferences', {
type: 'primary',
size: 'small',
secondary: true,
onClick: () => BrowserOpenURL(pageUrl),
onClick: () => BrowserOpenURL(downUrl),
},
() => i18nGlobal.t('dialogue.upgrade.download_now'),
),
]),
]),
onPositiveClick: () => BrowserOpenURL(pageUrl),
onPositiveClick: () => BrowserOpenURL(downUrl),
})
return
}

View File

@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
import { TabItem } from '@/objects/tabItem.js'
import useBrowserStore from 'stores/browser.js'
import { i18nGlobal } from '@/utils/i18n.js'
import { BrowserTabType } from '@/consts/browser_tab_type.js'
const useTabStore = defineStore('tab', {
/**
@ -160,6 +161,7 @@ const useTabStore = defineStore('tab', {
* @param {boolean} [clearValue]
* @param {string} format
* @param {string} decode
* @param {boolean} forceSwitch
* @param {*} [value]
*/
upsertTab({
@ -176,9 +178,11 @@ const useTabStore = defineStore('tab', {
clearValue,
format = '',
decode = '',
forceSwitch = false,
}) {
let tabIndex = findIndex(this.tabList, { name: server })
if (tabIndex === -1) {
subTab = subTab || BrowserTabType.Status
const tabItem = new TabItem({
name: server,
title: server,
@ -198,10 +202,11 @@ const useTabStore = defineStore('tab', {
})
this.tabList.push(tabItem)
tabIndex = this.tabList.length - 1
this._setActivatedIndex(tabIndex, true, subTab)
} else {
const tab = this.tabList[tabIndex]
tab.blank = false
tab.subTab = subTab
tab.subTab = subTab || tab.subTab
// tab.title = db !== undefined ? `${server}/db${db}` : `${server}`
tab.title = server
tab.server = server
@ -218,8 +223,10 @@ const useTabStore = defineStore('tab', {
if (clearValue === true) {
tab.value = undefined
}
if (forceSwitch === true) {
this._setActivatedIndex(tabIndex, true, subTab)
}
}
this._setActivatedIndex(tabIndex, true, subTab)
},
/**
@ -289,9 +296,25 @@ const useTabStore = defineStore('tab', {
case 'list': // {v:string, dv:[string]}[]
tab.value = tab.value || []
if (prepend === true) {
tab.value = [...entries, ...tab.value]
const originList = tab.value
const list = []
let starIndex = 0
for (const entry of entries) {
entry.index = starIndex++
list.push(entry)
}
for (const entry of originList) {
entry.index = starIndex++
list.push(entry)
}
tab.value = list
} else {
tab.value.push(...entries)
const list = tab.value
let starIndex = list.length
for (const entry of entries) {
entry.index = starIndex++
list.push(entry)
}
}
tab.length += size(entries)
break
@ -390,6 +413,7 @@ const useTabStore = defineStore('tab', {
for (const entry of entries) {
if (size(tab.value) > entry.index) {
tab.value[entry.index] = {
index: entry.index,
v: entry.v,
dv: entry.dv,
}

View File

@ -174,7 +174,7 @@ body {
}
.auto-rotate {
animation: rotate 2s linear infinite;
animation: rotate 2s steps(60) infinite;
}
.pre-wrap {

View File

@ -10,3 +10,7 @@ export async function loadEnvironment() {
export function isMacOS() {
return os === 'darwin'
}
export function isWindows() {
return os === 'windows'
}

View File

@ -29,4 +29,11 @@ export default defineConfig({
wailsjs: rootPath + 'wailsjs',
},
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
},
})

39
go.mod
View File

@ -1,54 +1,53 @@
module tinyrdm
go 1.21
go 1.24.0
require (
github.com/adrg/sysfont v0.1.2
github.com/andybalholm/brotli v1.1.0
github.com/andybalholm/brotli v1.1.1
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.17.9
github.com/redis/go-redis/v9 v9.5.3
github.com/klauspost/compress v1.18.0
github.com/pierrec/lz4/v4 v4.1.22
github.com/redis/go-redis/v9 v9.7.1
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
github.com/wailsapp/wails/v2 v2.9.1
golang.org/x/crypto v0.24.0
golang.org/x/net v0.26.0
github.com/wailsapp/wails/v2 v2.10.1
golang.org/x/crypto v0.36.0
golang.org/x/net v0.37.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/adrg/strutil v0.3.1 // indirect
github.com/adrg/xdg v0.4.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/echo/v4 v4.11.4 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wailsapp/go-webview2 v1.0.10 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
// install latest wails: go install github.com/wailsapp/wails/v2/cmd/wails@latest
// replace github.com/wailsapp/wails/v2 v2.9.1 => ~/go/pkg/mod
// replace github.com/wailsapp/wails/v2 v2.10.1 => ~/go/pkg/mod

95
go.sum
View File

@ -4,19 +4,18 @@ github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6M
github.com/adrg/sysfont v0.1.2 h1:MSU3KREM4RhsQ+7QgH7wPEPTgAgBIz0Hw6Nd4u7QgjE=
github.com/adrg/sysfont v0.1.2/go.mod h1:6d3l7/BSjX9VaeXWJt9fcrftFaD/t7l11xgSywCPZGk=
github.com/adrg/xdg v0.3.0/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -30,60 +29,61 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@ -94,34 +94,41 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 h1:Ah2/69Z24rwD6OByyOdpJDmttftz0FTF8Q4QZ/SF1E4=
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68/go.mod h1:EqKqAeKddSL9XSGnfXd/7iLncccKhR16HBKVva7ENw8=
github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlEdc=
github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

23
main.go
View File

@ -3,6 +3,7 @@ package main
import (
"context"
"embed"
"fmt"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
@ -25,6 +26,8 @@ var icon []byte
var version = "0.0.0"
var gaMeasurementID, gaSecretKey string
const appName = "Tiny RDM"
func main() {
// Create an instance of the app structure
sysSvc := services.System()
@ -35,6 +38,7 @@ func main() {
pubsubSvc := services.Pubsub()
prefSvc := services.Preferences()
prefSvc.SetAppVersion(version)
prefSvc.UpdateEnv()
windowWidth, windowHeight, maximised := prefSvc.GetWindowSize()
windowStartState := options.Normal
if maximised {
@ -42,8 +46,9 @@ func main() {
}
// menu
isMacOS := runtime.GOOS == "darwin"
appMenu := menu.NewMenu()
if runtime.GOOS == "darwin" {
if isMacOS {
appMenu.Append(menu.AppMenu())
appMenu.Append(menu.EditMenu())
appMenu.Append(menu.WindowMenu())
@ -51,19 +56,19 @@ func main() {
// Create application with options
err := wails.Run(&options.App{
Title: "Tiny RDM",
Title: appName,
Width: windowWidth,
Height: windowHeight,
MinWidth: consts.MIN_WINDOW_WIDTH,
MinHeight: consts.MIN_WINDOW_HEIGHT,
WindowStartState: windowStartState,
Frameless: runtime.GOOS != "darwin",
Frameless: !isMacOS,
Menu: appMenu,
EnableDefaultContextMenu: true,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: options.NewRGBA(27, 38, 54, 0),
BackgroundColour: options.NewRGBA(255, 255, 255, 0),
StartHidden: true,
OnStartup: func(ctx context.Context) {
sysSvc.Start(ctx, version)
@ -104,7 +109,7 @@ func main() {
Mac: &mac.Options{
TitleBar: mac.TitleBarHiddenInset(),
About: &mac.AboutInfo{
Title: "Tiny RDM " + version,
Title: fmt.Sprintf("%s %s", appName, version),
Message: "A modern lightweight cross-platform Redis desktop client.\n\nCopyright © 2024",
Icon: icon,
},
@ -112,12 +117,12 @@ func main() {
WindowIsTranslucent: false,
},
Windows: &windows.Options{
WebviewIsTransparent: true,
WindowIsTranslucent: true,
DisableFramelessWindowDecorations: true,
WebviewIsTransparent: false,
WindowIsTranslucent: false,
DisableFramelessWindowDecorations: false,
},
Linux: &linux.Options{
ProgramName: "Tiny RDM",
ProgramName: appName,
Icon: icon,
WebviewGpuPolicy: linux.WebviewGpuPolicyOnDemand,
WindowIsTranslucent: true,