package state import ( "encoding/json" "os" "path/filepath" "sync" "time" ) // AppState holds persistent client state. type AppState struct { SelectedServer string `json:"selected_server"` SelectedMode string `json:"selected_mode"` LastSync time.Time `json:"last_sync"` AutoConnect bool `json:"auto_connect"` LocalProxyPort int `json:"local_proxy_port,omitempty"` EnabledRuleSets map[string]bool `json:"enabled_rule_sets,omitempty"` CustomBypass []string `json:"custom_bypass_processes,omitempty"` RecommendedServer string `json:"recommended_server,omitempty"` RecommendedNodeID string `json:"recommended_node_id,omitempty"` LastRecommendation time.Time `json:"last_recommendation,omitempty"` RecommendationFetched bool `json:"recommendation_fetched,omitempty"` // true if recommendation was ever fetched } // Store manages persistent state on disk. type Store struct { mu sync.Mutex path string dataDir string data AppState } // NewStore creates a state store at the given path. func NewStore(dataDir string) *Store { return &Store{ path: filepath.Join(dataDir, "state.json"), dataDir: dataDir, data: AppState{ SelectedMode: "Комбо (приложения + Re-filter)", AutoConnect: false, }, } } // DataDir returns the data directory path. func (s *Store) DataDir() string { s.mu.Lock() defer s.mu.Unlock() return s.dataDir } // Load reads state from disk. Returns default state if file doesn't exist. func (s *Store) Load() error { s.mu.Lock() defer s.mu.Unlock() data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return nil } return err } return json.Unmarshal(data, &s.data) } // Save writes state to disk. func (s *Store) Save() error { s.mu.Lock() defer s.mu.Unlock() if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return err } data, err := json.MarshalIndent(s.data, "", " ") if err != nil { return err } return os.WriteFile(s.path, data, 0o644) } // Get returns a copy of the current state. func (s *Store) Get() AppState { s.mu.Lock() defer s.mu.Unlock() return s.data } // SetServer updates the selected server. func (s *Store) SetServer(tag string) { s.mu.Lock() s.data.SelectedServer = tag s.mu.Unlock() } // SetMode updates the selected routing mode. func (s *Store) SetMode(mode string) { s.mu.Lock() s.data.SelectedMode = mode s.mu.Unlock() } // SetLastSync records the last sync time. func (s *Store) SetLastSync(t time.Time) { s.mu.Lock() s.data.LastSync = t s.mu.Unlock() } // SetAutoConnect updates the auto-connect setting. func (s *Store) SetAutoConnect(v bool) { s.mu.Lock() s.data.AutoConnect = v s.mu.Unlock() } func (s *Store) SetLocalProxyPort(port int) { s.mu.Lock() s.data.LocalProxyPort = port s.mu.Unlock() } // SetRuleSetEnabled enables/disables an optional rule-set. func (s *Store) SetRuleSetEnabled(tag string, enabled bool) { s.mu.Lock() if s.data.EnabledRuleSets == nil { s.data.EnabledRuleSets = make(map[string]bool) } s.data.EnabledRuleSets[tag] = enabled s.mu.Unlock() } // IsRuleSetEnabled checks if a rule-set is enabled. func (s *Store) IsRuleSetEnabled(tag string) bool { s.mu.Lock() defer s.mu.Unlock() if s.data.EnabledRuleSets == nil { return false } return s.data.EnabledRuleSets[tag] } // SetCustomBypass sets custom bypass processes. func (s *Store) SetCustomBypass(processes []string) { s.mu.Lock() s.data.CustomBypass = processes s.mu.Unlock() } // GetCustomBypass returns custom bypass processes. func (s *Store) GetCustomBypass() []string { s.mu.Lock() defer s.mu.Unlock() return append([]string{}, s.data.CustomBypass...) } // SetRecommendation saves the server recommendation. func (s *Store) SetRecommendation(serverIP, nodeID string) { s.mu.Lock() s.data.RecommendedServer = serverIP s.data.RecommendedNodeID = nodeID s.data.LastRecommendation = time.Now() s.data.RecommendationFetched = true s.mu.Unlock() } // GetRecommendation returns the current recommendation. func (s *Store) GetRecommendation() (serverIP, nodeID string, at time.Time) { s.mu.Lock() defer s.mu.Unlock() return s.data.RecommendedServer, s.data.RecommendedNodeID, s.data.LastRecommendation }