Files
u-desk/app.go
T

326 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// [fs-only] 数据库客户端模块已移除(feature/fs-only 分支)
// 保留模块:文件系统 | Markdown编辑器 | 版本历史(抽屉) | 系统信息 | 更新检查 | PDF导出
// 顶部Tab仅:file-system(数据库 db-cli 已删除)
package main
import (
"context"
"fmt"
"log/slog"
"sync"
"time"
"u-desk/internal/api"
"u-desk/internal/filewatch"
"u-desk/internal/filesystem"
"u-desk/internal/hotkey"
osssvc "u-desk/internal/ossdrv"
"u-desk/internal/service"
"u-desk/internal/sftp"
"u-desk/internal/storage"
"github.com/wailsapp/wails/v3/pkg/application"
)
// 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
fileWatcher *filewatch.Watcher
isAlwaysOnTop bool
mu sync.Mutex
unregisterHotkey func()
}
// App 方法命名约定:
// - 多参数操作 → XxxRequest 结构体(Wails 自动生成 TS 类型)
// - 单参数查询/简单操作 → 直接参数
// NewApp 创建新的应用实例
func NewApp() *App {
return &App{}
}
// emitEvent 向前端发送事件
func (a *App) emitEvent(name string, data ...any) {
if a.mainWindow != nil {
a.mainWindow.EmitEvent(name, data...)
}
}
// SetMainWindow 设置主窗口引用(由 main.go 在创建窗口后调用)
func (a *App) SetMainWindow(w *application.WebviewWindow) {
a.mainWindow = w
a.fileWatcher = filewatch.NewWatcher(func(name string, data ...any) {
if a.mainWindow != nil {
a.mainWindow.EmitEvent(name, data...)
}
})
}
// RegisterGlobalHotkey 注册 Ctrl+Shift+B 全局热键(需在窗口创建后调用)
func (a *App) RegisterGlobalHotkey() {
if a.mainWindow == nil {
return
}
a.mu.Lock()
defer a.mu.Unlock()
if a.unregisterHotkey != nil {
return
}
hwnd := uintptr(a.mainWindow.NativeWindow())
if hwnd == 0 {
slog.Warn("HWND 为 0,注册跳过")
return
}
const id int32 = 1
if err := hotkey.Register(hwnd, id, hotkey.ModControl|hotkey.ModShift, 0x42); err != nil {
slog.Error("RegisterHotKey Ctrl+Shift+B 失败", "error", err)
return
}
slog.Info("Ctrl+Shift+B 已注册")
a.unregisterHotkey = func() { hotkey.Unregister(hwnd, id) }
}
// 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
// dev 模式打开 DevTools
openDevTools(a.mainWindow)
// 1. 核心初始化:SQLite(必须同步,很快)
if _, err := storage.InitFast(); err != nil {
return fmt.Errorf("SQLite 初始化失败,应用无法启动: %w", err)
}
// 2. 初始化配置服务
configService, err := api.NewConfigAPI()
if err != nil {
return fmt.Errorf("配置服务初始化失败: %w", err)
}
a.configAPI = configService
// 2.5. 迁移旧配置
if err := a.configAPI.MigrateTabConfig(); err != nil {
slog.Warn("配置迁移失败", "error", err)
}
// 2.6. 初始化PDF导出API
slog.Info("初始化PDF导出模块")
pdfAPI, err := api.NewPdfAPI()
if err != nil {
slog.Error("PDF导出API初始化失败", "error", err)
} else {
a.pdfAPI = pdfAPI
slog.Info("PDF导出模块初始化完成")
}
// 3. 初始化版本号(提前触发缓存,避免后续重复计算)
version := service.GetCurrentVersion()
slog.Info("当前版本", "version", version)
// 4. 读取配置,获取可见的 Tabs
visibleTabs := a.getVisibleTabs()
slog.Info("可用的模块", "modules", visibleTabs)
// 4. 根据配置初始化模块(条件初始化)
if err := a.initModulesByConfig(visibleTabs); err != nil {
return fmt.Errorf("模块初始化失败: %w", err)
}
// 5. 清理过期的下载缓存
storage.CleanupExpiredCache()
// 6. 异步初始化:UpdateAPI(涉及网络请求,完全异步)
go func() {
checkURL := "https://c.1216.top/last-version.json"
if cfg, err := service.LoadUpdateConfig(); err == nil && cfg.CheckURL != "" {
checkURL = cfg.CheckURL
}
if updateAPI, err := api.NewUpdateAPI(checkURL); err == nil {
a.mu.Lock()
a.updateAPI = updateAPI
a.mu.Unlock()
a.updateAPI.SetContext(ctx)
a.updateAPI.SetEventEmitter(func(name string, data ...any) {
if a.mainWindow != nil {
a.mainWindow.EmitEvent(name, data...)
}
})
a.startAutoUpdateCheck()
}
}()
// 延迟注册全局热键(轮询等待原生窗口创建完成)
// 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
}
}
slog.Warn("等待窗口超时")
}()
return nil
}
// ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown)
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()
if a.filesystem != nil {
slog.Info("文件系统服务正在关闭")
if err := a.filesystem.Close(shutdownCtx); err != nil {
slog.Error("文件系统服务关闭失败", "error", err)
} else {
slog.Info("文件系统服务已关闭")
}
}
slog.Info("文件服务器正在关闭")
if err := filesystem.ShutdownLocalFileServer(); err != nil {
slog.Error("文件服务器关闭失败", "error", err)
} else {
slog.Info("文件服务器已关闭")
}
// 关闭所有 SFTP 连接 + 清理临时文件
if a.sftpService != nil {
sftp.GetManager().Shutdown()
}
storage.CleanupExpiredCache()
// 关闭所有 OSS 连接
osssvc.GetManager().Shutdown()
return nil
}
// WriteFileRequest 写入文件请求结构体
type WriteFileRequest struct {
Path string `json:"path"`
Content string `json:"content"`
}
// SaveBase64FileRequest 保存 Base64 编码的二进制文件
type SaveBase64FileRequest struct {
Path string `json:"path"`
Content string `json:"content"` // base64 编码的文件内容
}
// RenamePathRequest 重命名文件或目录请求结构体
type RenamePathRequest struct {
OldPath string `json:"oldPath"`
NewPath string `json:"newPath"`
}
// SaveAppConfigRequest 保存应用配置请求
type SaveAppConfigRequest struct {
Tabs []api.AppTabDefinition `json:"tabs"`
VisibleTabs []string `json:"visibleTabs"`
DefaultTab string `json:"defaultTab"`
}
// 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"`
KeyPassphrase string `json:"key_passphrase"`
}
// SftpWriteFileRequest SFTP 写入请求
type SftpWriteFileRequest struct {
SessionID string `json:"session_id"`
Path string `json:"path"`
Content string `json:"content"`
}
// SftpRenamePathRequest SFTP 重命名请求
type SftpRenamePathRequest struct {
SessionID string `json:"session_id"`
OldPath string `json:"old_path"`
NewPath string `json:"new_path"`
}
type OssConnectRequest struct {
Provider string `json:"provider"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Endpoint string `json:"endpoint"`
}
// OssRenamePathRequest OSS 重命名请求
type OssRenamePathRequest struct {
ConnID string `json:"conn_id"`
OldPath string `json:"old_path"`
NewPath string `json:"new_path"`
}
// --- 连接配置 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"`
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"`
}
// ========== BGM 播放列表持久化 ==========
// BgmPlaylistItem 播放列表条目
type BgmPlaylistItem struct {
Name string `json:"name"`
Path string `json:"path"`
ProfileID string `json:"profile_id"`
}