mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-04 10:16:24 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> - Add 10-second timeout to GetGeopositionAsync to prevent infinite spinner - Pre-check location services availability when dialog opens; disable Detect Location button with message if unavailable - Show user-friendly error messages for timeout and unavailable scenarios - Add LocationErrorText UI element and localized string resources <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #45860 - [x] Closes: #42852 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
422 lines
17 KiB
C#
422 lines
17 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.IO.Abstractions;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Common.UI;
|
|
using ManagedCommon;
|
|
using Microsoft.PowerToys.Settings.UI.Helpers;
|
|
using Microsoft.PowerToys.Settings.UI.Library;
|
|
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
|
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
|
using Microsoft.UI.Dispatching;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using PowerToys.GPOWrapper;
|
|
using Settings.UI.Library;
|
|
using Windows.Devices.Geolocation;
|
|
|
|
namespace Microsoft.PowerToys.Settings.UI.Views
|
|
{
|
|
public sealed partial class LightSwitchPage : NavigablePage, IRefreshablePage
|
|
{
|
|
private static readonly TimeSpan GeoLocationTimeout = TimeSpan.FromSeconds(10);
|
|
|
|
private readonly string appName = "LightSwitch";
|
|
private readonly SettingsUtils settingsUtils;
|
|
private readonly Func<string, int> sendConfigMsg = ShellPage.SendDefaultIPCMessage;
|
|
|
|
private readonly SettingsRepository<GeneralSettings> generalSettingsRepository;
|
|
private readonly SettingsRepository<LightSwitchSettings> moduleSettingsRepository;
|
|
|
|
private readonly IFileSystem fileSystem;
|
|
private readonly IFileSystemWatcher fileSystemWatcher;
|
|
private readonly DispatcherQueue dispatcherQueue;
|
|
private bool suppressViewModelUpdates;
|
|
|
|
private LightSwitchViewModel ViewModel { get; set; }
|
|
|
|
public LightSwitchPage()
|
|
{
|
|
this.settingsUtils = SettingsUtils.Default;
|
|
this.sendConfigMsg = ShellPage.SendDefaultIPCMessage;
|
|
|
|
this.generalSettingsRepository = SettingsRepository<GeneralSettings>.GetInstance(this.settingsUtils);
|
|
this.moduleSettingsRepository = SettingsRepository<LightSwitchSettings>.GetInstance(this.settingsUtils);
|
|
|
|
// Get settings from JSON (or defaults if JSON missing)
|
|
var darkSettings = this.moduleSettingsRepository.SettingsConfig;
|
|
|
|
// Pass them into the ViewModel
|
|
this.ViewModel = new LightSwitchViewModel(this.generalSettingsRepository, darkSettings, ShellPage.SendDefaultIPCMessage);
|
|
this.ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
|
|
|
this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository);
|
|
|
|
DataContext = this.ViewModel;
|
|
|
|
var settingsPath = this.settingsUtils.GetSettingsFilePath(this.appName);
|
|
|
|
this.dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
|
this.fileSystem = new FileSystem();
|
|
|
|
this.fileSystemWatcher = this.fileSystem.FileSystemWatcher.New();
|
|
this.fileSystemWatcher.Path = this.fileSystem.Path.GetDirectoryName(settingsPath);
|
|
this.fileSystemWatcher.Filter = this.fileSystem.Path.GetFileName(settingsPath);
|
|
this.fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime;
|
|
this.fileSystemWatcher.Changed += Settings_Changed;
|
|
this.fileSystemWatcher.EnableRaisingEvents = true;
|
|
|
|
this.InitializeComponent();
|
|
Loaded += LightSwitchPage_Loaded;
|
|
Loaded += (s, e) => this.ViewModel.OnPageLoaded();
|
|
}
|
|
|
|
public void RefreshEnabledState()
|
|
{
|
|
this.ViewModel.RefreshEnabledState();
|
|
}
|
|
|
|
private void LightSwitchPage_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
if (this.ViewModel.SearchLocations.Count == 0)
|
|
{
|
|
foreach (var city in SearchLocationLoader.GetAll())
|
|
{
|
|
this.ViewModel.SearchLocations.Add(city);
|
|
}
|
|
}
|
|
|
|
this.ViewModel.InitializeScheduleMode();
|
|
}
|
|
|
|
private async void GetGeoLocation_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
this.LatitudeBox.IsEnabled = false;
|
|
this.LongitudeBox.IsEnabled = false;
|
|
this.SyncButton.IsEnabled = false;
|
|
this.SyncLoader.IsActive = true;
|
|
this.SyncLoader.Visibility = Visibility.Visible;
|
|
this.LocationResultPanel.Visibility = Visibility.Collapsed;
|
|
this.LocationErrorText.Visibility = Visibility.Collapsed;
|
|
|
|
try
|
|
{
|
|
// Request access
|
|
var accessStatus = await Geolocator.RequestAccessAsync();
|
|
if (accessStatus != GeolocationAccessStatus.Allowed)
|
|
{
|
|
ShowLocationError(ResourceLoaderInstance.ResourceLoader.GetString("LightSwitch_LocationError_Unavailable"));
|
|
return;
|
|
}
|
|
|
|
var geolocator = new Geolocator { DesiredAccuracy = PositionAccuracy.Default };
|
|
|
|
using var cts = new CancellationTokenSource(GeoLocationTimeout);
|
|
var positionTask = geolocator.GetGeopositionAsync().AsTask(cts.Token);
|
|
Geoposition pos = await positionTask;
|
|
|
|
double latitude = Math.Round(pos.Coordinate.Point.Position.Latitude);
|
|
double longitude = Math.Round(pos.Coordinate.Point.Position.Longitude);
|
|
|
|
ViewModel.LocationPanelLatitude = latitude;
|
|
ViewModel.LocationPanelLongitude = longitude;
|
|
|
|
var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
|
|
|
|
this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute;
|
|
this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute;
|
|
|
|
// Since we use this mode, we can remove the selected city data.
|
|
this.ViewModel.SelectedCity = null;
|
|
|
|
this.SyncButton.IsEnabled = true;
|
|
this.SyncLoader.IsActive = false;
|
|
this.SyncLoader.Visibility = Visibility.Collapsed;
|
|
this.LocationDialog.IsPrimaryButtonEnabled = true;
|
|
this.LatitudeBox.IsEnabled = true;
|
|
this.LongitudeBox.IsEnabled = true;
|
|
this.LocationResultPanel.Visibility = Visibility.Visible;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
ShowLocationError(ResourceLoaderInstance.ResourceLoader.GetString("LightSwitch_LocationError_Timeout"));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ShowLocationError(ResourceLoaderInstance.ResourceLoader.GetString("LightSwitch_LocationError_Timeout"));
|
|
Logger.LogInfo($"Location error: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private void ShowLocationError(string message)
|
|
{
|
|
this.SyncButton.IsEnabled = true;
|
|
this.SyncLoader.IsActive = false;
|
|
this.SyncLoader.Visibility = Visibility.Collapsed;
|
|
this.LocationResultPanel.Visibility = Visibility.Collapsed;
|
|
this.LatitudeBox.IsEnabled = true;
|
|
this.LongitudeBox.IsEnabled = true;
|
|
this.LocationErrorText.Text = message;
|
|
this.LocationErrorText.Visibility = Visibility.Visible;
|
|
}
|
|
|
|
private void LatLonBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
|
|
{
|
|
double latitude = this.LatitudeBox.Value;
|
|
double longitude = this.LongitudeBox.Value;
|
|
|
|
if (double.IsNaN(latitude) || double.IsNaN(longitude) || (latitude == 0 && longitude == 0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
|
|
|
|
this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute;
|
|
this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute;
|
|
|
|
this.LocationResultPanel.Visibility = Visibility.Visible;
|
|
if (this.LocationDialog != null)
|
|
{
|
|
this.LocationDialog.IsPrimaryButtonEnabled = true;
|
|
}
|
|
}
|
|
|
|
private void LocationDialog_PrimaryButtonClick(object sender, ContentDialogButtonClickEventArgs args)
|
|
{
|
|
if (double.IsNaN(this.LatitudeBox.Value) || double.IsNaN(this.LongitudeBox.Value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
double latitude = this.LatitudeBox.Value;
|
|
double longitude = this.LongitudeBox.Value;
|
|
|
|
// need to save the values
|
|
this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture);
|
|
this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture);
|
|
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}°, {this.ViewModel.Longitude}°";
|
|
|
|
var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
|
|
|
|
this.ViewModel.LightTime = (result.SunriseHour * 60) + result.SunriseMinute;
|
|
this.ViewModel.DarkTime = (result.SunsetHour * 60) + result.SunsetMinute;
|
|
|
|
this.SunriseModeChartState();
|
|
}
|
|
|
|
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
if (this.suppressViewModelUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.PropertyName == "IsEnabled")
|
|
{
|
|
if (this.ViewModel.IsEnabled != this.generalSettingsRepository.SettingsConfig.Enabled.LightSwitch)
|
|
{
|
|
this.generalSettingsRepository.SettingsConfig.Enabled.LightSwitch = this.ViewModel.IsEnabled;
|
|
|
|
var generalSettingsMessage = new OutGoingGeneralSettings(this.generalSettingsRepository.SettingsConfig).ToString();
|
|
Logger.LogInfo($"Saved general settings from Light Switch page.");
|
|
|
|
this.sendConfigMsg?.Invoke(generalSettingsMessage);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (this.ViewModel.ModuleSettings != null)
|
|
{
|
|
SndLightSwitchSettings currentSettings = new(this.moduleSettingsRepository.SettingsConfig);
|
|
SndModuleSettings<SndLightSwitchSettings> csIpcMessage = new(currentSettings);
|
|
|
|
SndLightSwitchSettings outSettings = new(this.ViewModel.ModuleSettings);
|
|
SndModuleSettings<SndLightSwitchSettings> outIpcMessage = new(outSettings);
|
|
|
|
string csMessage = csIpcMessage.ToJsonString();
|
|
string outMessage = outIpcMessage.ToJsonString();
|
|
|
|
if (!csMessage.Equals(outMessage, StringComparison.Ordinal))
|
|
{
|
|
Logger.LogInfo($"Saved Light Switch settings from Light Switch page.");
|
|
|
|
this.sendConfigMsg?.Invoke(outMessage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LoadSettings(SettingsRepository<GeneralSettings> generalSettingsRepository, SettingsRepository<LightSwitchSettings> moduleSettingsRepository)
|
|
{
|
|
if (generalSettingsRepository != null)
|
|
{
|
|
if (moduleSettingsRepository != null)
|
|
{
|
|
UpdateViewModelSettings(moduleSettingsRepository.SettingsConfig, generalSettingsRepository.SettingsConfig);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentNullException(nameof(moduleSettingsRepository));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentNullException(nameof(generalSettingsRepository));
|
|
}
|
|
}
|
|
|
|
private void UpdateViewModelSettings(LightSwitchSettings lightSwitchSettings, GeneralSettings generalSettings)
|
|
{
|
|
if (lightSwitchSettings != null)
|
|
{
|
|
if (generalSettings != null)
|
|
{
|
|
this.ViewModel.IsEnabled = generalSettings.Enabled.LightSwitch;
|
|
this.ViewModel.ModuleSettings = (LightSwitchSettings)lightSwitchSettings.Clone();
|
|
|
|
UpdateEnabledState(generalSettings.Enabled.LightSwitch);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentNullException(nameof(generalSettings));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentNullException(nameof(lightSwitchSettings));
|
|
}
|
|
}
|
|
|
|
private void Settings_Changed(object sender, FileSystemEventArgs e)
|
|
{
|
|
this.dispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
this.suppressViewModelUpdates = true;
|
|
|
|
this.moduleSettingsRepository.ReloadSettings();
|
|
this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository);
|
|
|
|
this.suppressViewModelUpdates = false;
|
|
});
|
|
}
|
|
|
|
private void UpdateEnabledState(bool recommendedState)
|
|
{
|
|
ViewModel.RefreshEnabledState();
|
|
}
|
|
|
|
private async void SyncLocationButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
this.LocationDialog.IsPrimaryButtonEnabled = false;
|
|
this.LocationResultPanel.Visibility = Visibility.Collapsed;
|
|
this.LocationErrorText.Visibility = Visibility.Collapsed;
|
|
|
|
// Pre-check location services availability
|
|
var accessStatus = await Geolocator.RequestAccessAsync();
|
|
if (accessStatus != GeolocationAccessStatus.Allowed)
|
|
{
|
|
this.SyncButton.IsEnabled = false;
|
|
this.LocationErrorText.Text = ResourceLoaderInstance.ResourceLoader.GetString("LightSwitch_LocationError_Unavailable");
|
|
this.LocationErrorText.Visibility = Visibility.Visible;
|
|
}
|
|
else
|
|
{
|
|
this.SyncButton.IsEnabled = true;
|
|
}
|
|
|
|
await this.LocationDialog.ShowAsync();
|
|
}
|
|
|
|
private void CityAutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
|
{
|
|
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput && !string.IsNullOrWhiteSpace(sender.Text))
|
|
{
|
|
string query = sender.Text.ToLower(CultureInfo.CurrentCulture);
|
|
|
|
// Filter your cities (assuming ViewModel.Cities is a List<City>)
|
|
var filtered = this.ViewModel.SearchLocations
|
|
.Where(c =>
|
|
(c.City?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false) ||
|
|
(c.Country?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false))
|
|
.ToList();
|
|
|
|
sender.ItemsSource = filtered;
|
|
}
|
|
}
|
|
|
|
/* private void CityAutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
|
|
{
|
|
if (args.SelectedItem is SearchLocation location)
|
|
{
|
|
ViewModel.SelectedCity = location;
|
|
|
|
// CityAutoSuggestBox.Text = $"{location.City}, {location.Country}";
|
|
LocationDialog.IsPrimaryButtonEnabled = true;
|
|
LocationResultPanel.Visibility = Visibility.Visible;
|
|
}
|
|
} */
|
|
|
|
private void ModeSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
switch (this.ViewModel.ScheduleMode)
|
|
{
|
|
case "FixedHours":
|
|
VisualStateManager.GoToState(this, "ManualState", true);
|
|
this.TimelineCard.Visibility = Visibility.Visible;
|
|
break;
|
|
case "SunsetToSunrise":
|
|
VisualStateManager.GoToState(this, "SunsetToSunriseState", true);
|
|
this.SunriseModeChartState();
|
|
break;
|
|
case "FollowNightLight":
|
|
VisualStateManager.GoToState(this, "FollowNightLightState", true);
|
|
TimelineCard.Visibility = Visibility.Collapsed;
|
|
break;
|
|
default:
|
|
VisualStateManager.GoToState(this, "OffState", true);
|
|
this.TimelineCard.Visibility = Visibility.Collapsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void OpenNightLightSettings_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
Helpers.StartProcessHelper.Start(Helpers.StartProcessHelper.NightLightSettings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Error while trying to open the system night light settings", ex);
|
|
}
|
|
}
|
|
|
|
private void SunriseModeChartState()
|
|
{
|
|
if (this.ViewModel.Latitude != "0.0" && this.ViewModel.Longitude != "0.0")
|
|
{
|
|
this.TimelineCard.Visibility = Visibility.Visible;
|
|
this.LocationWarningBar.Visibility = Visibility.Collapsed;
|
|
}
|
|
else
|
|
{
|
|
this.TimelineCard.Visibility = Visibility.Collapsed;
|
|
this.LocationWarningBar.Visibility = Visibility.Visible;
|
|
}
|
|
}
|
|
|
|
private void NavigatePowerDisplaySettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
|
{
|
|
ShellPage.Navigate(typeof(PowerDisplayPage));
|
|
}
|
|
}
|
|
}
|