新增: SFTP直连+网站预览+OSS区域嗅探+热键+BGM播放
This commit is contained in:
228
app.go
228
app.go
@@ -8,16 +8,16 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
stdruntime "runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"u-desk/internal/api"
|
||||
"u-desk/internal/common"
|
||||
"u-desk/internal/filesystem"
|
||||
"u-desk/internal/hotkey"
|
||||
osssvc "u-desk/internal/ossdrv"
|
||||
"u-desk/internal/service"
|
||||
"u-desk/internal/sftp"
|
||||
@@ -25,24 +25,28 @@ import (
|
||||
"u-desk/internal/storage/models"
|
||||
"u-desk/internal/system"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// App 应用结构体
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
mainWindow *application.WebviewWindow
|
||||
updateAPI *api.UpdateAPI
|
||||
updateTicker *time.Ticker
|
||||
configAPI *api.ConfigAPI
|
||||
pdfAPI *api.PdfAPI
|
||||
filesystem *filesystem.FileSystemService
|
||||
sftpService *sftp.Service
|
||||
ossService *osssvc.Service
|
||||
profileSvc *service.ProfileService
|
||||
isAlwaysOnTop bool
|
||||
mu sync.Mutex
|
||||
ctx context.Context
|
||||
mainWindow *application.WebviewWindow
|
||||
updateAPI *api.UpdateAPI
|
||||
updateTicker *time.Ticker
|
||||
configAPI *api.ConfigAPI
|
||||
pdfAPI *api.PdfAPI
|
||||
filesystem *filesystem.FileSystemService
|
||||
sftpService *sftp.Service
|
||||
ossService *osssvc.Service
|
||||
profileSvc *service.ProfileService
|
||||
isAlwaysOnTop bool
|
||||
mu sync.Mutex
|
||||
unregisterHotkey func()
|
||||
}
|
||||
|
||||
// App 方法命名约定:
|
||||
@@ -59,6 +63,41 @@ func (a *App) SetMainWindow(w *application.WebviewWindow) {
|
||||
a.mainWindow = w
|
||||
}
|
||||
|
||||
// RegisterGlobalHotkey 注册 Ctrl+Shift+B 全局热键(需在窗口创建后调用)
|
||||
func (a *App) RegisterGlobalHotkey() {
|
||||
if a.mainWindow == nil {
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
if a.unregisterHotkey != nil {
|
||||
a.mu.Unlock()
|
||||
return
|
||||
}
|
||||
a.mu.Unlock()
|
||||
hwnd := uintptr(a.mainWindow.NativeWindow())
|
||||
if hwnd == 0 {
|
||||
fmt.Println("[全局热键] HWND 为 0,注册跳过")
|
||||
return
|
||||
}
|
||||
const id int32 = 1
|
||||
if err := hotkey.Register(hwnd, id, hotkey.ModControl|hotkey.ModShift, 0x42); err != nil {
|
||||
fmt.Printf("[全局热键] RegisterHotKey Ctrl+Shift+B 失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("[全局热键] Ctrl+Shift+B 已注册")
|
||||
a.mu.Lock()
|
||||
a.unregisterHotkey = func() { hotkey.Unregister(hwnd, id) }
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// HandleHotkey 处理全局热键回调:切换 BgmBar 显示/隐藏
|
||||
func (a *App) HandleHotkey() {
|
||||
if a.mainWindow == nil {
|
||||
return
|
||||
}
|
||||
a.mainWindow.EmitEvent("toggle-bgm-bar")
|
||||
}
|
||||
|
||||
// ServiceStartup Wails v3 服务启动生命周期(替代 v2 的 Startup)
|
||||
func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {
|
||||
a.ctx = ctx
|
||||
@@ -101,8 +140,8 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions)
|
||||
return fmt.Errorf("模块初始化失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 清理遗留的 SFTP 临时预览文件
|
||||
sftp.CleanupTempFiles()
|
||||
// 5. 清理过期的下载缓存
|
||||
storage.CleanupExpiredCache()
|
||||
|
||||
// 6. 异步初始化:UpdateAPI(涉及网络请求,完全异步)
|
||||
go func() {
|
||||
@@ -121,6 +160,24 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions)
|
||||
}
|
||||
}()
|
||||
|
||||
// 延迟注册全局热键(轮询等待原生窗口创建完成)
|
||||
// RegisterHotKey 必须在创建窗口的同一线程调用,
|
||||
// 通过 PostMessage 将注册请求投递到主线程消息循环
|
||||
go func() {
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
if a.mainWindow == nil {
|
||||
return
|
||||
}
|
||||
hwnd := uintptr(a.mainWindow.NativeWindow())
|
||||
if hwnd != 0 {
|
||||
hotkey.PostMessage(hwnd, hotkey.WM_APP_HOTKEY, 0, 0)
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Println("[全局热键] 等待窗口超时")
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -193,6 +250,9 @@ func (a *App) ServiceShutdown() error {
|
||||
if a.updateTicker != nil {
|
||||
a.updateTicker.Stop()
|
||||
}
|
||||
if a.unregisterHotkey != nil {
|
||||
a.unregisterHotkey()
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -217,7 +277,7 @@ func (a *App) ServiceShutdown() error {
|
||||
if a.sftpService != nil {
|
||||
sftp.GetManager().Shutdown()
|
||||
}
|
||||
sftp.CleanupTempFiles()
|
||||
storage.CleanupExpiredCache()
|
||||
|
||||
// 关闭所有 OSS 连接
|
||||
osssvc.GetManager().Shutdown()
|
||||
@@ -856,11 +916,11 @@ func (a *App) ensureSftpService() *sftp.Service {
|
||||
|
||||
// SftpConnectRequest SFTP 连接请求
|
||||
type SftpConnectRequest struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
KeyPath string `json:"key_path"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
KeyPath string `json:"key_path"`
|
||||
KeyPassphrase string `json:"key_passphrase"`
|
||||
}
|
||||
|
||||
@@ -874,8 +934,12 @@ func (a *App) SftpConnect(req SftpConnectRequest) (string, error) {
|
||||
KeyPath: req.KeyPath,
|
||||
KeyPassphrase: req.KeyPassphrase,
|
||||
}
|
||||
if config.Port == 0 { config.Port = 22 }
|
||||
if config.Timeout == 0 { config.Timeout = 15 * time.Second }
|
||||
if config.Port == 0 {
|
||||
config.Port = 22
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 15 * time.Second
|
||||
}
|
||||
|
||||
svc := a.ensureSftpService()
|
||||
_, err := svc.GetManager().Connect(config)
|
||||
@@ -967,6 +1031,16 @@ func (a *App) SftpDownloadToTemp(connID string, remotePath string) (string, erro
|
||||
return a.ensureSftpService().DownloadToTemp(connID, remotePath)
|
||||
}
|
||||
|
||||
// SftpDownloadSiteForPreview 下载 HTML 及其网站资源到本地临时目录
|
||||
func (a *App) SftpDownloadSiteForPreview(connID string, remotePath string) (string, error) {
|
||||
return a.ensureSftpService().DownloadSiteForPreview(connID, remotePath)
|
||||
}
|
||||
|
||||
// SftpDownloadToTempCached 带缓存的 SFTP 下载(命中缓存直接返回本地路径)
|
||||
func (a *App) SftpDownloadToTempCached(connID string, remotePath string, fileSize int64, modTime string) (string, error) {
|
||||
return a.ensureSftpService().DownloadToTempCached(connID, remotePath, fileSize, modTime)
|
||||
}
|
||||
|
||||
// SftpGetCommonPaths 获取 SFTP 远程主机常用路径
|
||||
func (a *App) SftpGetCommonPaths(connID string) (map[string]string, error) {
|
||||
return a.ensureSftpService().GetCommonPaths(connID)
|
||||
@@ -1018,6 +1092,16 @@ func (a *App) OssDownloadToTemp(connID string, key string) (string, error) {
|
||||
return a.ensureOssService().DownloadToTemp(connID, key)
|
||||
}
|
||||
|
||||
// OssDownloadSiteForPreview OSS 下载 HTML 及其引用的资源到临时目录
|
||||
func (a *App) OssDownloadSiteForPreview(connID string, key string) (string, error) {
|
||||
return a.ensureOssService().DownloadSiteForPreview(connID, key)
|
||||
}
|
||||
|
||||
// OssDownloadToTempCached 带缓存的 OSS 下载(命中缓存直接返回本地路径)
|
||||
func (a *App) OssDownloadToTempCached(connID string, key string, fileSize int64, modTime string) (string, error) {
|
||||
return a.ensureOssService().DownloadToTempCached(connID, key, fileSize, modTime)
|
||||
}
|
||||
|
||||
// OssReadFile OSS 读取文件
|
||||
func (a *App) OssReadFile(connID string, key string) (string, error) {
|
||||
return a.ensureOssService().ReadFile(connID, key)
|
||||
@@ -1078,21 +1162,22 @@ func (a *App) OssGetSignedURL(connID string, key string) (string, error) {
|
||||
// --- 连接配置 CRUD (SQLite 持久化) ---
|
||||
|
||||
type SaveProfileRequest struct {
|
||||
ID *uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
KeyPath string `json:"key_path"`
|
||||
Type string `json:"type"`
|
||||
Token string `json:"token"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
LastConnected *int64 `json:"last_connected"`
|
||||
ID *uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
KeyPath string `json:"key_path"`
|
||||
Type string `json:"type"`
|
||||
Provider string `json:"provider"`
|
||||
Token string `json:"token"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
LastConnected *int64 `json:"last_connected"`
|
||||
}
|
||||
|
||||
func (a *App) ensureProfileSvc() *service.ProfileService {
|
||||
@@ -1106,7 +1191,9 @@ func (a *App) ensureProfileSvc() *service.ProfileService {
|
||||
|
||||
func (a *App) LoadConnectionProfiles() ([]map[string]interface{}, error) {
|
||||
list, err := a.ensureProfileSvc().ListProfiles()
|
||||
if err != nil { return nil, err }
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]map[string]interface{}, len(list))
|
||||
for i, p := range list {
|
||||
result[i] = map[string]interface{}{
|
||||
@@ -1118,6 +1205,7 @@ func (a *App) LoadConnectionProfiles() ([]map[string]interface{}, error) {
|
||||
"password": p.Password,
|
||||
"keyPath": p.KeyPath,
|
||||
"type": p.Type,
|
||||
"provider": p.Provider,
|
||||
"token": p.Token,
|
||||
"accessKey": p.AccessKey,
|
||||
"secretKey": p.SecretKey,
|
||||
@@ -1135,7 +1223,7 @@ func (a *App) SaveConnectionProfile(req SaveProfileRequest) (map[string]interfac
|
||||
p := &models.ConnectionProfile{
|
||||
Name: req.Name, Host: req.Host, Port: req.Port,
|
||||
Username: req.Username, Password: req.Password,
|
||||
KeyPath: req.KeyPath, Type: req.Type, Token: req.Token,
|
||||
KeyPath: req.KeyPath, Type: req.Type, Provider: req.Provider, Token: req.Token,
|
||||
AccessKey: req.AccessKey, SecretKey: req.SecretKey,
|
||||
Bucket: req.Bucket, Region: req.Region, Endpoint: req.Endpoint,
|
||||
}
|
||||
@@ -1161,18 +1249,70 @@ func (a *App) GetLocalSystemInfo() (map[string]interface{}, error) {
|
||||
|
||||
cpuInfo, err := system.GetCPUInfo()
|
||||
if err == nil && cpuInfo != nil {
|
||||
if v, ok := cpuInfo["usage"].(string); ok { info["cpu_usage"] = v }
|
||||
if v, ok := cpuInfo["usage"].(string); ok {
|
||||
info["cpu_usage"] = v
|
||||
}
|
||||
}
|
||||
|
||||
memInfo, err := system.GetMemoryInfo()
|
||||
if err == nil && memInfo != nil {
|
||||
if v, ok := memInfo["usage"].(string); ok { info["mem_usage"] = v }
|
||||
if v, ok := memInfo["usage"].(string); ok {
|
||||
info["mem_usage"] = v
|
||||
}
|
||||
}
|
||||
|
||||
diskInfos, err := system.GetDiskInfo()
|
||||
if err == nil && len(diskInfos) > 0 {
|
||||
if v, ok := diskInfos[0]["usage"].(string); ok { info["disk_usage"] = v }
|
||||
if v, ok := diskInfos[0]["usage"].(string); ok {
|
||||
info["disk_usage"] = v
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// ========== BGM 播放列表持久化 ==========
|
||||
|
||||
// BgmPlaylistItem 播放列表条目
|
||||
type BgmPlaylistItem struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
ProfileID string `json:"profile_id"`
|
||||
}
|
||||
|
||||
// BgmGetPlaylist 获取播放列表
|
||||
func (a *App) BgmGetPlaylist() ([]BgmPlaylistItem, error) {
|
||||
db := storage.GetDB()
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("数据库未初始化")
|
||||
}
|
||||
var rows []models.BgmPlaylist
|
||||
db.Order("sort ASC").Find(&rows)
|
||||
items := make([]BgmPlaylistItem, len(rows))
|
||||
for i, r := range rows {
|
||||
items[i] = BgmPlaylistItem{Name: r.Name, Path: r.Path, ProfileID: r.ProfileID}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// BgmSavePlaylist 全量保存播放列表(前端调用时传完整列表)
|
||||
func (a *App) BgmSavePlaylist(items []BgmPlaylistItem) error {
|
||||
db := storage.GetDB()
|
||||
if db == nil {
|
||||
return fmt.Errorf("数据库未初始化")
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec("DELETE FROM bgm_playlist").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
sort := uint(0)
|
||||
for _, item := range items {
|
||||
if item.Name == "" || item.Path == "" {
|
||||
continue
|
||||
}
|
||||
tx.Create(&models.BgmPlaylist{Name: item.Name, Path: item.Path, ProfileID: item.ProfileID, Sort: sort})
|
||||
sort++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user