settings_re8

This commit is contained in:
lincube
2026-03-14 22:45:09 +08:00
parent 91f9f3d6fb
commit 689be7b585
54 changed files with 5356 additions and 30 deletions

View File

@@ -0,0 +1,365 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentIcons.Common;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.ViewModels;
public enum LauncherHiddenItemKind
{
Folder,
Shortcut
}
public sealed partial class LauncherHiddenItemViewModel : ObservableObject
{
private readonly Action<LauncherHiddenItemViewModel> _restoreAction;
public LauncherHiddenItemViewModel(
LauncherHiddenItemKind kind,
string key,
string displayName,
string typeLabel,
Symbol iconSymbol,
string restoreButtonText,
Action<LauncherHiddenItemViewModel> restoreAction)
{
Kind = kind;
Key = key;
DisplayName = displayName;
TypeLabel = typeLabel;
IconSymbol = iconSymbol;
RestoreButtonText = restoreButtonText;
_restoreAction = restoreAction ?? throw new ArgumentNullException(nameof(restoreAction));
}
public LauncherHiddenItemKind Kind { get; }
public string Key { get; }
public string DisplayName { get; }
public string TypeLabel { get; }
public Symbol IconSymbol { get; }
public string RestoreButtonText { get; }
[RelayCommand]
private void Restore()
{
_restoreAction(this);
}
}
public sealed partial class LauncherSettingsPageViewModel : ViewModelBase, IDisposable
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _disposed;
public LauncherSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
ReloadData();
_settingsFacade.Settings.Changed += OnSettingsChanged;
}
public ObservableCollection<LauncherHiddenItemViewModel> HiddenItems { get; } = [];
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
[ObservableProperty]
private string _launcherHeader = string.Empty;
[ObservableProperty]
private string _launcherSubtitle = string.Empty;
[ObservableProperty]
private string _hiddenHeader = string.Empty;
[ObservableProperty]
private string _hiddenDescription = string.Empty;
[ObservableProperty]
private string _hiddenHint = string.Empty;
[ObservableProperty]
private string _hiddenEmptyText = string.Empty;
[ObservableProperty]
private string _hiddenSummary = string.Empty;
[ObservableProperty]
private string _hiddenCountText = "0";
[ObservableProperty]
private bool _hasHiddenItems;
[ObservableProperty]
private bool _isHiddenItemsEmpty = true;
public void Dispose()
{
if (_disposed)
{
return;
}
_settingsFacade.Settings.Changed -= OnSettingsChanged;
_disposed = true;
}
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
if (e.Scope != SettingsScope.Launcher)
{
return;
}
Dispatcher.UIThread.Post(ReloadData, DispatcherPriority.Background);
}
private void ReloadData()
{
var root = LoadCatalogSafe();
var snapshot = _settingsFacade.LauncherPolicy.Get()?.Clone() ?? new LauncherSettingsSnapshot();
var hiddenItems = BuildHiddenItems(root, snapshot);
HiddenItems.Clear();
foreach (var hiddenItem in hiddenItems)
{
HiddenItems.Add(hiddenItem);
}
HasHiddenItems = HiddenItems.Count > 0;
IsHiddenItemsEmpty = !HasHiddenItems;
HiddenCountText = HiddenItems.Count.ToString(CultureInfo.CurrentCulture);
HiddenSummary = string.Format(
ResolveCulture(),
L("settings.launcher.hidden_summary_format", "{0} hidden items"),
HiddenItems.Count);
}
private StartMenuFolderNode LoadCatalogSafe()
{
try
{
return _settingsFacade.LauncherCatalog.LoadCatalog() ?? new StartMenuFolderNode(L("launcher.title", "App Launcher"), string.Empty);
}
catch (Exception ex)
{
AppLogger.Warn("Launcher.Settings", "Failed to load launcher catalog for settings page.", ex);
return new StartMenuFolderNode(L("launcher.title", "App Launcher"), string.Empty);
}
}
private IReadOnlyList<LauncherHiddenItemViewModel> BuildHiddenItems(StartMenuFolderNode root, LauncherSettingsSnapshot snapshot)
{
var items = new List<LauncherHiddenItemViewModel>();
var seenFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenApps = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
CollectHiddenItems(root, snapshot, items, seenFolders, seenApps);
foreach (var key in snapshot.HiddenLauncherFolderPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
{
var normalizedKey = NormalizeLauncherHiddenKey(key);
if (string.IsNullOrWhiteSpace(normalizedKey) || !seenFolders.Add(normalizedKey))
{
continue;
}
items.Add(CreateHiddenItem(
LauncherHiddenItemKind.Folder,
normalizedKey,
BuildLauncherHiddenFallbackDisplayName(normalizedKey)));
}
foreach (var key in snapshot.HiddenLauncherAppPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
{
var normalizedKey = NormalizeLauncherHiddenKey(key);
if (string.IsNullOrWhiteSpace(normalizedKey) || !seenApps.Add(normalizedKey))
{
continue;
}
items.Add(CreateHiddenItem(
LauncherHiddenItemKind.Shortcut,
normalizedKey,
BuildLauncherHiddenFallbackDisplayName(normalizedKey)));
}
return items
.OrderBy(item => item.DisplayName, StringComparer.CurrentCultureIgnoreCase)
.ThenBy(item => item.Key, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private void CollectHiddenItems(
StartMenuFolderNode folder,
LauncherSettingsSnapshot snapshot,
List<LauncherHiddenItemViewModel> items,
HashSet<string> seenFolders,
HashSet<string> seenApps)
{
foreach (var subFolder in folder.Folders)
{
var folderKey = NormalizeLauncherHiddenKey(subFolder.RelativePath);
if (!string.IsNullOrWhiteSpace(folderKey) &&
snapshot.HiddenLauncherFolderPaths.Contains(folderKey, StringComparer.OrdinalIgnoreCase) &&
seenFolders.Add(folderKey))
{
items.Add(CreateHiddenItem(
LauncherHiddenItemKind.Folder,
folderKey,
subFolder.Name));
}
CollectHiddenItems(subFolder, snapshot, items, seenFolders, seenApps);
}
foreach (var app in folder.Apps)
{
var appKey = NormalizeLauncherHiddenKey(app.RelativePath);
if (string.IsNullOrWhiteSpace(appKey) ||
!snapshot.HiddenLauncherAppPaths.Contains(appKey, StringComparer.OrdinalIgnoreCase) ||
!seenApps.Add(appKey))
{
continue;
}
items.Add(CreateHiddenItem(
LauncherHiddenItemKind.Shortcut,
appKey,
app.DisplayName));
}
}
private LauncherHiddenItemViewModel CreateHiddenItem(
LauncherHiddenItemKind kind,
string key,
string displayName)
{
var typeLabel = kind == LauncherHiddenItemKind.Folder
? L("settings.launcher.hidden_type_folder", "Folder")
: L("settings.launcher.hidden_type_shortcut", "Shortcut");
var iconSymbol = kind == LauncherHiddenItemKind.Folder
? Symbol.Folder
: Symbol.Apps;
return new LauncherHiddenItemViewModel(
kind,
key,
displayName,
typeLabel,
iconSymbol,
L("settings.launcher.restore_button", "Unhide"),
RestoreHiddenItem);
}
private void RestoreHiddenItem(LauncherHiddenItemViewModel item)
{
var snapshot = _settingsFacade.LauncherPolicy.Get()?.Clone() ?? new LauncherSettingsSnapshot();
var normalizedKey = NormalizeLauncherHiddenKey(item.Key);
if (string.IsNullOrWhiteSpace(normalizedKey))
{
return;
}
IReadOnlyCollection<string>? changedKeys = item.Kind switch
{
LauncherHiddenItemKind.Folder => RemoveKey(snapshot.HiddenLauncherFolderPaths, normalizedKey)
? [nameof(LauncherSettingsSnapshot.HiddenLauncherFolderPaths)]
: null,
LauncherHiddenItemKind.Shortcut => RemoveKey(snapshot.HiddenLauncherAppPaths, normalizedKey)
? [nameof(LauncherSettingsSnapshot.HiddenLauncherAppPaths)]
: null,
_ => null
};
if (changedKeys is null)
{
return;
}
_settingsFacade.Settings.SaveSnapshot(SettingsScope.Launcher, snapshot, changedKeys: changedKeys);
ReloadData();
}
private void RefreshLocalizedText()
{
PageTitle = L("settings.launcher.title", "App Launcher");
PageDescription = L("settings.launcher.description", "Manage hidden apps and folders in the App Launcher.");
LauncherHeader = L("launcher.title", "App Launcher");
LauncherSubtitle = OperatingSystem.IsLinux()
? L("launcher.subtitle_linux", "Displays installed apps discovered from Linux desktop entries.")
: L("launcher.subtitle", "Displays all apps and folders based on the Windows Start menu structure.");
HiddenHeader = L("settings.launcher.hidden_header", "Hidden Items");
HiddenDescription = L("settings.launcher.hidden_desc", "Review hidden launcher entries and show them again.");
HiddenHint = L("settings.launcher.hidden_hint", "In desktop edit mode, select a launcher icon and click Hide. Hidden entries appear here.");
HiddenEmptyText = L("settings.launcher.hidden_empty", "No hidden items.");
}
private CultureInfo ResolveCulture()
{
try
{
return CultureInfo.GetCultureInfo(_languageCode);
}
catch (CultureNotFoundException)
{
return CultureInfo.InvariantCulture;
}
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
private static string NormalizeLauncherHiddenKey(string? key)
=> string.IsNullOrWhiteSpace(key) ? string.Empty : key.Trim();
private static string BuildLauncherHiddenFallbackDisplayName(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
return "Unknown";
}
var normalized = key.Replace('\\', '/');
var fileName = Path.GetFileNameWithoutExtension(normalized);
return string.IsNullOrWhiteSpace(fileName)
? key
: fileName;
}
private static bool RemoveKey(ICollection<string> values, string key)
{
var existing = values.FirstOrDefault(value => string.Equals(value, key, StringComparison.OrdinalIgnoreCase));
if (existing is null)
{
return false;
}
values.Remove(existing);
return true;
}
}

View File

@@ -0,0 +1,678 @@
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)
{
_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();
SearchKeyword = weatherState.LocationQuery;
SelectedLocationMode = LocationModes.FirstOrDefault(option =>
string.Equals(option.Value, weatherState.LocationMode, StringComparison.OrdinalIgnoreCase))
?? LocationModes[0];
_isInitializing = true;
Latitude = weatherState.Latitude;
Longitude = weatherState.Longitude;
LocationKey = weatherState.LocationKey;
LocationName = weatherState.LocationName;
AutoRefreshLocation = weatherState.AutoRefreshLocation;
ExcludedAlerts = weatherState.ExcludedAlerts;
NoTlsRequests = weatherState.NoTlsRequests;
_isInitializing = false;
IsLocationSupported = _locationService.IsSupported;
UpdateModeVisibility();
UpdateCurrentLocationSummary();
LocationActionStatus = IsLocationSupported
? LocationReadyText
: LocationUnsupportedText;
_ = RefreshPreviewAsync();
}
public IReadOnlyList<SelectionOption> LocationModes { get; }
public ObservableCollection<WeatherLocation> 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 visualKind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(visualKind));
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 = snapshot.Current.WeatherText ?? L("settings.weather.preview_unknown", "Unknown");
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;
}
}
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<SelectionOption> 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";
}
}