From 71bfed37449b3819be66613f870486a7ee823a2c Mon Sep 17 00:00:00 2001 From: ming Date: Fri, 4 Jul 2025 17:39:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=85=BC=E5=AE=B9darwin=E7=89=88?= =?UTF-8?q?=E6=9C=AC=20#30?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 255 ++----------- app_darwin.go | 492 +++++-------------------- app_windows.go | 185 ++++++++++ backend/data/settings_api.go | 2 +- backend/data/stock_data_api.go | 45 --- backend/data/stock_data_api_darwin.go | 37 ++ backend/data/stock_data_api_windows.go | 50 +++ frontend/package.json.md5 | 2 +- frontend/src/components/market.vue | 10 +- frontend/wailsjs/go/main/App.d.ts | 0 frontend/wailsjs/go/main/App.js | 0 frontend/wailsjs/go/models.ts | 0 go.mod | 10 +- go.sum | 30 +- 14 files changed, 447 insertions(+), 671 deletions(-) create mode 100644 app_windows.go create mode 100644 backend/data/stock_data_api_darwin.go create mode 100644 backend/data/stock_data_api_windows.go mode change 100644 => 100755 frontend/wailsjs/go/main/App.d.ts mode change 100644 => 100755 frontend/wailsjs/go/main/App.js mode change 100644 => 100755 frontend/wailsjs/go/models.ts diff --git a/app.go b/app.go index 71da0f0..a0d3692 100644 --- a/app.go +++ b/app.go @@ -1,11 +1,8 @@ -//go:build windows - package main import ( "context" "encoding/base64" - "encoding/json" "fmt" "go-stock/backend/data" "go-stock/backend/db" @@ -21,13 +18,9 @@ import ( "github.com/duke-git/lancet/v2/mathutil" "github.com/duke-git/lancet/v2/slice" "github.com/duke-git/lancet/v2/strutil" - "github.com/energye/systray" "github.com/go-resty/resty/v2" - "github.com/go-toast/toast" "github.com/robfig/cron/v3" - "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/runtime" - "golang.org/x/sys/windows/registry" ) // App struct @@ -107,60 +100,6 @@ func AddTools(tools []data.Tool) []data.Tool { return tools } -// startup is called at application startup -func (a *App) startup(ctx context.Context) { - defer PanicHandler() - runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) { - logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData) - }) - logger.SugaredLogger.Infof("Version:%s", Version) - // Perform your setup here - a.ctx = ctx - - // 创建系统托盘 - //systray.RunWithExternalLoop(func() { - // onReady(a) - //}, func() { - // onExit(a) - //}) - runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) { - logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData) - config := &data.Settings{} - setMap := optionalData[0].(map[string]interface{}) - - // 将 map 转换为 JSON 字节切片 - jsonData, err := json.Marshal(setMap) - if err != nil { - logger.SugaredLogger.Errorf("Marshal error:%s", err.Error()) - return - } - // 将 JSON 字节切片解析到结构体中 - err = json.Unmarshal(jsonData, config) - if err != nil { - logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error()) - return - } - - logger.SugaredLogger.Infof("updateSettings config:%+v", config) - if config.DarkTheme { - runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1) - runtime.WindowSetDarkTheme(ctx) - } else { - runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1) - runtime.WindowSetLightTheme(ctx) - } - runtime.WindowReloadApp(ctx) - - }) - go systray.Run(func() { - onReady(a) - }, func() { - onExit(a) - }) - - logger.SugaredLogger.Infof(" application startup Version:%s", Version) -} - func (a *App) CheckUpdate() { releaseVersion := &models.GitHubReleaseVersion{} _, err := resty.New().R(). @@ -525,49 +464,6 @@ func MonitorFundPrices(a *App) { } } -func MonitorStockPrices(a *App) { - dest := &[]data.FollowedStock{} - db.Dao.Model(&data.FollowedStock{}).Find(dest) - total := float64(0) - //for _, follow := range *dest { - // stockData := getStockInfo(follow) - // total += stockData.ProfitAmountToday - // price, _ := convertor.ToFloat(stockData.Price) - // if stockData.PrePrice != price { - // go runtime.EventsEmit(a.ctx, "stock_price", stockData) - // } - //} - - stockInfos := GetStockInfos(*dest...) - for _, stockInfo := range *stockInfos { - if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) { - continue - } - if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) { - continue - } - if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) { - continue - } - - total += stockInfo.ProfitAmountToday - price, _ := convertor.ToFloat(stockInfo.Price) - - if stockInfo.PrePrice != price { - //logger.SugaredLogger.Infof("-----------sz------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA) - go runtime.EventsEmit(a.ctx, "stock_price", stockInfo) - } - - } - if total != 0 { - title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total) - systray.SetTooltip(title) - } - - go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total)) - //runtime.WindowSetTitle(a.ctx, title) - -} func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo { stockInfos := make([]data.StockInfo, 0) stockCodes := make([]string, 0) @@ -685,35 +581,6 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) { } } -// beforeClose is called when the application is about to quit, -// either by clicking the window close button or calling runtime.Quit. -// Returning true will cause the application to continue, false will continue shutdown as normal. -func (a *App) beforeClose(ctx context.Context) (prevent bool) { - defer PanicHandler() - - dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ - Type: runtime.QuestionDialog, - Title: "go-stock", - Message: "确定关闭吗?", - Buttons: []string{"确定"}, - Icon: icon, - CancelButton: "取消", - }) - - if err != nil { - logger.SugaredLogger.Errorf("dialog error:%s", err.Error()) - return false - } - logger.SugaredLogger.Debugf("dialog:%s", dialog) - if dialog == "No" { - return true - } else { - systray.Quit() - a.cron.Stop() - return false - } -} - // shutdown is called at application termination func (a *App) shutdown(ctx context.Context) { defer PanicHandler() @@ -834,40 +701,40 @@ func (a *App) GetVersionInfo() *models.VersionInfo { } } -// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装 -func checkChromeOnWindows() bool { - key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) - if err != nil { - // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) - key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) - if err != nil { - return false - } - defer key.Close() - } - defer key.Close() - _, _, err = key.GetValue("Path", nil) - return err == nil -} - -// checkEdgeOnWindows 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径 -func checkEdgeOnWindows() (string, bool) { - key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) - if err != nil { - // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) - key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) - if err != nil { - return "", false - } - defer key.Close() - } - defer key.Close() - path, _, err := key.GetStringValue("Path") - if err != nil { - return "", false - } - return path, true -} +//// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装 +//func checkChromeOnWindows() bool { +// key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) +// if err != nil { +// // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) +// key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) +// if err != nil { +// return false +// } +// defer key.Close() +// } +// defer key.Close() +// _, _, err = key.GetValue("Path", nil) +// return err == nil +//} +// +//// checkEdgeOnWindows 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径 +//func checkEdgeOnWindows() (string, bool) { +// key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) +// if err != nil { +// // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) +// key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) +// if err != nil { +// return "", false +// } +// defer key.Close() +// } +// defer key.Close() +// path, _, err := key.GetStringValue("Path") +// if err != nil { +// return "", false +// } +// return path, true +//} func GetImageBase(bytes []byte) string { return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bytes) @@ -924,44 +791,6 @@ func onExit(a *App) { //runtime.Quit(a.ctx) } -func onReady(a *App) { - - // 初始化操作 - logger.SugaredLogger.Infof("systray onReady") - systray.SetIcon(icon2) - systray.SetTitle("go-stock") - systray.SetTooltip("go-stock 股票行情实时获取") - // 创建菜单项 - show := systray.AddMenuItem("显示", "显示应用程序") - show.Click(func() { - //logger.SugaredLogger.Infof("显示应用程序") - runtime.WindowShow(a.ctx) - }) - hide := systray.AddMenuItem("隐藏", "隐藏应用程序") - hide.Click(func() { - //logger.SugaredLogger.Infof("隐藏应用程序") - runtime.WindowHide(a.ctx) - }) - systray.AddSeparator() - mQuitOrig := systray.AddMenuItem("退出", "退出应用程序") - mQuitOrig.Click(func() { - //logger.SugaredLogger.Infof("退出应用程序") - runtime.Quit(a.ctx) - }) - systray.SetOnRClick(func(menu systray.IMenu) { - menu.ShowMenu() - //logger.SugaredLogger.Infof("SetOnRClick") - }) - systray.SetOnClick(func(menu systray.IMenu) { - //logger.SugaredLogger.Infof("SetOnClick") - menu.ShowMenu() - }) - systray.SetOnDClick(func(menu systray.IMenu) { - menu.ShowMenu() - //logger.SugaredLogger.Infof("SetOnDClick") - }) -} - func (a *App) UpdateConfig(settings *data.Settings) string { //logger.SugaredLogger.Infof("UpdateConfig:%+v", settings) if settings.RefreshInterval > 0 { @@ -1096,22 +925,6 @@ func (a *App) SetStockAICron(cronText, stockCode string) { a.cronEntrys[stockCode] = id } -func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { - notification := toast.Notification{ - AppID: "go-stock", - Title: "go-stock", - Message: "程序已经在运行了", - Icon: "", - Duration: "short", - Audio: toast.Default, - } - err := notification.Push() - if err != nil { - logger.SugaredLogger.Error(err) - } - time.Sleep(time.Second * 3) -} - func (a *App) AddGroup(group data.Group) string { ok := data.NewStockGroupApi(db.Dao).AddGroup(group) if ok { diff --git a/app_darwin.go b/app_darwin.go index f764ca5..0aa26a6 100644 --- a/app_darwin.go +++ b/app_darwin.go @@ -5,304 +5,149 @@ package main import ( "context" + "encoding/json" "fmt" + "github.com/duke-git/lancet/v2/convertor" + "github.com/duke-git/lancet/v2/strutil" + "github.com/gen2brain/beeep" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/runtime" "go-stock/backend/data" "go-stock/backend/db" "go-stock/backend/logger" - "go-stock/backend/models" - "strings" + "log" "time" - - "github.com/PuerkitoBio/goquery" - "github.com/coocood/freecache" - "github.com/duke-git/lancet/v2/convertor" - "github.com/duke-git/lancet/v2/mathutil" - "github.com/duke-git/lancet/v2/slice" - "github.com/go-resty/resty/v2" - "github.com/wailsapp/wails/v2/pkg/runtime" ) -// App struct -type App struct { - ctx context.Context - cache *freecache.Cache -} - -// NewApp creates a new App application struct -func NewApp() *App { - cacheSize := 512 * 1024 - cache := freecache.NewCache(cacheSize) - return &App{ - cache: cache, - } -} - -// startup is called at application startup +// startup 在应用程序启动时调用 func (a *App) startup(ctx context.Context) { + defer PanicHandler() + runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) { + logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData) + }) logger.SugaredLogger.Infof("Version:%s", Version) // Perform your setup here a.ctx = ctx - // TODO 创建系统托盘 + // 监听设置更新事件 + runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) { + logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData) + config := &data.Settings{} + setMap := optionalData[0].(map[string]interface{}) -} - -func checkUpdate(a *App) { - releaseVersion := &models.GitHubReleaseVersion{} - _, err := resty.New().R(). - SetResult(releaseVersion). - Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest") - if err != nil { - logger.SugaredLogger.Errorf("get github release version error:%s", err.Error()) - return - } - logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName) - if releaseVersion.TagName != Version { - go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion) - } -} - -// domReady is called after front-end resources have been loaded -func (a *App) domReady(ctx context.Context) { - // Add your action here - //定时更新数据 - go func() { - config := data.NewSettingsApi(&data.Settings{}).GetConfig() - interval := config.RefreshInterval - if interval <= 0 { - interval = 1 + // 将 map 转换为 JSON 字节切片 + jsonData, err := json.Marshal(setMap) + if err != nil { + logger.SugaredLogger.Errorf("Marshal error:%s", err.Error()) + return } - ticker := time.NewTicker(time.Second * time.Duration(interval)) - defer ticker.Stop() - for range ticker.C { - if isTradingTime(time.Now()) { - MonitorStockPrices(a) - } - } - }() - - go func() { - ticker := time.NewTicker(time.Second * time.Duration(60)) - defer ticker.Stop() - for range ticker.C { - telegraph := refreshTelegraphList() - if telegraph != nil { - go runtime.EventsEmit(a.ctx, "telegraph", telegraph) - } + // 将 JSON 字节切片解析到结构体中 + err = json.Unmarshal(jsonData, config) + if err != nil { + logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error()) + return } - }() - go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList()) - go MonitorStockPrices(a) - - //检查新版本 - go func() { - checkUpdate(a) - }() -} - -func refreshTelegraphList() *[]string { - url := "https://www.cls.cn/telegraph" - response, err := resty.New().R(). - SetHeader("Referer", "https://www.cls.cn/"). - SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60"). - Get(fmt.Sprintf(url)) - if err != nil { - return &[]string{} - } - //logger.SugaredLogger.Info(string(response.Body())) - document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body()))) - if err != nil { - return &[]string{} - } - var telegraph []string - document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) { - //logger.SugaredLogger.Info(selection.Text()) - telegraph = append(telegraph, selection.Text()) + logger.SugaredLogger.Infof("updateSettings config:%+v", config) + if config.DarkTheme { + runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1) + runtime.WindowSetDarkTheme(ctx) + } else { + runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1) + runtime.WindowSetLightTheme(ctx) + } + runtime.WindowReloadApp(ctx) }) - return &telegraph + + // 创建 macOS 托盘 + go func() { + // 使用 Beeep 库替代 Windows 的托盘库 + err := beeep.Notify("go-stock", "应用程序已启动", "") + if err != nil { + log.Fatalf("系统通知失败: %v", err) + } + }() + logger.SugaredLogger.Infof(" application startup Version:%s", Version) } -// isTradingDay 判断是否是交易日 -func isTradingDay(date time.Time) bool { - weekday := date.Weekday() - // 判断是否是周末 - if weekday == time.Saturday || weekday == time.Sunday { - return false +// OnSecondInstanceLaunch 处理第二实例启动时的通知 +func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { + err := beeep.Notify("go-stock", "程序已经在运行了", "") + if err != nil { + logger.SugaredLogger.Error(err) } - // 这里可以添加具体的节假日判断逻辑 - // 例如:判断是否是春节、国庆节等 - return true -} - -// isTradingTime 判断是否是交易时间 -func isTradingTime(date time.Time) bool { - if !isTradingDay(date) { - return false - } - - hour, minute, _ := date.Clock() - - // 判断是否在9:15到11:30之间 - if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) { - return true - } - - // 判断是否在13:00到15:00之间 - if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) { - return true - } - - return false + time.Sleep(time.Second * 3) } func MonitorStockPrices(a *App) { dest := &[]data.FollowedStock{} db.Dao.Model(&data.FollowedStock{}).Find(dest) total := float64(0) - //for _, follow := range *dest { - // stockData := getStockInfo(follow) - // total += stockData.ProfitAmountToday - // price, _ := convertor.ToFloat(stockData.Price) - // if stockData.PrePrice != price { - // go runtime.EventsEmit(a.ctx, "stock_price", stockData) - // } - //} + // 股票信息处理逻辑 stockInfos := GetStockInfos(*dest...) for _, stockInfo := range *stockInfos { + if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) { + continue + } + if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) { + continue + } + if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) { + continue + } + total += stockInfo.ProfitAmountToday price, _ := convertor.ToFloat(stockInfo.Price) + if stockInfo.PrePrice != price { go runtime.EventsEmit(a.ctx, "stock_price", stockInfo) } } + + // 计算总收益并更新状态 if total != 0 { - // title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total) - // systray.SetTooltip(title) + // 使用通知替代 systray 更新 Tooltip + title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total) + + // 发送通知显示实时数据 + err := beeep.Notify("go-stock", title, "") + if err != nil { + logger.SugaredLogger.Errorf("发送通知失败: %v", err) + } } + // 触发实时利润事件 go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total)) - //runtime.WindowSetTitle(a.ctx, title) - } -func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo { - stockCodes := make([]string, 0) - for _, follow := range follows { - stockCodes = append(stockCodes, follow.StockCode) - } - stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...) + +// onReady 在应用程序准备好时调用 +func onReady(a *App) { + // 初始化操作 + logger.SugaredLogger.Infof("onReady") + + // 使用 Beeep 发送通知 + err := beeep.Notify("go-stock", "应用程序已准备就绪", "") if err != nil { - logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error()) - return nil + log.Fatalf("系统通知失败: %v", err) } - stockInfos := make([]data.StockInfo, 0) - for _, info := range *stockData { - v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool { - return follow.StockCode == info.Code - }) - if ok { - addStockFollowData(v, &info) - stockInfos = append(stockInfos, info) - } - } - return &stockInfos -} -func getStockInfo(follow data.FollowedStock) *data.StockInfo { - stockCode := follow.StockCode - stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode) - if err != nil || len(*stockDatas) == 0 { - return &data.StockInfo{} - } - stockData := (*stockDatas)[0] - addStockFollowData(follow, &stockData) - return &stockData + + // 显示应用窗口 + runtime.WindowShow(a.ctx) + + // 在 macOS 上没有系统托盘图标菜单,通常我们通过通知或其他方式提供与用户交互的界面 } -func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) { - stockData.PrePrice = follow.Price //上次当前价格 - stockData.Sort = follow.Sort - stockData.CostPrice = follow.CostPrice //成本价 - stockData.CostVolume = follow.Volume //成本量 - stockData.AlarmChangePercent = follow.AlarmChangePercent - stockData.AlarmPrice = follow.AlarmPrice - - //当前价格 - price, _ := convertor.ToFloat(stockData.Price) - //当前价格为0 时 使用卖一价格作为当前价格 - if price == 0 { - price, _ = convertor.ToFloat(stockData.A1P) - } - //当前价格依然为0 时 使用买一报价作为当前价格 - if price == 0 { - price, _ = convertor.ToFloat(stockData.B1P) - } - - //昨日收盘价 - preClosePrice, _ := convertor.ToFloat(stockData.PreClose) - - //当前价格依然为0 时 使用昨日收盘价为当前价格 - if price == 0 { - price = preClosePrice - } - - //今日最高价 - highPrice, _ := convertor.ToFloat(stockData.High) - if highPrice == 0 { - highPrice, _ = convertor.ToFloat(stockData.Open) - } - - //今日最低价 - lowPrice, _ := convertor.ToFloat(stockData.Low) - if lowPrice == 0 { - lowPrice, _ = convertor.ToFloat(stockData.Open) - } - //开盘价 - //openPrice, _ := convertor.ToFloat(stockData.Open) - - if price > 0 { - stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2) - stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3) - } - if highPrice > 0 { - stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3) - } - if lowPrice > 0 { - stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3) - } - if follow.CostPrice > 0 && follow.Volume > 0 { - if price > 0 { - stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3) - stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2) - stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2) - } else { - //未开盘时当前价格为昨日收盘价 - stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3) - stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2) - // 未开盘时,今日盈亏为 0 - stockData.ProfitAmountToday = 0 - } - - } - - //logger.SugaredLogger.Debugf("stockData:%+v", stockData) - if follow.Price != price && price > 0 { - go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{ - "price": price, - }) - } -} - -// beforeClose is called when the application is about to quit, -// either by clicking the window close button or calling runtime.Quit. -// Returning true will cause the application to continue, false will continue shutdown as normal. +// beforeClose 在应用程序关闭前调用,显示确认对话框 func (a *App) beforeClose(ctx context.Context) (prevent bool) { + defer PanicHandler() + // 在 macOS 上使用 MessageDialog 显示确认窗口 dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ Type: runtime.QuestionDialog, Title: "go-stock", Message: "确定关闭吗?", - Buttons: []string{"确定"}, + Buttons: []string{"确定", "取消"}, Icon: icon, CancelButton: "取消", }) @@ -311,150 +156,13 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) { logger.SugaredLogger.Errorf("dialog error:%s", err.Error()) return false } + logger.SugaredLogger.Debugf("dialog:%s", dialog) - if dialog == "No" { - return true - } - return false -} - -// shutdown is called at application termination -func (a *App) shutdown(ctx context.Context) { - // Perform your teardown here - // systray.Quit() -} - -// Greet returns a greeting for the given name -func (a *App) Greet(stockCode string) *data.StockInfo { - //stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode) - - follow := &data.FollowedStock{ - StockCode: stockCode, - } - db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow) - stockInfo := getStockInfo(*follow) - return stockInfo -} - -func (a *App) Follow(stockCode string) string { - return data.NewStockDataApi().Follow(stockCode) -} - -func (a *App) UnFollow(stockCode string) string { - return data.NewStockDataApi().UnFollow(stockCode) -} - -func (a *App) GetFollowList() []data.FollowedStock { - return data.NewStockDataApi().GetFollowList() -} - -func (a *App) GetStockList(key string) []data.StockBasic { - return data.NewStockDataApi().GetStockList(key) -} - -func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string { - return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode) -} - -func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string { - return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode) -} -func (a *App) SetStockSort(sort int64, stockCode string) { - data.NewStockDataApi().SetStockSort(sort, stockCode) -} -func (a *App) SendDingDingMessage(message string, stockCode string) string { - ttl, _ := a.cache.TTL([]byte(stockCode)) - logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl) - if ttl > 0 { - return "" - } - err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5) - if err != nil { - logger.SugaredLogger.Errorf("set cache error:%s", err.Error()) - return "" - } - return data.NewDingDingAPI().SendDingDingMessage(message) -} - -// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警 -func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string { - ttl, _ := a.cache.TTL([]byte(stockCode)) - logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl) - if ttl > 0 { - return "" - } - err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType)) - if err != nil { - logger.SugaredLogger.Errorf("set cache error:%s", err.Error()) - return "" - } - stockInfo := &data.StockInfo{} - db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo) - go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification() - return data.NewDingDingAPI().SendDingDingMessage(message) -} - -func (a *App) NewChat(stock string) string { - return data.NewDeepSeekOpenAi().NewChat(stock) -} - -func (a *App) NewChatStream(stock, stockCode string) { - msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode) - for msg := range msgs { - runtime.EventsEmit(a.ctx, "newChatStream", msg) - } - runtime.EventsEmit(a.ctx, "newChatStream", "DONE") -} - -func GenNotificationMsg(stockInfo *data.StockInfo) string { - Price, err := convertor.ToFloat(stockInfo.Price) - if err != nil { - Price = 0 - } - PreClose, err := convertor.ToFloat(stockInfo.PreClose) - if err != nil { - PreClose = 0 - } - var RF float64 - if PreClose > 0 { - RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2) - } - - return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time -} - -// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟) -func getMsgTypeTTL(msgType int) int { - switch msgType { - case 1: - return 60 * 5 - case 2: - return 60 * 30 - case 3: - return 60 * 30 - default: - return 60 * 5 + if dialog == "取消" { + return true // 如果选择了取消,不关闭应用 + } else { + // 在 macOS 上应用退出时执行清理工作 + a.cron.Stop() // 停止定时任务 + return false // 如果选择了确定,继续关闭应用 } } - -func getMsgTypeName(msgType int) string { - switch msgType { - case 1: - return "涨跌报警" - case 2: - return "股价报警" - case 3: - return "成本价报警" - default: - return "未知类型" - } -} - -func (a *App) UpdateConfig(settings *data.Settings) string { - logger.SugaredLogger.Infof("UpdateConfig:%+v", settings) - return data.NewSettingsApi(settings).UpdateConfig() -} - -func (a *App) GetConfig() *data.Settings { - return data.NewSettingsApi(&data.Settings{}).GetConfig() -} diff --git a/app_windows.go b/app_windows.go new file mode 100644 index 0000000..0b502fb --- /dev/null +++ b/app_windows.go @@ -0,0 +1,185 @@ +//go:build windows +// +build windows + +package main + +// startup is called at application startup +func (a *App) startup(ctx context.Context) { + defer PanicHandler() + runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) { + logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData) + }) + logger.SugaredLogger.Infof("Version:%s", Version) + // Perform your setup here + a.ctx = ctx + + // 创建系统托盘 + //systray.RunWithExternalLoop(func() { + // onReady(a) + //}, func() { + // onExit(a) + //}) + runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) { + logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData) + config := &data.Settings{} + setMap := optionalData[0].(map[string]interface{}) + + // 将 map 转换为 JSON 字节切片 + jsonData, err := json.Marshal(setMap) + if err != nil { + logger.SugaredLogger.Errorf("Marshal error:%s", err.Error()) + return + } + // 将 JSON 字节切片解析到结构体中 + err = json.Unmarshal(jsonData, config) + if err != nil { + logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error()) + return + } + + logger.SugaredLogger.Infof("updateSettings config:%+v", config) + if config.DarkTheme { + runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1) + runtime.WindowSetDarkTheme(ctx) + } else { + runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1) + runtime.WindowSetLightTheme(ctx) + } + runtime.WindowReloadApp(ctx) + + }) + go systray.Run(func() { + onReady(a) + }, func() { + onExit(a) + }) + + logger.SugaredLogger.Infof(" application startup Version:%s", Version) +} + +func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { + notification := toast.Notification{ + AppID: "go-stock", + Title: "go-stock", + Message: "程序已经在运行了", + Icon: "", + Duration: "short", + Audio: toast.Default, + } + err := notification.Push() + if err != nil { + logger.SugaredLogger.Error(err) + } + time.Sleep(time.Second * 3) +} + +func MonitorStockPrices(a *App) { + dest := &[]data.FollowedStock{} + db.Dao.Model(&data.FollowedStock{}).Find(dest) + total := float64(0) + //for _, follow := range *dest { + // stockData := getStockInfo(follow) + // total += stockData.ProfitAmountToday + // price, _ := convertor.ToFloat(stockData.Price) + // if stockData.PrePrice != price { + // go runtime.EventsEmit(a.ctx, "stock_price", stockData) + // } + //} + + stockInfos := GetStockInfos(*dest...) + for _, stockInfo := range *stockInfos { + if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) { + continue + } + if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) { + continue + } + if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) { + continue + } + + total += stockInfo.ProfitAmountToday + price, _ := convertor.ToFloat(stockInfo.Price) + + if stockInfo.PrePrice != price { + //logger.SugaredLogger.Infof("-----------sz------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA) + go runtime.EventsEmit(a.ctx, "stock_price", stockInfo) + } + + } + if total != 0 { + title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total) + systray.SetTooltip(title) + } + + go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total)) + //runtime.WindowSetTitle(a.ctx, title) + +} + +func onReady(a *App) { + + // 初始化操作 + logger.SugaredLogger.Infof("systray onReady") + systray.SetIcon(icon2) + systray.SetTitle("go-stock") + systray.SetTooltip("go-stock 股票行情实时获取") + // 创建菜单项 + show := systray.AddMenuItem("显示", "显示应用程序") + show.Click(func() { + //logger.SugaredLogger.Infof("显示应用程序") + runtime.WindowShow(a.ctx) + }) + hide := systray.AddMenuItem("隐藏", "隐藏应用程序") + hide.Click(func() { + //logger.SugaredLogger.Infof("隐藏应用程序") + runtime.WindowHide(a.ctx) + }) + systray.AddSeparator() + mQuitOrig := systray.AddMenuItem("退出", "退出应用程序") + mQuitOrig.Click(func() { + //logger.SugaredLogger.Infof("退出应用程序") + runtime.Quit(a.ctx) + }) + systray.SetOnRClick(func(menu systray.IMenu) { + menu.ShowMenu() + //logger.SugaredLogger.Infof("SetOnRClick") + }) + systray.SetOnClick(func(menu systray.IMenu) { + //logger.SugaredLogger.Infof("SetOnClick") + menu.ShowMenu() + }) + systray.SetOnDClick(func(menu systray.IMenu) { + menu.ShowMenu() + //logger.SugaredLogger.Infof("SetOnDClick") + }) +} + +// beforeClose is called when the application is about to quit, +// either by clicking the window close button or calling runtime.Quit. +// Returning true will cause the application to continue, false will continue shutdown as normal. +func (a *App) beforeClose(ctx context.Context) (prevent bool) { + defer PanicHandler() + + dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ + Type: runtime.QuestionDialog, + Title: "go-stock", + Message: "确定关闭吗?", + Buttons: []string{"确定"}, + Icon: icon, + CancelButton: "取消", + }) + + if err != nil { + logger.SugaredLogger.Errorf("dialog error:%s", err.Error()) + return false + } + logger.SugaredLogger.Debugf("dialog:%s", dialog) + if dialog == "No" { + return true + } else { + systray.Quit() + a.cron.Stop() + return false + } +} diff --git a/backend/data/settings_api.go b/backend/data/settings_api.go index 5134c45..91f9d32 100644 --- a/backend/data/settings_api.go +++ b/backend/data/settings_api.go @@ -128,7 +128,7 @@ func (s SettingsApi) GetConfig() *Settings { } } if settings.BrowserPath == "" { - settings.BrowserPath, _ = CheckBrowserOnWindows() + settings.BrowserPath, _ = CheckBrowser() } if settings.BrowserPoolSize <= 0 { settings.BrowserPoolSize = 1 diff --git a/backend/data/stock_data_api.go b/backend/data/stock_data_api.go index f88b590..e65f3fe 100644 --- a/backend/data/stock_data_api.go +++ b/backend/data/stock_data_api.go @@ -20,7 +20,6 @@ import ( "go-stock/backend/db" "go-stock/backend/logger" "go-stock/backend/models" - "golang.org/x/sys/windows/registry" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" "gorm.io/gorm" @@ -1298,50 +1297,6 @@ func SearchStockInfoByCode(stock string) *[]string { return &messages } -// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装 -func checkChromeOnWindows() (string, bool) { - key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) - if err != nil { - // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) - key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) - if err != nil { - return "", false - } - defer key.Close() - } - defer key.Close() - path, _, err := key.GetStringValue("Path") - //logger.SugaredLogger.Infof("Chrome安装路径:%s", path) - if err != nil { - return "", false - } - return path + "\\chrome.exe", true -} - -// CheckBrowserOnWindows 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径 -func CheckBrowserOnWindows() (string, bool) { - if path, ok := checkChromeOnWindows(); ok { - return path, true - } - - key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) - if err != nil { - // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) - key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) - if err != nil { - return "", false - } - defer key.Close() - } - defer key.Close() - path, _, err := key.GetStringValue("Path") - //logger.SugaredLogger.Infof("Edge安装路径:%s", path) - if err != nil { - return "", false - } - return path + "\\msedge.exe", true -} - // 分时数据 func (receiver StockDataApi) GetStockMinutePriceData(stockCode string) (*[]MinuteData, string) { url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=%s", stockCode) diff --git a/backend/data/stock_data_api_darwin.go b/backend/data/stock_data_api_darwin.go new file mode 100644 index 0000000..1ab7303 --- /dev/null +++ b/backend/data/stock_data_api_darwin.go @@ -0,0 +1,37 @@ +//go:build darwin +// +build darwin + +package data + +import "os" + +// CheckChrome 检查 macOS 是否安装了 Chrome 浏览器 +func CheckChrome() (string, bool) { + // 检查 /Applications 目录下是否存在 Chrome + locations := []string{ + // Mac + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + } + path := "" + for _, location := range locations { + _, err := os.Stat(location) + if err != nil { + continue + } + path = location + } + if path == "" { + return "", false + } + + return path, true +} + +// CheckBrowser 检查 macOS 是否安装了浏览器,并返回安装路径 +func CheckBrowser() (string, bool) { + if path, ok := CheckChrome(); ok { + return path, ok + } + return "", false +} diff --git a/backend/data/stock_data_api_windows.go b/backend/data/stock_data_api_windows.go new file mode 100644 index 0000000..a825f06 --- /dev/null +++ b/backend/data/stock_data_api_windows.go @@ -0,0 +1,50 @@ +//go:build windows +// +build windows + +package data + +import "golang.org/x/sys/windows/registry" + +// CheckChrome 在 Windows 系统上检查谷歌浏览器是否安装 +func CheckChrome() (string, bool) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) + if err != nil { + // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) + key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE) + if err != nil { + return "", false + } + defer key.Close() + } + defer key.Close() + path, _, err := key.GetStringValue("Path") + //logger.SugaredLogger.Infof("Chrome安装路径:%s", path) + if err != nil { + return "", false + } + return path + "\\chrome.exe", true +} + +// CheckBrowser 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径 +func CheckBrowser() (string, bool) { + if path, ok := checkChromeOnWindows(); ok { + return path, true + } + + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) + if err != nil { + // 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序) + key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE) + if err != nil { + return "", false + } + defer key.Close() + } + defer key.Close() + path, _, err := key.GetStringValue("Path") + //logger.SugaredLogger.Infof("Edge安装路径:%s", path) + if err != nil { + return "", false + } + return path + "\\msedge.exe", true +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index fce852e..799119d 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -2d63c3a999d797889c01d6c96451b197 \ No newline at end of file +8d3264f90073dfceb29c3619775d830d \ No newline at end of file diff --git a/frontend/src/components/market.vue b/frontend/src/components/market.vue index f8e98f8..4882526 100644 --- a/frontend/src/components/market.vue +++ b/frontend/src/components/market.vue @@ -130,16 +130,20 @@ EventsOn("changeMarketTab", async (msg) => { }) EventsOn("newTelegraph", (data) => { - for (let i = 0; i < data.length; i++) { - telegraphList.value.pop() + if (data!=null) { + for (let i = 0; i < data.length; i++) { + telegraphList.value.pop() + } + telegraphList.value.unshift(...data) } - telegraphList.value.unshift(...data) }) EventsOn("newSinaNews", (data) => { + if (data!=null) { for (let i = 0; i < data.length; i++) { sinaNewsList.value.pop() } sinaNewsList.value.unshift(...data) + } }) //获取页面高度 diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts old mode 100644 new mode 100755 diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js old mode 100644 new mode 100755 diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod index 7d5571c..6071f02 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/chromedp/chromedp v0.11.2 github.com/coocood/freecache v1.2.4 github.com/duke-git/lancet/v2 v2.3.4 - github.com/energye/systray v1.0.2 + github.com/gen2brain/beeep v0.11.1 github.com/glebarez/sqlite v1.11.0 github.com/go-ego/gse v0.80.3 github.com/go-resty/resty/v2 v2.16.2 @@ -28,6 +28,7 @@ require ( ) require ( + git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -35,6 +36,7 @@ require ( github.com/chromedp/sysutil v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/esiqveland/notify v0.13.3 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect @@ -42,6 +44,7 @@ require ( github.com/gobwas/ws v1.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jackmordaunt/icns/v3 v3.0.1 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -55,13 +58,16 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect + github.com/sergeymakinen/go-bmp v1.0.0 // indirect + github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect + github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect diff --git a/go.sum b/go.sum index bdbc206..1ab5e97 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE= +git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo= github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU= github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= @@ -14,14 +16,17 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M= github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= +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/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls= github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/energye/systray v1.0.2 h1:63R4prQkANtpM2CIA4UrDCuwZFt+FiygG77JYCsNmXc= -github.com/energye/systray v1.0.2/go.mod h1:sp7Q/q/I4/w5ebvpSuJVep71s9Bg7L9ZVp69gBASehM= +github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o= +github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE= +github.com/gen2brain/beeep v0.11.1 h1:EbSIhrQZFDj1K2fzlMpAYlFOzV8YuNe721A58XcCTYI= +github.com/gen2brain/beeep v0.11.1/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= @@ -42,7 +47,6 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -50,6 +54,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o= +github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ= 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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -91,6 +97,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= @@ -115,10 +123,20 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M= +github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY= +github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ= +github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw= -github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= +github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= +github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -183,7 +201,6 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -238,6 +255,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=