using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Views.Components; namespace LanMountainDesktop.ViewModels; public sealed partial class WeatherSettingsPageViewModel : ViewModelBase { private readonly ISettingsFacadeService _settingsFacade; private readonly LocalizationService _localizationService; private readonly ILocationService _locationService; private readonly WeatherLocationRefreshService _weatherLocationRefreshService; private string _languageCode; private bool _isInitializing; public WeatherSettingsPageViewModel( ISettingsFacadeService settingsFacade, LocalizationService localizationService, ILocationService locationService, WeatherLocationRefreshService weatherLocationRefreshService, bool enableStartupPreviewRefresh = true) { _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _locationService = locationService ?? throw new ArgumentNullException(nameof(locationService)); _weatherLocationRefreshService = weatherLocationRefreshService ?? throw new ArgumentNullException(nameof(weatherLocationRefreshService)); var regionState = _settingsFacade.Region.Get(); _languageCode = _localizationService.NormalizeLanguageCode(regionState.LanguageCode); RefreshLocalizedText(); LocationModes = CreateLocationModes(); var weatherState = _settingsFacade.Weather.Get(); _isInitializing = true; ApplySavedState(weatherState, save: false); _isInitializing = false; IsLocationSupported = _locationService.IsSupported; UpdateModeVisibility(); UpdateCurrentLocationSummary(); LocationActionStatus = IsLocationSupported ? LocationReadyText : LocationUnsupportedText; if (enableStartupPreviewRefresh) { _ = RefreshPreviewAsync(); } } public IReadOnlyList LocationModes { get; } public ObservableCollection SearchResults { get; } = []; [ObservableProperty] private string _pageTitle = string.Empty; [ObservableProperty] private string _pageDescription = string.Empty; [ObservableProperty] private string _previewHeader = string.Empty; [ObservableProperty] private string _previewDescription = string.Empty; [ObservableProperty] private string _locationSourceHeader = string.Empty; [ObservableProperty] private string _locationSourceDescription = string.Empty; [ObservableProperty] private string _citySearchHeader = string.Empty; [ObservableProperty] private string _citySearchDescription = string.Empty; [ObservableProperty] private string _coordinatesHeader = string.Empty; [ObservableProperty] private string _coordinatesDescription = string.Empty; [ObservableProperty] private string _locationServicesHeader = string.Empty; [ObservableProperty] private string _locationServicesDescription = string.Empty; [ObservableProperty] private string _alertFilterHeader = string.Empty; [ObservableProperty] private string _alertFilterDescription = string.Empty; [ObservableProperty] private string _requestHeader = string.Empty; [ObservableProperty] private string _requestDescription = string.Empty; [ObservableProperty] private string _searchPlaceholder = string.Empty; [ObservableProperty] private string _searchButtonText = string.Empty; [ObservableProperty] private string _applyCityButtonText = string.Empty; [ObservableProperty] private string _refreshButtonText = string.Empty; [ObservableProperty] private string _applyCoordinatesButtonText = string.Empty; [ObservableProperty] private string _useCurrentLocationButtonText = string.Empty; [ObservableProperty] private string _autoRefreshLabel = string.Empty; [ObservableProperty] private string _latitudeLabel = string.Empty; [ObservableProperty] private string _longitudeLabel = string.Empty; [ObservableProperty] private string _locationKeyPlaceholder = string.Empty; [ObservableProperty] private string _locationNamePlaceholder = string.Empty; [ObservableProperty] private string _noTlsToggleText = string.Empty; [ObservableProperty] private string _locationUnsupportedText = string.Empty; [ObservableProperty] private string _locationReadyText = string.Empty; [ObservableProperty] private string _locationRefreshingText = string.Empty; [ObservableProperty] private string _footerHint = string.Empty; [ObservableProperty] private SelectionOption _selectedLocationMode = new("CitySearch", "City Search"); [ObservableProperty] private bool _isCitySearchMode = true; [ObservableProperty] private bool _isCoordinatesMode; [ObservableProperty] private bool _isLocationSupported; [ObservableProperty] private string _searchKeyword = string.Empty; [ObservableProperty] private WeatherLocation? _selectedSearchResult; [ObservableProperty] private string _searchStatus = string.Empty; [ObservableProperty] private bool _isSearching; [ObservableProperty] private double _latitude; [ObservableProperty] private double _longitude; [ObservableProperty] private string _locationKey = string.Empty; [ObservableProperty] private string _locationName = string.Empty; [ObservableProperty] private bool _autoRefreshLocation; [ObservableProperty] private string _excludedAlerts = string.Empty; [ObservableProperty] private bool _noTlsRequests; [ObservableProperty] private string _currentLocationSummary = string.Empty; [ObservableProperty] private string _locationActionStatus = string.Empty; [ObservableProperty] private bool _isRefreshingLocation; [ObservableProperty] private bool _isRefreshingPreview; [ObservableProperty] private IImage? _previewIcon; [ObservableProperty] private string _previewLocation = string.Empty; [ObservableProperty] private string _previewTemperature = string.Empty; [ObservableProperty] private string _previewCondition = string.Empty; [ObservableProperty] private string _previewUpdated = string.Empty; [ObservableProperty] private string _previewStatus = string.Empty; partial void OnSelectedLocationModeChanged(SelectionOption value) { UpdateModeVisibility(); UpdateCurrentLocationSummary(); if (_isInitializing || value is null) { return; } _settingsFacade.Weather.Save(CreateEditableState(value.Value)); _ = RefreshPreviewAsync(); } partial void OnAutoRefreshLocationChanged(bool value) { _ = value; if (_isInitializing) { return; } _settingsFacade.Weather.Save(CreateEditableState()); } partial void OnExcludedAlertsChanged(string value) { _ = value; if (_isInitializing) { return; } _settingsFacade.Weather.Save(CreateEditableState()); } partial void OnNoTlsRequestsChanged(bool value) { _ = value; if (_isInitializing) { return; } _settingsFacade.Weather.Save(CreateEditableState()); } [RelayCommand] private async Task SearchAsync() { SearchStatus = string.Empty; SearchResults.Clear(); SelectedSearchResult = null; if (string.IsNullOrWhiteSpace(SearchKeyword)) { SearchStatus = L("settings.weather.search_required", "Please enter a city keyword first."); return; } IsSearching = true; try { var result = await _settingsFacade.Weather.SearchLocationsAsync( SearchKeyword.Trim(), NormalizeWeatherLocale(_languageCode)); if (!result.Success) { SearchStatus = string.Format( ResolveCulture(), L("settings.weather.search_failed_format", "Search failed: {0}"), result.ErrorMessage ?? result.ErrorCode ?? L("settings.weather.preview_unknown", "Unknown")); return; } foreach (var item in result.Data ?? []) { SearchResults.Add(item); } SearchStatus = SearchResults.Count == 0 ? L("settings.weather.search_no_results", "No locations were found.") : string.Format( ResolveCulture(), L("settings.weather.search_result_count_format", "Found {0} locations."), SearchResults.Count); SelectedSearchResult = SearchResults.FirstOrDefault(); } finally { IsSearching = false; } } [RelayCommand] private async Task ApplyCitySelectionAsync() { if (SelectedSearchResult is null) { SearchStatus = L("settings.weather.search_select_required", "Please select one location from search results."); return; } var selected = SelectedSearchResult; var nextState = new WeatherSettingsState( "CitySearch", selected.LocationKey, selected.Name, selected.Latitude, selected.Longitude, AutoRefreshLocation, ExcludedAlerts ?? string.Empty, "HyperOS3", NoTlsRequests, SearchKeyword?.Trim() ?? string.Empty); ApplySavedState(nextState); SearchStatus = string.Format( ResolveCulture(), L("settings.weather.search_applied_format", "Location applied: {0}"), selected.Name); await RefreshPreviewAsync(); } [RelayCommand] private async Task ApplyCoordinatesAsync() { var nextState = CreateEditableState("Coordinates"); _settingsFacade.Weather.Save(nextState); ApplySavedState(nextState, save: false); SearchStatus = string.Format( ResolveCulture(), L("settings.weather.coordinates_saved_format", "Coordinates saved: {0:F4}, {1:F4}"), nextState.Latitude, nextState.Longitude); await RefreshPreviewAsync(); } [RelayCommand] private async Task UseCurrentLocationAsync() { if (!IsLocationSupported) { LocationActionStatus = LocationUnsupportedText; return; } IsRefreshingLocation = true; LocationActionStatus = LocationRefreshingText; try { var result = await _weatherLocationRefreshService.RefreshCurrentLocationAsync(); if (!result.Success || result.AppliedState is null) { LocationActionStatus = string.Format( ResolveCulture(), L("settings.weather.location_refresh_failed_format", "Failed to get current location: {0}"), result.ErrorMessage ?? result.LocationResult?.FailureReason.ToString() ?? L("settings.weather.preview_unknown", "Unknown")); return; } ApplySavedState(result.AppliedState, save: false); LocationActionStatus = string.Format( ResolveCulture(), L("settings.weather.location_refresh_success_format", "Current location applied: {0}"), result.AppliedState.LocationName); await RefreshPreviewAsync(); } finally { IsRefreshingLocation = false; } } [RelayCommand] private async Task RefreshPreviewAsync() { IsRefreshingPreview = true; try { var state = ResolvePreviewState(); if (string.IsNullOrWhiteSpace(state.LocationKey)) { PreviewStatus = L("settings.weather.preview_missing_location", "Please apply one weather location before testing."); PreviewIcon = null; PreviewLocation = CurrentLocationSummary; PreviewTemperature = "--"; PreviewCondition = string.Empty; PreviewUpdated = string.Empty; return; } var result = await _settingsFacade.Weather.GetWeatherInfoService().GetWeatherAsync( new WeatherQuery( state.LocationKey, state.Latitude, state.Longitude, ForecastDays: 3, Locale: NormalizeWeatherLocale(_languageCode), ForceRefresh: true)); if (!result.Success || result.Data is null) { PreviewStatus = string.Format( ResolveCulture(), L("settings.weather.preview_failed_format", "Test fetch failed: {0}"), result.ErrorMessage ?? result.ErrorCode ?? L("settings.weather.preview_unknown", "Unknown")); PreviewIcon = null; return; } var snapshot = result.Data; var isNight = snapshot.Current.IsDaylight.HasValue ? !snapshot.Current.IsDaylight.Value : _settingsFacade.Theme.Get().IsNightMode; var preview = XiaomiWeatherVisualResolver.Resolve( snapshot.Current.WeatherText, snapshot.Current.WeatherCode, isNight, _languageCode); PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(preview.PrimaryIconAsset); PreviewLocation = string.IsNullOrWhiteSpace(snapshot.LocationName) ? state.LocationName : snapshot.LocationName!; PreviewTemperature = snapshot.Current.TemperatureC.HasValue ? string.Format(CultureInfo.InvariantCulture, "{0:0.#}°C", snapshot.Current.TemperatureC.Value) : "--"; PreviewCondition = preview.DisplayText; var updatedAt = (snapshot.ObservationTime ?? snapshot.FetchedAt).ToLocalTime(); PreviewUpdated = string.Format( ResolveCulture(), L("settings.weather.preview_updated_format", "Updated {0}"), updatedAt.ToString("g", ResolveCulture())); PreviewStatus = string.Format( ResolveCulture(), L("settings.weather.preview_success_format", "Test success: {0} · {1} · {2}"), PreviewLocation, PreviewTemperature, PreviewCondition); } finally { IsRefreshingPreview = false; } } internal void ApplyDesignTimePreview() { _isInitializing = true; var previewLocation = new WeatherLocation( "Shenzhen Nanshan", "101280601", 22.5431, 114.0579, "Guangdong, China"); var alternateLocation = new WeatherLocation( "Shanghai Pudong", "101020600", 31.2304, 121.4737, "Shanghai, China"); SelectedLocationMode = LocationModes.FirstOrDefault(option => string.Equals(option.Value, "CitySearch", StringComparison.OrdinalIgnoreCase)) ?? LocationModes[0]; SearchKeyword = "shenzhen"; SelectedSearchResult = previewLocation; SearchResults.Clear(); SearchResults.Add(previewLocation); SearchResults.Add(alternateLocation); Latitude = previewLocation.Latitude; Longitude = previewLocation.Longitude; LocationKey = previewLocation.LocationKey; LocationName = previewLocation.Name; AutoRefreshLocation = true; ExcludedAlerts = "Heat\nThunderstorm"; NoTlsRequests = false; IsLocationSupported = true; IsRefreshingLocation = false; IsRefreshingPreview = false; _isInitializing = false; UpdateModeVisibility(); UpdateCurrentLocationSummary(); var preview = XiaomiWeatherVisualResolver.Resolve( "Partly cloudy", 4, isNight: false, _languageCode); SearchStatus = "2 sample locations are shown for design preview."; LocationActionStatus = "Using mocked Windows location support in design mode."; PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(preview.PrimaryIconAsset); PreviewLocation = previewLocation.Name; PreviewTemperature = "24 deg C"; PreviewCondition = preview.DisplayText; PreviewUpdated = "Updated 09:42"; PreviewStatus = "Preview data is mocked for Avalonia design mode."; } private void RefreshLocalizedText() { PageTitle = L("settings.weather.title", "Weather"); PageDescription = L("settings.weather.description", "Configure weather location, automatic positioning, and Xiaomi weather preview."); PreviewHeader = L("settings.weather.preview_panel_header", "Weather Preview"); PreviewDescription = L("settings.weather.preview_panel_desc", "Refresh and verify current weather service status."); LocationSourceHeader = L("settings.weather.location_source_header", "Location Source"); LocationSourceDescription = L("settings.weather.location_source_desc", "Choose how weather widgets resolve location."); CitySearchHeader = L("settings.weather.city_search_header", "City Search"); CitySearchDescription = L("settings.weather.city_search_desc", "Search cities and apply one weather location."); CoordinatesHeader = L("settings.weather.coordinates_header", "Coordinates"); CoordinatesDescription = L("settings.weather.coordinates_desc", "Set latitude/longitude and optional key/name."); LocationServicesHeader = L("settings.weather.location_services_header", "Location Service"); LocationServicesDescription = L("settings.weather.location_services_desc", "Use the current Windows location and decide whether it refreshes automatically at startup."); AlertFilterHeader = L("settings.weather.alert_filter_header", "Excluded Alerts"); AlertFilterDescription = L("settings.weather.alert_filter_desc", "Alerts containing these words will not be shown. One rule per line."); RequestHeader = L("settings.weather.no_tls_header", "No TLS Weather Request"); RequestDescription = L("settings.weather.no_tls_desc", "Not recommended. Enable only for incompatible network environments."); SearchPlaceholder = L("settings.weather.search_placeholder", "e.g. Beijing"); SearchButtonText = L("settings.weather.search_button", "Search"); ApplyCityButtonText = L("settings.weather.apply_city_button", "Apply City"); RefreshButtonText = L("settings.weather.refresh_button", "Refresh"); ApplyCoordinatesButtonText = L("settings.weather.apply_coordinates_button", "Apply Coordinates"); UseCurrentLocationButtonText = L("settings.weather.use_current_location", "Use Current Location"); AutoRefreshLabel = L("settings.weather.auto_refresh", "Auto refresh location on startup"); LatitudeLabel = L("settings.weather.latitude_label", "Latitude"); LongitudeLabel = L("settings.weather.longitude_label", "Longitude"); LocationKeyPlaceholder = L("settings.weather.location_key_placeholder", "Location key (optional)"); LocationNamePlaceholder = L("settings.weather.location_name_placeholder", "Display name (optional)"); NoTlsToggleText = L("settings.weather.no_tls_toggle", "Allow non-TLS request fallback"); LocationUnsupportedText = L("settings.weather.location_unsupported", "Current platform does not support retrieving the current location."); LocationReadyText = L("settings.weather.location_ready", "You can use the current Windows location."); LocationRefreshingText = L("settings.weather.location_refreshing", "Requesting current location…"); FooterHint = L("settings.weather.footer_hint", "Desktop weather widgets will reuse the location and alert exclusion settings configured here."); } private IReadOnlyList CreateLocationModes() { return [ new SelectionOption("CitySearch", L("settings.weather.mode_city_search", "City Search")), new SelectionOption("Coordinates", L("settings.weather.mode_coordinates", "Coordinates")) ]; } private void UpdateModeVisibility() { var mode = SelectedLocationMode?.Value ?? "CitySearch"; IsCitySearchMode = string.Equals(mode, "CitySearch", StringComparison.OrdinalIgnoreCase); IsCoordinatesMode = string.Equals(mode, "Coordinates", StringComparison.OrdinalIgnoreCase); } private void UpdateCurrentLocationSummary() { var state = CreateEditableState(); var modeLabel = SelectedLocationMode?.Label ?? state.LocationMode; if (string.Equals(state.LocationMode, "CitySearch", StringComparison.OrdinalIgnoreCase)) { CurrentLocationSummary = string.IsNullOrWhiteSpace(state.LocationKey) ? L("settings.weather.status_city_empty", "No city location is configured.") : string.Format( ResolveCulture(), L("settings.weather.status_city_format", "Mode: {0} | {1} | Key: {2}"), modeLabel, string.IsNullOrWhiteSpace(state.LocationName) ? L("settings.weather.location_not_selected", "No location selected") : state.LocationName, state.LocationKey); return; } CurrentLocationSummary = string.Format( ResolveCulture(), L("settings.weather.status_coordinates_format", "Mode: {0} | Lat {1:F4}, Lon {2:F4} | Key: {3}"), modeLabel, state.Latitude, state.Longitude, state.LocationKey); } private WeatherSettingsState CreateEditableState(string? locationMode = null) { var mode = locationMode ?? SelectedLocationMode?.Value ?? "CitySearch"; var locationKey = LocationKey?.Trim() ?? string.Empty; var locationName = LocationName?.Trim() ?? string.Empty; if (string.Equals(mode, "Coordinates", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrWhiteSpace(locationKey)) { locationKey = BuildCoordinateKey(Latitude, Longitude); } if (string.IsNullOrWhiteSpace(locationName)) { locationName = BuildCoordinateDisplayName(Latitude, Longitude); } } return new WeatherSettingsState( mode, locationKey, locationName, Latitude, Longitude, AutoRefreshLocation, ExcludedAlerts ?? string.Empty, "HyperOS3", NoTlsRequests, SearchKeyword?.Trim() ?? string.Empty); } private WeatherSettingsState ResolvePreviewState() { if (IsCitySearchMode && SelectedSearchResult is not null) { return new WeatherSettingsState( "CitySearch", SelectedSearchResult.LocationKey, SelectedSearchResult.Name, SelectedSearchResult.Latitude, SelectedSearchResult.Longitude, AutoRefreshLocation, ExcludedAlerts ?? string.Empty, "HyperOS3", NoTlsRequests, SearchKeyword?.Trim() ?? string.Empty); } return CreateEditableState(); } private void ApplySavedState(WeatherSettingsState state, bool save = true) { if (save) { _settingsFacade.Weather.Save(state); } _isInitializing = true; SelectedLocationMode = LocationModes.FirstOrDefault(option => string.Equals(option.Value, state.LocationMode, StringComparison.OrdinalIgnoreCase)) ?? LocationModes[0]; Latitude = state.Latitude; Longitude = state.Longitude; LocationKey = state.LocationKey; LocationName = state.LocationName; AutoRefreshLocation = state.AutoRefreshLocation; ExcludedAlerts = state.ExcludedAlerts; NoTlsRequests = state.NoTlsRequests; SearchKeyword = state.LocationQuery; _isInitializing = false; UpdateModeVisibility(); UpdateCurrentLocationSummary(); } private string BuildCoordinateDisplayName(double latitude, double longitude) { return string.Format( CultureInfo.InvariantCulture, L("settings.weather.coordinates_default_name_format", "Coordinate {0:F4}, {1:F4}"), latitude, longitude); } private static string BuildCoordinateKey(double latitude, double longitude) { return FormattableString.Invariant($"coord:{latitude:F4},{longitude:F4}"); } private string L(string key, string fallback) { return _localizationService.GetString(_languageCode, key, fallback); } private CultureInfo ResolveCulture() { try { return CultureInfo.GetCultureInfo(_languageCode); } catch (CultureNotFoundException) { return CultureInfo.InvariantCulture; } } private static string NormalizeWeatherLocale(string? languageCode) { return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase) ? "en_us" : "zh_cn"; } }