Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 92 additions & 76 deletions AutoDarkModeApp/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,90 +25,106 @@ public partial class App : Application
// https://docs.microsoft.com/dotnet/core/extensions/logging
public IHost Host { get; }

/// <summary>
/// Retrieves a registered service of the specified type from the application's dependency injection container.
/// </summary>
/// <remarks>Use this method to access services configured in the application's dependency injection
/// container. Ensure that the service type is registered in the ConfigureServices method of App.xaml.cs before
/// calling this method.</remarks>
/// <typeparam name="T">The type of service to retrieve. Must be a reference type and registered in the application's service
/// collection.</typeparam>
/// <returns>An instance of the requested service type <typeparamref name="T"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if the requested service type <typeparamref name="T"/> is not registered in the application's service
/// collection.</exception>
public static T GetService<T>()
where T : class
{
if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
throw new InvalidOperationException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}

return service;
}

public static MainWindow MainWindow { get; private set; } = null!;

public App()
{
CheckAppMutex();
InitializeComponent();

Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<ILocalSettingsService, LocalSettingsService>();
services.AddSingleton<IFileService, FileService>();

services.AddSingleton<IActivationService, ActivationService>();
services.AddSingleton<ICloseService, CloseService>();
services.AddSingleton<IPageService, PageService>();
services.AddSingleton<INavigationService, NavigationService>();

services.AddSingleton<IErrorService, ErrorService>();
services.AddSingleton<IGeolocatorService, GeolocatorService>();

// Window
// NOTE: The MainWindow is registered as a singleton because we only need one instance.
services.AddSingleton<MainWindow>();

// Views and ViewModels
services.AddTransient<ThemePickerViewModel>();
services.AddTransient<ThemePickerPage>();
services.AddTransient<CursorsViewModel>();
services.AddTransient<CursorsPage>();
services.AddTransient<ColorizationViewModel>();
services.AddTransient<ColorizationPage>();
services.AddTransient<SettingsViewModel>();
services.AddTransient<SettingsPage>();
services.AddTransient<AboutViewModel>();
services.AddTransient<AboutPage>();
services.AddTransient<DonationViewModel>();
services.AddTransient<DonationPage>();
services.AddTransient<ScriptsViewModel>();
services.AddTransient<ScriptsPage>();
services.AddTransient<PersonalizationViewModel>();
services.AddTransient<PersonalizationPage>();
services.AddTransient<SystemAreasViewModel>();
services.AddTransient<SystemAreasPage>();
services.AddTransient<ConditionsViewModel>();
services.AddTransient<ConditionsPage>();
services.AddTransient<HotkeysViewModel>();
services.AddTransient<HotkeysPage>();
services.AddTransient<WallpaperPickerViewModel>();
services.AddTransient<WallpaperPickerPage>();
services.AddTransient<TimeViewModel>();
services.AddTransient<TimePage>();

// Configuration
services.Configure<LocalSettingsOptions>(context.Configuration.GetSection(nameof(LocalSettingsOptions)));
}
)
.Build();

UnhandledException += App_UnhandledException;
}

public static void CheckAppMutex()
{
if (!Mutex.WaitOne(TimeSpan.FromMilliseconds(50), false) && !Debugger.IsAttached)
{
List<Process> processes = [.. Process.GetProcessesByName("AutoDarkModeApp")];
var processes = Process.GetProcessesByName("AutoDarkModeApp").Where(p => p.Id != Environment.ProcessId).ToList();
if (processes.Count > 0)
{
Helpers.WindowHelper.BringProcessToFront(processes[0]);
Environment.Exit(-1);
App.Current.Exit();
}
}
}

public static Window MainWindow { get; set; } = Window.Current;

public App()
{
CheckAppMutex();

InitializeComponent();

Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<ILocalSettingsService, LocalSettingsService>();
services.AddSingleton<IFileService, FileService>();

services.AddSingleton<IActivationService, ActivationService>();
services.AddSingleton<IPageService, PageService>();
services.AddSingleton<INavigationService, NavigationService>();

services.AddSingleton<IErrorService, ErrorService>();
services.AddSingleton<IGeolocatorService, GeolocatorService>();

// Views and ViewModels
services.AddTransient<ThemePickerViewModel>();
services.AddTransient<ThemePickerPage>();
services.AddTransient<CursorsViewModel>();
services.AddTransient<CursorsPage>();
services.AddTransient<ColorizationViewModel>();
services.AddTransient<ColorizationPage>();
services.AddTransient<SettingsViewModel>();
services.AddTransient<SettingsPage>();
services.AddTransient<AboutViewModel>();
services.AddTransient<AboutPage>();
services.AddTransient<DonationViewModel>();
services.AddTransient<DonationPage>();
services.AddTransient<ScriptsViewModel>();
services.AddTransient<ScriptsPage>();
services.AddTransient<PersonalizationViewModel>();
services.AddTransient<PersonalizationPage>();
services.AddTransient<SystemAreasViewModel>();
services.AddTransient<SystemAreasPage>();
services.AddTransient<ConditionsViewModel>();
services.AddTransient<ConditionsPage>();
services.AddTransient<HotkeysViewModel>();
services.AddTransient<HotkeysPage>();
services.AddTransient<WallpaperPickerViewModel>();
services.AddTransient<WallpaperPickerPage>();
services.AddTransient<TimeViewModel>();
services.AddTransient<TimePage>();

// Configuration
services.Configure<LocalSettingsOptions>(context.Configuration.GetSection(nameof(LocalSettingsOptions)));
}).
Build();

UnhandledException += App_UnhandledException;
}

private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// TODO: Log and handle exceptions as appropriate.
Expand All @@ -124,26 +140,26 @@ protected async override void OnLaunched(LaunchActivatedEventArgs args)
if (arguments.Length > 1)
{
new PipeClient().SendMessageAndGetReply(arguments[1]);
Environment.Exit(-1);
App.Current.Exit();
return;
}

// Set App and Svc language
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = await LanguageHelper.GetDefaultLanguageAsync();
var builder = AdmConfigBuilder.Instance();
builder.Load();
try
await Task.Run(() =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this block changed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because ActivationService starts to execute after the language is loaded, configuration file loading does not need to block UI threads, and it also unifies asynchronous business logic. (There won't even be performance improvement, but it looks much better visually)

{
builder.Config.Tunable.UICulture = LanguageHelper.SelectedLanguageCode; // save before activation SVC (think of first-launch scenario)
var builder = AdmConfigBuilder.Instance();
builder.Load();
builder.Config.Tunable.UICulture = LanguageHelper.SelectedLanguageCode; // For Svc and other services that need to know the UI culture
builder.Save();
}
catch (Exception)
{
// We can't show a dialog here
//await App.GetService<IErrorService>().ShowErrorMessage(ex, App.MainWindow.Content.XamlRoot, "Startup", "Failed to force-set Svc language");
}
});

// NOTE: Here we use the DI container to get the MainWindow and set it as a static property, which not only conforms to the standard, but also facilitates other places to access the MainWindow.
MainWindow = GetService<MainWindow>();
MainWindow.Closed += async (s, e) => await GetService<ICloseService>().CloseAsync();

MainWindow = new MainWindow();
await GetService<IActivationService>().ActivateAsync(args);

await App.GetService<IActivationService>().ActivateAsync(args);
// NOTE: The MainWindow must be activated (i.e. made visible) in the ActivationService, not here in App.xaml.cs, because there are navigation events and adjustment of window position and size.
}
}
6 changes: 6 additions & 0 deletions AutoDarkModeApp/Contracts/Services/ICloseService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace AutoDarkModeApp.Contracts.Services;

public interface ICloseService
{
Task CloseAsync();
}
6 changes: 3 additions & 3 deletions AutoDarkModeApp/Helpers/LanguageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ public static class LanguageHelper
{
public static string SelectedLanguageCode { get; set; } = "en-US"; // equal to <DefaultLanguage>

private static readonly ILocalSettingsService _localSettingsService = App.GetService<ILocalSettingsService>()!;
public static readonly string[] SupportedCultures =
[
// Left-to-Right (LTR) languages
Expand All @@ -23,7 +22,8 @@ public static class LanguageHelper

public static async Task<string> GetDefaultLanguageAsync()
{
var language = await _localSettingsService.ReadSettingAsync<string>("SelectedLanguageCode");
var localSettingsService = App.GetService<ILocalSettingsService>();
var language = await localSettingsService.ReadSettingAsync<string>("SelectedLanguageCode");
if (!string.IsNullOrEmpty(language) && SupportedCultures.Contains(language))
{
SelectedLanguageCode = language;
Expand Down Expand Up @@ -56,7 +56,7 @@ public static async Task<string> GetDefaultLanguageAsync()
}
// else keep the default "en-US"
}
await _localSettingsService.SaveSettingAsync("SelectedLanguageCode", SelectedLanguageCode);
await localSettingsService.SaveSettingAsync("SelectedLanguageCode", SelectedLanguageCode);
}
return SelectedLanguageCode;
}
Expand Down
68 changes: 15 additions & 53 deletions AutoDarkModeApp/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,41 @@ public sealed partial class MainWindow : Window
{
private readonly INavigationService _navigationService;

public MainWindow()
[DllImport("user32.dll")]
private static extern uint GetDpiForWindow(IntPtr hWnd);

public MainWindow(INavigationService navigationService)
{
_navigationService = App.GetService<INavigationService>();
_navigationService = navigationService;
InitializeComponent();

// TODO: Set the title bar icon by updating /Assets/WindowIcon.ico.
// A custom title bar is required for full window theme and Mica support.
// https://docs.microsoft.com/windows/apps/develop/title-bar?tabs=winui3#full-customization
Title = Debugger.IsAttached ? "Auto Dark Mode Debug" : "Auto Dark Mode";

ExtendsContentIntoTitleBar = true;
SetTitleBar(TitleBar);
TitleBar.Subtitle = Debugger.IsAttached ? "Debug" : "";
TitleBar.ActualThemeChanged += (s, e) => ApplySystemThemeToCaptionButtons();

ApplySystemThemeToCaptionButtons();

AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico"));
AppWindow.SetTaskbarIcon(Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico"));
AppWindow.SetTitleBarIcon(Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico"));
var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico");
AppWindow.SetIcon(iconPath);
AppWindow.SetTaskbarIcon(iconPath);
AppWindow.SetTitleBarIcon(iconPath);

Title = Debugger.IsAttached ? "Auto Dark Mode Debug" : "Auto Dark Mode";
IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
uint dpi = GetDpiForWindow(hwnd);
double scaleFactor = dpi / 96.0;

// TODO: No one knows what the correct way to use it is. Waiting for official examples.
[DllImport("user32.dll")]
static extern uint GetDpiForWindow([In] IntPtr hwnd);
IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var dpiForWindow = GetDpiForWindow(hWnd);
double scaleFactor = dpiForWindow / 96.0;
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.PreferredMinimumWidth = (int)(870 * scaleFactor);
presenter.PreferredMinimumWidth = (int)(860 * scaleFactor);
presenter.PreferredMinimumHeight = (int)(600 * scaleFactor);
presenter.PreferredMaximumHeight = 10000;
presenter.PreferredMaximumWidth = 10000;
}

_navigationService.Frame = NavigationFrame;
_navigationService.InitializeNavigationView(NavigationViewControl);

_navigationService.InitializeBreadcrumbBar(BreadcrumBarControl);

Closed += MainWindow_Closed;
}

private void NavViewTitleBar_BackRequested(Microsoft.UI.Xaml.Controls.TitleBar sender, object args)
Expand All @@ -74,36 +68,4 @@ private void ApplySystemThemeToCaptionButtons()
AppWindow.TitleBar.ButtonHoverBackgroundColor = backgroundHoverColor;
}

private async void MainWindow_Closed(object sender, WindowEventArgs args)
{
var presenter = AppWindow.Presenter as OverlappedPresenter;
var position = AppWindow.Position;
var size = AppWindow.Size;
var localSettings = App.GetService<ILocalSettingsService>();
await Task.Run(async () =>
{
if (presenter != null)
{
await localSettings.SaveSettingAsync("WindowState", (int)presenter.State);

if (presenter.State == OverlappedPresenterState.Restored)
{
await localSettings.SaveSettingAsync("X", position.X);
await localSettings.SaveSettingAsync("Y", position.Y);
await localSettings.SaveSettingAsync("Width", size.Width);
await localSettings.SaveSettingAsync("Height", size.Height);
}
}

//TODO: MapLocationFinder will make WinUI app hang on exit, more information on https://github.com/microsoft/microsoft-ui-xaml/issues/10229
try
{
Process.GetCurrentProcess().Kill();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
});
}
}
26 changes: 26 additions & 0 deletions AutoDarkModeApp/Services/CloseService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using AutoDarkModeApp.Contracts.Services;
using Microsoft.UI.Windowing;

namespace AutoDarkModeApp.Services;

public class CloseService(ILocalSettingsService localSettingsService) : ICloseService
{
public async Task CloseAsync()
{
if (App.MainWindow.AppWindow.Presenter is OverlappedPresenter presenter)
{
var position = App.MainWindow.AppWindow.Position;
var size = App.MainWindow.AppWindow.Size;

await localSettingsService.SaveSettingAsync("WindowState", (int)presenter.State);

if (presenter.State == OverlappedPresenterState.Restored)
{
await localSettingsService.SaveSettingAsync("X", position.X);
await localSettingsService.SaveSettingAsync("Y", position.Y);
await localSettingsService.SaveSettingAsync("Width", size.Width);
await localSettingsService.SaveSettingAsync("Height", size.Height);
}
}
}
}