Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1d8eb79ac0
|
102
.gitignore
vendored
102
.gitignore
vendored
@@ -1,99 +1,3 @@
|
||||
### Go template
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
config.toml
|
||||
.$database.drawio.*
|
||||
/tmp/build-errors.log
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
|
||||
10
.idea/UniappTool.xml
generated
Normal file
10
.idea/UniappTool.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="cn.fjdmy.uniapp.UniappProjectDataService">
|
||||
<option name="generalBasePath" value="$PROJECT_DIR$" />
|
||||
<option name="manifestPath" value="$PROJECT_DIR$/manifest.json" />
|
||||
<option name="pagesPath" value="$PROJECT_DIR$/pages.json" />
|
||||
<option name="scanNum" value="1" />
|
||||
<option name="type" value="store" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
15
.idea/git_toolbox_prj.xml
generated
Normal file
15
.idea/git_toolbox_prj.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
102
GETTING_STARTED.md
Normal file
102
GETTING_STARTED.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# 快速开始指南
|
||||
|
||||
## 1. 确保 Gotify 服务端已运行
|
||||
|
||||
首先需要有一个运行中的 Gotify 服务器。可以参考官方文档部署:
|
||||
- Docker: `docker run -p 8080:80 gotify/server`
|
||||
- 或者二进制文件运行
|
||||
|
||||
## 2. 创建应用和获取 Token
|
||||
|
||||
1. 访问 Gotify Web 界面 (默认: http://localhost:8080)
|
||||
2. 以管理员身份登录 (默认: admin/admin)
|
||||
3. 客户端需要获取两个 Token:
|
||||
- **User Token**: 用于接收消息
|
||||
- **App Token**: (可选) 用于发送消息
|
||||
|
||||
### 获取 User Token
|
||||
- 登录后,访问 `http://localhost:8080/clients/create`
|
||||
- 创建客户端,复制 `Token` 字段
|
||||
|
||||
## 3. 配置客户端
|
||||
|
||||
### 方式一: 通过界面配置
|
||||
|
||||
1. 运行应用: `wails dev` 或运行构建好的可执行文件
|
||||
2. 点击"配置"标签页
|
||||
3. 填写:
|
||||
- **服务器地址**: 例如 `127.0.0.1:8080` 或 `gotify.example.com:443`
|
||||
- **用户 Token**: 粘贴上一步获取的 User Token
|
||||
4. 点击"保存配置"
|
||||
5. 如果配置正确,状态栏将显示"已连接"
|
||||
|
||||
### 方式二: 手动编辑配置文件
|
||||
|
||||
编辑 `~/.gotify-client/config.yaml` (Windows: `C:\Users\用户名\.gotify-client\config.yaml`):
|
||||
|
||||
```yaml
|
||||
server:
|
||||
addr: "127.0.0.1:8080"
|
||||
userToken: "your-user-token-here"
|
||||
```
|
||||
|
||||
## 4. 接收测试消息
|
||||
|
||||
在 Gotify 管理界面发送测试消息,客户端将自动接收并:
|
||||
- 显示 Windows 桌面通知
|
||||
- 存储到 SQLite 数据库
|
||||
- 在"消息历史"标签页显示
|
||||
|
||||
## 5. 查看消息历史
|
||||
|
||||
- 点击"消息历史"标签页
|
||||
- 所有接收到的消息按时间倒序显示
|
||||
- 可以滚动查看历史记录
|
||||
- 点击"清空消息"按钮可删除所有记录
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 连接失败怎么办?
|
||||
|
||||
A: 检查以下几点:
|
||||
1. Gotify 服务器是否在运行
|
||||
2. 地址是否正确 (不要遗漏端口)
|
||||
3. Token 是否正确
|
||||
4. 防火墙是否阻止连接
|
||||
|
||||
### Q: 消息不显示?
|
||||
|
||||
A:
|
||||
1. 确认应用已连接 (状态栏显示"已连接")
|
||||
2. 在 Gotify Web 界面确认消息已发送
|
||||
3. 检查客户端日志 (运行 `wails dev` 时会在终端显示日志)
|
||||
|
||||
### Q: 如何更改配置?
|
||||
|
||||
A:
|
||||
- 通过界面修改配置会自动重新连接
|
||||
- 手动编辑配置文件后需要重启应用
|
||||
|
||||
### Q: 数据库文件在哪里?
|
||||
|
||||
A: `~/.gotify-client/messages.db` (Windows: `C:\Users\用户名\.gotify-client\messages.db`)
|
||||
|
||||
## 开发模式
|
||||
|
||||
```bash
|
||||
cd gotify-client-wails
|
||||
wails dev
|
||||
```
|
||||
|
||||
开发模式下:
|
||||
- 前端修改会自动热重载
|
||||
- 后端修改需要重启
|
||||
- 可以通过浏览器访问 http://localhost:34115 进行调试
|
||||
|
||||
## 构建发布
|
||||
|
||||
```bash
|
||||
wails build
|
||||
```
|
||||
|
||||
构建产物位于 `build/bin/` 目录。
|
||||
173
README.MD
173
README.MD
@@ -1,9 +1,170 @@
|
||||
# gotify-client
|
||||
# Gotify Client (Wails 版)
|
||||
|
||||
一个简易的 gotify windows 客户端
|
||||
基于 Wails v2 框架开发的 Gotify Windows 客户端,提供可视化配置界面、消息历史记录和桌面通知功能。
|
||||
|
||||
用于实时接收 gotify 推送的通知消息, 并在windows的通知栏显示.
|
||||
## 功能特性
|
||||
|
||||
## 服务
|
||||
配合 [winsw](https://github.com/winsw/winsw) 使用
|
||||
即可 作为 windows 服务运行, 并实现开机自启
|
||||
- ✅ **可视化配置界面**: 通过图形界面配置 Gotify 服务器地址和 Token
|
||||
- ✅ **消息历史记录**: 使用 SQLite 本地存储接收到的所有消息
|
||||
- ✅ **Windows 桌面通知**: 接收消息时自动弹出 Windows 系统通知
|
||||
- ✅ **实时连接状态**: 显示与 Gotify 服务器的连接状态
|
||||
- ✅ **断线自动重连**: 连接断开后每 3 秒自动重连
|
||||
- ✅ **消息优先级显示**: 根据优先级显示不同颜色和音效的通知
|
||||
- ✅ **消息管理**: 支持查询和清空历史消息
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端 (Go)
|
||||
- **Wails v2.8.0**: 跨平台桌面应用框架
|
||||
- **Go 1.21**: 主要编程语言
|
||||
- **SQLite**: 本地数据存储 (github.com/mattn/go-sqlite3)
|
||||
- **WebSocket**: 实时连接 Gotify 服务器 (github.com/gorilla/websocket)
|
||||
- **Windows Toast**: 桌面通知 (gopkg.in/toast.v1)
|
||||
- **YAML**: 配置文件格式 (gopkg.in/yaml.v3)
|
||||
|
||||
### 前端
|
||||
- **React**: UI 框架
|
||||
- **Vite**: 构建工具
|
||||
- **CSS3**: 样式设计,深色主题
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Go 1.21+
|
||||
- Node.js 16+
|
||||
- Wails CLI v2.8.0+
|
||||
- GCC 编译器 (Windows 下需安装 MinGW-w64 或 TDM-GCC)
|
||||
|
||||
### 开发模式运行
|
||||
|
||||
```bash
|
||||
wails dev
|
||||
```
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
wails build
|
||||
```
|
||||
|
||||
构建完成后,可执行文件位于 `build/bin/` 目录。
|
||||
|
||||
## 配置说明
|
||||
|
||||
配置文件位于用户目录下: `~/.gotify-client/config.yaml`
|
||||
(Windows: `C:\Users\用户名\.gotify-client\config.yaml`)
|
||||
|
||||
配置内容:
|
||||
```yaml
|
||||
server:
|
||||
addr: "127.0.0.1:8080" # Gotify 服务器地址
|
||||
userToken: "your-token-here" # 用户认证 Token
|
||||
```
|
||||
|
||||
### 配置方式
|
||||
|
||||
1. **通过界面配置**: 启动应用后,在"配置"标签页填写服务器信息
|
||||
2. **手动编辑文件**: 直接编辑配置文件后重启应用
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
gotify-client/
|
||||
├── main.go # 应用入口
|
||||
├── app.go # 主应用逻辑和 API
|
||||
├── wails.json # Wails 项目配置
|
||||
├── internal/
|
||||
│ ├── client/ # WebSocket 客户端
|
||||
│ │ └── client.go
|
||||
│ ├── config/ # 配置文件管理
|
||||
│ │ └── config.go
|
||||
│ ├── database/ # SQLite 数据库层
|
||||
│ │ └── database.go
|
||||
│ └── notify/ # Windows 桌面通知
|
||||
│ └── notify.go
|
||||
├── frontend/
|
||||
│ ├── dist/ # 构建输出目录
|
||||
│ ├── index.html # HTML 入口
|
||||
│ ├── package.json # 前端依赖
|
||||
│ ├── vite.config.js # Vite 配置
|
||||
│ ├── src/
|
||||
│ │ ├── App.jsx # 主界面组件
|
||||
│ │ ├── App.css # 界面样式
|
||||
│ │ └── main.jsx # React 渲染入口
|
||||
│ └── wailsjs/ # Wails 自动生成的绑定代码
|
||||
│ └── go/main/App.js
|
||||
├── build/ # Wails 构建输出
|
||||
└── .backup.old/ # 旧版本备份 (命令行版本)
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
后端导出以下接口给前端调用:
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `GetConfig()` | 获取当前配置 |
|
||||
| `SaveConfig(config)` | 保存配置 |
|
||||
| `GetMessages(limit, offset)` | 获取消息列表 |
|
||||
| `GetMessageCount()` | 获取消息总数 |
|
||||
| `ClearMessages()` | 清空所有消息 |
|
||||
| `GetConnectionStatus()` | 获取连接状态 |
|
||||
| `Reconnect()` | 重新连接服务器 |
|
||||
| `Disconnect()` | 断开连接 |
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### messages 表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER | 主键,自增 |
|
||||
| message_id | INTEGER | Gotify 消息 ID |
|
||||
| app_id | INTEGER | 应用 ID |
|
||||
| title | TEXT | 消息标题 |
|
||||
| message | TEXT | 消息内容 |
|
||||
| priority | INTEGER | 优先级 |
|
||||
| date | TIMESTAMP | 消息时间 |
|
||||
| created_at | TIMESTAMP | 创建时间 |
|
||||
|
||||
### 索引
|
||||
|
||||
- `idx_message_id`: 消息 ID 索引
|
||||
- `idx_date`: 日期降序索引
|
||||
|
||||
## 桌面通知
|
||||
|
||||
### 优先级与音效
|
||||
|
||||
| 优先级 | 音效 | 用途 |
|
||||
|--------|------|------|
|
||||
| ≥ 8 | Notification.Default | 高优先级告警 |
|
||||
| ≥ 5 | Notification.Reminder | 中等优先级提醒 |
|
||||
| ≥ 2 | Notification.Mail | 普通消息 |
|
||||
| < 2 | Notification.Mail | 低优先级消息 |
|
||||
|
||||
### 通知特性
|
||||
|
||||
- 中文 GBK 自动编码转换
|
||||
- 点击通知可关闭
|
||||
- 短时间自动消失
|
||||
|
||||
## 旧版本说明
|
||||
|
||||
`.backup.old/` 目录包含原始的命令行版本,使用 TOML 配置文件和纯 WebSocket 客户端。
|
||||
新版本使用 Wails 框架,提供图形界面和更多功能。
|
||||
|
||||
## 使用指南
|
||||
|
||||
详细使用指南请参考 [GETTING_STARTED.md](GETTING_STARTED.md)
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [Gotify 官网](https://gotify.net/)
|
||||
- [Wails 文档](https://wails.io/)
|
||||
- [原命令行版本](https://github.com/gotify/cli)
|
||||
|
||||
283
app.go
Normal file
283
app.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"gotify-client-wails/internal/client"
|
||||
"gotify-client-wails/internal/config"
|
||||
"gotify-client-wails/internal/database"
|
||||
"gotify-client-wails/internal/notify"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
dbPath string
|
||||
client *client.Client
|
||||
connected bool
|
||||
trayReady chan struct{}
|
||||
mu sync.RWMutex
|
||||
config *config.Config
|
||||
notifyMgr *notify.NotificationManager
|
||||
quitChan chan struct{}
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
notifyMgr: notify.NewNotificationManager(30 * time.Second),
|
||||
quitChan: make(chan struct{}),
|
||||
trayReady: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a.dbPath = filepath.Join(home, ".gotify-client", "messages.db")
|
||||
|
||||
if err := database.Init(a.dbPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a.config = cfg
|
||||
|
||||
if cfg.Server.Addr != "" && cfg.Server.UserToken != "" {
|
||||
a.startClient()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) StartTray() {
|
||||
select {
|
||||
case <-a.trayReady:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
go func() {
|
||||
close(a.trayReady)
|
||||
systray.Run(func() {
|
||||
a.setupTray()
|
||||
}, func() {
|
||||
if a.quitChan != nil {
|
||||
close(a.quitChan)
|
||||
}
|
||||
a.trayReady = make(chan struct{})
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) setupTray() {
|
||||
systray.SetIcon([]byte{})
|
||||
systray.SetTitle("Gotify Client")
|
||||
systray.SetTooltip("Gotify Client")
|
||||
|
||||
mShow := systray.AddMenuItem("显示", "显示主窗口")
|
||||
mQuit := systray.AddMenuItem("退出", "退出程序")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mShow.ClickedCh:
|
||||
runtime.WindowShow(a.ctx)
|
||||
case <-mQuit.ClickedCh:
|
||||
systray.Quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() (*config.Config, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.config, nil
|
||||
}
|
||||
|
||||
func (a *App) SaveConfig(cfg *config.Config) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config cannot be nil")
|
||||
}
|
||||
|
||||
oldConfig := a.config
|
||||
|
||||
if err := config.Save(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.config = cfg
|
||||
a.mu.Unlock()
|
||||
|
||||
if cfg.Server.Addr != "" && cfg.Server.UserToken != "" {
|
||||
a.mu.RLock()
|
||||
wasConnected := a.connected
|
||||
a.mu.RUnlock()
|
||||
if wasConnected {
|
||||
a.stopClient()
|
||||
}
|
||||
if err := a.startClient(); err != nil {
|
||||
return fmt.Errorf("client start failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
a.applyConfigChanges(oldConfig, cfg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) applyConfigChanges(oldCfg, newCfg *config.Config) {
|
||||
if newCfg.MinimizeOnStart && !oldCfg.MinimizeOnStart {
|
||||
runtime.WindowHide(a.ctx)
|
||||
}
|
||||
|
||||
if newCfg.MinimizeToTray && !oldCfg.MinimizeToTray {
|
||||
a.StartTray()
|
||||
}
|
||||
|
||||
if !newCfg.MinimizeToTray && oldCfg.MinimizeToTray {
|
||||
systray.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) UpdateMinimizeToTray(enable bool) error {
|
||||
a.mu.Lock()
|
||||
a.config.MinimizeToTray = enable
|
||||
a.mu.Unlock()
|
||||
|
||||
if enable {
|
||||
a.StartTray()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) startClient() error {
|
||||
a.mu.RLock()
|
||||
if a.config == nil {
|
||||
a.mu.RUnlock()
|
||||
return fmt.Errorf("config not initialized")
|
||||
}
|
||||
addr := a.config.Server.Addr
|
||||
token := a.config.Server.UserToken
|
||||
a.mu.RUnlock()
|
||||
|
||||
a.client = client.NewClient(addr, token)
|
||||
|
||||
if err := a.client.Connect(a.config.Server.Addr, a.config.Server.UserToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.connected = true
|
||||
a.mu.Unlock()
|
||||
|
||||
go a.handleMessages()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) stopClient() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.client != nil {
|
||||
a.client.Close()
|
||||
a.client = nil
|
||||
a.connected = false
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleMessages() {
|
||||
for {
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
case <-a.quitChan:
|
||||
return
|
||||
case msg := <-a.client.Messages():
|
||||
if err := a.saveMessage(msg); err != nil {
|
||||
fmt.Println("save message error:", err)
|
||||
} else {
|
||||
a.showNotification(msg)
|
||||
}
|
||||
case err := <-a.client.Errors():
|
||||
fmt.Println("client error:", err)
|
||||
a.mu.Lock()
|
||||
a.connected = false
|
||||
a.mu.Unlock()
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
if a.config.Server.Addr != "" && a.config.Server.UserToken != "" {
|
||||
a.startClient()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) saveMessage(msg *client.GotifyMessage) error {
|
||||
dbMsg := &database.Message{
|
||||
MessageID: msg.ID,
|
||||
AppID: msg.AppID,
|
||||
Title: msg.Title,
|
||||
Message: msg.Message,
|
||||
Priority: msg.Priority,
|
||||
Date: msg.Date,
|
||||
}
|
||||
return database.InsertMessage(dbMsg)
|
||||
}
|
||||
|
||||
func (a *App) showNotification(msg *client.GotifyMessage) {
|
||||
err := notify.ShowWithPriority(msg.Title, msg.Message, msg.Priority)
|
||||
if err != nil {
|
||||
fmt.Println("notification error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetMessages(limit, offset int) ([]*database.Message, error) {
|
||||
return database.GetMessages(limit, offset)
|
||||
}
|
||||
|
||||
func (a *App) GetMessageCount() (int, error) {
|
||||
return database.GetMessageCount()
|
||||
}
|
||||
|
||||
func (a *App) ClearMessages() error {
|
||||
return database.ClearAllMessages()
|
||||
}
|
||||
|
||||
func (a *App) GetConnectionStatus() bool {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.connected
|
||||
}
|
||||
|
||||
func (a *App) Reconnect() error {
|
||||
return a.startClient()
|
||||
}
|
||||
|
||||
func (a *App) Disconnect() {
|
||||
a.stopClient()
|
||||
}
|
||||
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
a.stopClient()
|
||||
database.Close()
|
||||
if a.quitChan != nil {
|
||||
close(a.quitChan)
|
||||
}
|
||||
systray.Quit()
|
||||
}
|
||||
BIN
build/appicon.png
Normal file
BIN
build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
BIN
build/windows/icon.ico
Normal file
BIN
build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
15
build/windows/info.json
Normal file
15
build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "{{.Info.ProductVersion}}"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||
"CompanyName": "{{.Info.CompanyName}}",
|
||||
"FileDescription": "{{.Info.ProductName}}",
|
||||
"LegalCopyright": "{{.Info.Copyright}}",
|
||||
"ProductName": "{{.Info.ProductName}}",
|
||||
"Comments": "{{.Info.Comments}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
build/windows/wails.exe.manifest
Normal file
15
build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
@@ -1,158 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/toast.v1"
|
||||
"gotify-client/internal/client"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"gotify-client/constants"
|
||||
"gotify-client/pkg/config"
|
||||
"gotify-client/pkg/config/toml"
|
||||
"gotify-client/pkg/logger"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
var notification = toast.Notification{
|
||||
AppID: "Gotify Client",
|
||||
Title: "",
|
||||
Message: "",
|
||||
Icon: "",
|
||||
ActivationType: "",
|
||||
ActivationArguments: "",
|
||||
Actions: nil,
|
||||
Audio: "",
|
||||
Loop: false,
|
||||
Duration: "",
|
||||
}
|
||||
|
||||
var conf = new(config.Config)
|
||||
|
||||
func readConfig() error {
|
||||
viper.SetConfigName(constants.ConfigFileName)
|
||||
viper.SetConfigType(constants.ConfigType)
|
||||
for _, path := range constants.ConfigPaths {
|
||||
viper.AddConfigPath(path)
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
var configFileNotFoundError viper.ConfigFileNotFoundError
|
||||
if errors.As(err, &configFileNotFoundError) {
|
||||
_ = toml.GenerateConfig()
|
||||
logger.Log().Fatalf("未找到配置文件, 已生成示例配置文件于运行路径下")
|
||||
}
|
||||
}
|
||||
|
||||
return viper.Unmarshal(conf)
|
||||
}
|
||||
|
||||
func Main() {
|
||||
err := readConfig()
|
||||
if err != nil {
|
||||
logger.Log().Fatalf("配置文件解析失败: %s, 请检查配置是否有误", err)
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "ws",
|
||||
Host: conf.Server.Addr,
|
||||
Path: "/stream",
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Add("token", conf.Server.UserToken)
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
logger.Log().Infof("url %s", u.String())
|
||||
dialer := &websocket.Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
wsBuilder := func() (*websocket.Conn, error) {
|
||||
ws, _, err := dialer.Dial(u.String(), nil)
|
||||
return ws, err
|
||||
}
|
||||
ws, err := wsBuilder()
|
||||
if err != nil {
|
||||
logger.Log().Fatalf("ws 连接失败: %s, 请检查配置是否有误", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
log.Println("panic:", err)
|
||||
ws, _ = wsBuilder()
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
hasError := false
|
||||
|
||||
preHandler := func() {
|
||||
if hasError {
|
||||
for {
|
||||
if ws != nil {
|
||||
_ = ws.Close()
|
||||
}
|
||||
|
||||
logger.Log().Warnf("尝试断线重连...")
|
||||
ws, err = wsBuilder()
|
||||
if err != nil {
|
||||
logger.Log().Errorf("ws 重连异常: %s, 3秒后重试...", err)
|
||||
hasError = true
|
||||
time.Sleep(time.Second * 3)
|
||||
} else {
|
||||
hasError = false
|
||||
logger.Log().Warnf("ws 断线重连成功")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler := func() {
|
||||
defer func() {
|
||||
_ = ws.Close()
|
||||
}()
|
||||
|
||||
preHandler()
|
||||
for {
|
||||
_, rawMessage, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
logger.Log().Errorf("ws 消息接收异常: %s", err)
|
||||
hasError = true
|
||||
preHandler()
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log().Debugf("接收 ws 消息: %s", rawMessage)
|
||||
data := &client.GotifyMessage{}
|
||||
_ = json.Unmarshal(rawMessage, data)
|
||||
message := fmt.Sprintf("%s", data.Message)
|
||||
|
||||
// windows 下 默认GBK中文编码转换
|
||||
retTitle, _ := simplifiedchinese.GBK.NewEncoder().String(data.Title)
|
||||
retMessage, _ := simplifiedchinese.GBK.NewEncoder().String(message)
|
||||
|
||||
notification.Title = retTitle
|
||||
notification.Message = retMessage
|
||||
_ = notification.Push()
|
||||
}
|
||||
}
|
||||
|
||||
handler()
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
<-quit
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package pusher
|
||||
|
||||
func Main() {
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
ConfigFileName = "config"
|
||||
ConfigType = "toml"
|
||||
)
|
||||
|
||||
var ConfigPaths = []string{
|
||||
".",
|
||||
"./conf",
|
||||
"/config",
|
||||
}
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>gotify-client-wails</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./src/main.jsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1319
frontend/package-lock.json
generated
Normal file
1319
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
frontend/package.json
Normal file
21
frontend/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
1
frontend/package.json.md5
Normal file
1
frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
||||
2f783b65f3a88c5b7de1cd85fd4d4d57
|
||||
342
frontend/src/App.css
Normal file
342
frontend/src/App.css
Normal file
@@ -0,0 +1,342 @@
|
||||
.app {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1b2638;
|
||||
color: white;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #2d3748;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
padding: 15px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tabs button:hover {
|
||||
color: #e2e8f0;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
color: #4299e1;
|
||||
border-bottom-color: #4299e1;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background: #2d3748;
|
||||
border-bottom: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
color: #f56565;
|
||||
}
|
||||
|
||||
.status-bar button {
|
||||
padding: 8px 16px;
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.status-bar button:hover {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
.status-bar button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.header button.danger {
|
||||
background: #e53e3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header button.danger:hover {
|
||||
background: #c53030;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.messages-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.messages-list::-webkit-scrollbar-track {
|
||||
background: #2d3748;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.messages-list::-webkit-scrollbar-thumb {
|
||||
background: #4a5568;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.messages-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #718096;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #718096;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
background: #2d3748;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border-left: 4px solid #4a5568;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.message-card:hover {
|
||||
background: #3a4556;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.message-priority {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
color: #a0aec0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-footer {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #cbd5e0;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: #2d3748;
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: #4299e1;
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.form-actions button {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.form-actions button:hover:not(:disabled) {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
.form-actions button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #4299e1;
|
||||
}
|
||||
|
||||
.checkbox-group span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.checkbox-group small {
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.config-info {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: #2d3748;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
.config-info p {
|
||||
margin: 8px 0;
|
||||
color: #a0aec0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-info p strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
308
frontend/src/App.jsx
Normal file
308
frontend/src/App.jsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import './App.css'
|
||||
import { GetConfig, SaveConfig, GetMessages, GetMessageCount, ClearMessages, GetConnectionStatus, Reconnect, Disconnect } from '../wailsjs/go/main/App'
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState('messages')
|
||||
const [config, setConfig] = useState(null)
|
||||
const [tempConfig, setTempConfig] = useState({
|
||||
server: { addr: '', userToken: '' },
|
||||
minimizeOnStart: false,
|
||||
minimizeToTray: true
|
||||
})
|
||||
const [messages, setMessages] = useState([])
|
||||
const [messageCount, setMessageCount] = useState(0)
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const cfg = await GetConfig()
|
||||
if (cfg) {
|
||||
const normalizedConfig = {
|
||||
server: cfg.server || cfg.Server || { addr: '', userToken: '' },
|
||||
minimizeOnStart: cfg.minimizeOnStart ?? cfg.MinimizeOnStart ?? false,
|
||||
minimizeToTray: cfg.minimizeToTray ?? cfg.MinimizeToTray ?? false
|
||||
}
|
||||
setConfig(cfg)
|
||||
setTempConfig(normalizedConfig)
|
||||
console.log('配置已加载:', cfg)
|
||||
console.log('标准化配置:', normalizedConfig)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载配置失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMessages = async () => {
|
||||
try {
|
||||
const msgs = await GetMessages(50, 0)
|
||||
setMessages(msgs || [])
|
||||
} catch (err) {
|
||||
console.error('加载消息失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMessageCount = async () => {
|
||||
try {
|
||||
const count = await GetMessageCount()
|
||||
setMessageCount(count || 0)
|
||||
} catch (err) {
|
||||
console.error('加载消息数量失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const checkConnectionStatus = async () => {
|
||||
try {
|
||||
const connected = await GetConnectionStatus()
|
||||
setIsConnected(connected || false)
|
||||
} catch (err) {
|
||||
console.error('检查连接状态失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig()
|
||||
loadMessages()
|
||||
loadMessageCount()
|
||||
checkConnectionStatus()
|
||||
|
||||
const interval = setInterval(() => {
|
||||
loadMessages()
|
||||
loadMessageCount()
|
||||
checkConnectionStatus()
|
||||
}, 5000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!tempConfig.server.addr || !tempConfig.server.userToken) {
|
||||
setError('请填写完整的配置信息')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await SaveConfig(tempConfig)
|
||||
setConfig(tempConfig)
|
||||
setError(null)
|
||||
alert('配置已保存,请重启应用以生效托盘设置')
|
||||
setTimeout(() => checkConnectionStatus(), 1000)
|
||||
} catch (err) {
|
||||
setError('保存配置失败: ' + err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearMessages = async () => {
|
||||
if (confirm('确定要清空所有消息吗?')) {
|
||||
try {
|
||||
await ClearMessages()
|
||||
loadMessages()
|
||||
loadMessageCount()
|
||||
} catch (err) {
|
||||
setError('清空消息失败: ' + err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleReconnect = async () => {
|
||||
if (!config || !config.server.addr || !config.server.userToken) {
|
||||
setError('请先配置服务器信息')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await Reconnect()
|
||||
setTimeout(() => checkConnectionStatus(), 1000)
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError('重连失败: ' + err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
try {
|
||||
await Disconnect()
|
||||
setTimeout(() => checkConnectionStatus(), 1000)
|
||||
} catch (err) {
|
||||
setError('断开连接失败: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
} catch {
|
||||
return '未知时间'
|
||||
}
|
||||
}
|
||||
|
||||
const getPriorityColor = (priority) => {
|
||||
if (priority >= 8) return '#ef4444'
|
||||
if (priority >= 5) return '#f97316'
|
||||
if (priority >= 2) return '#eab308'
|
||||
return '#22c55e'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{error && (
|
||||
<div className="notification error">
|
||||
{error}
|
||||
<button onClick={() => setError(null)}>×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="tabs">
|
||||
<button
|
||||
className={activeTab === 'messages' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('messages')}
|
||||
>
|
||||
消息历史
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === 'config' ? 'active' : ''}
|
||||
onClick={() => {
|
||||
setActiveTab('config')
|
||||
loadConfig()
|
||||
}}
|
||||
>
|
||||
配置
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div className="status-bar">
|
||||
<span className={`status-indicator ${isConnected ? 'connected' : 'disconnected'}`}>
|
||||
{isConnected ? '● 已连接' : '● 未连接'}
|
||||
</span>
|
||||
{isConnected ? (
|
||||
<button onClick={handleDisconnect} disabled={loading}>
|
||||
断开连接
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleReconnect} disabled={loading}>
|
||||
重新连接
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === 'messages' && (
|
||||
<div className="tab-content">
|
||||
<div className="header">
|
||||
<h2>消息历史 ({messageCount})</h2>
|
||||
<button onClick={handleClearMessages} className="danger">
|
||||
清空消息
|
||||
</button>
|
||||
</div>
|
||||
<div className="messages-list">
|
||||
{messages.length === 0 ? (
|
||||
<div className="empty-state">暂无消息</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className="message-card">
|
||||
<div className="message-header">
|
||||
<span className="message-title">{msg.title}</span>
|
||||
<span
|
||||
className="message-priority"
|
||||
style={{ color: getPriorityColor(msg.priority) }}
|
||||
>
|
||||
优先级: {msg.priority}
|
||||
</span>
|
||||
</div>
|
||||
<div className="message-content">{msg.message}</div>
|
||||
<div className="message-footer">
|
||||
<span className="message-time">{formatDate(msg.date)}</span>
|
||||
<span className="message-app-id">App ID: {msg.appId}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'config' && (
|
||||
<div className="tab-content">
|
||||
<h2>服务器配置</h2>
|
||||
<div className="form-group">
|
||||
<label>服务器地址:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tempConfig?.server?.addr || ''}
|
||||
onChange={(e) =>
|
||||
setTempConfig({
|
||||
...tempConfig,
|
||||
server: { ...tempConfig.server, addr: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder="例如: 127.0.0.1:8080"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>用户Token:</label>
|
||||
<input
|
||||
type="password"
|
||||
value={tempConfig?.server?.userToken || ''}
|
||||
onChange={(e) =>
|
||||
setTempConfig({
|
||||
...tempConfig,
|
||||
server: { ...tempConfig.server, userToken: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder="请输入您的 Gotify User Token"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tempConfig?.minimizeOnStart || false}
|
||||
onChange={(e) =>
|
||||
setTempConfig({ ...tempConfig, minimizeOnStart: e.target.checked })
|
||||
}
|
||||
/>
|
||||
<span>启动时最小化到任务栏</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tempConfig?.minimizeToTray || false}
|
||||
onChange={(e) =>
|
||||
setTempConfig({ ...tempConfig, minimizeToTray: e.target.checked })
|
||||
}
|
||||
/>
|
||||
<span>最小化到系统托盘</span>
|
||||
</label>
|
||||
<small>启用后,关闭窗口将最小化到系统托盘,不会退出程序</small>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button onClick={handleSaveConfig} disabled={loading}>
|
||||
{loading ? '保存中...' : '保存配置'}
|
||||
</button>
|
||||
</div>
|
||||
{config && (
|
||||
<div className="config-info">
|
||||
<p><strong>当前配置:</strong></p>
|
||||
<p>服务器: {config.server?.addr || config.Server?.addr || '未配置'}</p>
|
||||
<p>Token: {config.server?.userToken || config.Server?.userToken ? '***已配置***' : '未配置'}</p>
|
||||
<p>启动时最小化: {config.minimizeOnStart || config.MinimizeOnStart ? '是' : '否'}</p>
|
||||
<p>最小化到托盘: {config.minimizeToTray || config.MinimizeToTray ? '是' : '否'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
93
frontend/src/assets/fonts/OFL.txt
Normal file
93
frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/images/logo-universal.png
Normal file
BIN
frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
14
frontend/src/main.jsx
Normal file
14
frontend/src/main.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import './style.css'
|
||||
import App from './App'
|
||||
|
||||
const container = document.getElementById('root')
|
||||
|
||||
const root = createRoot(container)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
)
|
||||
26
frontend/src/style.css
Normal file
26
frontend/src/style.css
Normal file
@@ -0,0 +1,26 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
7
frontend/vite.config.js
Normal file
7
frontend/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
24
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
24
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {config} from '../models';
|
||||
import {database} from '../models';
|
||||
|
||||
export function ClearMessages():Promise<void>;
|
||||
|
||||
export function Disconnect():Promise<void>;
|
||||
|
||||
export function GetConfig():Promise<config.Config>;
|
||||
|
||||
export function GetConnectionStatus():Promise<boolean>;
|
||||
|
||||
export function GetMessageCount():Promise<number>;
|
||||
|
||||
export function GetMessages(arg1:number,arg2:number):Promise<Array<database.Message>>;
|
||||
|
||||
export function Reconnect():Promise<void>;
|
||||
|
||||
export function SaveConfig(arg1:config.Config):Promise<void>;
|
||||
|
||||
export function StartTray():Promise<void>;
|
||||
|
||||
export function UpdateMinimizeToTray(arg1:boolean):Promise<void>;
|
||||
43
frontend/wailsjs/go/main/App.js
Normal file
43
frontend/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function ClearMessages() {
|
||||
return window['go']['main']['App']['ClearMessages']();
|
||||
}
|
||||
|
||||
export function Disconnect() {
|
||||
return window['go']['main']['App']['Disconnect']();
|
||||
}
|
||||
|
||||
export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetConnectionStatus() {
|
||||
return window['go']['main']['App']['GetConnectionStatus']();
|
||||
}
|
||||
|
||||
export function GetMessageCount() {
|
||||
return window['go']['main']['App']['GetMessageCount']();
|
||||
}
|
||||
|
||||
export function GetMessages(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetMessages'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function Reconnect() {
|
||||
return window['go']['main']['App']['Reconnect']();
|
||||
}
|
||||
|
||||
export function SaveConfig(arg1) {
|
||||
return window['go']['main']['App']['SaveConfig'](arg1);
|
||||
}
|
||||
|
||||
export function StartTray() {
|
||||
return window['go']['main']['App']['StartTray']();
|
||||
}
|
||||
|
||||
export function UpdateMinimizeToTray(arg1) {
|
||||
return window['go']['main']['App']['UpdateMinimizeToTray'](arg1);
|
||||
}
|
||||
71
frontend/wailsjs/go/models.ts
Normal file
71
frontend/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export namespace config {
|
||||
|
||||
|
||||
export class ServerConfig {
|
||||
addr: string;
|
||||
userToken: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ServerConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.addr = source["addr"];
|
||||
this.userToken = source["userToken"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace database {
|
||||
|
||||
export class Message {
|
||||
id: number;
|
||||
messageId: number;
|
||||
appId: number;
|
||||
title: string;
|
||||
message: string;
|
||||
priority: number;
|
||||
// Go type: time
|
||||
date: any;
|
||||
// Go type: time
|
||||
createdAt: any;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Message(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.messageId = source["messageId"];
|
||||
this.appId = source["appId"];
|
||||
this.title = source["title"];
|
||||
this.message = source["message"];
|
||||
this.priority = source["priority"];
|
||||
this.date = this.convertValues(source["date"], null);
|
||||
this.createdAt = this.convertValues(source["createdAt"], null);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
235
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
235
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): Promise<Size>;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
202
frontend/wailsjs/runtime/runtime.js
Normal file
202
frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
68
go.mod
68
go.mod
@@ -1,35 +1,57 @@
|
||||
module gotify-client
|
||||
module gotify-client-wails
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
toolchain go1.23.5
|
||||
|
||||
require (
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/pelletier/go-toml/v2 v2.2.1
|
||||
github.com/spf13/viper v1.18.2
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
github.com/mattn/go-sqlite3 v1.14.18
|
||||
github.com/wailsapp/wails/v2 v2.8.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
|
||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.10 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.8.0 => C:\Users\Administrator\go\pkg\mod
|
||||
|
||||
165
go.sum
165
go.sum
@@ -1,75 +1,130 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
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=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
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/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
|
||||
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
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.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0/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/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.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
|
||||
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
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.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
|
||||
github.com/tkrajina/go-reflector v0.5.6/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.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
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/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.8.0 h1:b2NNn99uGPiN6P5bDsnPwOJZWtAOUhNLv7Vl+YxMTr4=
|
||||
github.com/wailsapp/wails/v2 v2.8.0/go.mod h1:EFUGWkUX3KofO4fmKR/GmsLy3HhPH7NbyOEaMt8lBF0=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/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-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo=
|
||||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NKt867Xe18emOmtsO3eAKbDaon0o=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
100
internal/client/client.go
Normal file
100
internal/client/client.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GotifyMessage struct {
|
||||
ID int `json:"id"`
|
||||
AppID int `json:"appid"`
|
||||
Message string `json:"message"`
|
||||
Title string `json:"title"`
|
||||
Priority int `json:"priority"`
|
||||
Extras map[string]interface{} `json:"extras"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
conn *websocket.Conn
|
||||
dialer *websocket.Dialer
|
||||
message chan *GotifyMessage
|
||||
error chan error
|
||||
}
|
||||
|
||||
func NewClient(serverAddr, userToken string) *Client {
|
||||
return &Client{
|
||||
dialer: &websocket.Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
message: make(chan *GotifyMessage, 100),
|
||||
error: make(chan error, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Connect(serverAddr, userToken string) error {
|
||||
u := url.URL{
|
||||
Scheme: "ws",
|
||||
Host: serverAddr,
|
||||
Path: "/stream",
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Add("token", userToken)
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
conn, _, err := c.dialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
|
||||
go c.readMessages()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) readMessages() {
|
||||
defer c.Close()
|
||||
|
||||
for {
|
||||
_, rawMessage, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
c.error <- err
|
||||
return
|
||||
}
|
||||
|
||||
var msg GotifyMessage
|
||||
if err := json.Unmarshal(rawMessage, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case c.message <- &msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Messages() <-chan *GotifyMessage {
|
||||
return c.message
|
||||
}
|
||||
|
||||
func (c *Client) Errors() <-chan error {
|
||||
return c.error
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.conn != nil {
|
||||
return c.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
return c.conn != nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package client
|
||||
|
||||
import "gotify-client/pkg/utils/time"
|
||||
|
||||
type GotifyMessage struct {
|
||||
Id int `json:"id"`
|
||||
Appid int `json:"appid"`
|
||||
Message string `json:"message"`
|
||||
Title string `json:"title"`
|
||||
Priority int `json:"priority"`
|
||||
Extras map[string]any `json:"extras"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
79
internal/config/config.go
Normal file
79
internal/config/config.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
MinimizeOnStart bool `yaml:"minimizeOnStart"`
|
||||
MinimizeToTray bool `yaml:"minimizeToTray"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Addr string `yaml:"addr" json:"addr"`
|
||||
UserToken string `yaml:"userToken" json:"userToken"`
|
||||
}
|
||||
|
||||
const configFile = "config.yaml"
|
||||
|
||||
func Load() (*Config, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return defaultConfig(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func Save(config *Config) error {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
func getConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".gotify-client", configFile), nil
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Addr: "127.0.0.1:8080",
|
||||
UserToken: "",
|
||||
},
|
||||
MinimizeOnStart: false,
|
||||
MinimizeToTray: true,
|
||||
}
|
||||
}
|
||||
151
internal/database/database.go
Normal file
151
internal/database/database.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
ID int64 `json:"id"`
|
||||
MessageID int `json:"messageId"`
|
||||
AppID int `json:"appId"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Priority int `json:"priority"`
|
||||
Date time.Time `json:"date"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func Init(dbPath string) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
db, err = sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = db.Ping(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = createTables(); err != nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func createTables() error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_id INTEGER NOT NULL,
|
||||
app_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
priority INTEGER DEFAULT 0,
|
||||
date TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_message_id ON messages(message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_date ON messages(date DESC);
|
||||
`
|
||||
_, err := db.Exec(query)
|
||||
return err
|
||||
}
|
||||
|
||||
func InsertMessage(msg *Message) error {
|
||||
query := `
|
||||
INSERT INTO messages (message_id, app_id, title, message, priority, date)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
_, err := db.Exec(query, msg.MessageID, msg.AppID, msg.Title, msg.Message, msg.Priority, msg.Date)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetMessages(limit int, offset int) ([]*Message, error) {
|
||||
query := `
|
||||
SELECT id, message_id, app_id, title, message, priority, date, created_at
|
||||
FROM messages
|
||||
ORDER BY date DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
rows, err := db.Query(query, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []*Message
|
||||
for rows.Next() {
|
||||
var msg Message
|
||||
err := rows.Scan(&msg.ID, &msg.MessageID, &msg.AppID, &msg.Title, &msg.Message,
|
||||
&msg.Priority, &msg.Date, &msg.CreatedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, &msg)
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func GetMessagesByDate(startDate, endDate time.Time) ([]*Message, error) {
|
||||
query := `
|
||||
SELECT id, message_id, app_id, title, message, priority, date, created_at
|
||||
FROM messages
|
||||
WHERE date BETWEEN ? AND ?
|
||||
ORDER BY date DESC
|
||||
`
|
||||
rows, err := db.Query(query, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []*Message
|
||||
for rows.Next() {
|
||||
var msg Message
|
||||
err := rows.Scan(&msg.ID, &msg.MessageID, &msg.AppID, &msg.Title, &msg.Message,
|
||||
&msg.Priority, &msg.Date, &msg.CreatedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, &msg)
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func DeleteMessage(id int64) error {
|
||||
query := `DELETE FROM messages WHERE id = ?`
|
||||
_, err := db.Exec(query, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func ClearAllMessages() error {
|
||||
query := `DELETE FROM messages`
|
||||
_, err := db.Exec(query)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetMessageCount() (int, error) {
|
||||
var count int
|
||||
query := `SELECT COUNT(*) FROM messages`
|
||||
err := db.QueryRow(query).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
if db != nil {
|
||||
return db.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
112
internal/notify/notify.go
Normal file
112
internal/notify/notify.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"gopkg.in/toast.v1"
|
||||
)
|
||||
|
||||
var appID = "Gotify Client"
|
||||
|
||||
func Show(title, message string) error {
|
||||
retTitle, err := simplifiedchinese.GBK.NewEncoder().String(title)
|
||||
if err != nil {
|
||||
retTitle = title
|
||||
}
|
||||
|
||||
retMessage, err := simplifiedchinese.GBK.NewEncoder().String(message)
|
||||
if err != nil {
|
||||
retMessage = message
|
||||
}
|
||||
|
||||
notification := toast.Notification{
|
||||
AppID: appID,
|
||||
Title: retTitle,
|
||||
Message: retMessage,
|
||||
Icon: "",
|
||||
ActivationType: "protocol",
|
||||
ActivationArguments: "",
|
||||
Actions: []toast.Action{
|
||||
{Type: "protocol", Label: "Dismiss"},
|
||||
},
|
||||
Audio: "Notification.Default",
|
||||
Loop: false,
|
||||
Duration: "short",
|
||||
}
|
||||
|
||||
return notification.Push()
|
||||
}
|
||||
|
||||
func ShowWithPriority(title, message string, priority int) error {
|
||||
audio := toast.Default
|
||||
|
||||
if priority >= 8 {
|
||||
audio = toast.Default
|
||||
} else if priority >= 5 {
|
||||
audio = toast.Reminder
|
||||
} else if priority >= 2 {
|
||||
audio = toast.Mail
|
||||
}
|
||||
|
||||
retTitle, err := simplifiedchinese.GBK.NewEncoder().String(title)
|
||||
if err != nil {
|
||||
retTitle = title
|
||||
}
|
||||
|
||||
retMessage, err := simplifiedchinese.GBK.NewEncoder().String(message)
|
||||
if err != nil {
|
||||
retMessage = message
|
||||
}
|
||||
|
||||
notification := toast.Notification{
|
||||
AppID: appID,
|
||||
Title: retTitle,
|
||||
Message: retMessage,
|
||||
Icon: "",
|
||||
ActivationType: "protocol",
|
||||
ActivationArguments: "",
|
||||
Actions: []toast.Action{
|
||||
{Type: "protocol", Label: "Dismiss"},
|
||||
},
|
||||
Audio: audio,
|
||||
Loop: false,
|
||||
Duration: "short",
|
||||
}
|
||||
|
||||
return notification.Push()
|
||||
}
|
||||
|
||||
func SetAppID(id string) {
|
||||
appID = id
|
||||
}
|
||||
|
||||
type NotificationManager struct {
|
||||
lastNotifyTime map[string]time.Time
|
||||
cooldown time.Duration
|
||||
}
|
||||
|
||||
func NewNotificationManager(cooldown time.Duration) *NotificationManager {
|
||||
return &NotificationManager{
|
||||
lastNotifyTime: make(map[string]time.Time),
|
||||
cooldown: cooldown,
|
||||
}
|
||||
}
|
||||
|
||||
func (nm *NotificationManager) Show(title, message string) error {
|
||||
return nm.ShowWithCooldown(title, message, 0, "")
|
||||
}
|
||||
|
||||
func (nm *NotificationManager) ShowWithCooldown(title, message string, cooldown time.Duration, key string) error {
|
||||
if key != "" {
|
||||
if lastTime, exists := nm.lastNotifyTime[key]; exists {
|
||||
if time.Since(lastTime) < cooldown {
|
||||
return fmt.Errorf("notification cooldown active")
|
||||
}
|
||||
}
|
||||
nm.lastNotifyTime[key] = time.Now()
|
||||
}
|
||||
|
||||
return Show(title, message)
|
||||
}
|
||||
66
main.go
66
main.go
@@ -1,13 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "gotify-client/pkg/logger"
|
||||
"context"
|
||||
"embed"
|
||||
|
||||
"gotify-client/cmd/client"
|
||||
"time"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"gotify-client-wails/internal/config"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
_, _ = time.LoadLocation("Asia/Shanghai")
|
||||
client.Main()
|
||||
appInstance := NewApp()
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Addr: "127.0.0.1:8080",
|
||||
UserToken: "",
|
||||
},
|
||||
MinimizeOnStart: false,
|
||||
MinimizeToTray: false,
|
||||
}
|
||||
}
|
||||
|
||||
wailsConfig := &options.App{
|
||||
Title: "Gotify Client",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: appInstance.Startup,
|
||||
OnShutdown: appInstance.Shutdown,
|
||||
OnBeforeClose: func(ctx context.Context) bool {
|
||||
appCfg, _ := appInstance.GetConfig()
|
||||
if appCfg != nil && appCfg.MinimizeToTray {
|
||||
runtime.WindowHide(ctx)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
Bind: []interface{}{
|
||||
appInstance,
|
||||
},
|
||||
}
|
||||
|
||||
if cfg.MinimizeOnStart {
|
||||
wailsConfig.WindowStartState = options.Normal
|
||||
wailsConfig.StartHidden = true
|
||||
}
|
||||
|
||||
if cfg.MinimizeToTray {
|
||||
appInstance.StartTray()
|
||||
}
|
||||
|
||||
err = wails.Run(wailsConfig)
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Server *ServerConfig `comment:"gotify 服务端配置"`
|
||||
Apps []*AppConfig `comment:"应用配置(用于推送消息)"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Addr string `comment:"服务端地址"`
|
||||
UserToken string `comment:"用户token(用于接收消息)"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
AppToken string `comment:"应用token"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Server: &ServerConfig{
|
||||
Addr: "127.0.0.1",
|
||||
UserToken: "userToken",
|
||||
},
|
||||
Apps: []*AppConfig{
|
||||
{AppToken: "appToken1"},
|
||||
{AppToken: "appToken2"},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"gotify-client/pkg/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func GenerateConfig() error {
|
||||
p, _ := filepath.Abs("./config.toml")
|
||||
|
||||
flag := os.O_RDWR
|
||||
_, err := os.Stat(p)
|
||||
exist := !os.IsNotExist(err)
|
||||
|
||||
if !exist {
|
||||
f, err := os.OpenFile(p, flag|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
encoder := toml.NewEncoder(f)
|
||||
encoder.SetIndentTables(true)
|
||||
_ = encoder.Encode(config.DefaultConfig())
|
||||
_ = f.Sync()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadConfig() (*config.Config, error) {
|
||||
p, _ := filepath.Abs("./config.toml")
|
||||
|
||||
flag := os.O_RDWR
|
||||
_, err := os.Stat(p)
|
||||
exist := !os.IsNotExist(err)
|
||||
|
||||
if !exist {
|
||||
f, err := os.OpenFile(p, flag|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
encoder := toml.NewEncoder(f)
|
||||
encoder.SetIndentTables(true)
|
||||
_ = encoder.Encode(config.DefaultConfig())
|
||||
_ = f.Sync()
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(p, flag, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
c := &config.Config{}
|
||||
decoder := toml.NewDecoder(f)
|
||||
err = decoder.Decode(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package toml
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
_, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/atomic"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
var sugarLogger *zap.SugaredLogger
|
||||
var level = &atomic.String{}
|
||||
|
||||
func init() {
|
||||
SetLevel(zapcore.DebugLevel)
|
||||
encoder := zapcore.NewConsoleEncoder(DefaultEncoderConfig())
|
||||
multiWriteSyncer := zapcore.NewMultiWriteSyncer(DefaultConsoleSyncer())
|
||||
core := zapcore.NewCore(encoder, multiWriteSyncer, DefaultLevelEnabler())
|
||||
logger = zap.New(core, zap.AddCaller())
|
||||
_ = logger.Sync()
|
||||
sugarLogger = logger.Sugar()
|
||||
_ = sugarLogger.Sync()
|
||||
}
|
||||
|
||||
func SetLevel(l zapcore.Level) {
|
||||
level.Store(l.String())
|
||||
}
|
||||
|
||||
func DefaultLevelEnabler() zap.LevelEnablerFunc {
|
||||
return func(z zapcore.Level) bool {
|
||||
l, _ := zapcore.ParseLevel(level.Load())
|
||||
return z >= l
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultTimeEncoder() (timeEncoder zapcore.TimeEncoder) {
|
||||
timeEncoder = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000")
|
||||
return
|
||||
}
|
||||
|
||||
func DefaultEncoderConfig() (encoderConfig zapcore.EncoderConfig) {
|
||||
encoderConfig = zap.NewProductionEncoderConfig()
|
||||
encoderConfig.EncodeTime = DefaultTimeEncoder()
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
return
|
||||
}
|
||||
|
||||
func DefaultConsoleSyncer() zapcore.WriteSyncer {
|
||||
return zapcore.AddSync(os.Stdout)
|
||||
}
|
||||
|
||||
func Log() *zap.SugaredLogger {
|
||||
return sugarLogger
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
func Json(data interface{}) string {
|
||||
jsonBytes, _ := json.MarshalIndent(data, "", " ")
|
||||
return string(jsonBytes)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package time
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Time time.Time
|
||||
|
||||
const (
|
||||
timeFormat = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
func (t *Time) String() string {
|
||||
return fmt.Sprintf("%s", time.Time(*t).Format(timeFormat))
|
||||
}
|
||||
|
||||
// MarshalJSON on Json Time format Time field with %Y-%m-%d %H:%M:%S
|
||||
func (t *Time) MarshalJSON() ([]byte, error) {
|
||||
// 重写time转换成json之后的格式
|
||||
var tmp = fmt.Sprintf("\"%s\"", t.String())
|
||||
return []byte(tmp), nil
|
||||
}
|
||||
|
||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
// Ignore null, like in the main JSON package.
|
||||
if string(data) == "null" {
|
||||
return nil
|
||||
}
|
||||
// Fractional seconds are handled implicitly by Parse.
|
||||
var err error
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
rawT, err := time.ParseInLocation(`"`+timeFormat+`"`, string(data), loc)
|
||||
*t = Time(rawT)
|
||||
return err
|
||||
}
|
||||
13
wails.json
Normal file
13
wails.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "gotify-client",
|
||||
"outputfilename": "gotify-client",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "shikong",
|
||||
"email": "919411476@qq.com"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user