diff --git a/ChineseChess.GUI.Core/ChineseChess.GUI.Core.csproj b/ChineseChess.GUI.Core/ChineseChess.GUI.Core.csproj new file mode 100644 index 0000000..8c0ef43 --- /dev/null +++ b/ChineseChess.GUI.Core/ChineseChess.GUI.Core.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + ChineseChess.GUI.Core + + + + + + + + + + diff --git a/ChineseChess.GUI.Core/Contracts/Services/IFileService.cs b/ChineseChess.GUI.Core/Contracts/Services/IFileService.cs new file mode 100644 index 0000000..806ad86 --- /dev/null +++ b/ChineseChess.GUI.Core/Contracts/Services/IFileService.cs @@ -0,0 +1,11 @@ +namespace ChineseChess.GUI.Core.Contracts.Services +{ + public interface IFileService + { + T Read(string folderPath, string fileName); + + void Save(string folderPath, string fileName, T content); + + void Delete(string folderPath, string fileName); + } +} diff --git a/ChineseChess.GUI.Core/Services/FileService.cs b/ChineseChess.GUI.Core/Services/FileService.cs new file mode 100644 index 0000000..ae23112 --- /dev/null +++ b/ChineseChess.GUI.Core/Services/FileService.cs @@ -0,0 +1,43 @@ +using System.IO; +using System.Text; + +using ChineseChess.GUI.Core.Contracts.Services; + +using Newtonsoft.Json; + +namespace ChineseChess.GUI.Core.Services +{ + public class FileService : IFileService + { + public T Read(string folderPath, string fileName) + { + var path = Path.Combine(folderPath, fileName); + if (File.Exists(path)) + { + var json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); + } + + return default; + } + + public void Save(string folderPath, string fileName, T content) + { + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + var fileContent = JsonConvert.SerializeObject(content); + File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, Encoding.UTF8); + } + + public void Delete(string folderPath, string fileName) + { + if (fileName != null && File.Exists(Path.Combine(folderPath, fileName))) + { + File.Delete(Path.Combine(folderPath, fileName)); + } + } + } +} diff --git a/ChineseChess.GUI.Core/readme.txt b/ChineseChess.GUI.Core/readme.txt new file mode 100644 index 0000000..dd20676 --- /dev/null +++ b/ChineseChess.GUI.Core/readme.txt @@ -0,0 +1,2 @@ +This core project is a .net standard project. +It's a great place to put all your logic that is not platform dependent (e.g. model/helper classes) so they can be reused. \ No newline at end of file diff --git a/ChineseChess.GUI.Tests.xUnit/ChineseChess.GUI.Tests.xUnit.csproj b/ChineseChess.GUI.Tests.xUnit/ChineseChess.GUI.Tests.xUnit.csproj new file mode 100644 index 0000000..46d3728 --- /dev/null +++ b/ChineseChess.GUI.Tests.xUnit/ChineseChess.GUI.Tests.xUnit.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp3.1 + false + uap10.0.18362 + x64;x86;AnyCPU + ChineseChess.GUI.Tests.xUnit + + + + x86 + + + + x64 + + + + AnyCPU + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ChineseChess.GUI.Tests.xUnit/PagesTests.cs b/ChineseChess.GUI.Tests.xUnit/PagesTests.cs new file mode 100644 index 0000000..f7d6507 --- /dev/null +++ b/ChineseChess.GUI.Tests.xUnit/PagesTests.cs @@ -0,0 +1,98 @@ +using System.IO; +using System.Reflection; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Core.Contracts.Services; +using ChineseChess.GUI.Core.Services; +using ChineseChess.GUI.Models; +using ChineseChess.GUI.Services; +using ChineseChess.GUI.ViewModels; +using ChineseChess.GUI.Views; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using Xunit; + +namespace ChineseChess.GUI.Tests.XUnit +{ + public class PagesTests + { + private readonly IHost _host; + + public PagesTests() + { + var appLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + _host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration(c => c.SetBasePath(appLocation)) + .ConfigureServices(ConfigureServices) + .Build(); + } + + private void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + // Core Services + services.AddSingleton(); + + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // ViewModels + services.AddTransient(); + services.AddTransient(); + + // Configuration + services.Configure(context.Configuration.GetSection(nameof(AppConfig))); + } + + // TODO WTS: Add tests for functionality you add to SettingsViewModel. + [Fact] + public void TestSettingsViewModelCreation() + { + var vm = _host.Services.GetService(typeof(SettingsViewModel)); + Assert.NotNull(vm); + } + + [Fact] + public void TestGetSettingsPageType() + { + if (_host.Services.GetService(typeof(IPageService)) is IPageService pageService) + { + var pageType = pageService.GetPageType(typeof(SettingsViewModel).FullName); + Assert.Equal(typeof(SettingsPage), pageType); + } + else + { + Assert.True(false, $"Can't resolve {nameof(IPageService)}"); + } + } + + // TODO WTS: Add tests for functionality you add to MainViewModel. + [Fact] + public void TestMainViewModelCreation() + { + var vm = _host.Services.GetService(typeof(MainViewModel)); + Assert.NotNull(vm); + } + + [Fact] + public void TestGetMainPageType() + { + if (_host.Services.GetService(typeof(IPageService)) is IPageService pageService) + { + var pageType = pageService.GetPageType(typeof(MainViewModel).FullName); + Assert.Equal(typeof(MainPage), pageType); + } + else + { + Assert.True(false, $"Can't resolve {nameof(IPageService)}"); + } + } + } +} diff --git a/ChineseChess.GUI.Tests.xUnit/SettingsViewModelTests.cs b/ChineseChess.GUI.Tests.xUnit/SettingsViewModelTests.cs new file mode 100644 index 0000000..bcc903f --- /dev/null +++ b/ChineseChess.GUI.Tests.xUnit/SettingsViewModelTests.cs @@ -0,0 +1,66 @@ +using System; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Models; +using ChineseChess.GUI.ViewModels; + +using Microsoft.Extensions.Options; + +using Moq; + +using Xunit; + +namespace ChineseChess.GUI.Tests.XUnit +{ + public class SettingsViewModelTests + { + public SettingsViewModelTests() + { + } + + [Fact] + public void TestSettingsViewModel_SetCurrentTheme() + { + var mockThemeSelectorService = new Mock(); + mockThemeSelectorService.Setup(mock => mock.GetCurrentTheme()).Returns(AppTheme.Light); + var mockAppConfig = new Mock>(); + var mockSystemService = new Mock(); + var mockApplicationInfoService = new Mock(); + + var settingsVm = new SettingsViewModel(mockAppConfig.Object, mockThemeSelectorService.Object, mockSystemService.Object, mockApplicationInfoService.Object); + settingsVm.OnNavigatedTo(null); + + Assert.Equal(AppTheme.Light, settingsVm.Theme); + } + + [Fact] + public void TestSettingsViewModel_SetCurrentVersion() + { + var mockThemeSelectorService = new Mock(); + var mockAppConfig = new Mock>(); + var mockSystemService = new Mock(); + var mockApplicationInfoService = new Mock(); + var testVersion = new Version(1, 2, 3, 4); + mockApplicationInfoService.Setup(mock => mock.GetVersion()).Returns(testVersion); + + var settingsVm = new SettingsViewModel(mockAppConfig.Object, mockThemeSelectorService.Object, mockSystemService.Object, mockApplicationInfoService.Object); + settingsVm.OnNavigatedTo(null); + + Assert.Equal($"ChineseChess.GUI - {testVersion}", settingsVm.VersionDescription); + } + + [Fact] + public void TestSettingsViewModel_SetThemeCommand() + { + var mockThemeSelectorService = new Mock(); + var mockAppConfig = new Mock>(); + var mockSystemService = new Mock(); + var mockApplicationInfoService = new Mock(); + + var settingsVm = new SettingsViewModel(mockAppConfig.Object, mockThemeSelectorService.Object, mockSystemService.Object, mockApplicationInfoService.Object); + settingsVm.SetThemeCommand.Execute(AppTheme.Light.ToString()); + + mockThemeSelectorService.Verify(mock => mock.SetTheme(AppTheme.Light)); + } + } +} diff --git a/ChineseChess.GUI.Tests.xUnit/Tests.cs b/ChineseChess.GUI.Tests.xUnit/Tests.cs new file mode 100644 index 0000000..9493604 --- /dev/null +++ b/ChineseChess.GUI.Tests.xUnit/Tests.cs @@ -0,0 +1,13 @@ +using Xunit; + +namespace ChineseChess.GUI.Tests.XUnit +{ + // TODO WTS: Add appropriate unit tests. + public class Tests + { + [Fact] + public void TestMethod1() + { + } + } +} diff --git a/ChineseChess.GUI/App.xaml b/ChineseChess.GUI/App.xaml new file mode 100644 index 0000000..bc8f9f7 --- /dev/null +++ b/ChineseChess.GUI/App.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ChineseChess.GUI/App.xaml.cs b/ChineseChess.GUI/App.xaml.cs new file mode 100644 index 0000000..8032844 --- /dev/null +++ b/ChineseChess.GUI/App.xaml.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Reflection; +using System.Windows; +using System.Windows.Threading; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Contracts.Views; +using ChineseChess.GUI.Core.Contracts.Services; +using ChineseChess.GUI.Core.Services; +using ChineseChess.GUI.Models; +using ChineseChess.GUI.Services; +using ChineseChess.GUI.ViewModels; +using ChineseChess.GUI.Views; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace ChineseChess.GUI +{ + public partial class App : Application + { + private IHost _host; + + public T GetService() + where T : class + => _host.Services.GetService(typeof(T)) as T; + + public App() + { + } + + private async void OnStartup(object sender, StartupEventArgs e) + { + var appLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + + // For more information about .NET generic host see https://docs.microsoft.com/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.0 + _host = Host.CreateDefaultBuilder(e.Args) + .ConfigureAppConfiguration(c => + { + c.SetBasePath(appLocation); + }) + .ConfigureServices(ConfigureServices) + .Build(); + + await _host.StartAsync(); + } + + private void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + // TODO WTS: Register your services, viewmodels and pages here + + // App Host + services.AddHostedService(); + + // Activation Handlers + + // Core Services + services.AddSingleton(); + + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + // Configuration + services.Configure(context.Configuration.GetSection(nameof(AppConfig))); + } + + private async void OnExit(object sender, ExitEventArgs e) + { + await _host.StopAsync(); + _host.Dispose(); + _host = null; + } + + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + // TODO WTS: Please log and handle the exception as appropriate to your scenario + // For more info see https://docs.microsoft.com/dotnet/api/system.windows.application.dispatcherunhandledexception?view=netcore-3.0 + } + } +} diff --git a/ChineseChess.GUI/ChineseChess.GUI.csproj b/ChineseChess.GUI/ChineseChess.GUI.csproj new file mode 100644 index 0000000..339e969 --- /dev/null +++ b/ChineseChess.GUI/ChineseChess.GUI.csproj @@ -0,0 +1,38 @@ + + + WinExe + netcoreapp3.1 + ChineseChess.GUI + true + app.manifest + + + + + + + + + + + + + True + True + Resources.resx + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + Always + + + diff --git a/ChineseChess.GUI/Contracts/Activation/IActivationHandler.cs b/ChineseChess.GUI/Contracts/Activation/IActivationHandler.cs new file mode 100644 index 0000000..afc6890 --- /dev/null +++ b/ChineseChess.GUI/Contracts/Activation/IActivationHandler.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace ChineseChess.GUI.Contracts.Activation +{ + public interface IActivationHandler + { + bool CanHandle(); + + Task HandleAsync(); + } +} diff --git a/ChineseChess.GUI/Contracts/Services/IApplicationInfoService.cs b/ChineseChess.GUI/Contracts/Services/IApplicationInfoService.cs new file mode 100644 index 0000000..fa3c5c0 --- /dev/null +++ b/ChineseChess.GUI/Contracts/Services/IApplicationInfoService.cs @@ -0,0 +1,9 @@ +using System; + +namespace ChineseChess.GUI.Contracts.Services +{ + public interface IApplicationInfoService + { + Version GetVersion(); + } +} diff --git a/ChineseChess.GUI/Contracts/Services/INavigationService.cs b/ChineseChess.GUI/Contracts/Services/INavigationService.cs new file mode 100644 index 0000000..e61ef3e --- /dev/null +++ b/ChineseChess.GUI/Contracts/Services/INavigationService.cs @@ -0,0 +1,22 @@ +using System; +using System.Windows.Controls; + +namespace ChineseChess.GUI.Contracts.Services +{ + public interface INavigationService + { + event EventHandler Navigated; + + bool CanGoBack { get; } + + void Initialize(Frame shellFrame); + + bool NavigateTo(string pageKey, object parameter = null, bool clearNavigation = false); + + void GoBack(); + + void UnsubscribeNavigation(); + + void CleanNavigation(); + } +} diff --git a/ChineseChess.GUI/Contracts/Services/IPageService.cs b/ChineseChess.GUI/Contracts/Services/IPageService.cs new file mode 100644 index 0000000..678a6d2 --- /dev/null +++ b/ChineseChess.GUI/Contracts/Services/IPageService.cs @@ -0,0 +1,12 @@ +using System; +using System.Windows.Controls; + +namespace ChineseChess.GUI.Contracts.Services +{ + public interface IPageService + { + Type GetPageType(string key); + + Page GetPage(string key); + } +} diff --git a/ChineseChess.GUI/Contracts/Services/IPersistAndRestoreService.cs b/ChineseChess.GUI/Contracts/Services/IPersistAndRestoreService.cs new file mode 100644 index 0000000..c9b9cb3 --- /dev/null +++ b/ChineseChess.GUI/Contracts/Services/IPersistAndRestoreService.cs @@ -0,0 +1,9 @@ +namespace ChineseChess.GUI.Contracts.Services +{ + public interface IPersistAndRestoreService + { + void RestoreData(); + + void PersistData(); + } +} diff --git a/ChineseChess.GUI/Contracts/Services/ISystemService.cs b/ChineseChess.GUI/Contracts/Services/ISystemService.cs new file mode 100644 index 0000000..22658d1 --- /dev/null +++ b/ChineseChess.GUI/Contracts/Services/ISystemService.cs @@ -0,0 +1,7 @@ +namespace ChineseChess.GUI.Contracts.Services +{ + public interface ISystemService + { + void OpenInWebBrowser(string url); + } +} diff --git a/ChineseChess.GUI/Contracts/Services/IThemeSelectorService.cs b/ChineseChess.GUI/Contracts/Services/IThemeSelectorService.cs new file mode 100644 index 0000000..aed1073 --- /dev/null +++ b/ChineseChess.GUI/Contracts/Services/IThemeSelectorService.cs @@ -0,0 +1,15 @@ +using System; + +using ChineseChess.GUI.Models; + +namespace ChineseChess.GUI.Contracts.Services +{ + public interface IThemeSelectorService + { + void InitializeTheme(); + + void SetTheme(AppTheme theme); + + AppTheme GetCurrentTheme(); + } +} diff --git a/ChineseChess.GUI/Contracts/ViewModels/INavigationAware.cs b/ChineseChess.GUI/Contracts/ViewModels/INavigationAware.cs new file mode 100644 index 0000000..7f449c9 --- /dev/null +++ b/ChineseChess.GUI/Contracts/ViewModels/INavigationAware.cs @@ -0,0 +1,9 @@ +namespace ChineseChess.GUI.Contracts.ViewModels +{ + public interface INavigationAware + { + void OnNavigatedTo(object parameter); + + void OnNavigatedFrom(); + } +} diff --git a/ChineseChess.GUI/Contracts/Views/IShellWindow.cs b/ChineseChess.GUI/Contracts/Views/IShellWindow.cs new file mode 100644 index 0000000..e450467 --- /dev/null +++ b/ChineseChess.GUI/Contracts/Views/IShellWindow.cs @@ -0,0 +1,13 @@ +using System.Windows.Controls; + +namespace ChineseChess.GUI.Contracts.Views +{ + public interface IShellWindow + { + Frame GetNavigationFrame(); + + void ShowWindow(); + + void CloseWindow(); + } +} diff --git a/ChineseChess.GUI/Converters/EnumToBooleanConverter.cs b/ChineseChess.GUI/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000..75b1cf1 --- /dev/null +++ b/ChineseChess.GUI/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace ChineseChess.GUI.Converters +{ + public class EnumToBooleanConverter : IValueConverter + { + public Type EnumType { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is string enumString) + { + if (Enum.IsDefined(EnumType, value)) + { + var enumValue = Enum.Parse(EnumType, enumString); + + return enumValue.Equals(value); + } + } + + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is string enumString) + { + return Enum.Parse(EnumType, enumString); + } + + return null; + } + } +} diff --git a/ChineseChess.GUI/Helpers/FrameExtensions.cs b/ChineseChess.GUI/Helpers/FrameExtensions.cs new file mode 100644 index 0000000..c63852f --- /dev/null +++ b/ChineseChess.GUI/Helpers/FrameExtensions.cs @@ -0,0 +1,23 @@ +namespace System.Windows.Controls +{ + public static class FrameExtensions + { + public static object GetDataContext(this Frame frame) + { + if (frame.Content is FrameworkElement element) + { + return element.DataContext; + } + + return null; + } + + public static void CleanNavigation(this Frame frame) + { + while (frame.CanGoBack) + { + frame.RemoveBackEntry(); + } + } + } +} diff --git a/ChineseChess.GUI/Models/AppConfig.cs b/ChineseChess.GUI/Models/AppConfig.cs new file mode 100644 index 0000000..a03823c --- /dev/null +++ b/ChineseChess.GUI/Models/AppConfig.cs @@ -0,0 +1,11 @@ +namespace ChineseChess.GUI.Models +{ + public class AppConfig + { + public string ConfigurationsFolder { get; set; } + + public string AppPropertiesFileName { get; set; } + + public string PrivacyStatement { get; set; } + } +} diff --git a/ChineseChess.GUI/Models/AppTheme.cs b/ChineseChess.GUI/Models/AppTheme.cs new file mode 100644 index 0000000..d20c8b4 --- /dev/null +++ b/ChineseChess.GUI/Models/AppTheme.cs @@ -0,0 +1,9 @@ +namespace ChineseChess.GUI.Models +{ + public enum AppTheme + { + Default, + Light, + Dark + } +} diff --git a/ChineseChess.GUI/Properties/Resources.Designer.cs b/ChineseChess.GUI/Properties/Resources.Designer.cs new file mode 100644 index 0000000..4f1ae4b --- /dev/null +++ b/ChineseChess.GUI/Properties/Resources.Designer.cs @@ -0,0 +1,197 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ChineseChess.GUI.Properties { + using System; + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ChineseChess.GUI.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to wts.ItemName. + /// + public static string AppDisplayName { + get { + return ResourceManager.GetString("AppDisplayName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ShellGoBackButton. + /// + public static string ShellGoBackButton { + get { + return ResourceManager.GetString("ShellGoBackButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ShellHamburgerButtonName. + /// + public static string ShellHamburgerButtonName { + get { + return ResourceManager.GetString("ShellHamburgerButtonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Main. + /// + public static string MainPageTitle { + get { + return ResourceManager.GetString("MainPageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Main. + /// + public static string ShellMainPage { + get { + return ResourceManager.GetString("ShellMainPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings page placeholder text. Your app description goes here.. + /// + public static string SettingsPageAboutText { + get { + return ResourceManager.GetString("SettingsPageAboutText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to About this application. + /// + public static string SettingsPageAboutTitle { + get { + return ResourceManager.GetString("SettingsPageAboutTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose Theme. + /// + public static string SettingsPageChooseThemeText { + get { + return ResourceManager.GetString("SettingsPageChooseThemeText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Personalization. + /// + public static string SettingsPagePersonalizationTitle { + get { + return ResourceManager.GetString("SettingsPagePersonalizationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Privacy Statement. + /// + public static string SettingsPagePrivacyStatementText { + get { + return ResourceManager.GetString("SettingsPagePrivacyStatementText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dark. + /// + public static string SettingsPageRadioButtonDarkTheme { + get { + return ResourceManager.GetString("SettingsPageRadioButtonDarkTheme", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Light. + /// + public static string SettingsPageRadioButtonLightTheme { + get { + return ResourceManager.GetString("SettingsPageRadioButtonLightTheme", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default. + /// + public static string SettingsPageRadioButtonWindowsDefaultTheme { + get { + return ResourceManager.GetString("SettingsPageRadioButtonWindowsDefaultTheme", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string SettingsPageTitle { + get { + return ResourceManager.GetString("SettingsPageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string ShellSettingsPage { + get { + return ResourceManager.GetString("ShellSettingsPage", resourceCulture); + } + } + } +} diff --git a/ChineseChess.GUI/Properties/Resources.resx b/ChineseChess.GUI/Properties/Resources.resx new file mode 100644 index 0000000..4ef21cc --- /dev/null +++ b/ChineseChess.GUI/Properties/Resources.resx @@ -0,0 +1,147 @@ + + + + Settings + + + Settings + + + Settings page placeholder text. Your app description goes here. + + + About this application + + + Choose Theme + + + Personalization + + + Privacy Statement + + + Dark + + + Light + + + Default + + + Main + + + Main + + + Go back + + + Open or close navigation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ChineseChess.GUI + + diff --git a/ChineseChess.GUI/Services/ApplicationHostService.cs b/ChineseChess.GUI/Services/ApplicationHostService.cs new file mode 100644 index 0000000..e29fb7d --- /dev/null +++ b/ChineseChess.GUI/Services/ApplicationHostService.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using ChineseChess.GUI.Contracts.Activation; +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Contracts.Views; +using ChineseChess.GUI.ViewModels; + +using Microsoft.Extensions.Hosting; + +namespace ChineseChess.GUI.Services +{ + public class ApplicationHostService : IHostedService + { + private readonly IServiceProvider _serviceProvider; + private readonly INavigationService _navigationService; + private readonly IPersistAndRestoreService _persistAndRestoreService; + private readonly IThemeSelectorService _themeSelectorService; + private readonly IEnumerable _activationHandlers; + private IShellWindow _shellWindow; + private bool _isInitialized; + + public ApplicationHostService(IServiceProvider serviceProvider, IEnumerable activationHandlers, INavigationService navigationService, IThemeSelectorService themeSelectorService, IPersistAndRestoreService persistAndRestoreService) + { + _serviceProvider = serviceProvider; + _activationHandlers = activationHandlers; + _navigationService = navigationService; + _themeSelectorService = themeSelectorService; + _persistAndRestoreService = persistAndRestoreService; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + // Initialize services that you need before app activation + await InitializeAsync(); + + await HandleActivationAsync(); + + // Tasks after activation + await StartupAsync(); + _isInitialized = true; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _persistAndRestoreService.PersistData(); + await Task.CompletedTask; + } + + private async Task InitializeAsync() + { + if (!_isInitialized) + { + _persistAndRestoreService.RestoreData(); + _themeSelectorService.InitializeTheme(); + await Task.CompletedTask; + } + } + + private async Task StartupAsync() + { + if (!_isInitialized) + { + await Task.CompletedTask; + } + } + + private async Task HandleActivationAsync() + { + var activationHandler = _activationHandlers.FirstOrDefault(h => h.CanHandle()); + + if (activationHandler != null) + { + await activationHandler.HandleAsync(); + } + + await Task.CompletedTask; + + if (App.Current.Windows.OfType().Count() == 0) + { + // Default activation that navigates to the apps default page + _shellWindow = _serviceProvider.GetService(typeof(IShellWindow)) as IShellWindow; + _navigationService.Initialize(_shellWindow.GetNavigationFrame()); + _shellWindow.ShowWindow(); + _navigationService.NavigateTo(typeof(MainViewModel).FullName); + await Task.CompletedTask; + } + } + } +} diff --git a/ChineseChess.GUI/Services/ApplicationInfoService.cs b/ChineseChess.GUI/Services/ApplicationInfoService.cs new file mode 100644 index 0000000..02a8ae0 --- /dev/null +++ b/ChineseChess.GUI/Services/ApplicationInfoService.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics; +using System.Reflection; + +using ChineseChess.GUI.Contracts.Services; + +namespace ChineseChess.GUI.Services +{ + public class ApplicationInfoService : IApplicationInfoService + { + public ApplicationInfoService() + { + } + + public Version GetVersion() + { + // Set the app version in ChineseChess.GUI > Properties > Package > PackageVersion + string assemblyLocation = Assembly.GetExecutingAssembly().Location; + var version = FileVersionInfo.GetVersionInfo(assemblyLocation).FileVersion; + return new Version(version); + } + } +} diff --git a/ChineseChess.GUI/Services/NavigationService.cs b/ChineseChess.GUI/Services/NavigationService.cs new file mode 100644 index 0000000..93759e4 --- /dev/null +++ b/ChineseChess.GUI/Services/NavigationService.cs @@ -0,0 +1,101 @@ +using System; +using System.Windows.Controls; +using System.Windows.Navigation; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Contracts.ViewModels; + +namespace ChineseChess.GUI.Services +{ + public class NavigationService : INavigationService + { + private readonly IPageService _pageService; + private Frame _frame; + private object _lastParameterUsed; + + public event EventHandler Navigated; + + public bool CanGoBack => _frame.CanGoBack; + + public NavigationService(IPageService pageService) + { + _pageService = pageService; + } + + public void Initialize(Frame shellFrame) + { + if (_frame == null) + { + _frame = shellFrame; + _frame.Navigated += OnNavigated; + } + } + + public void UnsubscribeNavigation() + { + _frame.Navigated -= OnNavigated; + _frame = null; + } + + public void GoBack() + { + if (_frame.CanGoBack) + { + var vmBeforeNavigation = _frame.GetDataContext(); + _frame.GoBack(); + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + } + } + + public bool NavigateTo(string pageKey, object parameter = null, bool clearNavigation = false) + { + var pageType = _pageService.GetPageType(pageKey); + + if (_frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(_lastParameterUsed))) + { + _frame.Tag = clearNavigation; + var page = _pageService.GetPage(pageKey); + var navigated = _frame.Navigate(page, parameter); + if (navigated) + { + _lastParameterUsed = parameter; + var dataContext = _frame.GetDataContext(); + if (dataContext is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + } + + return navigated; + } + + return false; + } + + public void CleanNavigation() + => _frame.CleanNavigation(); + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (sender is Frame frame) + { + bool clearNavigation = (bool)frame.Tag; + if (clearNavigation) + { + frame.CleanNavigation(); + } + + var dataContext = frame.GetDataContext(); + if (dataContext is INavigationAware navigationAware) + { + navigationAware.OnNavigatedTo(e.ExtraData); + } + + Navigated?.Invoke(sender, dataContext.GetType().FullName); + } + } + } +} diff --git a/ChineseChess.GUI/Services/PageService.cs b/ChineseChess.GUI/Services/PageService.cs new file mode 100644 index 0000000..7acbd79 --- /dev/null +++ b/ChineseChess.GUI/Services/PageService.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Controls; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.ViewModels; +using ChineseChess.GUI.Views; + +using Microsoft.Toolkit.Mvvm.ComponentModel; + +namespace ChineseChess.GUI.Services +{ + public class PageService : IPageService + { + private readonly Dictionary _pages = new Dictionary(); + private readonly IServiceProvider _serviceProvider; + + public PageService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + Configure(); + Configure(); + } + + public Type GetPageType(string key) + { + Type pageType; + lock (_pages) + { + if (!_pages.TryGetValue(key, out pageType)) + { + throw new ArgumentException($"Page not found: {key}. Did you forget to call PageService.Configure?"); + } + } + + return pageType; + } + + public Page GetPage(string key) + { + var pageType = GetPageType(key); + return _serviceProvider.GetService(pageType) as Page; + } + + private void Configure() + where VM : ObservableObject + where V : Page + { + lock (_pages) + { + var key = typeof(VM).FullName; + if (_pages.ContainsKey(key)) + { + throw new ArgumentException($"The key {key} is already configured in PageService"); + } + + var type = typeof(V); + if (_pages.Any(p => p.Value == type)) + { + throw new ArgumentException($"This type is already configured with key {_pages.First(p => p.Value == type).Key}"); + } + + _pages.Add(key, type); + } + } + } +} diff --git a/ChineseChess.GUI/Services/PersistAndRestoreService.cs b/ChineseChess.GUI/Services/PersistAndRestoreService.cs new file mode 100644 index 0000000..ea052e1 --- /dev/null +++ b/ChineseChess.GUI/Services/PersistAndRestoreService.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.IO; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Core.Contracts.Services; +using ChineseChess.GUI.Models; + +using Microsoft.Extensions.Options; + +namespace ChineseChess.GUI.Services +{ + public class PersistAndRestoreService : IPersistAndRestoreService + { + private readonly IFileService _fileService; + private readonly AppConfig _appConfig; + private readonly string _localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + public PersistAndRestoreService(IFileService fileService, IOptions appConfig) + { + _fileService = fileService; + _appConfig = appConfig.Value; + } + + public void PersistData() + { + if (App.Current.Properties != null) + { + var folderPath = Path.Combine(_localAppData, _appConfig.ConfigurationsFolder); + var fileName = _appConfig.AppPropertiesFileName; + _fileService.Save(folderPath, fileName, App.Current.Properties); + } + } + + public void RestoreData() + { + var folderPath = Path.Combine(_localAppData, _appConfig.ConfigurationsFolder); + var fileName = _appConfig.AppPropertiesFileName; + var properties = _fileService.Read(folderPath, fileName); + if (properties != null) + { + foreach (DictionaryEntry property in properties) + { + App.Current.Properties.Add(property.Key, property.Value); + } + } + } + } +} diff --git a/ChineseChess.GUI/Services/SystemService.cs b/ChineseChess.GUI/Services/SystemService.cs new file mode 100644 index 0000000..0a9b9dc --- /dev/null +++ b/ChineseChess.GUI/Services/SystemService.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; + +using ChineseChess.GUI.Contracts.Services; + +namespace ChineseChess.GUI.Services +{ + public class SystemService : ISystemService + { + public SystemService() + { + } + + public void OpenInWebBrowser(string url) + { + // For more info see https://github.com/dotnet/corefx/issues/10361 + var psi = new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }; + Process.Start(psi); + } + } +} diff --git a/ChineseChess.GUI/Services/ThemeSelectorService.cs b/ChineseChess.GUI/Services/ThemeSelectorService.cs new file mode 100644 index 0000000..2b569b9 --- /dev/null +++ b/ChineseChess.GUI/Services/ThemeSelectorService.cs @@ -0,0 +1,63 @@ +using System; +using System.Windows; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Models; + +using ControlzEx.Theming; + +using MahApps.Metro.Theming; + +namespace ChineseChess.GUI.Services +{ + public class ThemeSelectorService : IThemeSelectorService + { + private const string HcDarkTheme = "pack://application:,,,/Styles/Themes/HC.Dark.Blue.xaml"; + private const string HcLightTheme = "pack://application:,,,/Styles/Themes/HC.Light.Blue.xaml"; + + public ThemeSelectorService() + { + } + + public void InitializeTheme() + { + // TODO WTS: Mahapps.Metro supports syncronization with high contrast but you have to provide custom high contrast themes + // We've added basic high contrast dictionaries for Dark and Light themes + // Please complete these themes following the docs on https://mahapps.com/docs/themes/thememanager#creating-custom-themes + ThemeManager.Current.AddLibraryTheme(new LibraryTheme(new Uri(HcDarkTheme), MahAppsLibraryThemeProvider.DefaultInstance)); + ThemeManager.Current.AddLibraryTheme(new LibraryTheme(new Uri(HcLightTheme), MahAppsLibraryThemeProvider.DefaultInstance)); + + var theme = GetCurrentTheme(); + SetTheme(theme); + } + + public void SetTheme(AppTheme theme) + { + if (theme == AppTheme.Default) + { + ThemeManager.Current.ThemeSyncMode = ThemeSyncMode.SyncAll; + ThemeManager.Current.SyncTheme(); + } + else + { + ThemeManager.Current.ThemeSyncMode = ThemeSyncMode.SyncWithHighContrast; + ThemeManager.Current.SyncTheme(); + ThemeManager.Current.ChangeTheme(Application.Current, $"{theme}.Blue", SystemParameters.HighContrast); + } + + App.Current.Properties["Theme"] = theme.ToString(); + } + + public AppTheme GetCurrentTheme() + { + if (App.Current.Properties.Contains("Theme")) + { + var themeName = App.Current.Properties["Theme"].ToString(); + Enum.TryParse(themeName, out AppTheme theme); + return theme; + } + + return AppTheme.Default; + } + } +} diff --git a/ChineseChess.GUI/Styles/MetroWindow.xaml b/ChineseChess.GUI/Styles/MetroWindow.xaml new file mode 100644 index 0000000..2ddb27e --- /dev/null +++ b/ChineseChess.GUI/Styles/MetroWindow.xaml @@ -0,0 +1,14 @@ + + + + diff --git a/ChineseChess.GUI/Styles/TextBlock.xaml b/ChineseChess.GUI/Styles/TextBlock.xaml new file mode 100644 index 0000000..e8ad872 --- /dev/null +++ b/ChineseChess.GUI/Styles/TextBlock.xaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChineseChess.GUI/Styles/Themes/HC.Dark.Blue.xaml b/ChineseChess.GUI/Styles/Themes/HC.Dark.Blue.xaml new file mode 100644 index 0000000..4daf357 --- /dev/null +++ b/ChineseChess.GUI/Styles/Themes/HC.Dark.Blue.xaml @@ -0,0 +1,40 @@ + + + + Dark.Blue + ChineseChess.GUI + Blue (Dark) + Dark + Blue + true + #FF0078D7 + + + + + + + #FFFFFFFF + #FF000000 + #FFFFFFFF + + + + + + + + + \ No newline at end of file diff --git a/ChineseChess.GUI/Styles/Themes/HC.Light.Blue.xaml b/ChineseChess.GUI/Styles/Themes/HC.Light.Blue.xaml new file mode 100644 index 0000000..2b68ae6 --- /dev/null +++ b/ChineseChess.GUI/Styles/Themes/HC.Light.Blue.xaml @@ -0,0 +1,40 @@ + + + + Light.Blue + ChineseChess.GUI + Blue (Light) + Light + Blue + true + #FF0078D7 + + + + + + + #FF000000 + #FFFFFFFF + #FF000000 + + + + + + + + + \ No newline at end of file diff --git a/ChineseChess.GUI/Styles/_FontSizes.xaml b/ChineseChess.GUI/Styles/_FontSizes.xaml new file mode 100644 index 0000000..814c112 --- /dev/null +++ b/ChineseChess.GUI/Styles/_FontSizes.xaml @@ -0,0 +1,14 @@ + + + 12 + + 14 + + 18 + + 22 + + diff --git a/ChineseChess.GUI/Styles/_Thickness.xaml b/ChineseChess.GUI/Styles/_Thickness.xaml new file mode 100644 index 0000000..49abd94 --- /dev/null +++ b/ChineseChess.GUI/Styles/_Thickness.xaml @@ -0,0 +1,30 @@ + + + + 24,0,0,0 + 24,24,24,0 + 0,24,0,0 + 24,0,24,0 + 0, 0, 0, 24 + 24,24,24,24 + + + 12, 0, 0, 0 + 12, 12, 0, 0 + 12, 0, 12, 0 + 0, 12, 0, 0 + 0, 0, 12, 0 + 0, 12, 0, 12 + 12, 12, 12, 12 + + + 8, 0, 0, 0 + 0, 8, 0, 0 + 8, 8, 8, 8 + + + 0, 4, 0, 0 + + diff --git a/ChineseChess.GUI/TemplateSelectors/MenuItemTemplateSelector.cs b/ChineseChess.GUI/TemplateSelectors/MenuItemTemplateSelector.cs new file mode 100644 index 0000000..7bd94b5 --- /dev/null +++ b/ChineseChess.GUI/TemplateSelectors/MenuItemTemplateSelector.cs @@ -0,0 +1,29 @@ +using System.Windows; +using System.Windows.Controls; + +using MahApps.Metro.Controls; + +namespace ChineseChess.GUI.TemplateSelectors +{ + public class MenuItemTemplateSelector : DataTemplateSelector + { + public DataTemplate GlyphDataTemplate { get; set; } + + public DataTemplate ImageDataTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + if (item is HamburgerMenuGlyphItem) + { + return GlyphDataTemplate; + } + + if (item is HamburgerMenuImageItem) + { + return ImageDataTemplate; + } + + return base.SelectTemplate(item, container); + } + } +} diff --git a/ChineseChess.GUI/ViewModels/MainViewModel.cs b/ChineseChess.GUI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..ec59779 --- /dev/null +++ b/ChineseChess.GUI/ViewModels/MainViewModel.cs @@ -0,0 +1,14 @@ +using System; + +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; + +namespace ChineseChess.GUI.ViewModels +{ + public class MainViewModel : ObservableObject + { + public MainViewModel() + { + } + } +} diff --git a/ChineseChess.GUI/ViewModels/SettingsViewModel.cs b/ChineseChess.GUI/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..90ec165 --- /dev/null +++ b/ChineseChess.GUI/ViewModels/SettingsViewModel.cs @@ -0,0 +1,69 @@ +using System; +using System.Windows.Input; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Contracts.ViewModels; +using ChineseChess.GUI.Models; + +using Microsoft.Extensions.Options; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; + +namespace ChineseChess.GUI.ViewModels +{ + // TODO WTS: Change the URL for your privacy policy in the appsettings.json file, currently set to https://YourPrivacyUrlGoesHere + public class SettingsViewModel : ObservableObject, INavigationAware + { + private readonly AppConfig _appConfig; + private readonly IThemeSelectorService _themeSelectorService; + private readonly ISystemService _systemService; + private readonly IApplicationInfoService _applicationInfoService; + private AppTheme _theme; + private string _versionDescription; + private ICommand _setThemeCommand; + private ICommand _privacyStatementCommand; + + public AppTheme Theme + { + get { return _theme; } + set { SetProperty(ref _theme, value); } + } + + public string VersionDescription + { + get { return _versionDescription; } + set { SetProperty(ref _versionDescription, value); } + } + + public ICommand SetThemeCommand => _setThemeCommand ??= new RelayCommand(OnSetTheme); + + public ICommand PrivacyStatementCommand => _privacyStatementCommand ??= new RelayCommand(OnPrivacyStatement); + + public SettingsViewModel(IOptions appConfig, IThemeSelectorService themeSelectorService, ISystemService systemService, IApplicationInfoService applicationInfoService) + { + _appConfig = appConfig.Value; + _themeSelectorService = themeSelectorService; + _systemService = systemService; + _applicationInfoService = applicationInfoService; + } + + public void OnNavigatedTo(object parameter) + { + VersionDescription = $"{Properties.Resources.AppDisplayName} - {_applicationInfoService.GetVersion()}"; + Theme = _themeSelectorService.GetCurrentTheme(); + } + + public void OnNavigatedFrom() + { + } + + private void OnSetTheme(string themeName) + { + var theme = (AppTheme)Enum.Parse(typeof(AppTheme), themeName); + _themeSelectorService.SetTheme(theme); + } + + private void OnPrivacyStatement() + => _systemService.OpenInWebBrowser(_appConfig.PrivacyStatement); + } +} diff --git a/ChineseChess.GUI/ViewModels/ShellViewModel.cs b/ChineseChess.GUI/ViewModels/ShellViewModel.cs new file mode 100644 index 0000000..d37985b --- /dev/null +++ b/ChineseChess.GUI/ViewModels/ShellViewModel.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; + +using ChineseChess.GUI.Contracts.Services; +using ChineseChess.GUI.Properties; + +using MahApps.Metro.Controls; + +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; + +namespace ChineseChess.GUI.ViewModels +{ + public class ShellViewModel : ObservableObject + { + private readonly INavigationService _navigationService; + private HamburgerMenuItem _selectedMenuItem; + private HamburgerMenuItem _selectedOptionsMenuItem; + private RelayCommand _goBackCommand; + private ICommand _menuItemInvokedCommand; + private ICommand _optionsMenuItemInvokedCommand; + private ICommand _loadedCommand; + private ICommand _unloadedCommand; + + public HamburgerMenuItem SelectedMenuItem + { + get { return _selectedMenuItem; } + set { SetProperty(ref _selectedMenuItem, value); } + } + + public HamburgerMenuItem SelectedOptionsMenuItem + { + get { return _selectedOptionsMenuItem; } + set { SetProperty(ref _selectedOptionsMenuItem, value); } + } + + // TODO WTS: Change the icons and titles for all HamburgerMenuItems here. + public ObservableCollection MenuItems { get; } = new ObservableCollection() + { + new HamburgerMenuGlyphItem() { Label = Resources.ShellMainPage, Glyph = "\uE8A5", TargetPageType = typeof(MainViewModel) }, + }; + + public ObservableCollection OptionMenuItems { get; } = new ObservableCollection() + { + new HamburgerMenuGlyphItem() { Label = Resources.ShellSettingsPage, Glyph = "\uE713", TargetPageType = typeof(SettingsViewModel) } + }; + + public RelayCommand GoBackCommand => _goBackCommand ??= new RelayCommand(OnGoBack, CanGoBack); + + public ICommand MenuItemInvokedCommand => _menuItemInvokedCommand ??= new RelayCommand(OnMenuItemInvoked); + + public ICommand OptionsMenuItemInvokedCommand => _optionsMenuItemInvokedCommand ??= new RelayCommand(OnOptionsMenuItemInvoked); + + public ICommand LoadedCommand => _loadedCommand ??= new RelayCommand(OnLoaded); + + public ICommand UnloadedCommand => _unloadedCommand ??= new RelayCommand(OnUnloaded); + + public ShellViewModel(INavigationService navigationService) + { + _navigationService = navigationService; + } + + private void OnLoaded() + { + _navigationService.Navigated += OnNavigated; + } + + private void OnUnloaded() + { + _navigationService.Navigated -= OnNavigated; + } + + private bool CanGoBack() + => _navigationService.CanGoBack; + + private void OnGoBack() + => _navigationService.GoBack(); + + private void OnMenuItemInvoked() + => NavigateTo(SelectedMenuItem.TargetPageType); + + private void OnOptionsMenuItemInvoked() + => NavigateTo(SelectedOptionsMenuItem.TargetPageType); + + private void NavigateTo(Type targetViewModel) + { + if (targetViewModel != null) + { + _navigationService.NavigateTo(targetViewModel.FullName); + } + } + + private void OnNavigated(object sender, string viewModelName) + { + var item = MenuItems + .OfType() + .FirstOrDefault(i => viewModelName == i.TargetPageType?.FullName); + if (item != null) + { + SelectedMenuItem = item; + } + else + { + SelectedOptionsMenuItem = OptionMenuItems + .OfType() + .FirstOrDefault(i => viewModelName == i.TargetPageType?.FullName); + } + + GoBackCommand.NotifyCanExecuteChanged(); + } + } +} diff --git a/ChineseChess.GUI/Views/MainPage.xaml b/ChineseChess.GUI/Views/MainPage.xaml new file mode 100644 index 0000000..1e84b6a --- /dev/null +++ b/ChineseChess.GUI/Views/MainPage.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/ChineseChess.GUI/Views/MainPage.xaml.cs b/ChineseChess.GUI/Views/MainPage.xaml.cs new file mode 100644 index 0000000..03d94fe --- /dev/null +++ b/ChineseChess.GUI/Views/MainPage.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +using ChineseChess.GUI.ViewModels; + +namespace ChineseChess.GUI.Views +{ + public partial class MainPage : Page + { + public MainPage(MainViewModel viewModel) + { + InitializeComponent(); + DataContext = viewModel; + } + } +} diff --git a/ChineseChess.GUI/Views/SettingsPage.xaml b/ChineseChess.GUI/Views/SettingsPage.xaml new file mode 100644 index 0000000..b266adf --- /dev/null +++ b/ChineseChess.GUI/Views/SettingsPage.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChineseChess.GUI/Views/SettingsPage.xaml.cs b/ChineseChess.GUI/Views/SettingsPage.xaml.cs new file mode 100644 index 0000000..375059f --- /dev/null +++ b/ChineseChess.GUI/Views/SettingsPage.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +using ChineseChess.GUI.ViewModels; + +namespace ChineseChess.GUI.Views +{ + public partial class SettingsPage : Page + { + public SettingsPage(SettingsViewModel viewModel) + { + InitializeComponent(); + DataContext = viewModel; + } + } +} diff --git a/ChineseChess.GUI/Views/ShellWindow.xaml b/ChineseChess.GUI/Views/ShellWindow.xaml new file mode 100644 index 0000000..71adfe7 --- /dev/null +++ b/ChineseChess.GUI/Views/ShellWindow.xaml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChineseChess.GUI/Views/ShellWindow.xaml.cs b/ChineseChess.GUI/Views/ShellWindow.xaml.cs new file mode 100644 index 0000000..6fc4acc --- /dev/null +++ b/ChineseChess.GUI/Views/ShellWindow.xaml.cs @@ -0,0 +1,27 @@ +using System.Windows.Controls; + +using ChineseChess.GUI.Contracts.Views; +using ChineseChess.GUI.ViewModels; + +using MahApps.Metro.Controls; + +namespace ChineseChess.GUI.Views +{ + public partial class ShellWindow : MetroWindow, IShellWindow + { + public ShellWindow(ShellViewModel viewModel) + { + InitializeComponent(); + DataContext = viewModel; + } + + public Frame GetNavigationFrame() + => shellFrame; + + public void ShowWindow() + => Show(); + + public void CloseWindow() + => Close(); + } +} diff --git a/ChineseChess.GUI/WTS.ProjectConfig.xml b/ChineseChess.GUI/WTS.ProjectConfig.xml new file mode 100644 index 0000000..55fd5dd --- /dev/null +++ b/ChineseChess.GUI/WTS.ProjectConfig.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/ChineseChess.GUI/app.manifest b/ChineseChess.GUI/app.manifest new file mode 100644 index 0000000..d1b636a --- /dev/null +++ b/ChineseChess.GUI/app.manifest @@ -0,0 +1,11 @@ + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + \ No newline at end of file diff --git a/ChineseChess.GUI/appsettings.json b/ChineseChess.GUI/appsettings.json new file mode 100644 index 0000000..11ec1ba --- /dev/null +++ b/ChineseChess.GUI/appsettings.json @@ -0,0 +1,7 @@ +{ + "AppConfig": { + "configurationsFolder": "ChineseChess.GUI\\Configurations", + "appPropertiesFileName": "AppProperties.json", + "privacyStatement": "https://YourPrivacyUrlGoesHere/" + } +} diff --git a/ChineseChess.UI/App.config b/ChineseChess.UI/App.config deleted file mode 100644 index 88fa402..0000000 --- a/ChineseChess.UI/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ChineseChess.UI/App.xaml b/ChineseChess.UI/App.xaml deleted file mode 100644 index ea2515f..0000000 --- a/ChineseChess.UI/App.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/ChineseChess.UI/App.xaml.cs b/ChineseChess.UI/App.xaml.cs deleted file mode 100644 index 94815ff..0000000 --- a/ChineseChess.UI/App.xaml.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; - -namespace ChineseChess.UI -{ - /// - /// App.xaml 的交互逻辑 - /// - public partial class App : Application - { - } -} diff --git a/ChineseChess.UI/ChineseChess.UI.csproj b/ChineseChess.UI/ChineseChess.UI.csproj deleted file mode 100644 index d6d3259..0000000 --- a/ChineseChess.UI/ChineseChess.UI.csproj +++ /dev/null @@ -1,104 +0,0 @@ - - - - - Debug - AnyCPU - {92AB0092-581C-4518-916D-FFC5084282FB} - WinExe - ChineseChess.UI - ChineseChess.UI - v4.5.2 - 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 4 - true - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - 4.0 - - - - - - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - App.xaml - Code - - - MainWindow.xaml - Code - - - - - Code - - - True - True - Resources.resx - - - True - Settings.settings - True - - - ResXFileCodeGenerator - Resources.Designer.cs - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - - - - {6e2fa057-9341-496b-bd68-ec3e4ce3e18a} - ChineseChess.Core - - - - \ No newline at end of file diff --git a/ChineseChess.UI/MainWindow.xaml b/ChineseChess.UI/MainWindow.xaml deleted file mode 100644 index 7818bbf..0000000 --- a/ChineseChess.UI/MainWindow.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/ChineseChess.UI/MainWindow.xaml.cs b/ChineseChess.UI/MainWindow.xaml.cs deleted file mode 100644 index d3be992..0000000 --- a/ChineseChess.UI/MainWindow.xaml.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - -namespace ChineseChess.UI -{ - /// - /// MainWindow.xaml 的交互逻辑 - /// - public partial class MainWindow : Window - { - public MainWindow() - { - InitializeComponent(); - } - } -} diff --git a/ChineseChess.UI/Properties/AssemblyInfo.cs b/ChineseChess.UI/Properties/AssemblyInfo.cs deleted file mode 100644 index 8d121d6..0000000 --- a/ChineseChess.UI/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Windows; - -// 有关程序集的一般信息由以下 -// 控制。更改这些特性值可修改 -// 与程序集关联的信息。 -[assembly: AssemblyTitle("ChineseChess.UI")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ChineseChess.UI")] -[assembly: AssemblyCopyright("Copyright © 2021")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// 将 ComVisible 设置为 false 会使此程序集中的类型 -//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 -//请将此类型的 ComVisible 特性设置为 true。 -[assembly: ComVisible(false)] - -//若要开始生成可本地化的应用程序,请设置 -//.csproj 文件中的 CultureYouAreCodingWith -//例如,如果您在源文件中使用的是美国英语, -//使用的是美国英语,请将 设置为 en-US。 然后取消 -//对以下 NeutralResourceLanguage 特性的注释。 更新 -//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。 - -//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] - - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //主题特定资源词典所处位置 - //(未在页面中找到资源时使用, - //或应用程序资源字典中找到时使用) - ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置 - //(未在页面中找到资源时使用, - //、应用程序或任何主题专用资源字典中找到时使用) -)] - - -// 程序集的版本信息由下列四个值组成: -// -// 主版本 -// 次版本 -// 生成号 -// 修订号 -// -//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 -//通过使用 "*",如下所示: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ChineseChess.UI/Properties/Resources.Designer.cs b/ChineseChess.UI/Properties/Resources.Designer.cs deleted file mode 100644 index b6638a9..0000000 --- a/ChineseChess.UI/Properties/Resources.Designer.cs +++ /dev/null @@ -1,70 +0,0 @@ -//------------------------------------------------------------------------------ -// -// 此代码由工具生成。 -// 运行时版本: 4.0.30319.42000 -// -// 对此文件的更改可能导致不正确的行为,如果 -// 重新生成代码,则所做更改将丢失。 -// -//------------------------------------------------------------------------------ - - -namespace ChineseChess.UI.Properties -{ - /// - /// 强类型资源类,用于查找本地化字符串等。 - /// - // 此类是由 StronglyTypedResourceBuilder - // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 - // 若要添加或删除成员,请编辑 .ResX 文件,然后重新运行 ResGen - // (以 /str 作为命令选项),或重新生成 VS 项目。 - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// 返回此类使用的缓存 ResourceManager 实例。 - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ChineseChess.UI.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// 重写当前线程的 CurrentUICulture 属性,对 - /// 使用此强类型资源类的所有资源查找执行重写。 - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - } -} diff --git a/ChineseChess.UI/Properties/Resources.resx b/ChineseChess.UI/Properties/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/ChineseChess.UI/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/ChineseChess.UI/Properties/Settings.Designer.cs b/ChineseChess.UI/Properties/Settings.Designer.cs deleted file mode 100644 index 51bff2c..0000000 --- a/ChineseChess.UI/Properties/Settings.Designer.cs +++ /dev/null @@ -1,29 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - - -namespace ChineseChess.UI.Properties -{ - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} diff --git a/ChineseChess.UI/Properties/Settings.settings b/ChineseChess.UI/Properties/Settings.settings deleted file mode 100644 index 033d7a5..0000000 --- a/ChineseChess.UI/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/ChineseChess.sln b/ChineseChess.sln index 08ff48a..9a82c90 100644 --- a/ChineseChess.sln +++ b/ChineseChess.sln @@ -3,24 +3,72 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31129.286 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChineseChess.UI", "ChineseChess.UI\ChineseChess.UI.csproj", "{92AB0092-581C-4518-916D-FFC5084282FB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChineseChess.Core", "ChineseChess.Core\ChineseChess.Core.csproj", "{6E2FA057-9341-496B-BD68-EC3E4CE3E18A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChineseChess.Core", "ChineseChess.Core\ChineseChess.Core.csproj", "{6E2FA057-9341-496B-BD68-EC3E4CE3E18A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChineseChess.GUI", "ChineseChess.GUI\ChineseChess.GUI.csproj", "{0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChineseChess.GUI.Core", "ChineseChess.GUI.Core\ChineseChess.GUI.Core.csproj", "{FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChineseChess.GUI.Tests.xUnit", "ChineseChess.GUI.Tests.xUnit\ChineseChess.GUI.Tests.xUnit.csproj", "{649553A0-4005-4383-BBE5-E601198A49A7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {92AB0092-581C-4518-916D-FFC5084282FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92AB0092-581C-4518-916D-FFC5084282FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92AB0092-581C-4518-916D-FFC5084282FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92AB0092-581C-4518-916D-FFC5084282FB}.Release|Any CPU.Build.0 = Release|Any CPU {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Debug|x64.ActiveCfg = Debug|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Debug|x64.Build.0 = Debug|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Debug|x86.ActiveCfg = Debug|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Debug|x86.Build.0 = Debug|Any CPU {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Release|Any CPU.Build.0 = Release|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Release|x64.ActiveCfg = Release|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Release|x64.Build.0 = Release|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Release|x86.ActiveCfg = Release|Any CPU + {6E2FA057-9341-496B-BD68-EC3E4CE3E18A}.Release|x86.Build.0 = Release|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Debug|x64.Build.0 = Debug|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Debug|x86.Build.0 = Debug|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Release|Any CPU.Build.0 = Release|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Release|x64.ActiveCfg = Release|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Release|x64.Build.0 = Release|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Release|x86.ActiveCfg = Release|Any CPU + {0B8FD863-5EBC-48F0-9A7F-9604C1185F0B}.Release|x86.Build.0 = Release|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Debug|x64.Build.0 = Debug|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Debug|x86.Build.0 = Debug|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Release|Any CPU.Build.0 = Release|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Release|x64.ActiveCfg = Release|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Release|x64.Build.0 = Release|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Release|x86.ActiveCfg = Release|Any CPU + {FF15C546-6BA2-4DA8-8BB6-70C4ABB2B8E1}.Release|x86.Build.0 = Release|Any CPU + {649553A0-4005-4383-BBE5-E601198A49A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {649553A0-4005-4383-BBE5-E601198A49A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {649553A0-4005-4383-BBE5-E601198A49A7}.Debug|x64.ActiveCfg = Debug|x64 + {649553A0-4005-4383-BBE5-E601198A49A7}.Debug|x64.Build.0 = Debug|x64 + {649553A0-4005-4383-BBE5-E601198A49A7}.Debug|x86.ActiveCfg = Debug|x86 + {649553A0-4005-4383-BBE5-E601198A49A7}.Debug|x86.Build.0 = Debug|x86 + {649553A0-4005-4383-BBE5-E601198A49A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {649553A0-4005-4383-BBE5-E601198A49A7}.Release|Any CPU.Build.0 = Release|Any CPU + {649553A0-4005-4383-BBE5-E601198A49A7}.Release|x64.ActiveCfg = Release|x64 + {649553A0-4005-4383-BBE5-E601198A49A7}.Release|x64.Build.0 = Release|x64 + {649553A0-4005-4383-BBE5-E601198A49A7}.Release|x86.ActiveCfg = Release|x86 + {649553A0-4005-4383-BBE5-E601198A49A7}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE