From cf6f87bea88df3f90e33dfdc5bb16f47b8716054 Mon Sep 17 00:00:00 2001 From: "Emma@Rory&" Date: Mon, 24 Jul 2023 20:57:23 +0200 Subject: Start of MRU Desktop --- .idea/.idea.MatrixRoomUtils/.idea/avalonia.xml | 13 ++ MatrixRoomUtils.Desktop/App.axaml | 10 ++ MatrixRoomUtils.Desktop/App.axaml.cs | 43 +++++++ MatrixRoomUtils.Desktop/FileStorageProvider.cs | 35 ++++++ MatrixRoomUtils.Desktop/LoginWindow.axaml | 15 +++ MatrixRoomUtils.Desktop/LoginWindow.axaml.cs | 36 ++++++ MatrixRoomUtils.Desktop/MRUDesktopConfiguration.cs | 44 +++++++ MatrixRoomUtils.Desktop/MRUStorageWrapper.cs | 132 +++++++++++++++++++++ MatrixRoomUtils.Desktop/MainWindow.axaml | 17 +++ MatrixRoomUtils.Desktop/MainWindow.axaml.cs | 41 +++++++ .../MatrixRoomUtils.Desktop.csproj | 49 ++++++++ MatrixRoomUtils.Desktop/Program.cs | 31 +++++ .../Properties/launchSettings.json | 26 ++++ MatrixRoomUtils.Desktop/RoomListEntry.axaml | 11 ++ MatrixRoomUtils.Desktop/RoomListEntry.axaml.cs | 17 +++ MatrixRoomUtils.Desktop/app.manifest | 18 +++ .../appsettings.Development.json | 13 ++ MatrixRoomUtils.Desktop/appsettings.json | 13 ++ MatrixRoomUtils.sln | 12 ++ 19 files changed, 576 insertions(+) create mode 100644 .idea/.idea.MatrixRoomUtils/.idea/avalonia.xml create mode 100644 MatrixRoomUtils.Desktop/App.axaml create mode 100644 MatrixRoomUtils.Desktop/App.axaml.cs create mode 100644 MatrixRoomUtils.Desktop/FileStorageProvider.cs create mode 100644 MatrixRoomUtils.Desktop/LoginWindow.axaml create mode 100644 MatrixRoomUtils.Desktop/LoginWindow.axaml.cs create mode 100644 MatrixRoomUtils.Desktop/MRUDesktopConfiguration.cs create mode 100644 MatrixRoomUtils.Desktop/MRUStorageWrapper.cs create mode 100644 MatrixRoomUtils.Desktop/MainWindow.axaml create mode 100644 MatrixRoomUtils.Desktop/MainWindow.axaml.cs create mode 100644 MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj create mode 100644 MatrixRoomUtils.Desktop/Program.cs create mode 100644 MatrixRoomUtils.Desktop/Properties/launchSettings.json create mode 100644 MatrixRoomUtils.Desktop/RoomListEntry.axaml create mode 100644 MatrixRoomUtils.Desktop/RoomListEntry.axaml.cs create mode 100644 MatrixRoomUtils.Desktop/app.manifest create mode 100644 MatrixRoomUtils.Desktop/appsettings.Development.json create mode 100644 MatrixRoomUtils.Desktop/appsettings.json diff --git a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml b/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml new file mode 100644 index 0000000..ae7a314 --- /dev/null +++ b/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/MatrixRoomUtils.Desktop/App.axaml b/MatrixRoomUtils.Desktop/App.axaml new file mode 100644 index 0000000..9c99838 --- /dev/null +++ b/MatrixRoomUtils.Desktop/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/MatrixRoomUtils.Desktop/App.axaml.cs b/MatrixRoomUtils.Desktop/App.axaml.cs new file mode 100644 index 0000000..3dfcdee --- /dev/null +++ b/MatrixRoomUtils.Desktop/App.axaml.cs @@ -0,0 +1,43 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using MatrixRoomUtils.Core.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace MatrixRoomUtils.Desktop; + +public partial class App : Application { + public IHost host { get; set; } + + public override void Initialize() { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() { + host = Host.CreateDefaultBuilder().ConfigureServices((ctx, services) => { + services.AddScoped(); + services.AddScoped(x => + new( + cacheStorageProvider: new FileStorageProvider(x.GetService().CacheStoragePath), + dataStorageProvider: new FileStorageProvider(x.GetService().CacheStoragePath) + ) + ); + services.AddRoryLibMatrixServices(); + // foreach (var commandClass in new ClassCollector().ResolveFromAllAccessibleAssemblies()) { + // Console.WriteLine($"Adding command {commandClass.Name}"); + // services.AddScoped(typeof(ICommand), commandClass); + // } + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(this); + }).UseConsoleLifetime().Build(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + var scopeFac = host.Services.GetService(); + var scope = scopeFac.CreateScope(); + desktop.MainWindow = scope.ServiceProvider.GetRequiredService(); + } + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/MatrixRoomUtils.Desktop/FileStorageProvider.cs b/MatrixRoomUtils.Desktop/FileStorageProvider.cs new file mode 100644 index 0000000..36025eb --- /dev/null +++ b/MatrixRoomUtils.Desktop/FileStorageProvider.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using MatrixRoomUtils.Core.Extensions; +using MatrixRoomUtils.Core.Interfaces.Services; +using Microsoft.Extensions.Logging; + +namespace MatrixRoomUtils.Desktop; + +public class FileStorageProvider : IStorageProvider { + private readonly ILogger _logger; + + public string TargetPath { get; } + + /// + /// Creates a new instance of . + /// + /// + public FileStorageProvider(string targetPath) { + new Logger(new LoggerFactory()).LogInformation("test"); + Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}"); + TargetPath = targetPath; + if(!Directory.Exists(targetPath)) { + Directory.CreateDirectory(targetPath); + } + } + + public async Task SaveObjectAsync(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), ObjectExtensions.ToJson(value)); + + public async Task LoadObjectAsync(string key) => JsonSerializer.Deserialize(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); + + public async Task ObjectExistsAsync(string key) => File.Exists(Path.Join(TargetPath, key)); + + public async Task> GetAllKeysAsync() => Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList(); + + public async Task DeleteObjectAsync(string key) => File.Delete(Path.Join(TargetPath, key)); +} \ No newline at end of file diff --git a/MatrixRoomUtils.Desktop/LoginWindow.axaml b/MatrixRoomUtils.Desktop/LoginWindow.axaml new file mode 100644 index 0000000..d61bfd3 --- /dev/null +++ b/MatrixRoomUtils.Desktop/LoginWindow.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/MatrixRoomUtils.Desktop/LoginWindow.axaml.cs b/MatrixRoomUtils.Desktop/LoginWindow.axaml.cs new file mode 100644 index 0000000..1f31b05 --- /dev/null +++ b/MatrixRoomUtils.Desktop/LoginWindow.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace MatrixRoomUtils.Desktop; + +public partial class LoginWindow : Window { + private readonly MRUStorageWrapper _storage; + + public LoginWindow(MRUStorageWrapper storage) { + _storage = storage; + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() { + AvaloniaXamlLoader.Load(this); + } + + public string Username { get; set; } + public string Password { get; set; } + // ReSharper disable once AsyncVoidMethod + private async void Login(object? sender, RoutedEventArgs e) { + var res = await _storage.Login(Username.Split(':')[1], Username.Split(':')[0][1..], Password); + if (res is not null) { + await _storage.AddToken(res); + Close(); + } + else { + Password = ""; + } + } +} diff --git a/MatrixRoomUtils.Desktop/MRUDesktopConfiguration.cs b/MatrixRoomUtils.Desktop/MRUDesktopConfiguration.cs new file mode 100644 index 0000000..d321591 --- /dev/null +++ b/MatrixRoomUtils.Desktop/MRUDesktopConfiguration.cs @@ -0,0 +1,44 @@ +using System.Collections; +using ArcaneLibs.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MatrixRoomUtils.Desktop; + +public class MRUDesktopConfiguration { + private static ILogger _logger; + + public MRUDesktopConfiguration(ILogger logger, IConfiguration config, HostBuilderContext host) { + _logger = logger; + logger.LogInformation($"Loading configuration for environment: {host.HostingEnvironment.EnvironmentName}..."); + config.GetSection("MRUDesktop").Bind(this); + DataStoragePath = ExpandPath(DataStoragePath); + CacheStoragePath = ExpandPath(CacheStoragePath); + } + + public string DataStoragePath { get; set; } = ""; + public string CacheStoragePath { get; set; } = ""; + + private static string ExpandPath(string path, bool retry = true) { + _logger.LogInformation($"Expanding path `{path}`"); + + if (path.StartsWith("~")) { + path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), path[1..]); + } + + Environment.GetEnvironmentVariables().Cast().OrderByDescending(x => x.Key.ToString().Length).ToList().ForEach(x => { + path = path.Replace($"${x.Key}", x.Value.ToString()); + }); + + _logger.LogInformation($"Expanded path to `{path}`"); + int tries = 0; + while(retry && path.ContainsAnyOf("~$".Split())) { + if(tries++ > 100) + throw new Exception($"Path `{path}` contains unrecognised environment variables"); + path = ExpandPath(path, false); + } + + return path; + } +} diff --git a/MatrixRoomUtils.Desktop/MRUStorageWrapper.cs b/MatrixRoomUtils.Desktop/MRUStorageWrapper.cs new file mode 100644 index 0000000..27403dc --- /dev/null +++ b/MatrixRoomUtils.Desktop/MRUStorageWrapper.cs @@ -0,0 +1,132 @@ +using MatrixRoomUtils.Core; +using MatrixRoomUtils.Core.Responses; +using MatrixRoomUtils.Core.Services; + +namespace MatrixRoomUtils.Desktop; + +public class MRUStorageWrapper { + private readonly TieredStorageService _storageService; + private readonly HomeserverProviderService _homeserverProviderService; + + public MRUStorageWrapper( + TieredStorageService storageService, + HomeserverProviderService homeserverProviderService + ) { + _storageService = storageService; + _homeserverProviderService = homeserverProviderService; + } + + public async Task?> GetAllTokens() { + if(!await _storageService.DataStorageProvider.ObjectExistsAsync("mru.tokens")) { + return null; + } + return await _storageService.DataStorageProvider.LoadObjectAsync>("mru.tokens") ?? + new List(); + } + + public async Task GetCurrentToken() { + if(!await _storageService.DataStorageProvider.ObjectExistsAsync("token")) { + return null; + } + var currentToken = await _storageService.DataStorageProvider.LoadObjectAsync("token"); + var allTokens = await GetAllTokens(); + if (allTokens is null or { Count: 0 }) { + await SetCurrentToken(null); + return null; + } + + if (currentToken is null) { + await SetCurrentToken(currentToken = allTokens[0]); + } + + if (!allTokens.Any(x => x.AccessToken == currentToken.AccessToken)) { + await SetCurrentToken(currentToken = allTokens[0]); + } + + return currentToken; + } + + public async Task AddToken(LoginResponse loginResponse) { + var tokens = await GetAllTokens(); + if (tokens == null) { + tokens = new List(); + } + + tokens.Add(loginResponse); + await _storageService.DataStorageProvider.SaveObjectAsync("mru.tokens", tokens); + } + + private async Task GetCurrentSession() { + var token = await GetCurrentToken(); + if (token == null) { + return null; + } + + return await _homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken); + } + + public async Task GetCurrentSessionOrPrompt() { + AuthenticatedHomeServer? session = null; + + try { + //catch if the token is invalid + session = await GetCurrentSession(); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_UNKNOWN_TOKEN") { + var token = await GetCurrentToken(); + // _navigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken); + return null; + } + + throw; + } + + if (session is null) { + // _navigationManager.NavigateTo("/Login"); + var wnd = new LoginWindow(this); + wnd.Show(); + while (wnd.IsVisible) await Task.Delay(100); + session = await GetCurrentSession(); + } + + return session; + } + + public class Settings { + public DeveloperSettings DeveloperSettings { get; set; } = new(); + } + + public class DeveloperSettings { + public bool EnableLogViewers { get; set; } = false; + public bool EnableConsoleLogging { get; set; } = true; + public bool EnablePortableDevtools { get; set; } = false; + } + + public async Task RemoveToken(LoginResponse auth) { + var tokens = await GetAllTokens(); + if (tokens == null) { + return; + } + + tokens.RemoveAll(x => x.AccessToken == auth.AccessToken); + await _storageService.DataStorageProvider.SaveObjectAsync("mru.tokens", tokens); + } + + public async Task SetCurrentToken(LoginResponse? auth) { + _storageService.DataStorageProvider.SaveObjectAsync("token", auth); + } + + public async Task Login(string homeserver, string username, string password) { + try { + return await _homeserverProviderService.Login(homeserver, username, password); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_FORBIDDEN") { + return null; + } + + throw; + } + } +} diff --git a/MatrixRoomUtils.Desktop/MainWindow.axaml b/MatrixRoomUtils.Desktop/MainWindow.axaml new file mode 100644 index 0000000..bc01bee --- /dev/null +++ b/MatrixRoomUtils.Desktop/MainWindow.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/MatrixRoomUtils.Desktop/MainWindow.axaml.cs b/MatrixRoomUtils.Desktop/MainWindow.axaml.cs new file mode 100644 index 0000000..41e0888 --- /dev/null +++ b/MatrixRoomUtils.Desktop/MainWindow.axaml.cs @@ -0,0 +1,41 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace MatrixRoomUtils.Desktop; + +public partial class MainWindow : Window { + private readonly ILogger _logger; + private readonly MRUStorageWrapper _storageWrapper; + private readonly MRUDesktopConfiguration _configuration; + + public MainWindow(ILogger logger, IServiceScopeFactory scopeFactory) { + _logger = logger; + _configuration = scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + _storageWrapper = scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + _logger.LogInformation("Initialising MainWindow"); + + InitializeComponent(); + + _logger.LogInformation("Cache location: " + _configuration.CacheStoragePath); + _logger.LogInformation("Data location: " + _configuration.DataStoragePath); + + + for (int i = 0; i < 100; i++) { + roomList.Children.Add(new RoomListEntry()); + } + } + + // ReSharper disable once AsyncVoidMethod + protected override async void OnLoaded(RoutedEventArgs e) { + _logger.LogInformation("async onloaded override"); + var hs = await _storageWrapper.GetCurrentSessionOrPrompt(); + base.OnLoaded(e); + } + + // public Command + // protected void LoadedCommand() { + // _logger.LogInformation("async command"); + // } +} diff --git a/MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj b/MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj new file mode 100644 index 0000000..5b6d3f6 --- /dev/null +++ b/MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj @@ -0,0 +1,49 @@ + + + WinExe + net7.0 + enable + true + app.manifest + true + + preview + enable + true + true + true + true + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + diff --git a/MatrixRoomUtils.Desktop/Program.cs b/MatrixRoomUtils.Desktop/Program.cs new file mode 100644 index 0000000..74ab579 --- /dev/null +++ b/MatrixRoomUtils.Desktop/Program.cs @@ -0,0 +1,31 @@ +using Avalonia; +using Microsoft.Extensions.Hosting; +using Tmds.DBus.Protocol; + +namespace MatrixRoomUtils.Desktop; + +internal class Program { + private static IHost appHost; + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static async Task Main(string[] args) { + try { + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + catch (DBusException e) { } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/MatrixRoomUtils.Desktop/Properties/launchSettings.json b/MatrixRoomUtils.Desktop/Properties/launchSettings.json new file mode 100644 index 0000000..997e294 --- /dev/null +++ b/MatrixRoomUtils.Desktop/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Default": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + + } + }, + "Development": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Local config": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Local" + } + } + } +} diff --git a/MatrixRoomUtils.Desktop/RoomListEntry.axaml b/MatrixRoomUtils.Desktop/RoomListEntry.axaml new file mode 100644 index 0000000..c80ef2f --- /dev/null +++ b/MatrixRoomUtils.Desktop/RoomListEntry.axaml @@ -0,0 +1,11 @@ + + + + + + diff --git a/MatrixRoomUtils.Desktop/RoomListEntry.axaml.cs b/MatrixRoomUtils.Desktop/RoomListEntry.axaml.cs new file mode 100644 index 0000000..490316d --- /dev/null +++ b/MatrixRoomUtils.Desktop/RoomListEntry.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media.Imaging; + +namespace MatrixRoomUtils.Desktop; + +public partial class RoomListEntry : UserControl { + public RoomListEntry() { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) { + base.OnLoaded(e); + RoomName.Content = "asdf"; + RoomIcon.Source = new Bitmap("/home/root@Rory/giphy.gif"); + } +} diff --git a/MatrixRoomUtils.Desktop/app.manifest b/MatrixRoomUtils.Desktop/app.manifest new file mode 100644 index 0000000..35ffb0d --- /dev/null +++ b/MatrixRoomUtils.Desktop/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/MatrixRoomUtils.Desktop/appsettings.Development.json b/MatrixRoomUtils.Desktop/appsettings.Development.json new file mode 100644 index 0000000..20b09a7 --- /dev/null +++ b/MatrixRoomUtils.Desktop/appsettings.Development.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "MRUDesktop": { + "DataStoragePath": "mru-desktop/data", + "CacheStoragePath": "mru-desktop/cache" + } +} diff --git a/MatrixRoomUtils.Desktop/appsettings.json b/MatrixRoomUtils.Desktop/appsettings.json new file mode 100644 index 0000000..4164e87 --- /dev/null +++ b/MatrixRoomUtils.Desktop/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "MRUDesktop": { + "DataStoragePath": "~/.local/share/mru-desktop", + "CacheStoragePath": "~/.cache/mru-desktop" + } +} diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln index a29cf89..ff16a2b 100755 --- a/MatrixRoomUtils.sln +++ b/MatrixRoomUtils.sln @@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.Web.Server" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.Bot", "MatrixRoomUtils.Bot\MatrixRoomUtils.Bot.csproj", "{B397700A-4ABB-4CAF-8DB8-06E01F44514B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.DebugDataValidationApi", "MatrixRoomUtils.DebugDataValidationApi\MatrixRoomUtils.DebugDataValidationApi.csproj", "{FB0CF653-FD25-4701-9477-1E80221346DB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.Desktop", "MatrixRoomUtils.Desktop\MatrixRoomUtils.Desktop.csproj", "{27C08A4F-5AF0-4C2C-AFCB-050E3388C116}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +40,13 @@ Global {B397700A-4ABB-4CAF-8DB8-06E01F44514B}.Debug|Any CPU.Build.0 = Debug|Any CPU {B397700A-4ABB-4CAF-8DB8-06E01F44514B}.Release|Any CPU.ActiveCfg = Release|Any CPU {B397700A-4ABB-4CAF-8DB8-06E01F44514B}.Release|Any CPU.Build.0 = Release|Any CPU + {FB0CF653-FD25-4701-9477-1E80221346DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB0CF653-FD25-4701-9477-1E80221346DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB0CF653-FD25-4701-9477-1E80221346DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB0CF653-FD25-4701-9477-1E80221346DB}.Release|Any CPU.Build.0 = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal -- cgit 1.5.1