diff --git a/frontend/bindings/voidraft/internal/services/configservice.ts b/frontend/bindings/voidraft/internal/services/configservice.ts index 6c33a17..c5d038e 100644 --- a/frontend/bindings/voidraft/internal/services/configservice.ts +++ b/frontend/bindings/voidraft/internal/services/configservice.ts @@ -14,6 +14,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime"; // @ts-ignore: Unused imports import * as models$0 from "../models/models.js"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + /** * Get 获取配置项 */ @@ -83,34 +87,18 @@ export function Set(key: string, value: any): Promise & { cancel(): void } } /** - * SetBackupConfigChangeCallback 设置备份配置变更回调 + * Watch 注册配置变更监听器 */ -export function SetBackupConfigChangeCallback(callback: any): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3264871659, callback) as any; +export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } { + let $resultPromise = $Call.ByID(1143583035, path, callback) as any; return $resultPromise; } /** - * SetDataPathChangeCallback 设置数据路径配置变更回调 + * WatchWithContext 使用 Context 注册监听器 */ -export function SetDataPathChangeCallback(callback: any): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(393017412, callback) as any; - return $resultPromise; -} - -/** - * SetHotkeyChangeCallback 设置热键配置变更回调 - */ -export function SetHotkeyChangeCallback(callback: any): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(283872321, callback) as any; - return $resultPromise; -} - -/** - * SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调 - */ -export function SetWindowSnapConfigChangeCallback(callback: any): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2324961653, callback) as any; +export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1454973098, path, callback) as any; return $resultPromise; } diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index 45f6358..e7aee79 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -12,6 +12,12 @@ import * as http$0 from "../../../net/http/models.js"; // @ts-ignore: Unused imports import * as time$0 from "../../../time/models.js"; +/** + * CancelFunc 取消订阅函数 + * 调用此函数可以取消对配置的监听 + */ +export type CancelFunc = any; + /** * HttpRequest HTTP请求结构 */ @@ -260,6 +266,14 @@ export class OSInfo { } } +/** + * ObserverCallback 观察者回调函数 + * 参数: + * - oldValue: 配置变更前的值 + * - newValue: 配置变更后的值 + */ +export type ObserverCallback = any; + /** * SelfUpdateResult 自我更新结果 */ diff --git a/internal/common/hotkey/darwin/mainthread.m b/internal/common/hotkey/darwin/mainthread.m index e2527e9..3bfa8e3 100644 --- a/internal/common/hotkey/darwin/mainthread.m +++ b/internal/common/hotkey/darwin/mainthread.m @@ -12,10 +12,9 @@ void wakeupMainThread(void) { } // The following three lines of code must run on the main thread. -// For GUI applications (Wails, Cocoa), the framework handles this automatically. -// For CLI applications, see README for manual event loop setup. +// It must handle it using golang.design/x/mainthread. // -// Inspired from: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m +// inspired from here: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m void os_main(void) { [NSApplication sharedApplication]; [NSApp disableRelaunchOnLogin]; diff --git a/internal/services/backup_service.go b/internal/services/backup_service.go index 9c08fe2..02d13cf 100644 --- a/internal/services/backup_service.go +++ b/internal/services/backup_service.go @@ -36,6 +36,9 @@ type BackupService struct { isInitialized bool autoBackupTicker *time.Ticker autoBackupStop chan bool + + // 配置观察者取消函数 + cancelObserver CancelFunc } // NewBackupService 创建新的备份服务实例 @@ -48,12 +51,25 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService, } func (ds *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + ds.cancelObserver = ds.configService.Watch("backup", ds.onBackupConfigChange) + if err := ds.Initialize(); err != nil { return fmt.Errorf("initializing backup service: %w", err) } return nil } +// onBackupConfigChange 备份配置变更回调 +func (ds *BackupService) onBackupConfigChange(oldValue, newValue interface{}) { + // 重新加载配置 + config, err := ds.configService.GetConfig() + if err != nil { + return + } + // 处理配置变更 + _ = ds.HandleConfigChange(&config.Backup) +} + // Initialize 初始化备份服务 func (s *BackupService) Initialize() error { config, repoPath, err := s.getConfigAndPath() @@ -393,5 +409,9 @@ func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error // ServiceShutdown 服务关闭时的清理工作 func (s *BackupService) ServiceShutdown() { + // 取消配置观察者 + if s.cancelObserver != nil { + s.cancelObserver() + } s.StopAutoBackup() } diff --git a/internal/services/config_notification_service.go b/internal/services/config_notification_service.go deleted file mode 100644 index 10c2119..0000000 --- a/internal/services/config_notification_service.go +++ /dev/null @@ -1,483 +0,0 @@ -package services - -import ( - "context" - "crypto/sha256" - "encoding/json" - "fmt" - "reflect" - "sync" - "time" - "voidraft/internal/models" - - "github.com/knadh/koanf/v2" - "github.com/wailsapp/wails/v3/pkg/services/log" -) - -// ConfigChangeType 配置变更类型 -type ConfigChangeType string - -const ( - // ConfigChangeTypeHotkey 热键配置变更 - ConfigChangeTypeHotkey ConfigChangeType = "hotkey" - // ConfigChangeTypeDataPath 数据路径配置变更 - ConfigChangeTypeDataPath ConfigChangeType = "datapath" - // ConfigChangeTypeBackup 备份配置变更 - ConfigChangeTypeBackup ConfigChangeType = "backup" - // ConfigChangeTypeWindowSnap 窗口吸附配置变更 - ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap" -) - -// ConfigChangeCallback 配置变更回调函数类型 -type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error - -// ConfigListener 配置监听器 -type ConfigListener struct { - ID string // 监听器唯一ID - Name string // 监听器名称 - ChangeType ConfigChangeType // 监听的配置变更类型 - Callback ConfigChangeCallback // 回调函数(现在包含新旧配置) - DebounceDelay time.Duration // 防抖延迟时间 - GetConfigFunc func(*koanf.Koanf) *models.AppConfig // 获取相关配置的函数 - - // 内部状态 - mu sync.RWMutex // 监听器状态锁 - timer *time.Timer // 防抖定时器 - lastConfigHash string // 上次配置的哈希值,用于变更检测 - lastConfig *models.AppConfig // 上次的配置副本 - ctx context.Context - cancel context.CancelFunc -} - -// ConfigNotificationService 配置通知服务 -type ConfigNotificationService struct { - listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map - mu sync.RWMutex // 监听器map的读写锁 - logger *log.LogService // 日志服务 - koanf *koanf.Koanf // koanf实例 - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup -} - -// NewConfigNotificationService 创建配置通知服务 -func NewConfigNotificationService(k *koanf.Koanf, logger *log.LogService) *ConfigNotificationService { - ctx, cancel := context.WithCancel(context.Background()) - return &ConfigNotificationService{ - listeners: make(map[ConfigChangeType][]*ConfigListener), - logger: logger, - koanf: k, - ctx: ctx, - cancel: cancel, - } -} - -// RegisterListener 注册配置监听器 -func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error { - // 生成唯一ID如果没有提供 - if listener.ID == "" { - listener.ID = fmt.Sprintf("%s_%d", listener.Name, time.Now().UnixNano()) - } - - // 初始化新监听器 - listener.ctx, listener.cancel = context.WithCancel(cns.ctx) - if err := cns.initializeListenerState(listener); err != nil { - listener.cancel() - return fmt.Errorf("failed to initialize listener state: %w", err) - } - - // 添加到监听器列表 - cns.mu.Lock() - cns.listeners[listener.ChangeType] = append(cns.listeners[listener.ChangeType], listener) - cns.mu.Unlock() - - return nil -} - -// initializeListenerState 初始化监听器状态 -func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigListener) error { - if listener.GetConfigFunc == nil { - return fmt.Errorf("GetConfigFunc is required") - } - - if config := listener.GetConfigFunc(cns.koanf); config != nil { - listener.mu.Lock() - listener.lastConfig = deepCopyConfigReflect(config) - listener.lastConfigHash = computeConfigHash(config) - listener.mu.Unlock() - } - - return nil -} - -// UnregisterListener 注销指定ID的配置监听器 -func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType, listenerID string) { - cns.mu.Lock() - defer cns.mu.Unlock() - - listeners := cns.listeners[changeType] - for i, listener := range listeners { - if listener.ID == listenerID { - // 取消监听器 - listener.cancel() - // 从切片中移除 - cns.listeners[changeType] = append(listeners[:i], listeners[i+1:]...) - break - } - } - - // 如果该类型没有监听器了,删除整个条目 - if len(cns.listeners[changeType]) == 0 { - delete(cns.listeners, changeType) - } -} - -// UnregisterAllListeners 注销指定类型的所有监听器 -func (cns *ConfigNotificationService) UnregisterAllListeners(changeType ConfigChangeType) { - cns.mu.Lock() - defer cns.mu.Unlock() - - if listeners, exists := cns.listeners[changeType]; exists { - for _, listener := range listeners { - listener.cancel() - } - delete(cns.listeners, changeType) - } -} - -// CheckConfigChanges 检查配置变更并通知相关监听器 -func (cns *ConfigNotificationService) CheckConfigChanges() { - cns.mu.RLock() - allListeners := make(map[ConfigChangeType][]*ConfigListener) - for changeType, listeners := range cns.listeners { - // 创建监听器切片的副本以避免并发访问问题 - listenersCopy := make([]*ConfigListener, len(listeners)) - copy(listenersCopy, listeners) - allListeners[changeType] = listenersCopy - } - cns.mu.RUnlock() - - // 检查所有监听器 - for _, listeners := range allListeners { - for _, listener := range listeners { - cns.checkAndNotify(listener) - } - } -} - -// checkAndNotify 检查配置变更并通知 -func (cns *ConfigNotificationService) checkAndNotify(listener *ConfigListener) { - if listener.GetConfigFunc == nil { - return - } - - currentConfig := listener.GetConfigFunc(cns.koanf) - - listener.mu.RLock() - lastHash := listener.lastConfigHash - lastConfig := listener.lastConfig - listener.mu.RUnlock() - - var hasChanges bool - var currentHash string - - if currentConfig != nil { - currentHash = computeConfigHash(currentConfig) - hasChanges = currentHash != lastHash - } else { - hasChanges = lastConfig != nil - } - - if hasChanges { - listener.mu.Lock() - listener.lastConfig = deepCopyConfigReflect(currentConfig) - listener.lastConfigHash = currentHash - listener.mu.Unlock() - - cns.debounceNotify(listener, lastConfig, currentConfig) - } -} - -// computeConfigHash 计算配置的哈希值 -func computeConfigHash(config *models.AppConfig) string { - if config == nil { - return "" - } - - jsonBytes, err := json.Marshal(config) - if err != nil { - return fmt.Sprintf("%p", config) - } - - hash := sha256.Sum256(jsonBytes) - return fmt.Sprintf("%x", hash) -} - -// deepCopyConfigReflect 使用反射实现高效深拷贝 -func deepCopyConfigReflect(src *models.AppConfig) *models.AppConfig { - if src == nil { - return nil - } - - // 使用反射进行深拷贝 - srcValue := reflect.ValueOf(src).Elem() - dstValue := reflect.New(srcValue.Type()).Elem() - - deepCopyValue(srcValue, dstValue) - - return dstValue.Addr().Interface().(*models.AppConfig) -} - -// deepCopyValue 递归深拷贝reflect.Value -func deepCopyValue(src, dst reflect.Value) { - switch src.Kind() { - case reflect.Ptr: - if src.IsNil() { - return - } - dst.Set(reflect.New(src.Elem().Type())) - deepCopyValue(src.Elem(), dst.Elem()) - - case reflect.Struct: - for i := 0; i < src.NumField(); i++ { - if dst.Field(i).CanSet() { - deepCopyValue(src.Field(i), dst.Field(i)) - } - } - - case reflect.Slice: - if src.IsNil() { - return - } - dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Cap())) - for i := 0; i < src.Len(); i++ { - deepCopyValue(src.Index(i), dst.Index(i)) - } - - case reflect.Map: - if src.IsNil() { - return - } - dst.Set(reflect.MakeMap(src.Type())) - for _, key := range src.MapKeys() { - srcValue := src.MapIndex(key) - dstValue := reflect.New(srcValue.Type()).Elem() - deepCopyValue(srcValue, dstValue) - dst.SetMapIndex(key, dstValue) - } - - case reflect.Interface: - if src.IsNil() { - return - } - srcValue := src.Elem() - dstValue := reflect.New(srcValue.Type()).Elem() - deepCopyValue(srcValue, dstValue) - dst.Set(dstValue) - - case reflect.Array: - for i := 0; i < src.Len(); i++ { - deepCopyValue(src.Index(i), dst.Index(i)) - } - - default: - // 对于基本类型和string,直接赋值 - if dst.CanSet() { - dst.Set(src) - } - } -} - -// debounceNotify 防抖通知 -func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) { - listener.mu.Lock() - defer listener.mu.Unlock() - - // 取消之前的定时器 - if listener.timer != nil { - listener.timer.Stop() - } - - // 创建配置副本,避免在闭包中持有原始引用 - oldConfigCopy := deepCopyConfigReflect(oldConfig) - newConfigCopy := deepCopyConfigReflect(newConfig) - - changeType := listener.ChangeType - - listener.timer = time.AfterFunc(listener.DebounceDelay, func() { - cns.executeCallback(listener.ctx, changeType, listener.Callback, oldConfigCopy, newConfigCopy) - }) -} - -// executeCallback 执行回调函数 -func (cns *ConfigNotificationService) executeCallback( - ctx context.Context, - changeType ConfigChangeType, - callback ConfigChangeCallback, - oldConfig, newConfig *models.AppConfig, -) { - cns.wg.Add(1) - go func() { - defer cns.wg.Done() - defer func() { - if r := recover(); r != nil { - cns.logger.Error("config callback panic", "error", r) - } - }() - - callbackCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - done := make(chan error, 1) - go func() { - done <- callback(changeType, oldConfig, newConfig) - }() - - select { - case <-callbackCtx.Done(): - cns.logger.Error("config callback timeout") - case err := <-done: - if err != nil { - cns.logger.Error("config callback error", "error", err) - } - } - }() -} - -// Cleanup 清理所有监听器 -func (cns *ConfigNotificationService) Cleanup() { - cns.cancel() // 取消所有context - - cns.mu.Lock() - for changeType, listeners := range cns.listeners { - for _, listener := range listeners { - listener.cancel() - } - delete(cns.listeners, changeType) - } - cns.mu.Unlock() - - cns.wg.Wait() // 等待所有协程完成 -} - -// GetListeners 获取指定类型的所有监听器 -func (cns *ConfigNotificationService) GetListeners(changeType ConfigChangeType) []*ConfigListener { - cns.mu.RLock() - defer cns.mu.RUnlock() - - listeners := cns.listeners[changeType] - result := make([]*ConfigListener, len(listeners)) - copy(result, listeners) - return result -} - -// CreateHotkeyListener 创建热键配置监听器 -func CreateHotkeyListener(name string, callback func(enable bool, hotkey *models.HotkeyCombo) error) *ConfigListener { - return &ConfigListener{ - Name: name, - ChangeType: ConfigChangeTypeHotkey, - Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { - if newConfig != nil { - return callback(newConfig.General.EnableGlobalHotkey, &newConfig.General.GlobalHotkey) - } - // 使用默认配置 - defaultConfig := models.NewDefaultAppConfig() - return callback(defaultConfig.General.EnableGlobalHotkey, &defaultConfig.General.GlobalHotkey) - }, - DebounceDelay: 200 * time.Millisecond, - GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig { - var config models.AppConfig - if err := k.Unmarshal("", &config); err != nil { - return nil - } - return &config - }, - } -} - -// CreateDataPathListener 创建数据路径配置监听器 -func CreateDataPathListener(name string, callback func() error) *ConfigListener { - return &ConfigListener{ - Name: name, - ChangeType: ConfigChangeTypeDataPath, - Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { - var oldPath, newPath string - - if oldConfig != nil { - oldPath = oldConfig.General.DataPath - } - - if newConfig != nil { - newPath = newConfig.General.DataPath - } else { - defaultConfig := models.NewDefaultAppConfig() - newPath = defaultConfig.General.DataPath - } - - if oldPath != newPath { - return callback() - } - return nil - }, - DebounceDelay: 100 * time.Millisecond, - GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig { - var config models.AppConfig - if err := k.Unmarshal("", &config); err != nil { - return nil - } - return &config - }, - } -} - -// CreateBackupConfigListener 创建备份配置监听器 -func CreateBackupConfigListener(name string, callback func(config *models.GitBackupConfig) error) *ConfigListener { - return &ConfigListener{ - Name: name, - ChangeType: ConfigChangeTypeBackup, - Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { - if newConfig == nil { - defaultConfig := models.NewDefaultAppConfig() - return callback(&defaultConfig.Backup) - } - return callback(&newConfig.Backup) - }, - DebounceDelay: 200 * time.Millisecond, - GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig { - var config models.AppConfig - if err := k.Unmarshal("", &config); err != nil { - return nil - } - return &config - }, - } -} - -// CreateWindowSnapConfigListener 创建窗口吸附配置监听器 -func CreateWindowSnapConfigListener(name string, callback func(enabled bool) error) *ConfigListener { - return &ConfigListener{ - Name: name, - ChangeType: ConfigChangeTypeWindowSnap, - Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { - if newConfig == nil { - defaultConfig := models.NewDefaultAppConfig() - return callback(defaultConfig.General.EnableWindowSnap) - } - return callback(newConfig.General.EnableWindowSnap) - }, - DebounceDelay: 200 * time.Millisecond, - GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig { - var config models.AppConfig - if err := k.Unmarshal("", &config); err != nil { - return nil - } - return &config - }, - } -} - -// ServiceShutdown 关闭服务 -func (cns *ConfigNotificationService) ServiceShutdown() error { - cns.Cleanup() - return nil -} diff --git a/internal/services/config_observer.go b/internal/services/config_observer.go new file mode 100644 index 0000000..d3224a4 --- /dev/null +++ b/internal/services/config_observer.go @@ -0,0 +1,243 @@ +package services + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/wailsapp/wails/v3/pkg/services/log" +) + +// ObserverCallback 观察者回调函数 +// 参数: +// - oldValue: 配置变更前的值 +// - newValue: 配置变更后的值 +type ObserverCallback func(oldValue, newValue interface{}) + +// CancelFunc 取消订阅函数 +// 调用此函数可以取消对配置的监听 +type CancelFunc func() + +// observer 内部观察者结构 +type observer struct { + id string // 唯一ID + path string // 监听的配置路径 + callback ObserverCallback // 回调函数 +} + +// ConfigObserver 配置观察者系统 +// 提供轻量级的配置变更监听机制 +type ConfigObserver struct { + observers map[string][]*observer // 路径 -> 观察者列表 + observerMu sync.RWMutex // 观察者锁 + nextObserverID atomic.Uint64 // 观察者ID生成器 + workerPool chan struct{} // Goroutine 池,限制并发数 + logger *log.LogService // 日志服务 + ctx context.Context // 全局 context + cancel context.CancelFunc // 取消函数 + wg sync.WaitGroup // 等待组,用于优雅关闭 +} + +// NewConfigObserver 创建新的配置观察者系统 +func NewConfigObserver(logger *log.LogService) *ConfigObserver { + ctx, cancel := context.WithCancel(context.Background()) + return &ConfigObserver{ + observers: make(map[string][]*observer), + workerPool: make(chan struct{}, 100), // 限制最多100个并发回调 + logger: logger, + ctx: ctx, + cancel: cancel, + } +} + +// Watch 注册配置变更监听器 +// 参数: +// - path: 配置路径,如 "general.enableGlobalHotkey" +// - callback: 变更回调函数,接收旧值和新值 +// +// 返回: +// - CancelFunc: 取消监听的函数,务必在不需要时调用以避免内存泄漏 +// +// 示例: +// +// cancel := observer.Watch("general.hotkey", func(old, new interface{}) { +// fmt.Printf("配置从 %v 变更为 %v\n", old, new) +// }) +// defer cancel() // 确保清理 +func (co *ConfigObserver) Watch(path string, callback ObserverCallback) CancelFunc { + // 生成唯一ID + id := fmt.Sprintf("obs_%d", co.nextObserverID.Add(1)) + + obs := &observer{ + id: id, + path: path, + callback: callback, + } + + // 添加到观察者列表 + co.observerMu.Lock() + co.observers[path] = append(co.observers[path], obs) + co.observerMu.Unlock() + + // 返回取消函数 + return func() { + co.removeObserver(path, id) + } +} + +// WatchWithContext 使用 Context 注册监听器,Context 取消时自动清理 +// 参数: +// - ctx: Context,取消时自动移除观察者 +// - path: 配置路径 +// - callback: 变更回调函数 +// +// 示例: +// +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// observer.WatchWithContext(ctx, "general.hotkey", callback) +// // Context 取消时自动清理 +func (co *ConfigObserver) WatchWithContext(ctx context.Context, path string, callback ObserverCallback) { + cancel := co.Watch(path, callback) + go func() { + select { + case <-ctx.Done(): + cancel() + case <-co.ctx.Done(): + return + } + }() +} + +// removeObserver 移除观察者 +func (co *ConfigObserver) removeObserver(path, id string) { + co.observerMu.Lock() + defer co.observerMu.Unlock() + + observers := co.observers[path] + for i, obs := range observers { + if obs.id == id { + // 从切片中移除 + co.observers[path] = append(observers[:i], observers[i+1:]...) + break + } + } + + // 如果没有观察者了,删除整个条目 + if len(co.observers[path]) == 0 { + delete(co.observers, path) + } +} + +// Notify 通知指定路径的所有观察者 +// 参数: +// - path: 配置路径 +// - oldValue: 旧值 +// - newValue: 新值 +// +// 注意:此方法会在独立的 goroutine 中异步执行回调,不会阻塞调用者 +func (co *ConfigObserver) Notify(path string, oldValue, newValue interface{}) { + // 获取该路径的所有观察者(拷贝以避免并发问题) + co.observerMu.RLock() + observers := co.observers[path] + if len(observers) == 0 { + co.observerMu.RUnlock() + return + } + + // 拷贝观察者列表 + callbacks := make([]ObserverCallback, len(observers)) + for i, obs := range observers { + callbacks[i] = obs.callback + } + co.observerMu.RUnlock() + + // 在独立 goroutine 中执行回调 + for _, callback := range callbacks { + co.executeCallback(callback, oldValue, newValue) + } +} + +// NotifyAll 通知所有匹配前缀的观察者 +func (co *ConfigObserver) NotifyAll(changes map[string]struct { + OldValue interface{} + NewValue interface{} +}) { + for path, change := range changes { + co.Notify(path, change.OldValue, change.NewValue) + } +} + +// executeCallback 执行回调函数 +func (co *ConfigObserver) executeCallback(callback ObserverCallback, oldValue, newValue interface{}) { + co.wg.Add(1) + + // 获取 worker(限制并发数) + select { + case co.workerPool <- struct{}{}: + // 成功获取 worker + case <-co.ctx.Done(): + // 系统正在关闭 + co.wg.Done() + return + } + + go func() { + defer co.wg.Done() + defer func() { <-co.workerPool }() // 释放 worker + + // Panic 恢复 + defer func() { + recover() + }() + + // 创建带超时的 context + ctx, cancel := context.WithTimeout(co.ctx, 5*time.Second) + defer cancel() + + // 在 channel 中执行回调,以便可以超时控制 + done := make(chan struct{}) + go func() { + defer close(done) + callback(oldValue, newValue) + }() + + // 等待完成或超时 + select { + case <-done: + // 正常完成 + case <-ctx.Done(): + } + }() +} + +// Clear 清空所有观察者 +func (co *ConfigObserver) Clear() { + co.observerMu.Lock() + co.observers = make(map[string][]*observer) + co.observerMu.Unlock() + +} + +// Shutdown 关闭观察者系统 +// 等待所有正在执行的回调完成 +func (co *ConfigObserver) Shutdown() { + // 取消 context + co.cancel() + + // 等待所有回调完成 + done := make(chan struct{}) + go func() { + co.wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(10 * time.Second): + } + // 清空所有观察者 + co.Clear() +} diff --git a/internal/services/config_service.go b/internal/services/config_service.go index a516311..a315fbc 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -1,10 +1,12 @@ package services import ( - "errors" + "context" + "encoding/json" "fmt" "os" "path/filepath" + "strings" "sync" "time" "voidraft/internal/models" @@ -25,36 +27,12 @@ type ConfigService struct { mu sync.RWMutex // 读写锁 fileProvider *file.File // 文件提供器,用于监听 - // 配置通知服务 - notificationService *ConfigNotificationService + observer *ConfigObserver // 配置迁移器 configMigrator *ConfigMigrator } -// ConfigError 配置错误 -type ConfigError struct { - Operation string // 操作名称 - Err error // 原始错误 -} - -// Error 实现error接口 -func (e *ConfigError) Error() string { - return fmt.Sprintf("config error during %s: %v", e.Operation, e.Err) -} - -// Unwrap 获取原始错误 -func (e *ConfigError) Unwrap() error { - return e.Err -} - -// Is 实现错误匹配 -func (e *ConfigError) Is(target error) bool { - var configError *ConfigError - ok := errors.As(target, &configError) - return ok -} - // NewConfigService 创建新的配置服务实例 func NewConfigService(logger *log.LogService) *ConfigService { // 获取用户主目录 @@ -74,8 +52,8 @@ func NewConfigService(logger *log.LogService) *ConfigService { koanf: koanf.New("."), } - // 初始化配置通知服务 - cs.notificationService = NewConfigNotificationService(cs.koanf, logger) + // 初始化配置观察者系统 + cs.observer = NewConfigObserver(logger) // 初始化配置迁移器 cs.configMigrator = NewConfigMigrator(logger, configDir, "settings", settingsPath) @@ -93,7 +71,7 @@ func (cs *ConfigService) setDefaults() error { defaultConfig := models.NewDefaultAppConfig() if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil { - return &ConfigError{Operation: "load_defaults", Err: err} + return err } return nil @@ -112,7 +90,7 @@ func (cs *ConfigService) initConfig() error { // 配置文件存在,直接加载现有配置 cs.fileProvider = file.Provider(cs.settingsPath) if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil { - return &ConfigError{Operation: "load_config_file", Err: err} + return err } return nil @@ -145,7 +123,7 @@ func (cs *ConfigService) MigrateConfig() error { func (cs *ConfigService) createDefaultConfig() error { // 确保配置目录存在 if err := os.MkdirAll(cs.configDir, 0755); err != nil { - return &ConfigError{Operation: "create_config_dir", Err: err} + return err } if err := cs.setDefaults(); err != nil { @@ -160,7 +138,7 @@ func (cs *ConfigService) createDefaultConfig() error { cs.fileProvider = file.Provider(cs.settingsPath) if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil { - return &ConfigError{Operation: "load_config_file", Err: err} + return err } return nil } @@ -170,21 +148,20 @@ func (cs *ConfigService) startWatching() { if cs.fileProvider == nil { return } - err := cs.fileProvider.Watch(func(event interface{}, err error) { + cs.fileProvider.Watch(func(event interface{}, err error) { if err != nil { return } - cs.koanf.Load(cs.fileProvider, jsonparser.Parser()) - // 使用配置通知服务检查所有已注册的配置变更 - if cs.notificationService != nil { - cs.notificationService.CheckConfigChanges() - } + cs.mu.Lock() + oldSnapshot := cs.createConfigSnapshot() + cs.koanf.Load(cs.fileProvider, jsonparser.Parser()) + cs.mu.Unlock() + + // 检测配置变更并通知观察者 + cs.detectAndNotifyChanges(oldSnapshot) }) - if err != nil { - cs.logger.Error("Failed to setup config file watcher", "error", err) - } } // stopWatching 停止配置文件监听 @@ -201,7 +178,7 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) { var config models.AppConfig if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil { - return nil, &ConfigError{Operation: "unmarshal_config", Err: err} + return nil, err } return &config, nil @@ -210,7 +187,9 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) { // Set 设置配置项 func (cs *ConfigService) Set(key string, value interface{}) error { cs.mu.Lock() - defer cs.mu.Unlock() + + // 获取旧值 + oldValue := cs.koanf.Get(key) // 设置值到koanf cs.koanf.Set(key, value) @@ -219,7 +198,18 @@ func (cs *ConfigService) Set(key string, value interface{}) error { cs.koanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339)) // 将配置写回文件 - return cs.writeConfigToFile() + err := cs.writeConfigToFile() + cs.mu.Unlock() + + if err != nil { + return err + } + + if cs.observer != nil { + cs.observer.Notify(key, oldValue, value) + } + + return nil } // Get 获取配置项 @@ -232,7 +222,9 @@ func (cs *ConfigService) Get(key string) interface{} { // ResetConfig 强制重置所有配置为默认值 func (cs *ConfigService) ResetConfig() error { cs.mu.Lock() - defer cs.mu.Unlock() + + // 保存旧配置快照 + oldSnapshot := cs.createConfigSnapshot() // 停止文件监听 if cs.fileProvider != nil { @@ -242,12 +234,14 @@ func (cs *ConfigService) ResetConfig() error { // 设置默认配置 if err := cs.setDefaults(); err != nil { - return &ConfigError{Operation: "reset_set_defaults", Err: err} + cs.mu.Unlock() + return err } // 写入配置文件 if err := cs.writeConfigToFile(); err != nil { - return &ConfigError{Operation: "reset_write_config", Err: err} + cs.mu.Unlock() + return err } // 重新创建koanf实例 @@ -255,7 +249,8 @@ func (cs *ConfigService) ResetConfig() error { // 重新加载默认配置到koanf if err := cs.setDefaults(); err != nil { - return &ConfigError{Operation: "reset_reload_defaults", Err: err} + cs.mu.Unlock() + return err } // 重新创建文件提供器 @@ -263,16 +258,17 @@ func (cs *ConfigService) ResetConfig() error { // 重新加载配置文件 if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil { - return &ConfigError{Operation: "reset_reload_config", Err: err} + cs.mu.Unlock() + return err } + cs.mu.Unlock() + // 重新启动文件监听 cs.startWatching() - // 手动触发配置变更检查,确保通知系统能感知到变更 - if cs.notificationService != nil { - cs.notificationService.CheckConfigChanges() - } + // 检测配置变更并通知观察者 + cs.detectAndNotifyChanges(oldSnapshot) return nil } @@ -281,71 +277,116 @@ func (cs *ConfigService) ResetConfig() error { func (cs *ConfigService) writeConfigToFile() error { configBytes, err := cs.koanf.Marshal(jsonparser.Parser()) if err != nil { - return &ConfigError{Operation: "marshal_config", Err: err} + return err } if err := os.WriteFile(cs.settingsPath, configBytes, 0644); err != nil { - return &ConfigError{Operation: "write_config_file", Err: err} + return err } return nil } -// SetHotkeyChangeCallback 设置热键配置变更回调 -func (cs *ConfigService) SetHotkeyChangeCallback(callback func(enable bool, hotkey *models.HotkeyCombo) error) error { - cs.mu.Lock() - defer cs.mu.Unlock() - - // 创建热键监听器并注册 - hotkeyListener := CreateHotkeyListener("DefaultHotkeyListener", callback) - return cs.notificationService.RegisterListener(hotkeyListener) +// Watch 注册配置变更监听器 +func (cs *ConfigService) Watch(path string, callback ObserverCallback) CancelFunc { + return cs.observer.Watch(path, callback) } -// SetDataPathChangeCallback 设置数据路径配置变更回调 -func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error { - cs.mu.Lock() - defer cs.mu.Unlock() - - // 创建数据路径监听器并注册 - dataPathListener := CreateDataPathListener("DefaultDataPathListener", callback) - return cs.notificationService.RegisterListener(dataPathListener) +// WatchWithContext 使用 Context 注册监听器 +func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback ObserverCallback) { + cs.observer.WatchWithContext(ctx, path, callback) } -// SetBackupConfigChangeCallback 设置备份配置变更回调 -func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *models.GitBackupConfig) error) error { - cs.mu.Lock() - defer cs.mu.Unlock() +// createConfigSnapshot 创建当前配置的快照 +func (cs *ConfigService) createConfigSnapshot() map[string]interface{} { + cs.mu.RLock() + defer cs.mu.RUnlock() + snapshot := make(map[string]interface{}) + allKeys := cs.koanf.All() - // 创建备份配置监听器并注册 - backupListener := CreateBackupConfigListener("DefaultBackupConfigListener", callback) - return cs.notificationService.RegisterListener(backupListener) + // 递归展平配置 + flattenMap("", allKeys, snapshot) + return snapshot } -// SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调 -func (cs *ConfigService) SetWindowSnapConfigChangeCallback(callback func(enabled bool) error) error { - cs.mu.Lock() - defer cs.mu.Unlock() +// flattenMap 递归展平嵌套的 map(使用 strings.Builder 优化字符串拼接) +func flattenMap(prefix string, data map[string]interface{}, result map[string]interface{}) { + var builder strings.Builder + for key, value := range data { + builder.Reset() + if prefix != "" { + builder.WriteString(prefix) + builder.WriteString(".") + } + builder.WriteString(key) + fullKey := builder.String() - // 创建窗口吸附配置监听器并注册 - windowSnapListener := CreateWindowSnapConfigListener("DefaultWindowSnapConfigListener", callback) - return cs.notificationService.RegisterListener(windowSnapListener) + if valueMap, ok := value.(map[string]interface{}); ok { + // 递归处理嵌套 map + flattenMap(fullKey, valueMap, result) + } else { + // 保存叶子节点 + result[fullKey] = value + } + } +} + +// detectAndNotifyChanges 检测配置变更并通知观察者 +func (cs *ConfigService) detectAndNotifyChanges(oldSnapshot map[string]interface{}) { + // 创建新快照 + newSnapshot := cs.createConfigSnapshot() + + // 检测变更 + changes := make(map[string]struct { + OldValue interface{} + NewValue interface{} + }) + + // 检查新增和修改的键 + for key, newValue := range newSnapshot { + oldValue, exists := oldSnapshot[key] + if !exists || !isEqual(oldValue, newValue) { + changes[key] = struct { + OldValue interface{} + NewValue interface{} + }{ + OldValue: oldValue, + NewValue: newValue, + } + } + } + + // 检查删除的键 + for key, oldValue := range oldSnapshot { + if _, exists := newSnapshot[key]; !exists { + changes[key] = struct { + OldValue interface{} + NewValue interface{} + }{ + OldValue: oldValue, + NewValue: nil, + } + } + } + + // 通知所有变更 + if cs.observer != nil && len(changes) > 0 { + cs.observer.NotifyAll(changes) + } +} + +// isEqual 值相等比较 +func isEqual(a, b interface{}) bool { + aJSON, _ := json.Marshal(a) + bJSON, _ := json.Marshal(b) + return string(aJSON) == string(bJSON) } // ServiceShutdown 关闭服务 func (cs *ConfigService) ServiceShutdown() error { cs.stopWatching() - if cs.notificationService != nil { - cs.notificationService.Cleanup() + if cs.observer != nil { + cs.observer.Shutdown() } return nil } - -// GetConfigDir 获取配置目录 -func (cs *ConfigService) GetConfigDir() string { - return cs.configDir -} - -// GetSettingsPath 获取设置文件路径 -func (cs *ConfigService) GetSettingsPath() string { - return cs.settingsPath -} diff --git a/internal/services/database_service.go b/internal/services/database_service.go index 963c0bc..bc9d074 100644 --- a/internal/services/database_service.go +++ b/internal/services/database_service.go @@ -96,6 +96,9 @@ type DatabaseService struct { mu sync.RWMutex ctx context.Context tableModels []TableModel // 注册的表模型 + + // 配置观察者取消函数 + cancelObserver CancelFunc } // NewDatabaseService creates a new database service @@ -130,9 +133,29 @@ func (ds *DatabaseService) registerAllModels() { // ServiceStartup initializes the service when the application starts func (ds *DatabaseService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { ds.ctx = ctx + + ds.cancelObserver = ds.configService.Watch("general.dataPath", ds.onDataPathChange) + return ds.initDatabase() } +// onDataPathChange 数据路径配置变更回调 +func (ds *DatabaseService) onDataPathChange(oldValue, newValue interface{}) { + oldPath := "" + newPath := "" + + if oldValue != nil { + oldPath = fmt.Sprintf("%v", oldValue) + } + if newValue != nil { + newPath = fmt.Sprintf("%v", newValue) + } + + if oldPath != newPath { + _ = ds.OnDataPathChanged() + } +} + // initDatabase initializes the SQLite database func (ds *DatabaseService) initDatabase() error { dbPath, err := ds.getDatabasePath() @@ -369,6 +392,11 @@ func getSQLTypeAndDefault(t reflect.Type) (string, string) { // ServiceShutdown shuts down the service when the application closes func (ds *DatabaseService) ServiceShutdown() error { + // 取消配置观察者 + if ds.cancelObserver != nil { + ds.cancelObserver() + } + if ds.db != nil { return ds.db.Close() } diff --git a/internal/services/hotkey_service.go b/internal/services/hotkey_service.go index 55f59ed..1f0560f 100644 --- a/internal/services/hotkey_service.go +++ b/internal/services/hotkey_service.go @@ -25,13 +25,14 @@ type HotkeyService struct { app *application.App registered atomic.Bool - hk *hotkey.Hotkey - stopChan chan struct{} - wg sync.WaitGroup + hk *hotkey.Hotkey + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + isShutdown atomic.Bool - // 防抖相关 - lastTriggerTime atomic.Int64 // 上次触发时间(Unix 纳秒) - debounceInterval time.Duration // 防抖间隔 + // 配置观察者取消函数 + cancelObservers []CancelFunc } // NewHotkeyService 创建热键服务实例 @@ -40,12 +41,13 @@ func NewHotkeyService(configService *ConfigService, logger *log.LogService) *Hot logger = log.New() } + ctx, cancel := context.WithCancel(context.Background()) return &HotkeyService{ - logger: logger, - configService: configService, - windowHelper: NewWindowHelper(), - stopChan: make(chan struct{}), - debounceInterval: 100 * time.Millisecond, + logger: logger, + configService: configService, + windowHelper: NewWindowHelper(), + ctx: ctx, + cancel: cancel, } } @@ -57,50 +59,69 @@ func (hs *HotkeyService) ServiceStartup(ctx context.Context, options application // Initialize 初始化热键服务 func (hs *HotkeyService) Initialize() error { + // 注册配置监听 + hs.cancelObservers = []CancelFunc{ + hs.configService.Watch("general.enableGlobalHotkey", hs.onHotkeyConfigChange), + hs.configService.Watch("general.globalHotkey", hs.onHotkeyConfigChange), + } + + // 加载初始配置 config, err := hs.configService.GetConfig() if err != nil { return fmt.Errorf("load config: %w", err) } if config.General.EnableGlobalHotkey { - if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil { - return err - } + _ = hs.RegisterHotkey(&config.General.GlobalHotkey) } return nil } +// onHotkeyConfigChange 热键配置变更回调 +func (hs *HotkeyService) onHotkeyConfigChange(oldValue, newValue interface{}) { + // 重新加载配置 + config, err := hs.configService.GetConfig() + if err != nil { + return + } + + // 更新热键 + _ = hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey) +} + // RegisterHotkey 注册全局热键 func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error { + if hs.isShutdown.Load() { + return errors.New("service is shutdown") + } + if !hs.isValidHotkey(combo) { return errors.New("invalid hotkey combination") } + // 如果已注册,先取消 + if hs.registered.Load() { + _ = hs.UnregisterHotkey() + } + // 转换为 hotkey 库的格式 key, mods, err := hs.convertHotkey(combo) if err != nil { return fmt.Errorf("convert hotkey: %w", err) } - // 创建新热键(在锁外创建,避免持锁时间过长) - newHk := hotkey.New(mods, key) - if err := newHk.Register(); err != nil { + hs.mu.Lock() + // 创建新的热键实例 + hs.hk = hotkey.New(mods, key) + if err := hs.hk.Register(); err != nil { + hs.mu.Unlock() return fmt.Errorf("register hotkey: %w", err) } - hs.mu.Lock() - defer hs.mu.Unlock() - - // 如果已注册,先取消旧热键 - if hs.registered.Load() { - hs.unregisterInternal() - } - - // 设置新热键 - hs.hk = newHk hs.registered.Store(true) hs.currentHotkey = combo + hs.mu.Unlock() // 启动监听 goroutine hs.wg.Add(1) @@ -111,36 +132,42 @@ func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error { // UnregisterHotkey 取消注册全局热键 func (hs *HotkeyService) UnregisterHotkey() error { - hs.mu.Lock() - defer hs.mu.Unlock() - return hs.unregisterInternal() -} - -// unregisterInternal 内部取消注册 -func (hs *HotkeyService) unregisterInternal() error { if !hs.registered.Load() { return nil } - // 停止监听 - close(hs.stopChan) - - // 取消注册热键 - if hs.hk != nil { - if err := hs.hk.Unregister(); err != nil { - hs.logger.Error("failed to unregister hotkey", "error", err) - } - hs.hk = nil - } - - // 等待 goroutine 结束 - hs.wg.Wait() - - hs.currentHotkey = nil + // 先标记为未注册 hs.registered.Store(false) - // 重新创建 stopChan - hs.stopChan = make(chan struct{}) + // 获取热键实例的引用 + hs.mu.RLock() + hk := hs.hk + hs.mu.RUnlock() + + if hk == nil { + return nil + } + + // 调用 Close() 确保完全清理 + _ = hk.Close() + + // 等待监听 goroutine 退出 + done := make(chan struct{}) + go func() { + hs.wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + } + + // 清理状态 + hs.mu.Lock() + hs.hk = nil + hs.currentHotkey = nil + hs.mu.Unlock() return nil } @@ -157,26 +184,28 @@ func (hs *HotkeyService) UpdateHotkey(enable bool, combo *models.HotkeyCombo) er func (hs *HotkeyService) listenHotkey() { defer hs.wg.Done() - // 缓存 channel 引用,避免每次循环都访问 hs.hk + // 获取热键实例和通道 hs.mu.RLock() - keydownChan := hs.hk.Keydown() + hk := hs.hk hs.mu.RUnlock() + if hk == nil { + return + } + + keydownChan := hk.Keydown() + for { select { - case <-hs.stopChan: + case <-hs.ctx.Done(): return - case <-keydownChan: - now := time.Now().UnixNano() - lastTrigger := hs.lastTriggerTime.Load() - - // 如果距离上次触发时间小于防抖间隔,忽略此次触发 - if lastTrigger > 0 && time.Duration(now-lastTrigger) < hs.debounceInterval { - continue + case _, ok := <-keydownChan: + if !ok { + return + } + if hs.registered.Load() { + hs.toggleWindow() } - // 更新最后触发时间 - hs.lastTriggerTime.Store(now) - hs.toggleWindow() } } } @@ -328,5 +357,18 @@ func (hs *HotkeyService) IsRegistered() bool { // ServiceShutdown 关闭服务 func (hs *HotkeyService) ServiceShutdown() error { + hs.isShutdown.Store(true) + + // 取消配置观察者 + for _, cancel := range hs.cancelObservers { + if cancel != nil { + cancel() + } + } + + // 取消 context + hs.cancel() + + // 取消注册热键 return hs.UnregisterHotkey() } diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index b8b96aa..1aac6a3 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -1,8 +1,6 @@ package services import ( - "voidraft/internal/models" - "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/dock" "github.com/wailsapp/wails/v3/pkg/services/log" @@ -103,38 +101,6 @@ func NewServiceManager() *ServiceManager { // 初始化测试服务(开发环境使用) testService := NewTestService(badgeService, notificationService, logger) - // 使用新的配置通知系统设置热键配置变更监听 - err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error { - return hotkeyService.UpdateHotkey(enable, hotkey) - }) - if err != nil { - panic(err) - } - - // 设置数据路径变更监听,处理配置重置和路径变更 - err = configService.SetDataPathChangeCallback(func() error { - return databaseService.OnDataPathChanged() - }) - if err != nil { - panic(err) - } - - // 设置备份配置变更监听,处理备份配置变更 - err = configService.SetBackupConfigChangeCallback(func(config *models.GitBackupConfig) error { - return backupService.HandleConfigChange(config) - }) - if err != nil { - panic(err) - } - - // 设置窗口吸附配置变更回调 - err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool) error { - return windowSnapService.OnWindowSnapConfigChanged(enabled) - }) - if err != nil { - panic(err) - } - return &ServiceManager{ configService: configService, databaseService: databaseService, diff --git a/internal/services/window_snap_service.go b/internal/services/window_snap_service.go index fa2c7cc..d19b402 100644 --- a/internal/services/window_snap_service.go +++ b/internal/services/window_snap_service.go @@ -53,6 +53,9 @@ type WindowSnapService struct { // 事件监听器清理函数 mainMoveUnhook func() // 主窗口移动监听清理函数 windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数 + + // 配置观察者取消函数 + cancelObserver CancelFunc } // NewWindowSnapService 创建新的窗口吸附服务实例 @@ -69,7 +72,7 @@ func NewWindowSnapService(logger *log.LogService, configService *ConfigService) snapEnabled = config.General.EnableWindowSnap } - return &WindowSnapService{ + wss := &WindowSnapService{ logger: logger, configService: configService, windowHelper: NewWindowHelper(), @@ -83,6 +86,23 @@ func NewWindowSnapService(logger *log.LogService, configService *ConfigService) isUpdatingPosition: make(map[int64]bool), windowMoveUnhooks: make(map[int64]func()), } + + // 注册窗口吸附配置监听 + wss.cancelObserver = configService.Watch("general.enableWindowSnap", wss.onWindowSnapConfigChange) + + return wss +} + +// onWindowSnapConfigChange 窗口吸附配置变更回调 +func (wss *WindowSnapService) onWindowSnapConfigChange(oldValue, newValue interface{}) { + enabled := false + if newValue != nil { + if val, ok := newValue.(bool); ok { + enabled = val + } + } + + _ = wss.OnWindowSnapConfigChanged(enabled) } // RegisterWindow 注册需要吸附管理的窗口 @@ -194,7 +214,6 @@ func (wss *WindowSnapService) GetCurrentThreshold() int { // OnWindowSnapConfigChanged 处理窗口吸附配置变更 func (wss *WindowSnapService) OnWindowSnapConfigChanged(enabled bool) error { wss.SetSnapEnabled(enabled) - // 阈值现在是自适应的,无需手动设置 return nil } @@ -625,6 +644,10 @@ func (wss *WindowSnapService) Cleanup() { // ServiceShutdown 实现服务关闭接口 func (wss *WindowSnapService) ServiceShutdown() error { + // 取消配置观察者 + if wss.cancelObserver != nil { + wss.cancelObserver() + } wss.Cleanup() return nil }