Compare commits

1 Commits
cli ... master

Author SHA1 Message Date
1d8eb79ac0 重构: 使用 Wails v2 框架重构为桌面应用版本
- 使用 Wails v2 + React 重构为跨平台桌面应用
- 新增可视化配置界面和消息历史记录功能
- 使用 SQLite 本地存储消息
- 实现 Windows 桌面通知和系统托盘功能
- 优化代码结构和错误处理
- 更新项目文档和快速开始指南
2026-01-22 01:20:47 +08:00
47 changed files with 4030 additions and 585 deletions

102
.gitignore vendored
View File

@@ -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
View 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
View 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
View 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
View 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
View File

@@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

15
build/windows/info.json Normal file
View 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}}"
}
}
}

View 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>

View File

@@ -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
}

View File

@@ -1,5 +0,0 @@
package pusher
func Main() {
}

View File

@@ -1,12 +0,0 @@
package constants
const (
ConfigFileName = "config"
ConfigType = "toml"
)
var ConfigPaths = []string{
".",
"./conf",
"/config",
}

13
frontend/index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

21
frontend/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1 @@
2f783b65f3a88c5b7de1cd85fd4d4d57

342
frontend/src/App.css Normal file
View 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
View 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

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

14
frontend/src/main.jsx Normal file
View 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
View 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
View 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
View 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>;

View 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);
}

View 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;
}
}
}

View 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
View 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>;

View 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
View File

@@ -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
View File

@@ -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
View 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
}

View File

@@ -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
View 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,
}
}

View 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
View 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
View File

@@ -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())
}
}

View File

@@ -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"},
},
}
}

View File

@@ -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
}

View File

@@ -1,10 +0,0 @@
package toml
import "testing"
func TestLoadConfig(t *testing.T) {
_, err := LoadConfig()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -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
}

View File

@@ -1,10 +0,0 @@
package json
import (
"github.com/goccy/go-json"
)
func Json(data interface{}) string {
jsonBytes, _ := json.MarshalIndent(data, "", " ")
return string(jsonBytes)
}

View File

@@ -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
View 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"
}
}