diff --git a/CHANGELOG.md b/CHANGELOG.md index b249c72..5e30005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # GameVault App Changelog +## 1.17.0 +Recommended Gamevault Server Version: `v15.0.0` +### Changes + +- Replaced setup wizard with a new login window +- Introduced multi-profile support: if no user profile is configured, the demo user is used by default +- Login window now dynamically adapts to the server configuration +- Upgraded all connections to use OAuth 2.0 authentication flow +- Added support for login and registration via configured identity providers (SSO) +- Implemented pending activation state in the login process +- Added options for logout from this device and logout from all devices +- Introduced support for multiple root directories, including selection during download +- Improved image cache performance +- Added support for additional request headers +- New game settings: default parameters for Un/Installer +- Cloud Saves: root path is now included in the generated config.yaml +- Offline cache now auto-renews when outdated +- Added visual indicator on the install game card when an update is available +- Implemented skeleton loading animations in the community tab to better indicate loading states +- Added "Go to Game" button after installation is complete +- Pressing F5 in the Library now also refreshes the list of installed games +- Build temporary offline cache if it does not exist, so that you can still see your installed games in offline mode even if you have deleted the offline cache or it is corrupted. +- Copy button for own users API key in the user settings +- Bug fix: extraction time remaining now displays correctly +- Bug fix: Duplicate entries when typing in the library search + ## 1.16.1 Recommended Gamevault Server Version: `v14.1.0` ### Changes diff --git a/gamevault/App.xaml.cs b/gamevault/App.xaml.cs index 132e7b6..c1b655b 100644 --- a/gamevault/App.xaml.cs +++ b/gamevault/App.xaml.cs @@ -43,7 +43,7 @@ public static App Instance } } #endregion - public static bool ShowToastMessage = true; + public static bool HideToSystemTray = true; public static bool IsWindowsPackage = false; public static CommandOptions? CommandLineOptions { get; internal set; } = null; @@ -51,68 +51,57 @@ public static App Instance private NotifyIcon m_Icon; private JumpList jumpList; - private GameTimeTracker m_gameTimeTracker; + protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); AnalyticsHelper.Instance.InitHeartBeat(); AnalyticsHelper.Instance.RegisterGlobalEvents(); - AnalyticsHelper.Instance.SendCustomEvent(CustomAnalyticsEventKeys.APP_INITIALIZED, AnalyticsHelper.Instance.GetSysInfo()); + AnalyticsHelper.Instance.SendCustomEvent(CustomAnalyticsEventKeys.APP_INITIALIZED, AnalyticsHelper.Instance.GetSysInfo()); } private async void Application_Startup(object sender, StartupEventArgs e) { Application.Current.DispatcherUnhandledException += new DispatcherUnhandledExceptionEventHandler(AppDispatcherUnhandledException); -#if DEBUG - AppFilePath.InitDebugPaths(); - CreateDirectories(); - RestoreTheme(); - await CacheHelper.OptimizeCache(); -#else + try { - CreateDirectories(); - RestoreTheme(); - UpdateWindow updateWindow = new UpdateWindow(); - updateWindow.ShowDialog(); + LoginWindow loginWindow = new LoginWindow(); + bool? result = loginWindow.ShowDialog(); + if (result == null || result == false) + Shutdown(); + loginWindow = null; +#if WINDOWS + InitNotifyIcon(); + InitJumpList(); +#endif } catch (Exception ex) { LogUnhandledException(ex); - //m_StoreHelper.NoInternetException(); } -#endif - await LoginManager.Instance.StartupLogin(); - await LoginManager.Instance.PhalcodeLogin(true); - - AnalyticsHelper.Instance.SendCustomEvent(CustomAnalyticsEventKeys.USER_SETTINGS, AnalyticsHelper.Instance.PrepareSettingsForAnalytics()); - m_gameTimeTracker = new GameTimeTracker(); - await m_gameTimeTracker.Start(); + //AnalyticsHelper.Instance.SendCustomEvent(CustomAnalyticsEventKeys.USER_SETTINGS, AnalyticsHelper.Instance.PrepareSettingsForAnalytics()); - bool startMinimizedByPreferences = false; - bool startMinimizedByCLI = false; + // bool startMinimizedByPreferences = false; + // bool startMinimizedByCLI = false; - if ((CommandLineOptions?.Minimized).HasValue) - startMinimizedByCLI = CommandLineOptions!.Minimized!.Value; - else if (SettingsViewModel.Instance.BackgroundStart) - startMinimizedByPreferences = true; + // if ((CommandLineOptions?.Minimized).HasValue) + // startMinimizedByCLI = CommandLineOptions!.Minimized!.Value; + // else if (SettingsViewModel.Instance.BackgroundStart) + // startMinimizedByPreferences = true; - if (!startMinimizedByPreferences && MainWindow == null) - { - MainWindow = new MainWindow(); - MainWindow.Show(); - } - if (startMinimizedByCLI && MainWindow != null) - { - MainWindow.Hide(); - } -#if WINDOWS - InitNotifyIcon(); - InitJumpList(); -#endif - // After the app is created and most things are instantiated, handle any special command line stuff + // if (!startMinimizedByPreferences && MainWindow == null) + // { + // MainWindow = new MainWindow(); + // MainWindow.Show(); + // } + // if (startMinimizedByCLI && MainWindow != null) + // { + // MainWindow.Hide(); + // } + // // After the app is created and most things are instantiated, handle any special command line stuff if (PipeServiceHandler.Instance != null) { // Strictly speaking we should hold up all commands until we have a confirmed login & setup is complete, but for now we'll assume that auto-login has worked @@ -136,10 +125,10 @@ public void LogUnhandledException(Exception e) Application.Current.DispatcherUnhandledException -= new DispatcherUnhandledExceptionEventHandler(AppDispatcherUnhandledException); string errorMessage = $"MESSAGE:\n{e.Message}\nINNER_EXCEPTION:{(e.InnerException != null ? "" + e.InnerException.Message : null)}"; string errorStackTrace = $"STACK_TRACE:\n{(e.StackTrace != null ? "" + e.StackTrace : null)}"; - string errorLogPath = $"{AppFilePath.ErrorLog}\\GameVault_ErrorLog_{DateTime.Now.ToString("yyyyMMddHHmmssfff")}.txt"; + string errorLogPath = $"{ProfileManager.ErrorLogDir}\\GameVault_ErrorLog_{DateTime.Now.ToString("yyyyMMddHHmmssfff")}.txt"; if (!File.Exists(errorLogPath)) { - Directory.CreateDirectory(AppFilePath.ErrorLog); + Directory.CreateDirectory(ProfileManager.ErrorLogDir); File.Create(errorLogPath).Close(); } File.WriteAllText(errorLogPath, errorMessage + "\n" + errorStackTrace); @@ -189,6 +178,7 @@ public void SetJumpListGames() { try { + jumpList.JumpItems.RemoveRange(5, jumpList.JumpItems.Count - 5);// Remove all previous games. Now we add the current game list var lastGames = InstallViewModel.Instance.InstalledGames.Take(5).ToArray(); foreach (var game in lastGames) { @@ -210,31 +200,22 @@ public void SetJumpListGames() } catch { } } - private void RestoreTheme() + public void ResetJumpListGames() { try { - string currentThemeString = Preferences.Get(AppConfigKey.Theme, AppFilePath.UserFile, true); - if (currentThemeString != string.Empty) - { - ThemeItem currentTheme = JsonSerializer.Deserialize(currentThemeString)!; - - if (App.Current.Resources.MergedDictionaries[0].Source.OriginalString != currentTheme.Path) - { - App.Current.Resources.MergedDictionaries[0] = new ResourceDictionary() { Source = new Uri(currentTheme.Path) }; - } - } + jumpList.JumpItems.RemoveRange(5, jumpList.JumpItems.Count - 5); + jumpList.Apply(); } catch { } } private void NotifyIcon_DoubleClick(Object sender, EventArgs e) { - if (MainWindow == null) - { - MainWindow = new MainWindow(); - MainWindow.Show(); - } - else if (MainWindow.IsVisible == false) + + if (MainWindow == null || MainWindow.GetType() != typeof(MainWindow)) + return; + + if (MainWindow.IsVisible == false) { MainWindow.Show(); } @@ -244,6 +225,16 @@ private void NotifyIcon_DoubleClick(Object sender, EventArgs e) } } + public void SetTheme(string themeUri) + { + App.Current.Resources.MergedDictionaries[0] = new ResourceDictionary() { Source = new Uri(themeUri) }; + App.Current.Resources.MergedDictionaries[1] = new ResourceDictionary() { Source = new Uri("pack://application:,,,/gamevault;component/Resources/Assets/Base.xaml") }; + } + public void ResetToDefaultTheme() + { + App.Current.Resources.MergedDictionaries[0] = new ResourceDictionary() { Source = new Uri("pack://application:,,,/gamevault;component/Resources/Assets/Themes/ThemeDefaultDark.xaml") }; + App.Current.Resources.MergedDictionaries[1] = new ResourceDictionary() { Source = new Uri("pack://application:,,,/gamevault;component/Resources/Assets/Base.xaml") }; + } private async void NotifyIcon_Exit_Click(Object sender, EventArgs e) { await ExitApp(); @@ -283,7 +274,7 @@ private void Navigate_Tab_Click(Object sender, EventArgs e) private void ShutdownApp() { - ShowToastMessage = false; + HideToSystemTray = false; ProcessShepherd.Instance.KillAllChildProcesses(); if (m_Icon != null) { @@ -292,25 +283,6 @@ private void ShutdownApp() } Shutdown(); } - private void CreateDirectories() - { - if (!Directory.Exists(AppFilePath.ImageCache)) - { - Directory.CreateDirectory(AppFilePath.ImageCache); - } - if (!Directory.Exists(AppFilePath.ConfigDir)) - { - Directory.CreateDirectory(AppFilePath.ConfigDir); - } - if (!Directory.Exists(AppFilePath.ThemesLoadDir)) - { - Directory.CreateDirectory(AppFilePath.ThemesLoadDir); - } - if (!Directory.Exists(AppFilePath.CloudSaveConfigDir)) - { - Directory.CreateDirectory(AppFilePath.CloudSaveConfigDir); - } - } public bool IsWindowActiveAndControlInFocus(MainControl control) { if (Current.MainWindow == null) @@ -318,5 +290,6 @@ public bool IsWindowActiveAndControlInFocus(MainControl control) return Current.MainWindow.IsActive && MainWindowViewModel.Instance.ActiveControlIndex == (int)control; } + } } diff --git a/gamevault/AssemblyInfo.cs b/gamevault/AssemblyInfo.cs index b222812..afcbc90 100644 --- a/gamevault/AssemblyInfo.cs +++ b/gamevault/AssemblyInfo.cs @@ -11,7 +11,7 @@ //(used if a resource is not found in the page, // app, or any theme specific resource dictionaries) )] -[assembly: AssemblyVersion("1.16.1.0")] +[assembly: AssemblyVersion("1.17.0.0")] [assembly: AssemblyCopyright(" Phalcode. All Rights Reserved.")] #if DEBUG [assembly: XmlnsDefinition("debug-mode", "Namespace")] diff --git a/gamevault/Converter/BoolToVisibilityConverter.cs b/gamevault/Converter/BoolToVisibilityConverter.cs new file mode 100644 index 0000000..8673418 --- /dev/null +++ b/gamevault/Converter/BoolToVisibilityConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace gamevault.Converter +{ + class BoolToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } +} diff --git a/gamevault/Converter/GameUpdateAvailableConverter.cs b/gamevault/Converter/GameUpdateAvailableConverter.cs new file mode 100644 index 0000000..93cfdb5 --- /dev/null +++ b/gamevault/Converter/GameUpdateAvailableConverter.cs @@ -0,0 +1,42 @@ +using gamevault.Models; +using gamevault.ViewModels; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace gamevault.Converter +{ + internal class GameUpdateAvailableConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + try + { + Game game = (Game)value; + KeyValuePair result = InstallViewModel.Instance.InstalledGames.Where(g => g.Key.ID == game.ID).FirstOrDefault(); + string execFile = Path.Combine(result.Value, "gamevault-exec"); + string installedVersion = Preferences.Get(AppConfigKey.InstalledGameVersion, execFile); + if(string.IsNullOrWhiteSpace(installedVersion)) + { + Preferences.Set(AppConfigKey.InstalledGameVersion, game.Version, execFile); + } + else if (installedVersion != game.Version) + { + return true; + } + } + catch { } + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } +} diff --git a/gamevault/Converter/ShowMappedTitleConverter.cs b/gamevault/Converter/ShowMappedTitleConverter.cs index 762d500..d30eedd 100644 --- a/gamevault/Converter/ShowMappedTitleConverter.cs +++ b/gamevault/Converter/ShowMappedTitleConverter.cs @@ -1,4 +1,5 @@ -using gamevault.Models; +using gamevault.Helper; +using gamevault.Models; using System; using System.Collections.Generic; using System.Globalization; @@ -14,7 +15,7 @@ public class ShowMappedTitleConverter : IValueConverter public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - bool showMappedTitle = Preferences.Get(AppConfigKey.ShowMappedTitle, AppFilePath.UserFile) == "1"; + bool showMappedTitle = Preferences.Get(AppConfigKey.ShowMappedTitle,LoginManager.Instance.GetUserProfile().UserConfigFile) == "1"; if (showMappedTitle && value is Game game && !string.IsNullOrWhiteSpace(game?.Metadata?.Title)) { return true; diff --git a/gamevault/Helper/AnalyticsHelper.cs b/gamevault/Helper/AnalyticsHelper.cs index 6a83328..9c971d5 100644 --- a/gamevault/Helper/AnalyticsHelper.cs +++ b/gamevault/Helper/AnalyticsHelper.cs @@ -11,11 +11,9 @@ using System.IO; using System.Linq; using System.Management; -using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; -using System.Runtime.Intrinsics.Arm; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -38,6 +36,7 @@ internal static class CustomAnalyticsEventKeys internal static string SERVER_USER_COUNT => "SERVER_USER_COUNT"; internal static string USER_SETTINGS => "USER_SETTINGS"; } + internal class AnalyticsHelper { #region Singleton @@ -59,11 +58,13 @@ public static AnalyticsHelper Instance } } #endregion + private Timer _heartBeatTimer; - private string timeZone; - private string language; - private HttpClient client; - private bool trackingEnabled = false; + private readonly string timeZone; + private readonly string language; + private readonly HttpClient client; + private readonly bool trackingEnabled = false; + internal AnalyticsHelper() { trackingEnabled = SettingsViewModel.Instance.SendAnonymousAnalytics; @@ -75,8 +76,8 @@ internal AnalyticsHelper() client = new HttpClient(); client.DefaultRequestHeaders.UserAgent.Clear(); - string plattform = IsWineRunning() ? "Linux" : "Windows NT"; - var userAgent = $"GameVault/{SettingsViewModel.Instance.Version} ({plattform})"; + string platform = IsWineRunning() ? "Linux" : "Windows NT"; + var userAgent = $"GameVault/{SettingsViewModel.Instance.Version} ({platform})"; client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); try { @@ -86,6 +87,7 @@ internal AnalyticsHelper() catch { } } + internal void InitHeartBeat() { if (!trackingEnabled) @@ -93,10 +95,12 @@ internal void InitHeartBeat() _heartBeatTimer = new Timer(HeartBeat, null, TimeSpan.Zero, TimeSpan.FromSeconds(30)); } + private void HeartBeat(object state) { SendHeartBeat(AnalyticsTargets.HB); } + internal void RegisterGlobalEvents() { if (!trackingEnabled) @@ -106,6 +110,7 @@ internal void RegisterGlobalEvents() EventManager.RegisterClassHandler(typeof(IconButton), IconButton.ClickEvent, new RoutedEventHandler(GlobalButton_Click)); } + private async void GlobalButton_Click(object sender, RoutedEventArgs e) { try @@ -119,33 +124,35 @@ private async void GlobalButton_Click(object sender, RoutedEventArgs e) } catch { } } + private string ParseMethodName(ButtonBase buttonBase) { var type = buttonBase.GetType(); - var eventHandlersStore = typeof(UIElement).GetProperty("EventHandlersStore", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var eventHandlersStoreValue = eventHandlersStore.GetValue(buttonBase, null); + var eventHandlersStore = typeof(UIElement).GetProperty("EventHandlersStore", BindingFlags.NonPublic | BindingFlags.Instance); + var eventHandlersStoreValue = eventHandlersStore?.GetValue(buttonBase); if (eventHandlersStoreValue != null) { var entriesField = eventHandlersStoreValue.GetType().GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance); - var entriesValue = entriesField.GetValue(eventHandlersStoreValue); + var entriesValue = entriesField?.GetValue(eventHandlersStoreValue); - var mapStoreField = entriesValue.GetType().GetField("_mapStore", BindingFlags.NonPublic | BindingFlags.Instance); - var mapStoreValue = mapStoreField.GetValue(entriesValue); + var mapStoreField = entriesValue?.GetType().GetField("_mapStore", BindingFlags.NonPublic | BindingFlags.Instance); + var mapStoreValue = mapStoreField?.GetValue(entriesValue); - var entry0Field = mapStoreValue.GetType().GetField("_entry0", BindingFlags.NonPublic | BindingFlags.Instance); - var entry0Value = entry0Field.GetValue(mapStoreValue); + var entry0Field = mapStoreValue?.GetType().GetField("_entry0", BindingFlags.NonPublic | BindingFlags.Instance); + var entry0Value = entry0Field?.GetValue(mapStoreValue); - var Value = entry0Value.GetType().GetField("Value").GetValue(entry0Value); + var valueField = entry0Value?.GetType().GetField("Value"); + var value = valueField?.GetValue(entry0Value); - var listStoreField = Value.GetType().GetField("_listStore", BindingFlags.NonPublic | BindingFlags.Instance); + var listStoreField = value?.GetType().GetField("_listStore", BindingFlags.NonPublic | BindingFlags.Instance); if (listStoreField == null) { string methodName = ((EventSetter)buttonBase.Style.Setters[0]).Handler.Method.Name; string className = ((EventSetter)buttonBase.Style.Setters[0]).Handler.Method.DeclaringType?.Name ?? "UnknownClass"; return $"{className}.{methodName}"; } - var listStoreValue = listStoreField.GetValue(Value); + var listStoreValue = listStoreField.GetValue(value); var loneEntryField = listStoreValue.GetType().GetField("_loneEntry", BindingFlags.NonPublic | BindingFlags.Instance); var loneEntryValue = loneEntryField.GetValue(listStoreValue); @@ -170,16 +177,27 @@ private string ParseMethodName(ButtonBase buttonBase) } return string.Empty; } + private async Task SendHeartBeat(string url) { try { var jsonContent = new StringContent(JsonSerializer.Serialize(new AnalyticsData()), Encoding.UTF8, "application/json"); - await client.PostAsync(url, jsonContent); + await client.PostAsync(url, jsonContent); } - catch (Exception e) { } + catch (Exception) + { + // swallow + } + + } + private string Truncate(string? value, int maxLength) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Length <= maxLength ? value : value.Substring(0, maxLength); } + public async Task SendPageView(UserControl page) { if (!trackingEnabled) @@ -191,9 +209,14 @@ public async Task SendPageView(UserControl page) var jsonContent = new StringContent(JsonSerializer.Serialize(new AnalyticsData() { Timezone = timeZone, CurrentPage = pageString, Language = language }), Encoding.UTF8, "application/json"); await client.PostAsync(AnalyticsTargets.LG, jsonContent); } - catch (Exception e) { } + catch (Exception) + { + // swallow + } } + + #region ** New Swetrix Error Structure ** public void SendErrorLog(Exception ex) { if (!trackingEnabled) @@ -203,128 +226,172 @@ public void SendErrorLog(Exception ex) { try { - var jsonContent = new StringContent(JsonSerializer.Serialize(new AnalyticsData() { ExceptionType = ex.GetType().ToString(), ExceptionMessage = $"Message:{ex.Message} | InnerException:{ex.InnerException?.Message} | StackTrace:{ex.StackTrace?.Substring(0, Math.Min(2000, ex.StackTrace.Length))} | Is Windows Package: {(App.IsWindowsPackage == true ? "True" : "False")} | Version: {SettingsViewModel.Instance.Version}", Timezone = timeZone, Language = language }), Encoding.UTF8, "application/json"); + // Extract first meaningful frame with file info if available + int? line = null; + int? column = null; + string? file = null; + + try + { + var st = new StackTrace(ex, true); + var frame = st.GetFrames()?.FirstOrDefault(f => f.GetFileLineNumber() > 0) ?? st.GetFrame(0); + if (frame != null) + { + line = frame.GetFileLineNumber(); + column = frame.GetFileColumnNumber(); + file = frame.GetFileName(); + } + } + catch { /* ignore parsing issues */ } + + var data = new AnalyticsData + { + Name = Truncate(ex.GetType().Name, 200), + Message = Truncate(ex.Message, 2000), + LineNo = line, + ColNo = column, + FileName = Truncate(file, 1000), + StackTrace = Truncate(ex.StackTrace, 7500), + Timezone = timeZone, + Language = language, + Metadata = new + { + InnerException = ex.InnerException?.Message, + IsWindowsPackage = App.IsWindowsPackage == true, + Version = SettingsViewModel.Instance.Version + } + }; + + var jsonContent = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json"); await client.PostAsync(AnalyticsTargets.ER, jsonContent); } - catch (Exception ex) { } + catch (Exception) + { + // swallow + } }); } + #endregion + public void SendCustomEvent(string eventName, object meta) { - if (!trackingEnabled) return; + if (!trackingEnabled) + return; Task.Run(async () => { try { var jsonContent = new StringContent(JsonSerializer.Serialize(new AnalyticsData() { Event = eventName, Metadata = meta, Timezone = timeZone, Language = language }), Encoding.UTF8, "application/json"); - HttpResponseMessage res = - await client.PostAsync(AnalyticsTargets.CU, jsonContent); + await client.PostAsync(AnalyticsTargets.CU, jsonContent); } catch { } }); } + private string? ParseUserControl(UserControl page) { switch (page) { case LibraryUserControl: - { - return "/library"; - - } + return "/library"; case DownloadsUserControl: - { - return "/downloads"; - - } + return "/downloads"; case CommunityUserControl: - { - return "/community"; - - } + return "/community"; case SettingsUserControl: - { - return "/settings"; - - } + return "/settings"; case AdminConsoleUserControl: - { - return "/admin"; - } - case Wizard: - { - return "/wizard"; - } + return "/admin"; case GameViewUserControl: - { - return "/game"; - } + return "/game"; default: - { - return null; - } + return null; } } + public object GetSysInfo() { try { var OS = new ManagementObjectSearcher("select * from Win32_OperatingSystem").Get().Cast().First(); - string os = $"{OS["Caption"]} - {OS["OSArchitecture"]} - Version.{OS["Version"]}"; os = os.Replace("NT 5.1.2600", "XP"); os = os.Replace("NT 5.2.3790", "Server 2003"); + string os = $"{OS["Caption"]} - {OS["OSArchitecture"]} - Version.{OS["Version"]}".Replace("NT 5.1.2600", "XP").Replace("NT 5.2.3790", "Server 2003"); string ram = $"{OS["TotalVisibleMemorySize"]} KB"; var CPU = new ManagementObjectSearcher("select * from Win32_Processor").Get().Cast().First(); string cpu = $"{CPU["Name"]} - {CPU["MaxClockSpeed"]} MHz - {CPU["NumberOfCores"]} Core"; - return new { app_version = SettingsViewModel.Instance.Version, hardware_os = os, hardware_ram = ram, hardware_cpu = cpu, }; + return new { app_version = SettingsViewModel.Instance.Version, hardware_os = os, hardware_ram = ram, hardware_cpu = cpu }; } catch (Exception ex) { return new { app_version = SettingsViewModel.Instance.Version, hardware_os = $"The system information could not be loaded due to an {ex.GetType().Name}" }; } } + public Dictionary PrepareSettingsForAnalytics() { try { var propertiesToExclude = new[] { "Instance", "UserName", "RootPath", "ServerUrl", "License", "RegistrationUser", "SendAnonymousAnalytics", "IgnoreList", "Themes", "CommunityThemes" }; var trimmedObject = SettingsViewModel.Instance.GetType() - .GetProperties() - .Where(prop => !propertiesToExclude.Contains(prop.Name)) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(SettingsViewModel.Instance)?.ToString()); + .GetProperties() + .Where(prop => !propertiesToExclude.Contains(prop.Name)) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(SettingsViewModel.Instance)?.ToString()); - trimmedObject.Add("HasLicence", (SettingsViewModel.Instance.License?.IsActive() == true).ToString()); + trimmedObject.Add("HasLicense", (SettingsViewModel.Instance.License?.IsActive() == true).ToString()); return trimmedObject; } catch { } return null; } + private bool IsWineRunning() { // Search for WINLOGON process int p = Process.GetProcessesByName("winlogon").Length; return p == 0; } + + #region ** DTOs & Helpers ** private class AnalyticsData { [JsonPropertyName("pid")] public string ProjectID => "N2kuL4i8qmOQ"; + // General Event Fields [JsonPropertyName("ev")] public string? Event { get; set; } + [JsonPropertyName("tz")] public string? Timezone { get; set; } + [JsonPropertyName("pg")] public string? CurrentPage { get; set; } [JsonPropertyName("lc")] public string? Language { get; set; } + [JsonPropertyName("meta")] - public object? Metadata { get; set; }//Properties of type string only - //Error + public object? Metadata { get; set; } + + // Error specific fields [JsonPropertyName("name")] - public string? ExceptionType { get; set; } + public string? Name { get; set; } + [JsonPropertyName("message")] - public string? ExceptionMessage { get; set; } + public string? Message { get; set; } + + [JsonPropertyName("lineno")] + public int? LineNo { get; set; } + + [JsonPropertyName("colno")] + public int? ColNo { get; set; } + + [JsonPropertyName("stackTrace")] + public string? StackTrace { get; set; } + + [JsonPropertyName("filename")] + public string? FileName { get; set; } } + private static class AnalyticsTargets { public static string HB => Encoding.UTF8.GetString(Convert.FromBase64String("aHR0cHM6Ly9hbmFseXRpY3MucGxhdGZvcm0ucGhhbGNvLmRlL2xvZy9oYg==")); @@ -332,5 +399,7 @@ private static class AnalyticsTargets public static string CU => Encoding.UTF8.GetString(Convert.FromBase64String("aHR0cHM6Ly9hbmFseXRpY3MucGxhdGZvcm0ucGhhbGNvLmRlL2xvZy9jdXN0b20=")); public static string ER => Encoding.UTF8.GetString(Convert.FromBase64String("aHR0cHM6Ly9hbmFseXRpY3MucGxhdGZvcm0ucGhhbGNvLmRlL2xvZy9lcnJvcg==")); } + #endregion } } + diff --git a/gamevault/Helper/GameTimeTracker.cs b/gamevault/Helper/GameTimeTracker.cs index 1b35077..3d605e5 100644 --- a/gamevault/Helper/GameTimeTracker.cs +++ b/gamevault/Helper/GameTimeTracker.cs @@ -27,18 +27,28 @@ public async Task Start() m_Timer.Elapsed += TimerCallback; m_Timer.Start(); } + public void Stop() + { + m_Timer?.Stop(); + } private void TimerCallback(object sender, ElapsedEventArgs e) { Task.Run(async () => { - string installationPath = Path.Combine(SettingsViewModel.Instance.RootPath, "GameVault\\Installations"); + //string installationPath = Path.Combine(SettingsViewModel.Instance.RootPath, "GameVault\\Installations"); - if (!Directory.Exists(installationPath)) + if (SettingsViewModel.Instance.RootDirectories.Count == 0) return; + List allDirectoriesFromRootDirectories = new List(); + foreach (DirectoryEntry dirEntry in SettingsViewModel.Instance.RootDirectories) + { + if (Directory.Exists(Path.Combine(dirEntry.Uri, "GameVault", "Installations"))) + allDirectoriesFromRootDirectories.AddRange(Directory.GetDirectories(Path.Combine(dirEntry.Uri, "GameVault", "Installations"))); + } Dictionary foundGames = new Dictionary(); - foreach (string dir in Directory.GetDirectories(installationPath)) + foreach (string dir in allDirectoriesFromRootDirectories) { var dirInf = new DirectoryInfo(dir); if (dirInf.GetFiles().Length != 0 || dirInf.GetDirectories().Length != 0) @@ -96,10 +106,10 @@ private void TimerCallback(object sender, ElapsedEventArgs e) } foreach (int gameid in gamesToCountUp) { - WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/user/{LoginManager.Instance.GetCurrentUser().ID}/game/{gameid}/increment", string.Empty); + await WebHelper.PutAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/user/{LoginManager.Instance.GetCurrentUser().ID}/game/{gameid}/increment", string.Empty); } DiscordHelper.Instance.SyncGameWithDiscordPresence(gamesToCountUp, foundGames); - await SaveGameHelper.Instance.BackupSaveGamesFromIds(gamesToCountUp); + await SaveGameHelper.Instance.BackupSaveGamesFromIds(gamesToCountUp);//Check which games are were closed and backup them } catch (Exception ex) { @@ -116,7 +126,7 @@ private bool AnyOfflineProgressToSend() { try { - return new FileInfo(AppFilePath.OfflineProgress).Length > 0; + return new FileInfo(LoginManager.Instance.GetUserProfile().OfflineProgress).Length > 0; } catch { @@ -129,33 +139,30 @@ private void SaveToOfflineProgress(List progress) { try { - string timeString = Preferences.Get(gameid.ToString(), AppFilePath.OfflineProgress, true); + string timeString = Preferences.Get(gameid.ToString(), LoginManager.Instance.GetUserProfile().OfflineProgress, true); int result = int.TryParse(timeString, out result) ? result : 0; result++; - Preferences.Set(gameid.ToString(), result, AppFilePath.OfflineProgress, true); + Preferences.Set(gameid.ToString(), result, LoginManager.Instance.GetUserProfile().OfflineProgress, true); } catch { } } } private async Task SendOfflineProgess() { - await Task.Run(() => - { - if (LoginManager.Instance.IsLoggedIn()) - { - foreach (string key in GetAllOfflineCacheKeys()) - { - try - { - string value = Preferences.Get(key, AppFilePath.OfflineProgress, true); - int.Parse(value); - WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/user/{LoginManager.Instance.GetCurrentUser().ID}/game/{key}/increment/{value}", string.Empty); - Preferences.DeleteKey(key, AppFilePath.OfflineProgress); - } - catch { } - } - } - }); + if (LoginManager.Instance.IsLoggedIn()) + { + foreach (string key in GetAllOfflineCacheKeys()) + { + try + { + string value = Preferences.Get(key, LoginManager.Instance.GetUserProfile().OfflineProgress, true); + int.Parse(value); + await WebHelper.PutAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/user/{LoginManager.Instance.GetCurrentUser().ID}/game/{key}/increment/{value}", string.Empty); + Preferences.DeleteKey(key, LoginManager.Instance.GetUserProfile().OfflineProgress); + } + catch { } + } + } } private int GetGameIdByDirectory(string dir) { @@ -171,10 +178,10 @@ private int GetGameIdByDirectory(string dir) } private string[] GetAllOfflineCacheKeys() { - if (File.Exists(AppFilePath.OfflineProgress)) + if (File.Exists(LoginManager.Instance.GetUserProfile().OfflineProgress)) { List keys = new List(); - foreach (string line in File.ReadAllLines(AppFilePath.OfflineProgress)) + foreach (string line in File.ReadAllLines(LoginManager.Instance.GetUserProfile().OfflineProgress)) { try { diff --git a/gamevault/Helper/Integrations/SaveGameHelper.cs b/gamevault/Helper/Integrations/SaveGameHelper.cs index 54f2c28..d802b81 100644 --- a/gamevault/Helper/Integrations/SaveGameHelper.cs +++ b/gamevault/Helper/Integrations/SaveGameHelper.cs @@ -63,73 +63,69 @@ internal async Task RestoreBackup(int gameId, string installationDir) if (!SettingsViewModel.Instance.CloudSaves) return CloudSaveStatus.SettingDisabled; - using (HttpClient client = new HttpClient()) + try { - try + + string installationId = GetGameInstallationId(installationDir); + string[] auth = WebHelper.GetCredentials(); + + string url = @$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}"; + using (HttpResponseMessage response = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", HttpCompletionOption.ResponseHeadersRead)) { - client.Timeout = TimeSpan.FromSeconds(15); - string installationId = GetGameInstallationId(installationDir); - string[] auth = WebHelper.GetCredentials(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{auth[0]}:{auth[1]}"))); - client.DefaultRequestHeaders.Add("User-Agent", $"GameVault/{SettingsViewModel.Instance.Version}"); - string url = @$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}"; - using (HttpResponseMessage response = await client.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", HttpCompletionOption.ResponseHeadersRead)) + response.EnsureSuccessStatusCode(); + string fileName = response.Content.Headers.ContentDisposition.FileName.Split('_')[1].Split('.')[0]; + if (fileName != installationId) { - response.EnsureSuccessStatusCode(); - string fileName = response.Content.Headers.ContentDisposition.FileName.Split('_')[1].Split('.')[0]; - if (fileName != installationId) - { - string tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(tempFolder); - string archive = Path.Combine(tempFolder, "backup.zip"); - using (Stream contentStream = await response.Content.ReadAsStreamAsync(), fileStream = new FileStream(archive, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) - { - await contentStream.CopyToAsync(fileStream); - } - - await zipHelper.ExtractArchive(archive, tempFolder); - var mappingFile = Directory.GetFiles(tempFolder, "mapping.yaml", SearchOption.AllDirectories); - string extractFolder = ""; - if (mappingFile.Length < 1) - throw new Exception("no savegame extracted"); - - extractFolder = Path.GetDirectoryName(Path.GetDirectoryName(mappingFile[0])); - PrepareConfigFile(installationDir, Path.Combine(AppFilePath.CloudSaveConfigDir, "config.yaml")); - Process process = new Process(); - ProcessShepherd.Instance.AddProcess(process); - process.StartInfo = CreateProcessHeader(); - //process.StartInfo.Arguments = $"--config {AppFilePath.CloudSaveConfigDir} restore --force --path \"{extractFolder}\""; - process.StartInfo.ArgumentList.Add("--config"); - process.StartInfo.ArgumentList.Add(AppFilePath.CloudSaveConfigDir); - process.StartInfo.ArgumentList.Add("restore"); - process.StartInfo.ArgumentList.Add("--force"); - process.StartInfo.ArgumentList.Add("--path"); - process.StartInfo.ArgumentList.Add(extractFolder); - process.Start(); - process.WaitForExit(); - ProcessShepherd.Instance.RemoveProcess(process); - Directory.Delete(tempFolder, true); - return CloudSaveStatus.RestoreSuccess; - } - else + string tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempFolder); + string archive = Path.Combine(tempFolder, "backup.zip"); + using (Stream contentStream = await response.Content.ReadAsStreamAsync(), fileStream = new FileStream(archive, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) { - return CloudSaveStatus.UpToDate; + await contentStream.CopyToAsync(fileStream); } + + await zipHelper.ExtractArchive(archive, tempFolder); + var mappingFile = Directory.GetFiles(tempFolder, "mapping.yaml", SearchOption.AllDirectories); + string extractFolder = ""; + if (mappingFile.Length < 1) + throw new Exception("no savegame extracted"); + + extractFolder = Path.GetDirectoryName(Path.GetDirectoryName(mappingFile[0])); + PrepareConfigFile(installationDir, Path.Combine(LoginManager.Instance.GetUserProfile().CloudSaveConfigDir, "config.yaml")); + Process process = new Process(); + ProcessShepherd.Instance.AddProcess(process); + process.StartInfo = CreateProcessHeader(); + process.StartInfo.ArgumentList.Add("--config"); + process.StartInfo.ArgumentList.Add(LoginManager.Instance.GetUserProfile().CloudSaveConfigDir); + process.StartInfo.ArgumentList.Add("restore"); + process.StartInfo.ArgumentList.Add("--force"); + process.StartInfo.ArgumentList.Add("--path"); + process.StartInfo.ArgumentList.Add(extractFolder); + process.Start(); + process.WaitForExit(); + ProcessShepherd.Instance.RemoveProcess(process); + Directory.Delete(tempFolder, true); + return CloudSaveStatus.RestoreSuccess; } - } - catch (Exception ex) - { - string statusCode = WebExceptionHelper.GetServerStatusCode(ex); - if (statusCode == "405") - { - MainWindowViewModel.Instance.AppBarText = CloudSaveStatus.ServerSettingDisabled; - } - else if (statusCode != "404") + else { - MainWindowViewModel.Instance.AppBarText = CloudSaveStatus.RestoreFailed; + return CloudSaveStatus.UpToDate; } } } + catch (Exception ex) + { + string statusCode = WebExceptionHelper.GetServerStatusCode(ex); + if (statusCode == "405") + { + MainWindowViewModel.Instance.AppBarText = CloudSaveStatus.ServerSettingDisabled; + } + else if (statusCode != "404") + { + MainWindowViewModel.Instance.AppBarText = CloudSaveStatus.RestoreFailed; + } + } + return CloudSaveStatus.RestoreFailed; } private string GetGameInstallationId(string installationDir) @@ -187,10 +183,14 @@ internal async Task BackupSaveGame(int gameId) var installedGame = InstallViewModel.Instance?.InstalledGames?.FirstOrDefault(g => g.Key?.ID == gameId); string gameMetadataTitle = installedGame?.Key?.Metadata?.Title ?? ""; + if (gameMetadataTitle == "") + { + gameMetadataTitle = installedGame?.Key?.Title ?? ""; + } string installationDir = installedGame?.Value ?? ""; if (gameMetadataTitle != "" && installationDir != "") { - PrepareConfigFile(installedGame?.Value!, Path.Combine(AppFilePath.CloudSaveConfigDir, "config.yaml")); + PrepareConfigFile(installedGame?.Value!, Path.Combine(LoginManager.Instance.GetUserProfile().CloudSaveConfigDir, "config.yaml")); string title = await SearchForLudusaviGameTitle(gameMetadataTitle); if (string.IsNullOrEmpty(title)) return CloudSaveStatus.BackupFailed; @@ -215,57 +215,64 @@ internal async Task BackupSaveGame(int gameId) public void PrepareConfigFile(string installationPath, string yamlPath) { string userFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var redirects = new Dictionary + + // Base configuration with redirects (always included) + var redirects = new List> + { + new Dictionary { - { "redirects", new List> - { - new Dictionary - { - { "kind", "bidirectional" }, - { "source", userFolder }, - { "target", "G:\\gamevault\\currentuser" } - }, - new Dictionary - { - { "kind", "bidirectional" }, - { "source", installationPath }, - { "target", "G:\\gamevault\\installation" } - } - } + { "kind", "bidirectional" }, + { "source", userFolder }, + { "target", "G:\\gamevault\\currentuser" } + }, + new Dictionary + { + { "kind", "bidirectional" }, + { "source", installationPath }, + { "target", "G:\\gamevault\\installation" } + } + }; + + + + var roots = new List>(); + foreach (DirectoryEntry rootPath in SettingsViewModel.Instance.RootDirectories) + { + roots.Add(new Dictionary + { + { "store", "other" }, + { "path", Path.Combine(rootPath.Uri,"GameVault","Installations") } + }); } - }; - // Simulating SettingsViewModel.Instance.CustomCloudSaveManifests - var customLudusaviManifests = SettingsViewModel.Instance.CustomCloudSaveManifests.Where(m => !string.IsNullOrWhiteSpace(m.Uri)); + // Start with base configuration (redirects and roots always included) + var yamlData = new Dictionary + { + { "redirects", redirects }, + { "roots", roots } + }; - Dictionary yamlData; + // Add manifest section if custom manifests exist (optional) + var customLudusaviManifests = SettingsViewModel.Instance.CustomCloudSaveManifests.Where(m => !string.IsNullOrWhiteSpace(m.Uri)); - if (customLudusaviManifests.Any()) // If manifests exist, merge with redirects + if (customLudusaviManifests.Any()) { var manifest = new Dictionary - { - { "enable", SettingsViewModel.Instance.UsePrimaryCloudSaveManifest }, - { "secondary", new List>() } - }; + { + { "enable", SettingsViewModel.Instance.UsePrimaryCloudSaveManifest }, + { "secondary", new List>() } + }; - foreach (LudusaviManifestEntry entry in customLudusaviManifests) + foreach (DirectoryEntry entry in customLudusaviManifests) { ((List>)manifest["secondary"]).Add(new Dictionary - { - { Uri.IsWellFormedUriString(entry.Uri, UriKind.Absolute) ? "url" : "path", entry.Uri }, - { "enable", true } - }); + { + { Uri.IsWellFormedUriString(entry.Uri, UriKind.Absolute) ? "url" : "path", entry.Uri }, + { "enable", true } + }); } - yamlData = new Dictionary - { - { "manifest", manifest }, - { "redirects", redirects["redirects"] } // Merge redirects - }; - } - else - { - yamlData = redirects; // Only redirects if no manifests + yamlData.Add("manifest", manifest); } var serializer = new SerializerBuilder() @@ -275,6 +282,7 @@ public void PrepareConfigFile(string installationPath, string yamlPath) string result = serializer.Serialize(yamlData); File.WriteAllText(yamlPath, result); } + internal async Task SearchForLudusaviGameTitle(string title) { return await Task.Run(() => @@ -284,7 +292,7 @@ internal async Task SearchForLudusaviGameTitle(string title) process.StartInfo = CreateProcessHeader(true); //process.StartInfo.Arguments = $"find \"{title}\" --fuzzy --api";//--normalized process.StartInfo.ArgumentList.Add("--config"); - process.StartInfo.ArgumentList.Add(AppFilePath.CloudSaveConfigDir); + process.StartInfo.ArgumentList.Add(LoginManager.Instance.GetUserProfile().CloudSaveConfigDir); process.StartInfo.ArgumentList.Add("find"); process.StartInfo.ArgumentList.Add(title); process.StartInfo.ArgumentList.Add("--fuzzy"); @@ -324,9 +332,9 @@ await Task.Run(() => Process process = new Process(); ProcessShepherd.Instance.AddProcess(process); process.StartInfo = CreateProcessHeader(); - //process.StartInfo.Arguments = $"--config {AppFilePath.CloudSaveConfigDir} backup --force --format \"zip\" --path \"{tempFolder}\" \"{lunusaviTitle}\""; + //process.StartInfo.Arguments = $"--config {LoginManager.Instance.GetUserProfile().CloudSaveConfigDir} backup --force --format \"zip\" --path \"{tempFolder}\" \"{lunusaviTitle}\""; process.StartInfo.ArgumentList.Add("--config"); - process.StartInfo.ArgumentList.Add(AppFilePath.CloudSaveConfigDir); + process.StartInfo.ArgumentList.Add(LoginManager.Instance.GetUserProfile().CloudSaveConfigDir); process.StartInfo.ArgumentList.Add("backup"); process.StartInfo.ArgumentList.Add("--force"); process.StartInfo.ArgumentList.Add("--format"); @@ -348,7 +356,7 @@ private async Task UploadSavegame(string saveFilePath, int gameId, string string installationId = GetGameInstallationId(installationDir); using (MemoryStream memoryStream = await FileToMemoryStreamAsync(saveFilePath)) { - await WebHelper.UploadFileAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", memoryStream, "x.zip", new KeyValuePair("X-Installation-Id", installationId)); + await WebHelper.UploadFileAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", memoryStream, "x.zip", new RequestHeader[] { new RequestHeader() { Name = "X-Installation-Id", Value = installationId } }); } } catch @@ -396,7 +404,7 @@ public struct CloudSaveStatus public static string ServerSettingDisabled = "Cloud Saves are not enabled on this Server"; public static string Offline = "Can not synchronize the cloud saves, because you are offline"; } - public class LudusaviManifestEntry + public class DirectoryEntry { public string Uri { get; set; } } diff --git a/gamevault/Helper/LoginManager.cs b/gamevault/Helper/LoginManager.cs index 62567fe..6aed09e 100644 --- a/gamevault/Helper/LoginManager.cs +++ b/gamevault/Helper/LoginManager.cs @@ -1,24 +1,21 @@ using gamevault.Models; using gamevault.ViewModels; +using gamevault.Windows; using IdentityModel.Client; using IdentityModel.OidcClient; -using Microsoft.IdentityModel.Tokens; +using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.Wpf; using System; -using System.Collections.Generic; -using System.Dynamic; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; -using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Timers; -using System.Windows.Documents; +using System.Windows; using System.Windows.Threading; -using Windows.Media.Protection.PlayReady; -using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel; namespace gamevault.Helper { @@ -27,7 +24,8 @@ public enum LoginState Success, Error, Unauthorized, - Forbidden + Forbidden, + NotActivated } internal class LoginManager { @@ -50,6 +48,7 @@ public static LoginManager Instance } } #endregion + private UserProfile userProfile { get; set; } private User? m_User { get; set; } private LoginState m_LoginState { get; set; } private string m_LoginMessage { get; set; } @@ -66,80 +65,188 @@ public LoginState GetState() { return m_LoginState; } - public string GetLoginMessage() + public string GetServerLoginResponseMessage() { return m_LoginMessage; } public void SwitchToOfflineMode() { MainWindowViewModel.Instance.OnlineState = System.Windows.Visibility.Visible; - m_User = null; + m_User = null; } - public async Task StartupLogin() + public UserProfile GetUserProfile() + { + return userProfile; + } + public void SetUserProfile(UserProfile profile) + { + userProfile = profile; + } + + public async Task Login(UserProfile profile, string username, string password) { LoginState state = LoginState.Success; - if (IsLoggedIn()) return; - User? user = await Task.Run(() => + bool sessionTokenReuseFailed = false; + WebHelper.SetCredentials(profile.ServerUrl, username, password); + try + { + if (await TryReuseSessionToken(profile)) + { + string result = await WebHelper.GetAsync(@$"{profile.ServerUrl}/api/users/me"); + m_User = JsonSerializer.Deserialize(result); + m_LoginState = LoginState.Success; + return m_LoginState; + } + sessionTokenReuseFailed = true; + } + catch (Exception ex) { sessionTokenReuseFailed = true; } + if (sessionTokenReuseFailed) { try { - WebHelper.SetCredentials(Preferences.Get(AppConfigKey.Username, AppFilePath.UserFile), Preferences.Get(AppConfigKey.Password, AppFilePath.UserFile, true)); - string result = WebHelper.GetRequest(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/me", 5000); - return JsonSerializer.Deserialize(result); + string result = await WebHelper.GetAsync(@$"{profile.ServerUrl}/api/users/me"); + m_User = JsonSerializer.Deserialize(result); + Preferences.Set(AppConfigKey.SessionToken, WebHelper.GetRefreshToken(), profile.UserConfigFile, true); } catch (Exception ex) { string code = WebExceptionHelper.GetServerStatusCode(ex); state = DetermineLoginState(code); - if (state == LoginState.Error) + if (state != LoginState.Success) + { m_LoginMessage = WebExceptionHelper.TryGetServerMessage(ex); - - return null; + } } - }); - m_User = user; + } m_LoginState = state; - InitOnlineTimer(); + return state; } - public async Task ManualLogin(string username, string password) + public async Task SSOLogin(UserProfile profile) { LoginState state = LoginState.Success; - User? user = await Task.Run(() => + bool sessionTokenReuseFailed = false; + WebHelper.SetCredentials(profile.ServerUrl, "", ""); + try { - try + if (await TryReuseSessionToken(profile)) { - WebHelper.OverrideCredentials(username, password); - string result = WebHelper.GetRequest(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/me"); - return JsonSerializer.Deserialize(result); + string result = await WebHelper.GetAsync(@$"{profile.ServerUrl}/api/users/me"); + m_User = JsonSerializer.Deserialize(result); + return LoginState.Success; } - catch (Exception ex) + sessionTokenReuseFailed = true; + } + catch (Exception ex) { sessionTokenReuseFailed = true; } + + + if (sessionTokenReuseFailed) + { + Window win = new Window() { - string code = WebExceptionHelper.GetServerStatusCode(ex); - state = DetermineLoginState(code); - if (state == LoginState.Error) - m_LoginMessage = WebExceptionHelper.TryGetServerMessage(ex); + Height = 600, + Width = 800, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + }; + WebView2 uiWebView = new WebView2(); + bool windowClosedByCompleted = false; - return null; - } - }); - m_User = user; - m_LoginState = state; - return state; + win.Content = uiWebView; + win.Show(); // Window is shown but may be hidden based on Visibility property + + var env = await CoreWebView2Environment.CreateAsync(null, profile.WebConfigDir); + await uiWebView.EnsureCoreWebView2Async(env); + uiWebView?.CoreWebView2?.CookieManager.DeleteAllCookies(); + // Create a TaskCompletionSource to await the navigation completion + var tcs = new TaskCompletionSource(); + + win.Closing += (s, e) => + { + // Only set the result if it hasn't been set already + if (!windowClosedByCompleted) + { + uiWebView.Dispose(); + m_LoginState = LoginState.Error; + m_LoginMessage = "Authentication canceled by user."; + tcs.SetResult(LoginState.Error); + } + }; + + uiWebView.NavigationCompleted += async (s, e) => + { + string content = await uiWebView.CoreWebView2.ExecuteScriptAsync("document.body.innerText"); + + try + { + string result = System.Text.Json.JsonSerializer.Deserialize(content); + var authResponse = JsonSerializer.Deserialize(result); + string accessToken = authResponse?.AccessToken; + string refreshToken = authResponse?.RefreshToken; + + if (!string.IsNullOrEmpty(accessToken)) + { + windowClosedByCompleted = true; + win.Close(); + uiWebView.Dispose(); + + WebHelper.InjectTokens(accessToken, refreshToken); + Preferences.Set(AppConfigKey.SessionToken, refreshToken, profile.UserConfigFile, true); + //Actual Login with gathered Tokens + try + { + string userResult = await WebHelper.GetAsync(@$"{profile.ServerUrl}/api/users/me"); + m_User = JsonSerializer.Deserialize(userResult); + } + catch (Exception ex) + { + string code = WebExceptionHelper.GetServerStatusCode(ex); + state = DetermineLoginState(code); + if (state == LoginState.Error) + { + m_LoginMessage = WebExceptionHelper.TryGetServerMessage(ex); + } + } + m_LoginState = state; + tcs.SetResult(state); + } + } + catch (Exception ex) + { + // Only set the result if it's a valid auth response + // Otherwise, let the navigation continue + } + }; + uiWebView.CoreWebView2.Navigate($"{profile.ServerUrl}/api/auth/oauth2/login"); + // Wait for the navigation to complete and tokens to be processed + return await tcs.Task; + } + return LoginState.Error; } - public void Logout() + private async Task TryReuseSessionToken(UserProfile profile) { - m_User = null; - m_LoginState = LoginState.Error; - WebHelper.OverrideCredentials(string.Empty, string.Empty); - MainWindowViewModel.Instance.Community.Reset(); + string sessionToken = Preferences.Get(AppConfigKey.SessionToken, profile.UserConfigFile, true); + if (!string.IsNullOrWhiteSpace(sessionToken)) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{profile.ServerUrl}/api/auth/refresh") { Content = new StringContent("", Encoding.UTF8, "application/json") }; + request.Headers.Add("Authorization", $"Bearer {sessionToken}"); + var response = await WebHelper.BaseSendRequest(request); + var authResponse = JsonSerializer.Deserialize(response); + string accessToken = authResponse?.AccessToken; + string refreshToken = authResponse?.RefreshToken; + WebHelper.InjectTokens(accessToken, refreshToken); + Preferences.Set(AppConfigKey.SessionToken, refreshToken, profile.UserConfigFile, true); + return true; + } + return false; } + private WpfEmbeddedBrowser wpfEmbeddedBrowser = null; - public async Task PhalcodeLogin(bool startHidden = false) + public async Task PhalcodeLogin(bool startHidden = false) { - string? provider = Preferences.Get(AppConfigKey.Phalcode1, AppFilePath.UserFile, true); + string returnMessage = ""; + string? provider = Preferences.Get(AppConfigKey.Phalcode1, ProfileManager.ProfileConfigFile, true); if (startHidden && provider == "") { - return; + return ""; } wpfEmbeddedBrowser = new WpfEmbeddedBrowser(startHidden); var options = new OidcClientOptions() @@ -182,7 +289,6 @@ public async Task PhalcodeLogin(bool startHidden = false) loginResult = await _oidcClient.LoginAsync(new LoginRequest() { FrontChannelExtraParameters = param }); timer.Stop(); - //string token = loginResult.AccessToken; username = loginResult.User == null ? null : loginResult.User.Identity.Name; SettingsViewModel.Instance.License = new PhalcodeProduct() { UserName = username }; @@ -193,21 +299,20 @@ public async Task PhalcodeLogin(bool startHidden = false) { provider = "phalcode"; } - Preferences.Set(AppConfigKey.Phalcode1, provider, AppFilePath.UserFile, true); + Preferences.Set(AppConfigKey.Phalcode1, provider, ProfileManager.ProfileConfigFile, true); } catch (System.Exception exception) { timer.Stop(); - MainWindowViewModel.Instance.AppBarText = exception.Message; + returnMessage = exception.Message; } if (loginResult != null && loginResult.IsError) { if (loginResult.Error == "UserCancel") { - loginResult.Error = "Phalcode Sign-in aborted. You can choose to sign in later in the settings."; - Preferences.DeleteKey(AppConfigKey.Phalcode1.ToString(), AppFilePath.UserFile); + returnMessage = "Phalcode Sign-in aborted. You can choose to sign in later in the settings."; + Preferences.DeleteKey(AppConfigKey.Phalcode1.ToString(), ProfileManager.ProfileConfigFile); } - MainWindowViewModel.Instance.AppBarText = loginResult.Error; } //#####GET LISENCE OBJECT##### @@ -231,49 +336,76 @@ public async Task PhalcodeLogin(bool startHidden = false) PhalcodeProduct[] licenseData = JsonSerializer.Deserialize(licenseResult); if (licenseData.Length == 0) { - return; + return ""; } licenseData[0].UserName = username; SettingsViewModel.Instance.License = licenseData[0]; - Preferences.Set(AppConfigKey.Phalcode2, JsonSerializer.Serialize(SettingsViewModel.Instance.License), AppFilePath.UserFile, true); + Preferences.Set(AppConfigKey.Phalcode2, JsonSerializer.Serialize(SettingsViewModel.Instance.License), ProfileManager.ProfileConfigFile, true); } } catch (Exception ex) { - //MainWindowViewModel.Instance.AppBarText = ex.Message; + returnMessage = ex.Message; try { - string data = Preferences.Get(AppConfigKey.Phalcode2, AppFilePath.UserFile, true); + string data = Preferences.Get(AppConfigKey.Phalcode2, ProfileManager.ProfileConfigFile, true); SettingsViewModel.Instance.License = JsonSerializer.Deserialize(data); } catch { - return; + return ""; } } try { if (!SettingsViewModel.Instance.License.IsActive()) { - Preferences.DeleteKey(AppConfigKey.Theme, AppFilePath.UserFile); + Preferences.DeleteKey(AppConfigKey.Theme, LoginManager.Instance.GetUserProfile()?.UserConfigFile); + } + else + { + returnMessage = ""; } } catch { } - return; + return returnMessage; } public void PhalcodeLogout() { SettingsViewModel.Instance.License = new PhalcodeProduct(); - Preferences.DeleteKey(AppConfigKey.Phalcode1.ToString(), AppFilePath.UserFile); - Preferences.DeleteKey(AppConfigKey.Phalcode2.ToString(), AppFilePath.UserFile); - Preferences.DeleteKey(AppConfigKey.Theme, AppFilePath.UserFile); + Preferences.DeleteKey(AppConfigKey.Phalcode1.ToString(), ProfileManager.ProfileConfigFile); + Preferences.DeleteKey(AppConfigKey.Phalcode2.ToString(), ProfileManager.ProfileConfigFile); + Preferences.DeleteKey(AppConfigKey.Theme, LoginManager.Instance.GetUserProfile()?.UserConfigFile); try { - Directory.Delete(AppFilePath.WebConfigDir, true); + Directory.Delete(ProfileManager.PhalcodeDir, true); //wpfEmbeddedBrowser.ClearAllCookies(); } catch (Exception ex) { } } + + + public async Task Register(LoginUser user) + { + try + { + string userObject = JsonSerializer.Serialize(new User { Username = user.Username, Password = user.Password, EMail = user.EMail, FirstName = user.FirstName, LastName = user.LastName, BirthDate = user.BirthDate }); + string newUser = await WebHelper.BasePostAsync($"{user.ServerUrl}/api/auth/basic/register", userObject); + User newUserObject = JsonSerializer.Deserialize(newUser); + if (newUserObject!.Activated != true) + { + return LoginState.NotActivated; + } + return LoginState.Success; + } + catch (Exception ex) + { + m_LoginMessage = WebExceptionHelper.TryGetServerMessage(ex); + return LoginState.Error; + } + } + + private LoginState DetermineLoginState(string code) { switch (code) @@ -286,32 +418,64 @@ private LoginState DetermineLoginState(string code) { return LoginState.Forbidden; } + case "406": + { + return LoginState.NotActivated; + } } return LoginState.Error; } - private void InitOnlineTimer() + public void InitOnlineTimer() { if (onlineTimer == null) { onlineTimer = new Timer(30000);//30 Seconds onlineTimer.AutoReset = true; onlineTimer.Elapsed += CheckOnlineStatus; - onlineTimer.Start(); + } + onlineTimer.Start(); + if (!IsLoggedIn()) + { + SwitchToOfflineMode(); + MainWindowViewModel.Instance.AppBarText = "No connection to the server. You are now in offline mode."; + } + } + public void StopOnlineTimer() + { + if (onlineTimer != null) + { + onlineTimer.Stop(); } } private async void CheckOnlineStatus(object sender, EventArgs e) { try { - string serverResonse = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/health"); if (!IsLoggedIn()) { - await StartupLogin(); + bool isLoggedInWithSSO = Preferences.Get(AppConfigKey.IsLoggedInWithSSO, GetUserProfile().UserConfigFile) == "1"; + if (!isLoggedInWithSSO) + { + string[] credencials = WebHelper.GetCredentials(); + if (await Login(GetUserProfile(), credencials[0], credencials[1]) != LoginState.Success) + SwitchToOfflineMode(); + } + else + { + if (await SSOLogin(GetUserProfile()) != LoginState.Success) + SwitchToOfflineMode(); + } if (IsLoggedIn()) { MainWindowViewModel.Instance.OnlineState = System.Windows.Visibility.Collapsed; + MainWindowViewModel.Instance.AppBarText = "Connected to the server. You’re back online."; } } + else + { + await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/status"); + MainWindowViewModel.Instance.OnlineState = System.Windows.Visibility.Collapsed; + } } catch (Exception ex) { diff --git a/gamevault/Helper/Media/CacheHelper.cs b/gamevault/Helper/Media/CacheHelper.cs index 8a9208f..cde2819 100644 --- a/gamevault/Helper/Media/CacheHelper.cs +++ b/gamevault/Helper/Media/CacheHelper.cs @@ -4,6 +4,7 @@ using LiveChartsCore.Drawing; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -24,7 +25,7 @@ internal static async Task CreateOfflineCacheAsync(Game game) { string serializedObject = JsonSerializer.Serialize(game); string compressedObject = StringCompressor.CompressString(serializedObject); - Preferences.Set(game.ID.ToString(), compressedObject, AppFilePath.OfflineCache); + Preferences.Set(game.ID.ToString(), compressedObject, LoginManager.Instance.GetUserProfile().OfflineCache); } catch { } } @@ -127,15 +128,15 @@ internal static async Task EnsureImageCacheForGame(Game game) await TaskQueue.Instance.WaitForProcessToFinish(game.Metadata.Cover.ID); } - string backGroundCacheFile = $"{AppFilePath.ImageCache}/gbg/{game.ID}.{game.Metadata.Background.ID}"; - string boxArtCacheFile = $"{AppFilePath.ImageCache}/gbox/{game.ID}.{game.Metadata.Cover.ID}"; - if (!Directory.Exists($"{AppFilePath.ImageCache}/gbg")) + string backGroundCacheFile = $"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbg/{game.ID}.{game.Metadata.Background.ID}"; + string boxArtCacheFile = $"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbox/{game.ID}.{game.Metadata.Cover.ID}"; + if (!Directory.Exists($"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbg")) { - Directory.CreateDirectory($"{AppFilePath.ImageCache}/gbg"); + Directory.CreateDirectory($"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbg"); } - if (!Directory.Exists($"{AppFilePath.ImageCache}/gbox")) + if (!Directory.Exists($"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbox")) { - Directory.CreateDirectory($"{AppFilePath.ImageCache}/gbox"); + Directory.CreateDirectory($"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbox"); } if (!File.Exists(backGroundCacheFile)) @@ -175,35 +176,46 @@ internal static async Task OptimizeCache() { await Task.Run(() => { - double maxHeight = SystemParameters.FullPrimaryScreenHeight / 2; - var files = Directory.GetFiles(AppFilePath.ImageCache, "*.*", SearchOption.AllDirectories); - foreach (string file in files) + try { - try + double maxHeight = SystemParameters.FullPrimaryScreenHeight / 2; + string imageOptimizationMetadata = Path.Combine(LoginManager.Instance.GetUserProfile().ImageCacheDir, "optmetadata"); + + bool lastOptimizedSet = DateTime.TryParse(Preferences.Get(AppConfigKey.LastImageOptimization, imageOptimizationMetadata), out DateTime lastOptimized); + var files = Directory.GetFiles(LoginManager.Instance.GetUserProfile().ImageCacheDir, "*.*", SearchOption.AllDirectories); + foreach (string file in files) { - var image = new FileInfo(file); - if (image.Length > 0) + try { - if (file.Contains("uico")) + var image = new FileInfo(file); + if (!lastOptimizedSet || lastOptimized < image.LastWriteTime) { - if (GifHelper.IsGif(file)) + if (image.Length > 0) { - uint maxGifHeightWidth = 400; - GifHelper.OptimizeGIF(file, maxGifHeightWidth); + if (file.Contains("uico")) + { + if (GifHelper.IsGif(file)) + { + uint maxGifHeightWidth = 400; + GifHelper.OptimizeGIF(file, maxGifHeightWidth); + image.Refresh(); + continue; + } + } + ResizeImage(file, Convert.ToUInt32(maxHeight)); image.Refresh(); - continue; + } + else + { + File.Delete(file); } } - ResizeImage(file, Convert.ToUInt32(maxHeight)); - image.Refresh(); - } - else - { - File.Delete(file); } + catch { } } - catch { } + Preferences.Set(AppConfigKey.LastImageOptimization, DateTime.Now.ToString(), imageOptimizationMetadata); } + catch { } }); } internal static async Task CreateHashAsync(string input) @@ -224,13 +236,24 @@ internal static async Task CreateHashAsync(string input) internal static Dictionary GetImageCacheForGame(Game game) { Dictionary imageCache = new Dictionary(); - string cachePath = AppFilePath.ImageCache; + string cachePath = LoginManager.Instance.GetUserProfile().ImageCacheDir; var boxArt = Directory.GetFiles(Path.Combine(cachePath, "gbox").Replace("/", "\\"), $"{game.ID}.*").FirstOrDefault(); var background = Directory.GetFiles(Path.Combine(cachePath, "gbg").Replace("/", "\\"), $"{game.ID}.*").FirstOrDefault(); imageCache.Add("gbox", boxArt); imageCache.Add("gbg", background); return imageCache; } + internal static string GetUserProfileAvatarPath(UserProfile profile) + { + if (Directory.Exists(Path.Combine(profile.ImageCacheDir, "uico"))) + { + if (int.TryParse(Preferences.Get(AppConfigKey.UserID, profile.UserConfigFile), out int userId)) + { + return Directory.GetFiles(Path.Combine(profile.ImageCacheDir, "uico"), $"{userId}.*", SearchOption.AllDirectories).FirstOrDefault() ?? ""; + } + } + return ""; + } private static void ResizeImage(string path, uint maxHeight) { using (var imageMagick = new MagickImage(path)) diff --git a/gamevault/Helper/PasswordBoxAttachedProperties.cs b/gamevault/Helper/PasswordBoxAttachedProperties.cs new file mode 100644 index 0000000..c0dc03f --- /dev/null +++ b/gamevault/Helper/PasswordBoxAttachedProperties.cs @@ -0,0 +1,232 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace gamevault.Helper +{ + public static class PasswordBoxAttachedProperties + { + // Property to enable/disable password mode + public static readonly DependencyProperty IsPasswordProperty = + DependencyProperty.RegisterAttached("IsPassword", typeof(bool), typeof(PasswordBoxAttachedProperties), + new PropertyMetadata(false, OnIsPasswordChanged)); + + // Property to store the actual password (bindable) + public static readonly DependencyProperty ActualPasswordProperty = + DependencyProperty.RegisterAttached("ActualPassword", typeof(string), typeof(PasswordBoxAttachedProperties), + new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnActualPasswordChanged)); + + // Property to store the password character (default is ●) + public static readonly DependencyProperty PasswordCharProperty = + DependencyProperty.RegisterAttached("PasswordChar", typeof(char), typeof(PasswordBoxAttachedProperties), + new PropertyMetadata('●')); + + public static bool GetIsPassword(DependencyObject obj) => (bool)obj.GetValue(IsPasswordProperty); + public static void SetIsPassword(DependencyObject obj, bool value) => obj.SetValue(IsPasswordProperty, value); + + public static string GetActualPassword(DependencyObject obj) => (string)obj.GetValue(ActualPasswordProperty); + public static void SetActualPassword(DependencyObject obj, string value) => obj.SetValue(ActualPasswordProperty, value); + + public static char GetPasswordChar(DependencyObject obj) => (char)obj.GetValue(PasswordCharProperty); + public static void SetPasswordChar(DependencyObject obj, char value) => obj.SetValue(PasswordCharProperty, value); + + private static void OnIsPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not TextBox textBox) return; + + if ((bool)e.NewValue) + { + textBox.TextChanged += MaskPassword; + textBox.PreviewTextInput += OnPreviewTextInput; + textBox.PreviewKeyDown += OnPreviewKeyDown; + DataObject.AddPastingHandler(textBox, OnPaste); + + if (textBox.IsLoaded) + UpdateText(textBox); + else + textBox.Loaded += TextBox_Loaded; + } + else + { + textBox.TextChanged -= MaskPassword; + textBox.PreviewTextInput -= OnPreviewTextInput; + textBox.PreviewKeyDown -= OnPreviewKeyDown; + DataObject.RemovePastingHandler(textBox, OnPaste); + textBox.Loaded -= TextBox_Loaded; + } + } + + private static void OnActualPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TextBox textBox && GetIsPassword(textBox)) + { + UpdateText(textBox); + } + } + + private static void TextBox_Loaded(object sender, RoutedEventArgs e) + { + if (sender is TextBox textBox) + { + textBox.Loaded -= TextBox_Loaded; + UpdateText(textBox); + } + } + + private static void UpdateText(TextBox textBox) + { + var actualText = GetActualPassword(textBox); + var passwordChar = GetPasswordChar(textBox); + + textBox.TextChanged -= MaskPassword; + textBox.Text = new string(passwordChar, actualText?.Length ?? 0); + textBox.TextChanged += MaskPassword; + } + + private static void MaskPassword(object sender, TextChangedEventArgs e) + { + if (sender is not TextBox textBox || !GetIsPassword(textBox)) + return; + + var passwordChar = GetPasswordChar(textBox); + var currentText = textBox.Text; + var actualPassword = GetActualPassword(textBox) ?? string.Empty; + + // If the text is being set to masked characters, ignore + if (currentText == new string(passwordChar, actualPassword.Length)) + return; + + // Handle text changes + textBox.TextChanged -= MaskPassword; + + int caretPos = textBox.CaretIndex; + + // Handle complete deletion (Ctrl+A then Delete or Backspace) + if (string.IsNullOrEmpty(currentText)) + { + SetActualPassword(textBox, string.Empty); + } + else if (currentText.Length > actualPassword.Length) + { + // Characters added (typing or pasting) + int addedChars = currentText.Length - actualPassword.Length; + int insertPosition = caretPos - addedChars; + + // Ensure we don't try to insert at negative position + insertPosition = Math.Max(0, insertPosition); + + // Get the actual characters that were added (not masked chars) + string newChars = currentText.Substring(insertPosition, addedChars) + .Replace(passwordChar.ToString(), ""); // Filter out any mask chars + + if (!string.IsNullOrEmpty(newChars)) + { + var newPassword = actualPassword.Insert(insertPosition, newChars); + SetActualPassword(textBox, newPassword); + } + } + else if (currentText.Length < actualPassword.Length) + { + // Characters deleted + int deletedCount = actualPassword.Length - currentText.Length; + + // Handle backspace/delete at different positions + if (caretPos < actualPassword.Length) + { + var newPassword = actualPassword.Remove(caretPos, deletedCount); + SetActualPassword(textBox, newPassword); + } + else + { + // Handle deletion at end + var newPassword = actualPassword.Substring(0, actualPassword.Length - deletedCount); + SetActualPassword(textBox, newPassword); + } + } + + // Update the display + textBox.Text = new string(passwordChar, GetActualPassword(textBox).Length); + textBox.CaretIndex = caretPos; + + textBox.TextChanged += MaskPassword; + } + + private static void OnPaste(object sender, DataObjectPastingEventArgs e) + { + if (sender is not TextBox textBox || !GetIsPassword(textBox)) + return; + + // Cancel the standard paste operation + e.CancelCommand(); + + // Get the paste text and sanitize it + string pasteText = (string)e.DataObject.GetData(typeof(string)); + if (string.IsNullOrEmpty(pasteText)) + return; + + // Remove any invalid characters (like the password mask character) + var passwordChar = GetPasswordChar(textBox); + pasteText = pasteText.Replace(passwordChar.ToString(), ""); + + if (string.IsNullOrEmpty(pasteText)) + return; + + // Get current selection info + int selectionStart = textBox.SelectionStart; + int selectionLength = textBox.SelectionLength; + string currentPassword = GetActualPassword(textBox) ?? string.Empty; + + // Handle the paste operation + string newPassword; + if (selectionLength > 0) + { + // Replace selected text + newPassword = currentPassword.Remove(selectionStart, selectionLength) + .Insert(selectionStart, pasteText); + } + else + { + // Insert at cursor position + newPassword = currentPassword.Insert(selectionStart, pasteText); + } + + // Update the actual password + SetActualPassword(textBox, newPassword); + + // Update the display + textBox.Text = new string(passwordChar, newPassword.Length); + textBox.CaretIndex = selectionStart + pasteText.Length; + } + + private static void OnPreviewTextInput(object sender, TextCompositionEventArgs e) + { + // Allow text input (handled in TextChanged) + e.Handled = false; + } + + private static void OnPreviewKeyDown(object sender, KeyEventArgs e) + { + if (sender is not TextBox textBox || !GetIsPassword(textBox)) + return; + + // Prevent spaces in password + if (e.Key == Key.Space) + { + e.Handled = true; + } + // Handle Ctrl+A select all + else if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) + { + textBox.SelectAll(); + e.Handled = true; + } + // Handle Ctrl+V paste (we handle it ourselves in OnPaste) + else if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) + { + // Let the paste handler deal with it + e.Handled = false; + } + } + } +} \ No newline at end of file diff --git a/gamevault/Helper/ProfileManager.cs b/gamevault/Helper/ProfileManager.cs new file mode 100644 index 0000000..5d54dfa --- /dev/null +++ b/gamevault/Helper/ProfileManager.cs @@ -0,0 +1,172 @@ +using gamevault.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace gamevault.Helper +{ + internal class ProfileManager + { + private static string ProfileRootDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "profiles"); + public static string ProfileConfigFile { get; private set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "profileconfig"); + public static string ErrorLogDir { get; private set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "errorlog"); + public static string PhalcodeDir { get; private set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "phalcode"); + public static void EnsureRootDirectory() + { + if (!Directory.Exists(ProfileRootDirectory)) + { + Directory.CreateDirectory(ProfileRootDirectory); + MigrateLegacyCache(); + MoveLegacyCache(); + } + } + private static void MigrateLegacyCache() + { + string legacyUserCacheFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "config", "user"); + if (File.Exists(legacyUserCacheFile)) + { + try + { + string username = Preferences.Get("Username", legacyUserCacheFile); + string password = Preferences.Get("Password", legacyUserCacheFile, true); + string serverurl = Preferences.Get("ServerUrl", legacyUserCacheFile, true); + string rootpath = Preferences.Get("RootPath", legacyUserCacheFile); + if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(serverurl)) + { + UserProfile profile = CreateUserProfile(WebHelper.RemoveSpecialCharactersFromUrl(serverurl)); + Preferences.Set(AppConfigKey.ServerUrl, serverurl, profile.UserConfigFile, true); + Preferences.Set(AppConfigKey.Username, username, profile.UserConfigFile); + Preferences.Set(AppConfigKey.Password, password, profile.UserConfigFile, true); + if (Directory.Exists(rootpath)) + { + Preferences.Set(AppConfigKey.RootDirectories, rootpath, profile.UserConfigFile); + } + } + } + catch { } + } + } + private static void MoveLegacyCache() + { + try + { + string cache = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "cache"); + string config = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "config"); + string themes = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "themes"); + string legacyDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "legacy", "1.16.1.0"); + + Directory.CreateDirectory(legacyDir); + + if (Directory.Exists(cache)) + { + string cacheDestination = Path.Combine(legacyDir, "cache"); + Directory.Move(cache, cacheDestination); + } + + if (Directory.Exists(config)) + { + string configDestination = Path.Combine(legacyDir, "config"); + Directory.Move(config, configDestination); + } + + if (Directory.Exists(themes)) + { + string themesDestination = Path.Combine(legacyDir, "themes"); + Directory.Move(themes, themesDestination); + } + } + catch { } + } + + public static UserProfile CreateUserProfile(string serverUrl) + { + string serverRootDirectory = Path.Combine(ProfileRootDirectory, serverUrl); + string serverImageCacheDirectory = Path.Combine(serverRootDirectory, "ImageCache"); + Directory.CreateDirectory(serverImageCacheDirectory); + string userProfileRootDirectory = Path.Combine(serverRootDirectory, Guid.NewGuid().ToString()); + + UserProfile userProfile = new UserProfile(userProfileRootDirectory, serverImageCacheDirectory); + userProfile.UserCacheAvatar = "";//So it will load the default image + EnsureUserProfileFileTree(userProfile); + return userProfile; + } + + public static List GetUserProfiles() + { + if (!Directory.Exists(ProfileRootDirectory)) + Directory.CreateDirectory(ProfileRootDirectory); + + + List users = new List(); + foreach (string serverDir in Directory.GetDirectories(ProfileRootDirectory)) + { + foreach (string userDir in Directory.GetDirectories(serverDir)) + { + if (Path.GetFileName(userDir).Equals("ImageCache")) + continue; + + foreach (string file in Directory.EnumerateFiles(userDir, "*", SearchOption.AllDirectories)) + { + try + { + if (Path.GetFileName(file).Equals("user")) + { + UserProfile userProfile = new UserProfile(userDir, Path.Combine(serverDir, "ImageCache")); + userProfile.Name = Preferences.Get(AppConfigKey.Username, userProfile.UserConfigFile); + userProfile.ServerUrl = Preferences.Get(AppConfigKey.ServerUrl, userProfile.UserConfigFile, true); + userProfile.UserCacheAvatar = CacheHelper.GetUserProfileAvatarPath(userProfile); + if (string.IsNullOrWhiteSpace(userProfile.ServerUrl) || string.IsNullOrWhiteSpace(userProfile.Name)) + { + break; + } + users.Add(userProfile); + break; + } + } + catch { } + } + } + } + return users; + } + public static void DeleteUserProfile(UserProfile userProfile) + { + if (Directory.Exists(userProfile.RootDir)) + { + Directory.Delete(userProfile.RootDir, true); + } + string serverRootDir = Path.GetDirectoryName(userProfile.RootDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + var subDirs = Directory.GetDirectories(serverRootDir); + if (subDirs.Length == 1 && Path.GetFileName(subDirs[0]).Equals("ImageCache", StringComparison.OrdinalIgnoreCase)) + { + // If the only subdirectory left is the ImageCache, delete the server root directory as well + Directory.Delete(serverRootDir, true); + } + } + public static void EnsureUserProfileFileTree(UserProfile userProfile) + { + try + { + if (!Directory.Exists(userProfile.ImageCacheDir)) + Directory.CreateDirectory(userProfile.ImageCacheDir); + + if (!Directory.Exists(userProfile.ThemesLoadDir)) + Directory.CreateDirectory(userProfile.ThemesLoadDir); + + if (!Directory.Exists(userProfile.WebConfigDir)) + Directory.CreateDirectory(userProfile.WebConfigDir); + + if (!Directory.Exists(userProfile.CloudSaveConfigDir)) + Directory.CreateDirectory(userProfile.CloudSaveConfigDir); + + if (!Directory.Exists(userProfile.CacheDir)) + Directory.CreateDirectory(userProfile.CacheDir); + + } + catch { } + } + } +} diff --git a/gamevault/Helper/ToggleButtonGroupBehavior.cs b/gamevault/Helper/ToggleButtonGroupBehavior.cs new file mode 100644 index 0000000..239dbb4 --- /dev/null +++ b/gamevault/Helper/ToggleButtonGroupBehavior.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls.Primitives; +using System.Windows.Media; +using System.Windows; + +namespace gamevault.Helper +{ + public static class ToggleButtonGroupBehavior + { + public static readonly DependencyProperty GroupNameProperty = + DependencyProperty.RegisterAttached( + "GroupName", + typeof(string), + typeof(ToggleButtonGroupBehavior), + new PropertyMetadata(null, OnGroupNameChanged)); + + public static string GetGroupName(DependencyObject obj) + { + return (string)obj.GetValue(GroupNameProperty); + } + + public static void SetGroupName(DependencyObject obj, string value) + { + obj.SetValue(GroupNameProperty, value); + } + + public static ToggleButton GetCheckedToggleButton(string groupName, DependencyObject searchRoot) + { + if (searchRoot == null || string.IsNullOrEmpty(groupName)) + return null; + + foreach (var toggleButton in FindVisualChildren(searchRoot)) + { + if (GetGroupName(toggleButton) == groupName && toggleButton.IsChecked == true) + { + return toggleButton; + } + } + + return null; + } + + public static void CheckToggleButtonByIndex(string groupName, DependencyObject searchRoot, int index) + { + if (searchRoot == null || string.IsNullOrEmpty(groupName) || index < 0) + return; + + var toggleButtons = new List(); + + // Find all toggle buttons in the specified group + foreach (var toggleButton in FindVisualChildren(searchRoot)) + { + if (GetGroupName(toggleButton) == groupName) + { + toggleButtons.Add(toggleButton); + } + } + + // Check if index is valid + if (index < toggleButtons.Count) + { + // Set the toggle button at the specified index to checked + toggleButtons[index].IsChecked = true; + + // This will trigger the Checked event, which will uncheck other buttons in the group + // due to the existing ToggleButton_Checked handler + } + } + + private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ToggleButton toggleButton) + { + if (e.NewValue != null) + { + toggleButton.Checked += ToggleButton_Checked; + toggleButton.Unchecked += ToggleButton_Unchecked; + } + else + { + toggleButton.Checked -= ToggleButton_Checked; + toggleButton.Unchecked -= ToggleButton_Unchecked; + } + } + } + + private static void ToggleButton_Checked(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton checkedButton) + { + string groupName = GetGroupName(checkedButton); + if (groupName == null) + return; + + var parent = VisualTreeHelper.GetParent(checkedButton); + while (parent != null && !(parent is Window)) + { + parent = VisualTreeHelper.GetParent(parent); + } + + if (parent != null) + { + var toggleButtons = FindVisualChildren(parent); + foreach (var button in toggleButtons) + { + if (button == checkedButton) + continue; + + if (GetGroupName(button) == groupName) + { + button.IsChecked = false; + } + } + } + } + } + + private static void ToggleButton_Unchecked(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton uncheckedButton) + { + string groupName = GetGroupName(uncheckedButton); + if (groupName == null) + return; + + var parent = VisualTreeHelper.GetParent(uncheckedButton); + while (parent != null && !(parent is Window)) + { + parent = VisualTreeHelper.GetParent(parent); + } + + if (parent != null) + { + // Check if any other button in the group is checked + bool anyOtherChecked = false; + var toggleButtons = FindVisualChildren(parent); + + foreach (var button in toggleButtons) + { + if (button != uncheckedButton && GetGroupName(button) == groupName && button.IsChecked == true) + { + anyOtherChecked = true; + break; + } + } + + // If no other button is checked, prevent unchecking by setting IsChecked back to true + if (!anyOtherChecked) + { + // Use dispatcher to avoid potential event handling issues + uncheckedButton.Dispatcher.BeginInvoke(new Action(() => + { + // Temporarily remove the event handler to avoid infinite loop + uncheckedButton.Unchecked -= ToggleButton_Unchecked; + uncheckedButton.IsChecked = true; + uncheckedButton.Unchecked += ToggleButton_Unchecked; + })); + } + } + } + } + + private static IEnumerable FindVisualChildren(DependencyObject depObj) where T : DependencyObject + { + if (depObj == null) + yield break; + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) + { + var child = VisualTreeHelper.GetChild(depObj, i); + if (child is T t) + yield return t; + + foreach (var childOfChild in FindVisualChildren(child)) + yield return childOfChild; + } + } + } +} diff --git a/gamevault/Helper/VisualHelper.cs b/gamevault/Helper/VisualHelper.cs index 96d81aa..4e2e364 100644 --- a/gamevault/Helper/VisualHelper.cs +++ b/gamevault/Helper/VisualHelper.cs @@ -6,6 +6,7 @@ using System.Windows.Controls; using System.Windows.Media; using System.Windows; +using MahApps.Metro.Controls; namespace gamevault.Helper { @@ -22,5 +23,38 @@ internal static T FindNextParentByType(DependencyObject child) while (parentDepObj != null); return default; } + internal static void AdjustWindowChrome(MetroWindow window) + { + try + { + var thumb = (FrameworkElement)window.Template.FindName("PART_WindowTitleThumb", window); + thumb.Margin = new Thickness(50, 0, 0, 0); + System.Windows.Controls.Panel.SetZIndex(thumb, 7); + var btnCommands = (FrameworkElement)window.Template.FindName("PART_WindowButtonCommands", window); + System.Windows.Controls.Panel.SetZIndex(btnCommands, 8); + } + catch { } + } + internal static void HideWindow(Window window) + { + window.Width = 0; + window.Height = 0; + window.WindowStartupLocation = WindowStartupLocation.Manual; + window.Top = int.MinValue; + window.Left = int.MinValue; + window.ShowInTaskbar = false; + } + internal static void RestoreHiddenWindow(Window window, int height, int width) + { + window.Width = width; + window.Height = height; + window.ShowInTaskbar = true; + double screenWidth = System.Windows.SystemParameters.PrimaryScreenWidth; + double screenHeight = System.Windows.SystemParameters.PrimaryScreenHeight; + double windowWidth = window.Width; + double windowHeight = window.Height; + window.Left = (screenWidth / 2) - (windowWidth / 2); + window.Top = (screenHeight / 2) - (windowHeight / 2); + } } } diff --git a/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs b/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs index 6f87c40..7a6ae8c 100644 --- a/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs +++ b/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs @@ -50,12 +50,12 @@ public async Task StartDownload(bool tryResume = false) } else { - //Edge case where the Library download overrrides the current download. But if its was a paused download, we have also have to reset the metadata + //Edge case where the Library download overrrides the current download. But if its was a paused download, we also have to reset the metadata if (File.Exists($"{DestinationFolderPath}\\gamevault-metadata")) File.Delete($"{DestinationFolderPath}\\gamevault-metadata"); } - using (var response = await HttpClient.GetAsync(DownloadUrl, HttpCompletionOption.ResponseHeadersRead)) + using (HttpResponseMessage response = await WebHelper.GetAsync(DownloadUrl, HttpCompletionOption.ResponseHeadersRead)) await DownloadFileFromHttpResponseMessage(response); } private void CreateHeader() @@ -78,7 +78,6 @@ private void InitResume() ResumePosition = long.Parse(resumeDataToProcess[0]); PreResumeSize = long.Parse(resumeDataToProcess[1]); HttpClient.DefaultRequestHeaders.Range = new RangeHeaderValue(long.Parse(resumeDataToProcess[0]), null); - //TriggerProgressChanged(PreResumeSize, 0, ResumePosition); } catch { } } @@ -86,8 +85,6 @@ private void InitResume() private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response) { - response.EnsureSuccessStatusCode(); - try { FileName = response.Content.Headers.ContentDisposition.FileName.Replace("\"", ""); @@ -168,10 +165,10 @@ private async Task ProcessContentStream(long currentDownloadSize, Stream content currentBytesRead += bytesRead; if ((DateTime.Now - LastTime).TotalMilliseconds > 2000) { - TriggerProgressChanged(currentDownloadSize, currentBytesRead, fileStream.Position); - LastTime = DateTime.Now; //Save checkpoints all two seconds in case the app is closed by the user, or hardly crashed Preferences.Set(AppConfigKey.DownloadProgress, $"{fileStream.Position};{(PreResumeSize == -1 ? currentDownloadSize : PreResumeSize)}", $"{DestinationFolderPath}\\gamevault-metadata"); + TriggerProgressChanged(currentDownloadSize, currentBytesRead, fileStream.Position); + LastTime = DateTime.Now; } } while (isMoreToRead); diff --git a/gamevault/Helper/Web/OAuthHttpClient.cs b/gamevault/Helper/Web/OAuthHttpClient.cs new file mode 100644 index 0000000..5342b4e --- /dev/null +++ b/gamevault/Helper/Web/OAuthHttpClient.cs @@ -0,0 +1,146 @@ +using gamevault.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Text.Json; +using gamevault.Models; + +namespace gamevault.Helper +{ + public class SSOHttpClient + { + private readonly HttpClient _httpClient; + private string _accessToken; + private string _refreshToken; + + public string ServerUrl; + public string UserName; + public string Password; + + public SSOHttpClient() + { + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"GameVault/{SettingsViewModel.Instance.Version}"); + _httpClient.Timeout = new TimeSpan(0, 0, 20); + } + public void InjectTokens(string accessToken, string refreshToken) + { + _accessToken = accessToken; + _refreshToken = refreshToken; + } + public string GetRefreshToken() + { + return _refreshToken; + } + public void Reset() + { + _accessToken = ""; + _refreshToken = ""; + } + public async Task LoginBasicAuthAsync(string username, string password) + { + var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue); + + var response = await _httpClient.GetAsync($"{ServerUrl}/api/auth/basic/login"); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + var responseContent = await response.Content.ReadAsStringAsync(); + var json = JsonSerializer.Deserialize(responseContent); + + _accessToken = json?.AccessToken; + _refreshToken = json?.RefreshToken; + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); + return true; + } + + private async Task SendAsync(HttpRequestMessage request, RequestHeader[]? additionalHeaders = null, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) + { + if (string.IsNullOrEmpty(_accessToken)) + { + await LoginBasicAuthAsync(UserName, Password); + } + + if (IsTokenExpired(_accessToken) && !await RefreshTokenAsync()) + throw new InvalidOperationException("Failed to refresh token."); + + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); + if (additionalHeaders != null) + { + foreach (var header in additionalHeaders) + { + request.Headers.TryAddWithoutValidation(header.Name, header.Value); + } + } + + return await _httpClient.SendAsync(request, option); + } + + public Task GetAsync(string url, RequestHeader[]? additionalHeaders = null, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) => + SendAsync(new HttpRequestMessage(HttpMethod.Get, url), additionalHeaders, option); + + public Task PostAsync(string url, HttpContent content, RequestHeader[]? additionalHeaders = null) => + SendAsync(new HttpRequestMessage(HttpMethod.Post, url) { Content = content }, additionalHeaders); + + public Task PutAsync(string url, HttpContent content, RequestHeader[]? additionalHeaders = null) => + SendAsync(new HttpRequestMessage(HttpMethod.Put, url) { Content = content }, additionalHeaders); + + public Task DeleteAsync(string url, RequestHeader[]? additionalHeaders = null) => + SendAsync(new HttpRequestMessage(HttpMethod.Delete, url), additionalHeaders); + + private async Task RefreshTokenAsync() + { + var content = new StringContent("", Encoding.UTF8, "application/json"); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _refreshToken); + + var response = await _httpClient.PostAsync($"{ServerUrl}/api/auth/refresh", content); + if (!response.IsSuccessStatusCode) return false; + + var responseContent = await response.Content.ReadAsStringAsync(); + var json = JsonSerializer.Deserialize(responseContent); + + _accessToken = json?.AccessToken; + _refreshToken = json?.RefreshToken; + + return true; + } + + private bool IsTokenExpired(string token) + { + var parts = token.Split('.'); + if (parts.Length != 3) return true; + + var payload = parts[1]; + var jsonBytes = Convert.FromBase64String(Base64UrlDecode(payload)); + + var json = JsonSerializer.Deserialize(Encoding.UTF8.GetString(jsonBytes)); + return json?.Exp == null || DateTimeOffset.FromUnixTimeSeconds(json.Exp) <= DateTimeOffset.UtcNow.AddMinutes(1); + } + + private string Base64UrlDecode(string input) + { + input = input.Replace('-', '+').Replace('_', '/'); + return input.PadRight(input.Length + (4 - input.Length % 4) % 4, '='); + } + } + public class AuthResponse + { + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + } + + public class JwtPayload + { + [JsonPropertyName("exp")] + public long Exp { get; set; } + } +} diff --git a/gamevault/Helper/Web/WebExceptionHelper.cs b/gamevault/Helper/Web/WebExceptionHelper.cs index 6385433..5a2c91d 100644 --- a/gamevault/Helper/Web/WebExceptionHelper.cs +++ b/gamevault/Helper/Web/WebExceptionHelper.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; @@ -36,6 +37,40 @@ internal static string TryGetServerMessage(Exception ex) } return ex.Message; } + public static async Task EnsureSuccessStatusCode(HttpResponseMessage msg) + { + if (!msg.IsSuccessStatusCode) + { + string? serverMessage = ""; + try + { + string message = await msg.Content.ReadAsStringAsync(); + using JsonDocument serverResponseJson = JsonDocument.Parse(message); + if (serverResponseJson.RootElement.TryGetProperty("message", out JsonElement messageElement)) + { + serverMessage = messageElement.ValueKind switch + { + JsonValueKind.String => messageElement.GetString(), + JsonValueKind.Array => string.Join("; ", messageElement.EnumerateArray().Select(e => e.GetString())), + _ => "" + }; + } + } + catch + { + msg.EnsureSuccessStatusCode(); + } + if (string.IsNullOrWhiteSpace(serverMessage)) + { + msg.EnsureSuccessStatusCode(); + } + else + { + serverMessage = $"Server responded: {serverMessage}"; + } + throw new HttpRequestException(serverMessage, null, msg.StatusCode); + } + } internal static string GetServerStatusCode(Exception ex) { if (ex is WebException webex) diff --git a/gamevault/Helper/Web/WebHelper.cs b/gamevault/Helper/Web/WebHelper.cs index 356c577..1141e8a 100644 --- a/gamevault/Helper/Web/WebHelper.cs +++ b/gamevault/Helper/Web/WebHelper.cs @@ -1,260 +1,191 @@ using gamevault.Models; -using gamevault.ViewModels; -using SkiaSharp; using System; using System.Collections.Generic; using System.IO; -using System.Net; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; -using System.Text.Json.Nodes; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; using System.Windows.Media.Imaging; -using Windows.Media.Protection.PlayReady; namespace gamevault.Helper { internal class WebHelper { - - private static string m_UserName { get; set; } - private static string m_Password { get; set; } - internal static void SetCredentials(string username, string password) + private static readonly SSOHttpClient HttpClient = new SSOHttpClient(); + private static readonly HttpClient BaseHttpClient = new HttpClient() { - m_UserName = username; - m_Password = password; - } - internal static string[] GetCredentials() + DefaultRequestHeaders = { { "User-Agent", "GameVault" } } + }; + private static RequestHeader[] AdditionalRequestHeaders; + static WebHelper() { } + internal static void SetCredentials(string serverUrl, string username, string password) { - return new string[] { m_UserName, m_Password }; + HttpClient.Reset(); + HttpClient.ServerUrl = serverUrl; + HttpClient.UserName = username; + HttpClient.Password = password; } + internal static string[] GetCredentials() => new[] { HttpClient.UserName, HttpClient.Password }; internal static void OverrideCredentials(string username, string password) { - m_UserName = username; - m_Password = password; - Preferences.Set(AppConfigKey.Username, m_UserName, AppFilePath.UserFile); - Preferences.Set(AppConfigKey.Password, m_Password, AppFilePath.UserFile, true); - } - internal static string GetRequest(string uri, int timeout = 0) - { - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); - if (timeout != 0) + HttpClient.UserName = username; + Preferences.Set(AppConfigKey.Username, HttpClient.UserName, LoginManager.Instance.GetUserProfile().UserConfigFile); + if (!string.IsNullOrWhiteSpace(password)) { - request.Timeout = 5000; + HttpClient.Password = password; + Preferences.Set(AppConfigKey.Password, HttpClient.Password, LoginManager.Instance.GetUserProfile().UserConfigFile, true); } + } + internal static void InjectTokens(string accessToken, string refreshToken) + { + HttpClient.InjectTokens(accessToken, refreshToken); + } + internal static string GetRefreshToken() + { + return HttpClient.GetRefreshToken(); + } + internal static void SetAdditionalRequestHeaders(RequestHeader[] additionalRequestHeaders) + { + AdditionalRequestHeaders = additionalRequestHeaders; - request.UserAgent = $"GameVault/{SettingsViewModel.Instance.Version}"; - request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) - using (Stream stream = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(stream)) + BaseHttpClient.DefaultRequestHeaders.Clear(); + BaseHttpClient.DefaultRequestHeaders.Add("User-Agent", "GameVault"); + foreach (var header in AdditionalRequestHeaders) { - return reader.ReadToEnd(); + BaseHttpClient.DefaultRequestHeaders.Add(header.Name, header.Value); } } - internal static async Task GetRequestAsync(string uri) + #region BASE REQUESTS + internal static async Task BaseGetAsync(string url) { - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); -#if DEBUG - //request.Timeout = 3000; -#endif - request.UserAgent = $"GameVault/{SettingsViewModel.Instance.Version}"; - request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync()) - using (Stream stream = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(stream)) - { - return await reader.ReadToEndAsync(); - } + var response = await BaseHttpClient.GetAsync(url); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return await response.Content.ReadAsStringAsync(); } - internal static string Put(string uri, string payload, bool returnBody = false) + internal static async Task BasePostAsync(string url, string payload) { - var request = (HttpWebRequest)WebRequest.Create(uri); - request.UserAgent = $"GameVault/{SettingsViewModel.Instance.Version}"; - request.Method = "PUT"; - request.ContentType = "application/json"; - if (payload != null) - { - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - request.ContentLength = System.Text.UTF8Encoding.UTF8.GetByteCount(payload); - Stream dataStream = request.GetRequestStream(); - using (StreamWriter sr = new StreamWriter(dataStream)) - { - sr.Write(payload); - } - dataStream.Close(); - } - HttpWebResponse response = (HttpWebResponse)request.GetResponse(); - if (returnBody) - { - using (StreamReader responseStreamReader = new StreamReader(response.GetResponseStream())) - { - return responseStreamReader.ReadToEnd(); - } - } - else - { - return response.StatusCode.ToString(); - } + var content = new StringContent(payload, Encoding.UTF8, "application/json"); + var response = await BaseHttpClient.PostAsync(url, content); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return await response.Content.ReadAsStringAsync(); } - internal static void Post(string uri, string payload) + internal static async Task BaseSendRequest(HttpRequestMessage request) { - var request = (HttpWebRequest)WebRequest.Create(uri); - request.UserAgent = $"GameVault/{SettingsViewModel.Instance.Version}"; - request.Method = "POST"; - request.ContentType = "application/json"; - if (payload != null) - { - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - request.ContentLength = System.Text.UTF8Encoding.UTF8.GetByteCount(payload); - Stream dataStream = request.GetRequestStream(); - using (StreamWriter sr = new StreamWriter(dataStream)) - { - sr.Write(payload); - } - dataStream.Close(); - } - HttpWebResponse response = (HttpWebResponse)request.GetResponse(); - string returnString = response.StatusCode.ToString(); + var response = await BaseHttpClient.SendAsync(request); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return await response.Content.ReadAsStringAsync(); } - internal static async Task PostAsync(string uri, string payload = "") + #endregion + internal static async Task GetAsync(string url) { - var request = (HttpWebRequest)WebRequest.Create(uri); - request.UserAgent = $"GameVault/{SettingsViewModel.Instance.Version}"; - request.Method = "POST"; - request.ContentType = "application/json"; - if (payload != null) - { - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - request.ContentLength = System.Text.UTF8Encoding.UTF8.GetByteCount(payload); - Stream dataStream = request.GetRequestStream(); - using (StreamWriter sr = new StreamWriter(dataStream)) - { - await sr.WriteAsync(payload); - } - dataStream.Close(); - } - var response = await request.GetResponseAsync(); - using (StreamReader reader = new StreamReader(response.GetResponseStream())) - { - return await reader.ReadToEndAsync(); - } + var response = await HttpClient.GetAsync(url, AdditionalRequestHeaders); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return await response.Content.ReadAsStringAsync(); } - internal static string Delete(string uri) + internal static async Task GetAsync(string url, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) { - var request = (HttpWebRequest)WebRequest.Create(uri); - request.UserAgent = $"GameVault/{SettingsViewModel.Instance.Version}"; - request.Method = "DELETE"; - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) - using (Stream stream = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(stream)) - { - return reader.ReadToEnd(); - } + var response = await HttpClient.GetAsync(url, AdditionalRequestHeaders, option); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return response; } - internal static async Task DeleteAsync(string uri) + internal static async Task PostAsync(string url, string payload) { - var request = (HttpWebRequest)WebRequest.Create(uri); - request.UserAgent = $"GameVault/{SettingsViewModel.Instance.Version}"; - request.Method = "DELETE"; - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - using (var response = await request.GetResponseAsync()) - using (Stream stream = ((HttpWebResponse)response).GetResponseStream()) - using (StreamReader reader = new StreamReader(stream)) - { - return await reader.ReadToEndAsync(); - } + var content = new StringContent(payload, Encoding.UTF8, "application/json"); + var response = await HttpClient.PostAsync(url, content, AdditionalRequestHeaders); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return await response.Content.ReadAsStringAsync(); + } + + internal static async Task PutAsync(string url, string payload) + { + var content = new StringContent(payload, Encoding.UTF8, "application/json"); + var response = await HttpClient.PutAsync(url, content, AdditionalRequestHeaders); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return await response.Content.ReadAsStringAsync(); + } + internal static async Task DeleteAsync(string url) + { + var response = await HttpClient.DeleteAsync(url, AdditionalRequestHeaders); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + return await response.Content.ReadAsStringAsync(); } public static async Task DownloadImageFromUrlAsync(string imageUrl, string cacheFile) { - using (WebClient client = new WebClient()) - { - client.Headers.Add(HttpRequestHeader.Authorization, "Basic " + Convert.ToBase64String(System.Text.UTF8Encoding.UTF8.GetBytes($"{m_UserName}:{m_Password}"))); - client.Headers.Add($"User-Agent: GameVault/{SettingsViewModel.Instance.Version}"); - await client.DownloadFileTaskAsync(new Uri(imageUrl), cacheFile); - } + var response = await HttpClient.GetAsync(imageUrl, AdditionalRequestHeaders); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + var imageBytes = await response.Content.ReadAsByteArrayAsync(); + await File.WriteAllBytesAsync(cacheFile, imageBytes); } public static async Task DownloadImageFromUrlAsync(string imageUrl) { - using (WebClient client = new WebClient()) + var response = await HttpClient.GetAsync(imageUrl, AdditionalRequestHeaders); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + var imageData = await response.Content.ReadAsByteArrayAsync(); + using (var memoryStream = new MemoryStream(imageData)) { - client.Headers.Add(HttpRequestHeader.Authorization, "Basic " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{m_UserName}:{m_Password}"))); - client.Headers.Add($"User-Agent: GameVault/{SettingsViewModel.Instance.Version}"); - - byte[] imageData = await client.DownloadDataTaskAsync(new Uri(imageUrl)); - - using (MemoryStream memoryStream = new MemoryStream(imageData)) - { - BitmapImage bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.StreamSource = memoryStream; - bitmap.CacheOption = BitmapCacheOption.OnLoad; - bitmap.EndInit(); - bitmap.Freeze(); // Freeze to make it cross-thread accessible - return bitmap; - } + BitmapImage bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = memoryStream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + return bitmap; } } - public static async Task UploadFileAsync(string apiUrl, Stream imageStream, string fileName, KeyValuePair? additionalHeader) + public static async Task UploadFileAsync(string apiUrl, Stream imageStream, string fileName, RequestHeader[]? additionalHeaders = null) { - using (var httpClient = new HttpClient()) + //Mix request headers + RequestHeader[]? mixedHeaders = null; + if (additionalHeaders != null && AdditionalRequestHeaders != null) { - var authenticationString = $"{m_UserName}:{m_Password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authenticationString)); - httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + base64EncodedAuthenticationString); - if (additionalHeader != null) - { - httpClient.DefaultRequestHeaders.Add(additionalHeader.Value.Key, additionalHeader.Value.Value); - } - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"GameVault/{SettingsViewModel.Instance.Version}"); - using (var formData = new MultipartFormDataContent()) - { - var imageContent = new StreamContent(imageStream); - string mimeType = MimeTypeHelper.GetMimeType(fileName); - imageContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); - formData.Add(imageContent, "file", fileName); - var response = await httpClient.PostAsync(apiUrl, formData); - if (response.IsSuccessStatusCode) - { - var responseContent = await response.Content.ReadAsStringAsync(); - return responseContent; - } - else - { - string responseContent = await response.Content.ReadAsStringAsync(); - dynamic obj = JsonNode.Parse(responseContent); - throw new HttpRequestException($"{response.StatusCode}: {obj["message"]}"); - } - } + mixedHeaders = AdditionalRequestHeaders.Concat(additionalHeaders).ToArray(); + } + else if (additionalHeaders == null) + { + mixedHeaders = AdditionalRequestHeaders; + } + else if (AdditionalRequestHeaders == null) + { + mixedHeaders = additionalHeaders; + } + using (var formData = new MultipartFormDataContent()) + { + var imageContent = new StreamContent(imageStream); + string mimeType = MimeTypeHelper.GetMimeType(fileName); + imageContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); + formData.Add(imageContent, "file", fileName); + var response = await HttpClient.PostAsync(apiUrl, formData, mixedHeaders); + await WebExceptionHelper.EnsureSuccessStatusCode(response); + var responseContent = await response.Content.ReadAsStringAsync(); + return responseContent; } } - public static async Task DownloadFileContentAsync(string url) + public static string RemoveSpecialCharactersFromUrl(string url) { - using (HttpClient client = new HttpClient()) + if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) + { + url = url.Substring(7); + } + else if (url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + url = url.Substring(8); + } + + StringBuilder sb = new StringBuilder(); + foreach (char c in url) { - client.DefaultRequestHeaders.Add("User-Agent", "C# App"); - HttpResponseMessage response = await client.GetAsync(url); - response.EnsureSuccessStatusCode(); - string content = await response.Content.ReadAsStringAsync(); - return content; + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '.' || c == '_') + { + sb.Append(c); + } } + return sb.ToString(); } + } } + + diff --git a/gamevault/Helper/Web/WpfEmbeddedBrowser.cs b/gamevault/Helper/Web/WpfEmbeddedBrowser.cs index a646a4a..b7cadbe 100644 --- a/gamevault/Helper/Web/WpfEmbeddedBrowser.cs +++ b/gamevault/Helper/Web/WpfEmbeddedBrowser.cs @@ -3,6 +3,7 @@ using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -22,14 +23,9 @@ public WpfEmbeddedBrowser(bool startWithoutWindow) { signinWindow = new Window() { - Width = 0, - Height = 0, - Title = "Phalcode Silent Sign-in", - WindowStartupLocation = WindowStartupLocation.Manual, - Top = int.MinValue, - Left = int.MinValue, - ShowInTaskbar = false, + Title = "Phalcode Silent Sign-in" }; + VisualHelper.HideWindow(signinWindow); } else { @@ -100,7 +96,12 @@ public async Task InvokeAsync(BrowserOptions options, Cancellatio { AdditionalBrowserArguments = "--disk-cache-size=1000000" }; - var env = await CoreWebView2Environment.CreateAsync(null, AppFilePath.WebConfigDir, startupOptions); + + if (!Directory.Exists(ProfileManager.PhalcodeDir)) + Directory.CreateDirectory(ProfileManager.PhalcodeDir); + + var env = await CoreWebView2Environment.CreateAsync(null, ProfileManager.PhalcodeDir, startupOptions); + await webView.EnsureCoreWebView2Async(env); // Delete existing Cookies so previous logins won't remembered @@ -118,16 +119,8 @@ public void ShowWindowIfHidden() { if (signinWindow.ShowInTaskbar == false) { - signinWindow.Width = 600; - signinWindow.Height = 800; + VisualHelper.RestoreHiddenWindow(signinWindow, 800, 600); signinWindow.Title = "Phalcode Sign-in"; - signinWindow.ShowInTaskbar = true; - double screenWidth = System.Windows.SystemParameters.PrimaryScreenWidth; - double screenHeight = System.Windows.SystemParameters.PrimaryScreenHeight; - double windowWidth = signinWindow.Width; - double windowHeight = signinWindow.Height; - signinWindow.Left = (screenWidth / 2) - (windowWidth / 2); - signinWindow.Top = (screenHeight / 2) - (windowHeight / 2); } } public void ClearAllCookies() diff --git a/gamevault/Lib/savegame/ludusavi.exe b/gamevault/Lib/savegame/ludusavi.exe index de99f11..58aef2e 100644 Binary files a/gamevault/Lib/savegame/ludusavi.exe and b/gamevault/Lib/savegame/ludusavi.exe differ diff --git a/gamevault/Models/AppInfo.cs b/gamevault/Models/AppInfo.cs index 4487154..7e41d9e 100644 --- a/gamevault/Models/AppInfo.cs +++ b/gamevault/Models/AppInfo.cs @@ -12,7 +12,8 @@ public enum AppConfigKey { Username, Password, - RootPath, + RootDirectories, + LastSelectedRootDirectory, Executable, BackgroundStart, ServerUrl, @@ -53,36 +54,15 @@ public enum AppConfigKey InstallationId, CustomCloudSaveManifests, UsePrimaryCloudSaveManifest, - MountIso - - } - public static class AppFilePath - { - internal static string ImageCache = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/cache/images"; - internal static string OfflineProgress = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/cache/prgs"; - internal static string OfflineCache = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/cache/local"; - internal static string ConfigDir = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/config"; - internal static string ThemesLoadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "themes"); - internal static string WebConfigDir = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/config/web"; - internal static string UserFile = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/config/user"; - internal static string IgnoreList = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/cache/ignorelist"; - internal static string ErrorLog = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/errorlog"; - internal static string CloudSaveConfigDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "config", "cloudsave"); - - - internal static void InitDebugPaths() - { - ImageCache = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/cache/images"; - OfflineProgress = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/cache/prgs"; - OfflineCache = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/cache/local"; - ConfigDir = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/config"; - ThemesLoadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "debug", "themes"); - WebConfigDir = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/config/web"; - UserFile = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/config/user"; - IgnoreList = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/cache/ignorelist"; - ErrorLog = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/GameVault/debug/errorlog"; - CloudSaveConfigDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GameVault", "debug", "config", "cloudsave"); - } + MountIso, + UserID, + LoginRememberMe, + LastUserProfile, + IsLoggedInWithSSO, + LastImageOptimization, + SessionToken, + InstalledGameVersion, + AdditionalRequestHeaders } public static class Globals { diff --git a/gamevault/Models/Metadata/GameMetadata.cs b/gamevault/Models/Metadata/GameMetadata.cs index 8584703..ad521a2 100644 --- a/gamevault/Models/Metadata/GameMetadata.cs +++ b/gamevault/Models/Metadata/GameMetadata.cs @@ -315,7 +315,16 @@ public string GenreNames [JsonPropertyName("launch_parameters")] public string? LaunchParameters { get; set; } + [JsonPropertyName("installer_parameters")] + public string? InstallerParameters { get; set; } + [JsonPropertyName("installer_executable")] public string? InstallerExecutable { get; set; } + + [JsonPropertyName("uninstaller_parameters")] + public string? UninstallerParameters { get; set; } + + [JsonPropertyName("uninstaller_executable")] + public string? UninstallerExecutable { get; set; } } } diff --git a/gamevault/Models/Metadata/UpdateGameUserMetadataDto.cs b/gamevault/Models/Metadata/UpdateGameUserMetadataDto.cs index 0c7a6a6..256dbf3 100644 --- a/gamevault/Models/Metadata/UpdateGameUserMetadataDto.cs +++ b/gamevault/Models/Metadata/UpdateGameUserMetadataDto.cs @@ -119,19 +119,26 @@ public decimal? Rating [JsonPropertyName("launch_executable")] public string? LaunchExecutable { get; set; } - /// - /// Predefined installer executable for the game. - /// - /// Predefined installer executable for the game. - + + [JsonPropertyName("installer_parameters")] + public string? InstallerParameters { get; set; } + [JsonPropertyName("installer_executable")] public string? InstallerExecutable { get; set; } + + [JsonPropertyName("uninstaller_parameters")] + public string? UninstallerParameters { get; set; } + + + [JsonPropertyName("uninstaller_executable")] + public string? UninstallerExecutable { get; set; } + /// /// URLs of externally hosted screenshots of the game /// /// URLs of externally hosted screenshots of the game - + [JsonPropertyName("url_screenshots")] public string[]? UrlScreenshots { get; set; } diff --git a/gamevault/Models/RequestHeader.cs b/gamevault/Models/RequestHeader.cs new file mode 100644 index 0000000..f4e666a --- /dev/null +++ b/gamevault/Models/RequestHeader.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace gamevault.Models +{ + public class RequestHeader + { + public string Name { get; set; } + public string Value { get; set; } + } +} diff --git a/gamevault/Models/ServerInfo.cs b/gamevault/Models/ServerInfo.cs index bcb0c8b..cd037da 100644 --- a/gamevault/Models/ServerInfo.cs +++ b/gamevault/Models/ServerInfo.cs @@ -1,9 +1,11 @@ -using System; +using gamevault.ViewModels; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; +using System.Windows.Controls; namespace gamevault.Models { @@ -13,5 +15,46 @@ public struct ServerInfo public string Status { get; set; } [JsonPropertyName("version")] public string Version { get; set; } + [JsonPropertyName("registration_enabled")] + public bool RegistrationEnabled { get; set; } + [JsonPropertyName("required_registration_fields")] + public string[] RequiredRegistrationFields { get; set; } + [JsonPropertyName("available_authentication_methods")] + public string[] AvailableAuthenticationMethods { get; set; } + } + public class BindableServerInfo + { + public bool IsAvailable { get; set; } + + public bool IsRegistrationEnabled { get; set; } + public bool IsFirstNameMandatory { get; set; } + public bool IsLastNameMandatory { get; set; } + public bool IsEMailMandatory { get; set; } + public bool IsBirthDateMandatory { get; set; } + + public bool IsBasicAuthEnabled { get; set; } + public bool IsSSOEnabled { get; set; } + public string ErrorMessage { get; set; } + public bool HasError { get; set; } + + public BindableServerInfo(string errorMessage = "") + { + if (errorMessage != "") + { + HasError = true; + ErrorMessage = errorMessage; + } + } + public BindableServerInfo(ServerInfo info) + { + IsAvailable = true; + IsRegistrationEnabled = info.RegistrationEnabled; + IsFirstNameMandatory = info.RequiredRegistrationFields.Contains("first_name"); + IsLastNameMandatory = info.RequiredRegistrationFields.Contains("last_name"); + IsEMailMandatory = info.RequiredRegistrationFields.Contains("email"); + IsBirthDateMandatory = info.RequiredRegistrationFields.Contains("birth_date"); + IsBasicAuthEnabled = info.AvailableAuthenticationMethods.Contains("basic"); + IsSSOEnabled = info.AvailableAuthenticationMethods.Contains("sso"); + } } } diff --git a/gamevault/Models/User.cs b/gamevault/Models/User.cs index abb933e..5497192 100644 --- a/gamevault/Models/User.cs +++ b/gamevault/Models/User.cs @@ -33,6 +33,8 @@ public class User [JsonPropertyName("password")] public string Password { get; set; } public string RepeatPassword { get; set; } + [JsonPropertyName("api_key")] + public string ApiKey { get; set; } [JsonPropertyName("progresses")] public Progress[]? Progresses { get; set; } [JsonPropertyName("role")] diff --git a/gamevault/Models/UserProfile.cs b/gamevault/Models/UserProfile.cs new file mode 100644 index 0000000..b6e7230 --- /dev/null +++ b/gamevault/Models/UserProfile.cs @@ -0,0 +1,53 @@ +using gamevault.ViewModels; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace gamevault.Models +{ + internal class UserProfile : ViewModelBase + { + + public UserProfile(string rootDirectory, string imageCacheDir) + { + RootDir = rootDirectory; + ImageCacheDir = imageCacheDir; + ThemesLoadDir = Path.Combine(rootDirectory, "themes"); + WebConfigDir = Path.Combine(rootDirectory, "config", "web"); + CloudSaveConfigDir = Path.Combine(rootDirectory, "config", "cloudsave"); + CacheDir = Path.Combine(rootDirectory, "cache"); + + OfflineProgress = Path.Combine(rootDirectory, "cache", "prgs"); + OfflineCache = Path.Combine(rootDirectory, "cache", "local"); + IgnoreList = Path.Combine(rootDirectory, "cache", "ignorelist"); + UserConfigFile = Path.Combine(rootDirectory, "config", "user"); + } + public string UserCacheAvatar { get; set; } + public string RootDir { get; set; } + private string name { get; set; } + public string Name + { + get { return name; } + set { name = value; OnPropertyChanged(); } + } + public string ServerUrl { get; set; } + + //Directories + public string ImageCacheDir { get; set; } + public string ThemesLoadDir { get; set; } + public string WebConfigDir { get; set; } + public string CloudSaveConfigDir { get; set; } + public string CacheDir { get; set; } + + //Files + public string OfflineProgress { get; set; } + public string OfflineCache { get; set; } + public string UserConfigFile { get; set; } + public string IgnoreList { get; set; } + + + } +} diff --git a/gamevault/PipeServiceHandler.cs b/gamevault/PipeServiceHandler.cs index 489980f..1bfbaab 100644 --- a/gamevault/PipeServiceHandler.cs +++ b/gamevault/PipeServiceHandler.cs @@ -69,7 +69,7 @@ public bool IsReadyForCommands _isReadyForCommands = value; } } - + public bool IsAppStartup = true; private PipeServiceHandler() { } @@ -411,7 +411,10 @@ private static void SafeDispose(IDisposable? disposable) // You should really implement new actions that you add throw new NotImplementedException($"Action {options.Action} not implemented"); } - + if (IsAppStartup) + { + showMainWindow = !SettingsViewModel.Instance.BackgroundStart; + } if (options.Minimized.HasValue) { // If we're provided a Minimized value then we can explicitly use that for whether or not to be shown @@ -443,7 +446,7 @@ await Dispatch(() => if (task != null) await task; } - + IsAppStartup = false; return null; } @@ -455,14 +458,15 @@ await Dispatch(() => private async Task GetInstalledGame(int id) { Game? game = null; - await Dispatch(async () => + if (!InstallViewModel.Instance.InstalledGames.Any()) { - if (!InstallViewModel.Instance.InstalledGames.Any()) + try { - await MainWindowViewModel.Instance.Library.GetGameInstalls().RestoreInstalledGames(); + await MainWindowViewModel.Instance.Library.GetGameInstalls().RestoreInstalledGames(true); } - game = InstallViewModel.Instance.InstalledGames.Where(g => g.Key.ID == id).Select(g => g.Key).FirstOrDefault(); - }); + catch { } + } + game = InstallViewModel.Instance.InstalledGames.Where(g => g.Key.ID == id).Select(g => g.Key).FirstOrDefault(); return game; } @@ -479,7 +483,7 @@ await Dispatch(async () => { try { - string result = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/{id}"); + string result = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/{id}"); game = JsonSerializer.Deserialize(result); } catch (Exception) @@ -492,7 +496,7 @@ await Dispatch(async () => { try { - string compressedStringObject = Preferences.Get(id.ToString(), AppFilePath.OfflineCache); + string compressedStringObject = Preferences.Get(id.ToString(), LoginManager.Instance.GetUserProfile().OfflineCache); if (!string.IsNullOrEmpty(compressedStringObject)) { string decompressedObject = StringCompressor.DecompressString(compressedStringObject); diff --git a/gamevault/Resources/Assets/Icons.xaml b/gamevault/Resources/Assets/Icons.xaml index 2641a95..17e013b 100644 --- a/gamevault/Resources/Assets/Icons.xaml +++ b/gamevault/Resources/Assets/Icons.xaml @@ -27,6 +27,9 @@ M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z + + M12 21q-.425 0-.712-.288T11 20v-7H4q-.425 0-.712-.288T3 12t.288-.712T4 11h7V4q0-.425.288-.712T12 3t.713.288T13 4v7h7q.425 0 .713.288T21 12t-.288.713T20 13h-7v7q0 .425-.288.713T12 21 + M18.3 5.71a.996.996 0 0 0-1.41 0L12 10.59L7.11 5.7A.996.996 0 1 0 5.7 7.11L10.59 12L5.7 16.89a.996.996 0 1 0 1.41 1.41L12 13.41l4.89 4.89a.996.996 0 1 0 1.41-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z @@ -57,6 +60,12 @@ M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h6q.425 0 .713.288T12 4q0 .425-.288.713T11 5H5v14h6q.425 0 .713.288T12 20q0 .425-.288.713T11 21zm12.175-8H10q-.425 0-.712-.288T9 12q0-.425.288-.712T10 11h7.175L15.3 9.125q-.275-.275-.275-.675t.275-.7q.275-.3.7-.313t.725.288L20.3 11.3q.3.3.3.7t-.3.7l-3.575 3.575q-.3.3-.712.288t-.713-.313q-.275-.3-.262-.712t.287-.688z + + M14 20v-1.25q0-.4.163-.763t.437-.637l4.925-4.925q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-4.925 4.925q-.275.275-.637.425t-.763.15H15q-.425 0-.712-.288T14 20M4 19v-1.8q0-.85.438-1.562T5.6 14.55q1.55-.775 3.15-1.162T12 13q.925 0 1.825.113t1.8.362l-2.75 2.75q-.425.425-.65.975T12 18.35V20H5q-.425 0-.712-.288T4 19m16.575-3.6l.925-.975l-.925-.925l-.95.95zM12 12q-1.65 0-2.825-1.175T8 8t1.175-2.825T12 4t2.825 1.175T16 8t-1.175 2.825T12 12 + + + M3.789 9.037c-.708.383-2.562 1.165-1.433 2.143c.552.478 1.167.82 1.94.82h4.409c.772 0 1.387-.342 1.939-.82c1.13-.978-.725-1.76-1.433-2.143c-1.659-.898-3.763-.898-5.422 0M8.75 4.273A2.26 2.26 0 0 1 6.5 6.545a2.26 2.26 0 0 1-2.25-2.272A2.26 2.26 0 0 1 6.5 2a2.26 2.26 0 0 1 2.25 2.273M4 15c0 3.317 2.683 6 6 6l-.857-1.714M20 9c0-3.317-2.683-6-6-6l.857 1.714m-.068 14.323c-.708.383-2.562 1.165-1.433 2.143c.552.478 1.167.82 1.94.82h4.409c.772 0 1.387-.342 1.939-.82c1.13-.978-.725-1.76-1.433-2.143c-1.659-.898-3.763-.898-5.422 0m4.961-4.764a2.26 2.26 0 0 1-2.25 2.273a2.26 2.26 0 0 1-2.25-2.273A2.26 2.26 0 0 1 17.5 12a2.26 2.26 0 0 1 2.25 2.273 + M13 21q-.425 0-.712-.288T12 20q0-.425.288-.712T13 19h6V5h-6q-.425 0-.712-.288T12 4q0-.425.288-.712T13 3h6q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm-1.825-8H4q-.425 0-.712-.288T3 12q0-.425.288-.712T4 11h7.175L9.3 9.125q-.275-.275-.275-.675t.275-.7q.275-.3.7-.313t.725.288L14.3 11.3q.3.3.3.7t-.3.7l-3.575 3.575q-.3.3-.712.288T9.3 16.25q-.275-.3-.262-.712t.287-.688z @@ -147,6 +156,12 @@ M16.775 19.575q-.275.175-.55.325t-.575.275q-.375.175-.762 0t-.538-.575q-.15-.375.038-.737t.562-.538q.175-.075.325-.162t.3-.188L12 14.8v2.775q0 .675-.612.938T10.3 18.3L7 15H4q-.425 0-.712-.288T3 14v-4q0-.425.288-.712T4 9h2.2L2.1 4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l17 17q.275.275.275.7t-.275.7t-.7.275t-.7-.275zm2.225-7.6q0-2.075-1.1-3.787t-2.95-2.563q-.375-.175-.55-.537t-.05-.738q.15-.4.538-.575t.787 0Q18.1 4.85 19.55 7.05T21 11.975q0 .825-.15 1.638t-.425 1.562q-.2.55-.612.688t-.763.012t-.562-.45t-.013-.75q.275-.65.4-1.312T19 11.975m-4.225-3.55Q15.6 8.95 16.05 10t.45 2v.25q0 .125-.025.25q-.05.325-.35.425t-.55-.15L14.3 11.5q-.15-.15-.225-.337T14 10.775V8.85q0-.3.263-.437t.512.012M9.75 6.95Q9.6 6.8 9.6 6.6t.15-.35l.55-.55q.475-.475 1.087-.213t.613.938V8q0 .35-.3.475t-.55-.125z + + m11.25 4.75l-6.5 6.5m0-6.5l6.5 6.5 + + + M12 17q.425 0 .713-.288T13 16q0-.425-.288-.713T12 15q-.425 0-.713.288T11 16q0 .425.288.713T12 17Zm0 5q-2.075 0-3.9-.788t-3.175-2.137q-1.35-1.35-2.137-3.175T2 12q0-2.075.788-3.9t2.137-3.175q1.35-1.35 3.175-2.137T12 2q2.075 0 3.9.788t3.175 2.137q1.35 1.35 2.138 3.175T22 12q0 2.075-.788 3.9t-2.137 3.175q-1.35 1.35-3.175 2.138T12 22Zm0-9q.425 0 .713-.288T13 12V8q0-.425-.288-.713T12 7q-.425 0-.713.288T11 8v4q0 .425.288.713T12 13Z + @@ -183,5 +198,8 @@ M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81c1.66 0 3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65c0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92 + + M13.923 18.616q.31 0 .54-.23t.23-.54t-.23-.54t-.54-.229t-.54.23t-.23.54q0 .309.23.539t.54.23m2.923 0q.31 0 .54-.23t.23-.54t-.23-.54q-.23-.229-.54-.229t-.54.23t-.229.54t.23.539t.54.23M4 13.5V4.616q0-.691.463-1.153T5.616 3h12.769q.69 0 1.153.463T20 4.616V13.5zM5.616 21q-.691 0-1.153-.462T4 19.385V14.5h16v4.885q0 .69-.462 1.152T18.384 21z + \ No newline at end of file diff --git a/gamevault/Resources/Assets/Styles.xaml b/gamevault/Resources/Assets/Styles.xaml index 0f04884..c6d0c4d 100644 --- a/gamevault/Resources/Assets/Styles.xaml +++ b/gamevault/Resources/Assets/Styles.xaml @@ -5,7 +5,7 @@ - + + + + + + + - + + + + - - + + + - - - + + + - - - - - - - + + + + diff --git a/gamevault/UserControls/AdminConsoleUserControl.xaml.cs b/gamevault/UserControls/AdminConsoleUserControl.xaml.cs index 365eda9..dbd994e 100644 --- a/gamevault/UserControls/AdminConsoleUserControl.xaml.cs +++ b/gamevault/UserControls/AdminConsoleUserControl.xaml.cs @@ -53,7 +53,7 @@ public async Task InitUserList() { try { - string userList = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/users"); + string userList = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/users"); ViewModel.Users = JsonSerializer.Deserialize(userList); } catch (Exception ex) @@ -84,7 +84,7 @@ private async void PermissionRole_SelectionChanged(object sender, SelectionChang return; } } - WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}", JsonSerializer.Serialize(new UpdateUserDto() { Role = selectedUser.Role })); + await WebHelper.PutAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}", JsonSerializer.Serialize(new UpdateUserDto() { Role = selectedUser.Role })); MainWindowViewModel.Instance.AppBarText = $"Successfully updated permission role of user '{selectedUser.Username}' to '{selectedUser.Role}'"; } catch (Exception ex) @@ -94,12 +94,12 @@ private async void PermissionRole_SelectionChanged(object sender, SelectionChang } } - private void Activated_Toggled(object sender, RoutedEventArgs e) + private async void Activated_Toggled(object sender, RoutedEventArgs e) { try { User selectedUser = (User)((FrameworkElement)sender).DataContext; - WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}", JsonSerializer.Serialize(new User() { Activated = selectedUser.Activated })); + await WebHelper.PutAsync($@"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}", JsonSerializer.Serialize(new User() { Activated = selectedUser.Activated })); string state = selectedUser.Activated == true ? "activated" : "deactivated"; MainWindowViewModel.Instance.AppBarText = $"Successfully {state} user '{selectedUser.Username}'"; } @@ -125,46 +125,39 @@ private async void DeleteUser_Clicked(object sender, RoutedEventArgs e) return; } this.IsEnabled = false; - await Task.Run(async () => + + try { - try + if (selectedUser.DeletedAt == null) { - if (selectedUser.DeletedAt == null) - { - WebHelper.Delete(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}"); - MainWindowViewModel.Instance.AppBarText = $"Successfully deleted user '{selectedUser.Username}'"; - await InitUserList(); - } - else - { - WebHelper.Post(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}/recover", ""); - MainWindowViewModel.Instance.AppBarText = $"Successfully recovered deleted user '{selectedUser.Username}'"; - await InitUserList(); - } + await WebHelper.DeleteAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}"); + MainWindowViewModel.Instance.AppBarText = $"Successfully deleted user '{selectedUser.Username}'"; + await InitUserList(); } - catch (Exception ex) + else { - string msg = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = msg; + await WebHelper.PostAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}/recover", ""); + MainWindowViewModel.Instance.AppBarText = $"Successfully recovered deleted user '{selectedUser.Username}'"; + await InitUserList(); } - }); + } + catch (Exception ex) + { + string msg = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = msg; + } + this.IsEnabled = true; } private void EditUser_Clicked(object sender, RoutedEventArgs e) { User user = JsonSerializer.Deserialize(JsonSerializer.Serialize((User)((FrameworkElement)sender).DataContext));//Dereference - MainWindowViewModel.Instance.OpenPopup(new UserSettingsUserControl(user) { Width = 1200, Height = 800, Margin = new Thickness(50) }); + MainWindowViewModel.Instance.OpenPopup(new UserSettingsUserControl(user.ID == LoginManager.Instance.GetCurrentUser()?.ID ? LoginManager.Instance.GetCurrentUser() : user) { Width = 1200, Height = 800, Margin = new Thickness(50) }); } private void BackupRestore_Click(object sender, RoutedEventArgs e) { - uiUserEditPopup.Visibility = Visibility.Visible; - var obj = new BackupRestoreUserControl(); - //obj.UserSaved += UserSaved; - if (uiUserEditPopup.Children.Count != 0) - { - uiUserEditPopup.Children.Clear(); - } - uiUserEditPopup.Children.Add(obj); + var obj = new BackupRestoreUserControl() { Margin = new Thickness(220) }; + MainWindowViewModel.Instance.OpenPopup(obj); } protected async void UserSaved(object sender, EventArgs e) { @@ -172,20 +165,19 @@ protected async void UserSaved(object sender, EventArgs e) this.IsEnabled = false; User selectedUser = (User)((Button)sender).DataContext; bool error = false; - await Task.Run(() => + + try { - try - { - WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}", JsonSerializer.Serialize(selectedUser)); - MainWindowViewModel.Instance.AppBarText = "Successfully saved user changes"; - } - catch (Exception ex) - { - error = true; - string msg = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = msg; - } - }); + await WebHelper.PutAsync($@"{SettingsViewModel.Instance.ServerUrl}/api/users/{selectedUser.ID}", JsonSerializer.Serialize(selectedUser)); + MainWindowViewModel.Instance.AppBarText = "Successfully saved user changes"; + } + catch (Exception ex) + { + error = true; + string msg = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = msg; + } + if (!error) { await HandleChangesOnCurrentUser(selectedUser); @@ -197,7 +189,16 @@ private async Task HandleChangesOnCurrentUser(User selectedUser) { if (LoginManager.Instance.GetCurrentUser().ID == selectedUser.ID) { - await LoginManager.Instance.ManualLogin(selectedUser.Username, string.IsNullOrEmpty(selectedUser.Password) ? WebHelper.GetCredentials()[1] : selectedUser.Password); + UserProfile profile = LoginManager.Instance.GetUserProfile(); + bool isLoggedInWithSSO = Preferences.Get(AppConfigKey.IsLoggedInWithSSO, profile.UserConfigFile) == "1"; + if (isLoggedInWithSSO) + { + await LoginManager.Instance.SSOLogin(profile); + } + else + { + await LoginManager.Instance.Login(profile, WebHelper.GetCredentials()[0], WebHelper.GetCredentials()[1]); + } MainWindowViewModel.Instance.UserAvatar = LoginManager.Instance.GetCurrentUser(); } await InitUserList(); @@ -212,19 +213,17 @@ private void ShowUser_Click(object sender, RoutedEventArgs e) private async void Reindex_Click(object sender, RoutedEventArgs e) { ((FrameworkElement)sender).IsEnabled = false; - await Task.Run(() => + + try { - try - { - WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/reindex", string.Empty); - MainWindowViewModel.Instance.AppBarText = "Successfully reindexed games"; - } - catch (Exception ex) - { - string msg = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = msg; - } - }); + await WebHelper.PutAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/reindex", string.Empty); + MainWindowViewModel.Instance.AppBarText = "Successfully reindexed games"; + } + catch (Exception ex) + { + string msg = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = msg; + } await MainWindowViewModel.Instance.Library.LoadLibrary(); ((FrameworkElement)sender).IsEnabled = true; } @@ -242,21 +241,16 @@ private async Task> GetServerVersionInfo() { try { - using (HttpClient httpClient = new HttpClient()) + var gitResponse = await WebHelper.BaseGetAsync("https://api.github.com/repos/Phalcode/gamevault-backend/releases"); + dynamic gitObj = JsonNode.Parse(gitResponse); + string newestServerVersion = (string)gitObj[0]["tag_name"]; + string serverResponse = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/status"); + string currentServerVersion = JsonSerializer.Deserialize(serverResponse).Version; + if (Convert.ToInt32(newestServerVersion.Replace(".", "")) > Convert.ToInt32(currentServerVersion.Replace(".", ""))) { - - httpClient.DefaultRequestHeaders.Add("User-Agent", "Other"); - var gitResponse = await httpClient.GetStringAsync("https://api.github.com/repos/Phalcode/gamevault-backend/releases"); - dynamic gitObj = JsonNode.Parse(gitResponse); - string newestServerVersion = (string)gitObj[0]["tag_name"]; - string serverResonse = WebHelper.GetRequest(@$"{SettingsViewModel.Instance.ServerUrl}/api/admin/health"); - string currentServerVersion = JsonSerializer.Deserialize(serverResonse).Version; - if (Convert.ToInt32(newestServerVersion.Replace(".", "")) > Convert.ToInt32(currentServerVersion.Replace(".", ""))) - { - return new KeyValuePair($"Server Version: {currentServerVersion}", (string)gitObj[0]["html_url"]); - } - return new KeyValuePair($"Server Version: {currentServerVersion}", ""); + return new KeyValuePair($"Server Version: {currentServerVersion}", (string)gitObj[0]["html_url"]); } + return new KeyValuePair($"Server Version: {currentServerVersion}", ""); } catch { diff --git a/gamevault/UserControls/CommunityUserControl.xaml b/gamevault/UserControls/CommunityUserControl.xaml index 6c6c544..1b24e85 100644 --- a/gamevault/UserControls/CommunityUserControl.xaml +++ b/gamevault/UserControls/CommunityUserControl.xaml @@ -16,6 +16,37 @@ + + + + + + + + + + @@ -36,6 +67,28 @@ + + + + + + + + + + + + + + + @@ -52,7 +105,7 @@ @@ -154,7 +154,32 @@ - + + + + + + + + + + + + diff --git a/gamevault/UserControls/GameDownloadUserControl.xaml.cs b/gamevault/UserControls/GameDownloadUserControl.xaml.cs index 02c439a..7c8f9be 100644 --- a/gamevault/UserControls/GameDownloadUserControl.xaml.cs +++ b/gamevault/UserControls/GameDownloadUserControl.xaml.cs @@ -33,7 +33,8 @@ public partial class GameDownloadUserControl : UserControl private bool isGameTypeForced = false; private double downloadRetryTimerTickValue = 10; private string mountedDrive = ""; - public GameDownloadUserControl(Game game, bool download) + + public GameDownloadUserControl(Game game, string rootDirectory, bool download) { InitializeComponent(); ViewModel = new GameDownloadViewModel(); @@ -43,9 +44,9 @@ public GameDownloadUserControl(Game game, bool download) ViewModel.ExtractionUIVisibility = System.Windows.Visibility.Hidden; ViewModel.DownloadFailedVisibility = System.Windows.Visibility.Hidden; - m_DownloadPath = $"{SettingsViewModel.Instance.RootPath}\\GameVault\\Downloads\\({ViewModel.Game.ID}){ViewModel.Game.Title}"; + m_DownloadPath = $"{rootDirectory}\\GameVault\\Downloads\\({ViewModel.Game.ID}){ViewModel.Game.Title}"; m_DownloadPath = m_DownloadPath.Replace(@"\\", @"\"); - ViewModel.InstallPath = $"{SettingsViewModel.Instance.RootPath}\\GameVault\\Installations\\({ViewModel.Game.ID}){ViewModel.Game.Title}"; + ViewModel.InstallPath = $"{rootDirectory}\\GameVault\\Installations\\({ViewModel.Game.ID}){ViewModel.Game.Title}"; ViewModel.InstallPath = ViewModel.InstallPath.Replace(@"\\", @"\"); sevenZipHelper = new SevenZipHelper(); gameSizeConverter = new GameSizeConverter(); @@ -202,7 +203,7 @@ private async Task DownloadGame(bool tryResume = false) if (downloadRetryTimer.Data != "error") { if (!App.Instance.IsWindowActiveAndControlInFocus(MainControl.Downloads)) - ToastMessageHelper.CreateToastMessage("Download Failed", ViewModel.Game.Title, $"{AppFilePath.ImageCache}/gbox/{ViewModel.Game.ID}.{ViewModel.Game.Metadata.Cover?.ID}"); + ToastMessageHelper.CreateToastMessage("Download Failed", ViewModel.Game.Title, $"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbox/{ViewModel.Game.ID}.{ViewModel.Game.Metadata.Cover?.ID}"); } StartRetryTimer(); MainWindowViewModel.Instance.UpdateTaskbarProgress(); @@ -259,6 +260,16 @@ private void PauseResume_Click(object sender, RoutedEventArgs e) ViewModel.State = "Download Paused"; } } + public void PauseDownload() + { + if (client == null || ViewModel.IsDownloadPaused || !IsDownloadActive) + return; + + ViewModel.IsDownloadPaused = true; + client.Pause(); + IsDownloadActive = false; + ViewModel.State = "Download Paused"; + } private void DownloadProgress(long totalFileSize, long currentBytesDownloaded, long totalBytesDownloaded, double? progressPercentage, long resumePosition) { App.Current.Dispatcher.Invoke((Action)delegate @@ -321,7 +332,7 @@ private void DownloadCompleted() MainWindowViewModel.Instance.Library.GetGameInstalls().AddSystemFileWatcher(ViewModel.InstallPath); if (!App.Instance.IsWindowActiveAndControlInFocus(MainControl.Downloads)) - ToastMessageHelper.CreateToastMessage("Download Complete", ViewModel.Game.Title, $"{AppFilePath.ImageCache}/gbox/{ViewModel.Game.ID}.{ViewModel.Game.Metadata?.Cover?.ID}"); + ToastMessageHelper.CreateToastMessage("Download Complete", ViewModel.Game.Title, $"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbox/{ViewModel.Game.ID}.{ViewModel.Game.Metadata?.Cover?.ID}"); if (SettingsViewModel.Instance.AutoExtract) { @@ -419,7 +430,7 @@ private void OpenDirectory_Click(object sender, RoutedEventArgs e) Process.Start("explorer.exe", m_DownloadPath); } - private void GameImage_Click(object sender, RoutedEventArgs e) + private void GoToGame_Click(object sender, RoutedEventArgs e) { MainWindowViewModel.Instance.SetActiveControl(new GameViewUserControl(ViewModel.Game, LoginManager.Instance.IsLoggedIn())); } @@ -430,6 +441,7 @@ private void ExtractionProgress(object sender, SevenZipProgressEventArgs e) { ViewModel.GameExtractionProgress = e.PercentageDone; long totalBytesDownloaded = (Convert.ToInt64(ViewModel.Game.Size) / 100) * e.PercentageDone; + downloadSpeedCalc.UpdateSpeed(totalBytesDownloaded); ViewModel.ExtractionInfo = $"{$"{FormatBytesHumanReadable(totalBytesDownloaded, (DateTime.Now - startTime).TotalSeconds, 1000)}/s"} - {FormatBytesHumanReadable(totalBytesDownloaded)} of {FormatBytesHumanReadable(Convert.ToInt64(ViewModel.Game.Size))} | Time left: {CalculateTimeLeft(Convert.ToInt64(ViewModel.Game.Size), totalBytesDownloaded, (DateTime.Now - startTime).TotalMilliseconds)}"; } catch { } @@ -462,7 +474,7 @@ private async Task MountISO(string ISOPath) { return string.Empty; } - } + } private async Task Extract() { if (!Directory.Exists(m_DownloadPath)) @@ -509,7 +521,7 @@ private async Task Extract() bool isEncrypted = await sevenZipHelper.IsArchiveEncrypted($"{m_DownloadPath}\\{files[0].Name}"); if (isEncrypted) { - string extractionPassword = Preferences.Get(AppConfigKey.ExtractionPassword, AppFilePath.UserFile, true); + string extractionPassword = Preferences.Get(AppConfigKey.ExtractionPassword, LoginManager.Instance.GetUserProfile().UserConfigFile, true); if (string.IsNullOrEmpty(extractionPassword)) { extractionPassword = await ((MetroWindow)App.Current.MainWindow).ShowInputAsync("Exctraction Message", "Your Archive reqires a Password to extract"); @@ -543,7 +555,7 @@ private async Task Extract() ViewModel.ExtractionUIVisibility = System.Windows.Visibility.Hidden; if (!App.Instance.IsWindowActiveAndControlInFocus(MainControl.Downloads)) - ToastMessageHelper.CreateToastMessage("Extraction Complete", ViewModel.Game.Title, $"{AppFilePath.ImageCache}/gbox/{ViewModel.Game?.ID}.{ViewModel.Game?.Metadata?.Cover?.ID}"); + ToastMessageHelper.CreateToastMessage("Extraction Complete", ViewModel.Game.Title, $"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbox/{ViewModel.Game?.ID}.{ViewModel.Game?.Metadata?.Cover?.ID}"); if (SettingsViewModel.Instance.AutoInstallPortable && (ViewModel.Game?.Type == GameType.WINDOWS_PORTABLE || ViewModel.Game?.Type == GameType.LINUX_PORTABLE)) { @@ -580,7 +592,7 @@ private async Task Extract() { ViewModel.State = "Something went wrong during extraction"; if (!App.Instance.IsWindowActiveAndControlInFocus(MainControl.Downloads)) - ToastMessageHelper.CreateToastMessage("Extraction Failed", ViewModel.Game.Title, $"{AppFilePath.ImageCache}/gbox/{ViewModel.Game?.ID}.{ViewModel.Game?.Metadata?.Cover?.ID}"); + ToastMessageHelper.CreateToastMessage("Extraction Failed", ViewModel.Game.Title, $"{LoginManager.Instance.GetUserProfile().ImageCacheDir}/gbox/{ViewModel.Game?.ID}.{ViewModel.Game?.Metadata?.Cover?.ID}"); } ViewModel.ExtractionUIVisibility = System.Windows.Visibility.Hidden; } @@ -638,6 +650,11 @@ private async void Install_Click(object s, RoutedEventArgs e) } private async Task Install() { + if (InstallViewModel.Instance.InstalledGames.Any(game => game.Key.ID == ViewModel.Game.ID)) + { + await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync($"The Game {ViewModel.Game.Title} is already installed at \n'{InstallViewModel.Instance.InstalledGames.First(game => game.Key.ID == ViewModel.Game.ID).Value}'", "", MessageDialogStyle.Affirmative, new MetroDialogSettings() { AffirmativeButtonText = "Ok", AnimateHide = false }); + return; + } string targedDir = (SettingsViewModel.Instance.MountIso && Directory.Exists(mountedDrive)) ? mountedDrive : $"{m_DownloadPath}\\Extract"; uiBtnInstallPortable.IsEnabled = false; @@ -716,13 +733,13 @@ await Task.Run(() => Process setupProcess = null; try { - setupProcess = ProcessHelper.StartApp(setupEexecutable); + setupProcess = ProcessHelper.StartApp(setupEexecutable, ViewModel.Game?.Metadata?.InstallerParameters?.Replace("%INSTALLDIR%", ViewModel.InstallPath)); } catch { try { - setupProcess = ProcessHelper.StartApp(setupEexecutable, "", true); + setupProcess = ProcessHelper.StartApp(setupEexecutable, ViewModel.Game?.Metadata?.InstallerParameters?.Replace("%INSTALLDIR%", ViewModel.InstallPath), true); } catch { @@ -750,6 +767,11 @@ await Task.Run(() => uiInstallOptions.Visibility = System.Windows.Visibility.Collapsed; uiProgressRingInstall.IsActive = false; uiBtnExtract.IsEnabled = true; + try + { + Preferences.Set(AppConfigKey.InstalledGameVersion, ViewModel?.Game?.Version, $"{ViewModel.InstallPath}\\gamevault-exec"); + } + catch { } //Save forced install type for uninstallation if (isGameTypeForced && Directory.Exists(ViewModel.InstallPath) && ViewModel?.Game?.Type != null) { @@ -859,6 +881,5 @@ private void InitOverwriteGameType_Click(object sender, RoutedEventArgs e) ViewModel.Game = null; ViewModel.Game = temp; } - } } diff --git a/gamevault/UserControls/GameSettingsUserControl.xaml b/gamevault/UserControls/GameSettingsUserControl.xaml index b32ba56..c6595b9 100644 --- a/gamevault/UserControls/GameSettingsUserControl.xaml +++ b/gamevault/UserControls/GameSettingsUserControl.xaml @@ -153,6 +153,7 @@ + @@ -633,7 +634,7 @@ - + @@ -642,6 +643,13 @@ + + + + + + + diff --git a/gamevault/UserControls/GameSettingsUserControl.xaml.cs b/gamevault/UserControls/GameSettingsUserControl.xaml.cs index 8b8041e..b6f6d67 100644 --- a/gamevault/UserControls/GameSettingsUserControl.xaml.cs +++ b/gamevault/UserControls/GameSettingsUserControl.xaml.cs @@ -22,6 +22,7 @@ using LiveChartsCore.SkiaSharpView.Painting; using gamevault.Models.Mapping; using IO.Swagger.Model; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; namespace gamevault.UserControls @@ -50,6 +51,8 @@ internal GameSettingsUserControl(Game game) if (Directory.Exists(ViewModel.Directory)) { ViewModel.LaunchParameter = Preferences.Get(AppConfigKey.LaunchParameter, $"{ViewModel.Directory}\\gamevault-exec"); + string installedVersion = Preferences.Get(AppConfigKey.InstalledGameVersion, $"{ViewModel.Directory}\\gamevault-exec"); + ViewModel.InstalledGameVersion = installedVersion == string.Empty ? null : installedVersion; } InitDiskUsagePieChart();//Task } @@ -93,7 +96,7 @@ private void Help_Click(object sender, MouseButtonEventArgs e) } case 2: { - url = "https://gamevau.lt/docs/client-docs/gui#edit-images"; + url = "https://gamevau.lt/docs/client-docs/gui/#edit-game-images"; break; } case 3: @@ -209,53 +212,77 @@ public async Task UninstallGame() MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync($"Are you sure you want to uninstall '{ViewModel.Game.Title}' ?\nAs this is a Windows Setup Game, you will need to select an uninstall executable manually", "", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false }); if (result == MessageDialogResult.Affirmative) { - using (var dialog = new System.Windows.Forms.OpenFileDialog()) + string selectedUninstallerExecutablePath = ""; + if (!string.IsNullOrWhiteSpace(ViewModel.Game?.Metadata?.UninstallerExecutable)) { - dialog.InitialDirectory = ViewModel.Directory; - dialog.Filter = "uninstall|*.exe"; - System.Windows.Forms.DialogResult fileResult = dialog.ShowDialog(); - if (fileResult == System.Windows.Forms.DialogResult.OK && File.Exists(dialog.FileName)) + var entry = Directory.GetFiles(ViewModel.Directory, "*", SearchOption.AllDirectories) + .Select((file) => new { Key = file.Substring(ViewModel.Directory.Length + 1), Value = file }) + .FirstOrDefault(item => item.Key.Contains(ViewModel.Game?.Metadata?.UninstallerExecutable.Replace("/", "\\"), StringComparison.OrdinalIgnoreCase)); + if (entry != null) { - MessageDialogResult pickResult = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync($"Are you sure you want to uninstall the game using '{Path.GetFileName(dialog.FileName)}' ?", "", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false }); - if (pickResult != MessageDialogResult.Affirmative) + selectedUninstallerExecutablePath = entry.Value; + if (!File.Exists(selectedUninstallerExecutablePath)) { - return; + selectedUninstallerExecutablePath = ""; } - Process uninstProcess = null; - try - { - uninstProcess = ProcessHelper.StartApp(dialog.FileName); - } - catch + } + } + if (selectedUninstallerExecutablePath == "") + { + using (var dialog = new System.Windows.Forms.OpenFileDialog()) + { + dialog.InitialDirectory = ViewModel.Directory; + dialog.Filter = "uninstall|*.exe"; + System.Windows.Forms.DialogResult fileResult = dialog.ShowDialog(); + if (fileResult == System.Windows.Forms.DialogResult.OK && File.Exists(dialog.FileName)) { - - try + MessageDialogResult pickResult = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync($"Are you sure you want to uninstall the game using '{Path.GetFileName(dialog.FileName)}' ?", "", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false }); + if (pickResult != MessageDialogResult.Affirmative) { - uninstProcess = ProcessHelper.StartApp(dialog.FileName, "", true); - } - catch - { - MainWindowViewModel.Instance.AppBarText = $"Can not execute '{dialog.FileName}'"; + return; } + selectedUninstallerExecutablePath = dialog.FileName; } - if (uninstProcess != null) + } + } + if (!File.Exists(selectedUninstallerExecutablePath)) + { + MainWindowViewModel.Instance.AppBarText = "No valid uninstall executable selected"; + return; + } + Process uninstProcess = null; + try + { + uninstProcess = ProcessHelper.StartApp(selectedUninstallerExecutablePath, ViewModel.Game?.Metadata?.UninstallerParameters); + } + catch + { + + try + { + uninstProcess = ProcessHelper.StartApp(selectedUninstallerExecutablePath, ViewModel.Game?.Metadata?.UninstallerParameters, true); + } + catch + { + MainWindowViewModel.Instance.AppBarText = $"Can not execute '{selectedUninstallerExecutablePath}'"; + } + } + if (uninstProcess != null) + { + await uninstProcess.WaitForExitAsync(); + try + { + if (Directory.Exists(ViewModel.Directory)) { - await uninstProcess.WaitForExitAsync(); - try - { - if (Directory.Exists(ViewModel.Directory)) - { - //Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(ViewModel.Directory, Microsoft.VisualBasic.FileIO.UIOption.AllDialogs, Microsoft.VisualBasic.FileIO.RecycleOption.DeletePermanently); - Directory.Delete(ViewModel.Directory, true); - } - - InstallViewModel.Instance.InstalledGames.Remove(InstallViewModel.Instance.InstalledGames.Where(g => g.Key.ID == ViewModel.Game.ID).First()); - DesktopHelper.RemoveShotcut(ViewModel.Game); - MainWindowViewModel.Instance.ClosePopup(); - } - catch { } + //Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(ViewModel.Directory, Microsoft.VisualBasic.FileIO.UIOption.AllDialogs, Microsoft.VisualBasic.FileIO.RecycleOption.DeletePermanently); + Directory.Delete(ViewModel.Directory, true); } + + InstallViewModel.Instance.InstalledGames.Remove(InstallViewModel.Instance.InstalledGames.Where(g => g.Key.ID == ViewModel.Game.ID).First()); + DesktopHelper.RemoveShotcut(ViewModel.Game); + MainWindowViewModel.Instance.ClosePopup(); } + catch { } } } } @@ -587,30 +614,29 @@ private async Task SaveImage(string tag) BitmapSource bitmapSource = tag == "box" ? (BitmapSource)ViewModel.GameCoverImageSource : (BitmapSource)ViewModel.BackgroundImageSource; string resp = await WebHelper.UploadFileAsync($"{SettingsViewModel.Instance.ServerUrl}/api/media", BitmapHelper.BitmapSourceToMemoryStream(bitmapSource), "x.jpg", null); Media? newImage = JsonSerializer.Deserialize(resp); - await Task.Run(() => + + try { - try + UpdateGameDto updateGame = new UpdateGameDto() { UserMetadata = new UpdateGameUserMetadataDto() }; + if (tag == "box") { - UpdateGameDto updateGame = new UpdateGameDto() { UserMetadata = new UpdateGameUserMetadataDto() }; - if (tag == "box") - { - updateGame.UserMetadata.Cover = newImage; - } - else - { - updateGame.UserMetadata.Background = newImage; - } - - string changedGame = WebHelper.Put($"{SettingsViewModel.Instance.ServerUrl}/api/games/{ViewModel.Game.ID}", JsonSerializer.Serialize(updateGame), true); - ViewModel.Game = JsonSerializer.Deserialize(changedGame); - success = true; - MainWindowViewModel.Instance.AppBarText = "Successfully updated image"; + updateGame.UserMetadata.Cover = newImage; } - catch (Exception ex) + else { - MainWindowViewModel.Instance.AppBarText = WebExceptionHelper.TryGetServerMessage(ex); + updateGame.UserMetadata.Background = newImage; } - }); + + string changedGame = await WebHelper.PutAsync($"{SettingsViewModel.Instance.ServerUrl}/api/games/{ViewModel.Game.ID}", JsonSerializer.Serialize(updateGame)); + ViewModel.Game = JsonSerializer.Deserialize(changedGame); + success = true; + MainWindowViewModel.Instance.AppBarText = "Successfully updated image"; + } + catch (Exception ex) + { + MainWindowViewModel.Instance.AppBarText = WebExceptionHelper.TryGetServerMessage(ex); + } + //Update Data Context for Library. So that the images are also refreshed there directly if (success) { @@ -759,7 +785,7 @@ private async Task GameMetadataSearch() this.Cursor = Cursors.Wait; try { - string currentShownUser = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/metadata/providers/{ViewModel.MetadataProviders?[ViewModel.SelectedMetadataProviderIndex]?.Slug}/search?query={GameMetadataSearchTimer.Data}"); + string currentShownUser = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/metadata/providers/{ViewModel.MetadataProviders?[ViewModel.SelectedMetadataProviderIndex]?.Slug}/search?query={GameMetadataSearchTimer.Data}"); ViewModel.RemapSearchResults = JsonSerializer.Deserialize(currentShownUser); } catch (Exception ex) @@ -813,22 +839,19 @@ private async Task RemapGame(string? providerId, string? providerSlug, int gameI { bool success = false; this.IsEnabled = false; - await Task.Run(() => + try { - try - { - UpdateGameDto updateGame = new UpdateGameDto() { MappingRequests = new List() { new MapGameDto() { ProviderSlug = providerSlug, ProviderDataId = providerId, ProviderPriority = priority } } }; - string remappedGame = WebHelper.Put($"{SettingsViewModel.Instance.ServerUrl}/api/games/{gameId}", JsonSerializer.Serialize(updateGame), true); - ViewModel.Game = JsonSerializer.Deserialize(remappedGame); - success = true; - MainWindowViewModel.Instance.AppBarText = $"Successfully re-mapped {ViewModel.Game.Title}"; - } - catch (Exception ex) - { - string errMessage = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = errMessage; - } - }); + UpdateGameDto updateGame = new UpdateGameDto() { MappingRequests = new List() { new MapGameDto() { ProviderSlug = providerSlug, ProviderDataId = providerId, ProviderPriority = priority } } }; + string remappedGame = await WebHelper.PutAsync($"{SettingsViewModel.Instance.ServerUrl}/api/games/{gameId}", JsonSerializer.Serialize(updateGame)); + ViewModel.Game = JsonSerializer.Deserialize(remappedGame); + success = true; + MainWindowViewModel.Instance.AppBarText = $"Successfully re-mapped {ViewModel.Game.Title}"; + } + catch (Exception ex) + { + string errMessage = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = errMessage; + } InstallViewModel.Instance.RefreshGame(ViewModel.Game); MainWindowViewModel.Instance.Library.RefreshGame(ViewModel.Game); if (success) @@ -847,7 +870,7 @@ private async Task LoadGameMedatataProviders() try { ViewModel.MetadataProvidersLoaded = false; - string result = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/metadata/providers"); + string result = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/metadata/providers"); var providers = JsonSerializer.Deserialize(result); foreach (GameMetadata gmd in ViewModel.Game.ProviderMetadata) { @@ -887,22 +910,20 @@ private async void SaveGameDetails_Click(object sender, RoutedEventArgs e) { this.IsEnabled = false; bool success = false; - await Task.Run(() => + + try { - try - { - string remappedGame = WebHelper.Put($"{SettingsViewModel.Instance.ServerUrl}/api/games/{ViewModel.Game.ID}", JsonSerializer.Serialize(ViewModel.UpdateGame), true); - ViewModel.Game = JsonSerializer.Deserialize(remappedGame); - success = true; - ViewModel.UpdateGame = new UpdateGameDto() { UserMetadata = new UpdateGameUserMetadataDto() }; - MainWindowViewModel.Instance.AppBarText = $"Successfully edited {ViewModel.Game.Title}"; - } - catch (Exception ex) - { - string errMessage = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = errMessage; - } - }); + string remappedGame = await WebHelper.PutAsync($"{SettingsViewModel.Instance.ServerUrl}/api/games/{ViewModel.Game.ID}", JsonSerializer.Serialize(ViewModel.UpdateGame)); + ViewModel.Game = JsonSerializer.Deserialize(remappedGame); + success = true; + ViewModel.UpdateGame = new UpdateGameDto() { UserMetadata = new UpdateGameUserMetadataDto() }; + MainWindowViewModel.Instance.AppBarText = $"Successfully edited {ViewModel.Game.Title}"; + } + catch (Exception ex) + { + string errMessage = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = errMessage; + } if (success) { if (MainWindowViewModel.Instance.ActiveControl.GetType() == typeof(GameViewUserControl)) diff --git a/gamevault/UserControls/GameViewUserControl.xaml b/gamevault/UserControls/GameViewUserControl.xaml index b9d22d6..8b7d268 100644 --- a/gamevault/UserControls/GameViewUserControl.xaml +++ b/gamevault/UserControls/GameViewUserControl.xaml @@ -257,7 +257,7 @@ - + diff --git a/gamevault/UserControls/GameViewUserControl.xaml.cs b/gamevault/UserControls/GameViewUserControl.xaml.cs index 0d3a12d..36101fb 100644 --- a/gamevault/UserControls/GameViewUserControl.xaml.cs +++ b/gamevault/UserControls/GameViewUserControl.xaml.cs @@ -150,7 +150,7 @@ private async void UserControl_Loaded(object sender, RoutedEventArgs e) { try { - string result = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/{gameID}"); + string result = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/{gameID}"); ViewModel.Game = JsonSerializer.Deserialize(result); ViewModel.UserProgresses = ViewModel.Game.Progresses.Where(p => p.User.ID != LoginManager.Instance.GetCurrentUser().ID).ToArray(); ViewModel.CurrentUserProgress = ViewModel.Game.Progresses.FirstOrDefault(progress => progress.User.ID == LoginManager.Instance.GetCurrentUser()?.ID) ?? new Progress { MinutesPlayed = 0, State = State.UNPLAYED.ToString() }; @@ -164,8 +164,13 @@ private async void UserControl_Loaded(object sender, RoutedEventArgs e) { try { - SaveGameHelper.Instance.PrepareConfigFile("", Path.Combine(AppFilePath.CloudSaveConfigDir, "config.yaml")); - ViewModel.CloudSaveMatchTitle = await SaveGameHelper.Instance.SearchForLudusaviGameTitle(ViewModel?.Game?.Metadata?.Title); + SaveGameHelper.Instance.PrepareConfigFile("", Path.Combine(LoginManager.Instance.GetUserProfile().CloudSaveConfigDir, "config.yaml")); + string gameMetadataTitle = ViewModel?.Game?.Metadata?.Title ?? ""; + if (gameMetadataTitle == "") + { + gameMetadataTitle = ViewModel?.Game?.Title ?? ""; + } + ViewModel.CloudSaveMatchTitle = await SaveGameHelper.Instance.SearchForLudusaviGameTitle(gameMetadataTitle); } catch { } }); @@ -196,7 +201,7 @@ private async Task ReloadGameView() this.IsEnabled = false; try { - string result = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/{gameID}"); + string result = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/games/{gameID}"); ViewModel.Game = JsonSerializer.Deserialize(result); ViewModel.UserProgresses = ViewModel.Game.Progresses.Where(p => p.User.ID != LoginManager.Instance.GetCurrentUser().ID).ToArray(); ViewModel.CurrentUserProgress = ViewModel.Game.Progresses.FirstOrDefault(progress => progress.User.ID == LoginManager.Instance.GetCurrentUser()?.ID) ?? new Progress { MinutesPlayed = 0, State = State.UNPLAYED.ToString() }; @@ -281,18 +286,15 @@ private async void GameState_SelectionChanged(object sender, SelectionChangedEve return; if (e.AddedItems.Count > 0) { - await Task.Run(() => + try { - try - { - WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/user/{LoginManager.Instance.GetCurrentUser().ID}/game/{gameID}", System.Text.Json.JsonSerializer.Serialize(new Progress() { State = ViewModel.CurrentUserProgress.State })); - } - catch (Exception ex) - { - string msg = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = msg; - } - }); + await WebHelper.PutAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/user/{LoginManager.Instance.GetCurrentUser().ID}/game/{gameID}", System.Text.Json.JsonSerializer.Serialize(new Progress() { State = ViewModel.CurrentUserProgress.State })); + } + catch (Exception ex) + { + string msg = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = msg; + } } } private void ShowProgressUser_Click(object sender, MouseButtonEventArgs e) @@ -328,7 +330,7 @@ private async void Bookmark_Click(object sender, RoutedEventArgs e) } else { - await WebHelper.PostAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/me/bookmark/{ViewModel.Game.ID}"); + await WebHelper.PostAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/users/me/bookmark/{ViewModel.Game.ID}", ""); ViewModel.Game.BookmarkedUsers = new List { LoginManager.Instance.GetCurrentUser()! }; } MainWindowViewModel.Instance.Library.RefreshGame(ViewModel.Game); diff --git a/gamevault/UserControls/GeneralControls/CacheImage.xaml.cs b/gamevault/UserControls/GeneralControls/CacheImage.xaml.cs index d4078f9..41d6244 100644 --- a/gamevault/UserControls/GeneralControls/CacheImage.xaml.cs +++ b/gamevault/UserControls/GeneralControls/CacheImage.xaml.cs @@ -33,17 +33,6 @@ public ImageCache ImageCacheType get { return (ImageCache)GetValue(ImageCacheTypeProperty); } set { SetValue(ImageCacheTypeProperty, value); } } - public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(object), typeof(CacheImage), new PropertyMetadata(OnDataChangedCallBack)); - //Add Data as DependencyProperty, because previous DataContext_Changed is called before ImageCacheType is set, when inside XAML DataTemplate. So there was a bug, where i could not choose ImageCacheType. - public object Data - { - get { return (ImageCache)GetValue(DataProperty); } - set { SetValue(DataProperty, value); } - } - private static async void OnDataChangedCallBack(DependencyObject sender, DependencyPropertyChangedEventArgs e) - { - await ((CacheImage)sender).DataChanged(e.NewValue); - } public static readonly DependencyProperty UseUriSourceProperty = DependencyProperty.Register("UseUriSource", typeof(bool), typeof(CacheImage)); public bool UseUriSource @@ -51,6 +40,7 @@ public bool UseUriSource get { return (bool)GetValue(UseUriSourceProperty); } set { SetValue(UseUriSourceProperty, value); } } + public static readonly DependencyProperty DoNotCacheProperty = DependencyProperty.Register("DoNotCache", typeof(bool), typeof(CacheImage)); public bool DoNotCache @@ -84,6 +74,17 @@ private static void OnCornerRadiusChangedCallBack(DependencyObject sender, Depen { ((CacheImage)sender).uiBorder.CornerRadius = (CornerRadius)e.NewValue; } + public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(object), typeof(CacheImage), new PropertyMetadata(OnDataChangedCallBack)); + //Add Data as DependencyProperty, because previous DataContext_Changed is called before ImageCacheType is set, when inside XAML DataTemplate. So there was a bug, where i could not choose ImageCacheType. + public object Data + { + get { return (ImageCache)GetValue(DataProperty); } + set { SetValue(DataProperty, value); } + } + private static async void OnDataChangedCallBack(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + await ((CacheImage)sender).DataChanged(e.NewValue); + } #endregion #region Convertion Object @@ -184,14 +185,15 @@ private async Task DataChanged(object newData) } catch (Exception ex) { - MainWindowViewModel.Instance.AppBarText = ex.Message; + uiImg.Source = CacheHelper.GetReplacementImage(ImageCacheType); + //MainWindowViewModel.Instance.AppBarText = ex.Message; } } return; } int imageId = -1; - string cachePath = AppFilePath.ImageCache; + string cachePath = LoginManager.Instance.GetUserProfile().ImageCacheDir; CacheImageMedia media = new CacheImageMedia(); try diff --git a/gamevault/UserControls/GeneralControls/MediaSlider.xaml.cs b/gamevault/UserControls/GeneralControls/MediaSlider.xaml.cs index 721c6b3..edef5d0 100644 --- a/gamevault/UserControls/GeneralControls/MediaSlider.xaml.cs +++ b/gamevault/UserControls/GeneralControls/MediaSlider.xaml.cs @@ -1,4 +1,5 @@ -using gamevault.Models; +using gamevault.Helper; +using gamevault.Models; using gamevault.ViewModels; using Microsoft.Web.WebView2.Core; using System; @@ -86,7 +87,7 @@ public void UnloadMediaSlider() } public async Task RestoreLastMediaVolume() { - string result = Preferences.Get(AppConfigKey.MediaSliderVolume, AppFilePath.UserFile); + string result = Preferences.Get(AppConfigKey.MediaSliderVolume,LoginManager.Instance.GetUserProfile().UserConfigFile); string lastMediaVolume = string.IsNullOrWhiteSpace(result) ? "0.0" : result; if (double.TryParse(lastMediaVolume.Replace(".", ","), out double volume)) { @@ -107,7 +108,7 @@ public async Task SetAndSaveMediaVolume() try { string result = uiVolumeSlider.Value.ToString().Replace(",", "."); - Preferences.Set(AppConfigKey.MediaSliderVolume, result, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.MediaSliderVolume, result,LoginManager.Instance.GetUserProfile().UserConfigFile); if (uiWebView == null || uiWebView.CoreWebView2 == null) return; @@ -153,7 +154,7 @@ public async Task InitVideoPlayer() { AdditionalBrowserArguments = "--disk-cache-size=1000000" }; - var env = await CoreWebView2Environment.CreateAsync(null, AppFilePath.WebConfigDir, options); + var env = await CoreWebView2Environment.CreateAsync(null, LoginManager.Instance.GetUserProfile().WebConfigDir, options); await uiWebView.EnsureCoreWebView2Async(env); uiWebView.NavigationCompleted += async (s, e) => { diff --git a/gamevault/UserControls/GeneralControls/NewsPopup.xaml.cs b/gamevault/UserControls/GeneralControls/NewsPopup.xaml.cs index 38d75b7..61b0d89 100644 --- a/gamevault/UserControls/GeneralControls/NewsPopup.xaml.cs +++ b/gamevault/UserControls/GeneralControls/NewsPopup.xaml.cs @@ -37,9 +37,9 @@ private async void UserControl_Loaded(object sender, RoutedEventArgs e) this.Focus(); try { - string gameVaultNews = await WebHelper.DownloadFileContentAsync("https://gamevau.lt/news.md"); + string gameVaultNews = await WebHelper.GetAsync("https://gamevau.lt/news.md"); uiGameVaultNews.Markdown = gameVaultNews; - string serverNews = await WebHelper.GetRequestAsync($"{SettingsViewModel.Instance.ServerUrl}/api/config/news"); + string serverNews = await WebHelper.GetAsync($"{SettingsViewModel.Instance.ServerUrl}/api/config/news"); uiServerNews.Markdown = serverNews; } catch { } diff --git a/gamevault/UserControls/GeneralControls/PillSelector.xaml.cs b/gamevault/UserControls/GeneralControls/PillSelector.xaml.cs index 576cc66..321b2ee 100644 --- a/gamevault/UserControls/GeneralControls/PillSelector.xaml.cs +++ b/gamevault/UserControls/GeneralControls/PillSelector.xaml.cs @@ -151,24 +151,21 @@ private async Task LoadSelectionEntries() }; Selection selection = SelectionType; - await Task.Run(() => + try { - try + string result = await WebHelper.GetAsync(url); + data = selection switch { - string result = WebHelper.GetRequest(url); - data = selection switch - { - Selection.Tags => JsonSerializer.Deserialize>(result).Data, - Selection.Genres => JsonSerializer.Deserialize>(result).Data, - Selection.Developers => JsonSerializer.Deserialize>(result).Data, - Selection.Publishers => JsonSerializer.Deserialize>(result).Data - }; - } - catch (Exception ex) - { - MainWindowViewModel.Instance.AppBarText = WebExceptionHelper.TryGetServerMessage(ex); - } - }); + Selection.Tags => JsonSerializer.Deserialize>(result).Data, + Selection.Genres => JsonSerializer.Deserialize>(result).Data, + Selection.Developers => JsonSerializer.Deserialize>(result).Data, + Selection.Publishers => JsonSerializer.Deserialize>(result).Data + }; + } + catch (Exception ex) + { + MainWindowViewModel.Instance.AppBarText = WebExceptionHelper.TryGetServerMessage(ex); + } } uiSelectionEntries.ItemsSource = data; } @@ -197,7 +194,7 @@ private void AddEntry_Click(object sender, MouseButtonEventArgs e) { if (selectedEntries.Contains((Pill)((FrameworkElement)sender).DataContext)) return; if (MaxSelection > 0 && selectedEntries.Count >= MaxSelection) return; - if(!IsMultiSelection) + if (!IsMultiSelection) { selectedEntries.Clear(); } diff --git a/gamevault/UserControls/InstallUserControl.xaml b/gamevault/UserControls/InstallUserControl.xaml index 4fce82a..ceb52c8 100644 --- a/gamevault/UserControls/InstallUserControl.xaml +++ b/gamevault/UserControls/InstallUserControl.xaml @@ -6,6 +6,7 @@ xmlns:helper="clr-namespace:gamevault.Helper" xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" xmlns:vm="clr-namespace:gamevault.ViewModels" + xmlns:conv="clr-namespace:gamevault.Converter" xmlns:local="clr-namespace:gamevault.UserControls" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" Loaded="UserControl_Loaded"> @@ -63,6 +64,15 @@ + + @@ -109,9 +119,15 @@ - + + + + + + + - + - - - - - diff --git a/gamevault/UserControls/SettingsComponents/RootPathUserControl.xaml.cs b/gamevault/UserControls/SettingsComponents/RootPathUserControl.xaml.cs deleted file mode 100644 index ed5b1b8..0000000 --- a/gamevault/UserControls/SettingsComponents/RootPathUserControl.xaml.cs +++ /dev/null @@ -1,33 +0,0 @@ -using gamevault.ViewModels; -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 gamevault.UserControls.SettingsComponents -{ - /// - /// Interaction logic for RootPathUserControl.xaml - /// - public partial class RootPathUserControl : UserControl - { - public RootPathUserControl() - { - InitializeComponent(); - } - private void RootPath_Click(object sender, RoutedEventArgs e) - { - SettingsViewModel.Instance.SelectDownloadPath(); - } - } -} diff --git a/gamevault/UserControls/SettingsComponents/ServerUrlUserControl.xaml b/gamevault/UserControls/SettingsComponents/ServerUrlUserControl.xaml deleted file mode 100644 index 11ccf32..0000000 --- a/gamevault/UserControls/SettingsComponents/ServerUrlUserControl.xaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - diff --git a/gamevault/UserControls/SettingsComponents/ServerUrlUserControl.xaml.cs b/gamevault/UserControls/SettingsComponents/ServerUrlUserControl.xaml.cs deleted file mode 100644 index 32b7a32..0000000 --- a/gamevault/UserControls/SettingsComponents/ServerUrlUserControl.xaml.cs +++ /dev/null @@ -1,50 +0,0 @@ -using ABI.System; -using gamevault.Models; -using gamevault.ViewModels; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using System.Windows; -using System.Windows.Controls; - - -namespace gamevault.UserControls.SettingsComponents -{ - /// - /// Interaction logic for ServerUrlUserControl.xaml - /// - public partial class ServerUrlUserControl : UserControl - { - public ServerUrlUserControl() - { - InitializeComponent(); - } - private void SaveServerUrl_Click(object sender, RoutedEventArgs e) - { - SaveServerURL(); - } - - private void Save_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) - { - if (e.Key == System.Windows.Input.Key.Enter) - { - SaveServerURL(); - } - } - private void SaveServerURL() - { - if (SettingsViewModel.Instance.ServerUrl.EndsWith("/")) - { - SettingsViewModel.Instance.ServerUrl = SettingsViewModel.Instance.ServerUrl.Substring(0, SettingsViewModel.Instance.ServerUrl.Length - 1); - } - if (!SettingsViewModel.Instance.ServerUrl.Contains(System.Uri.UriSchemeHttp)) - { - SettingsViewModel.Instance.ServerUrl = $"{System.Uri.UriSchemeHttps}://{SettingsViewModel.Instance.ServerUrl}"; - } - Preferences.Set(AppConfigKey.ServerUrl, SettingsViewModel.Instance.ServerUrl, AppFilePath.UserFile, true); - MainWindowViewModel.Instance.AppBarText = "Server URL saved"; - } - } -} diff --git a/gamevault/UserControls/SettingsUserControl.xaml b/gamevault/UserControls/SettingsUserControl.xaml index b54cd84..c56e7f7 100644 --- a/gamevault/UserControls/SettingsUserControl.xaml +++ b/gamevault/UserControls/SettingsUserControl.xaml @@ -34,11 +34,6 @@ - - - - - @@ -194,11 +189,13 @@ - - - - - + + + + + + + @@ -206,7 +203,40 @@ - + + + + + + + + + + + + + + + + + + + + + + @@ -501,7 +531,7 @@ - + @@ -622,6 +652,7 @@ + diff --git a/gamevault/UserControls/SettingsUserControl.xaml.cs b/gamevault/UserControls/SettingsUserControl.xaml.cs index 1fa22ca..7f6fb76 100644 --- a/gamevault/UserControls/SettingsUserControl.xaml.cs +++ b/gamevault/UserControls/SettingsUserControl.xaml.cs @@ -19,6 +19,10 @@ using System.Windows.Markup; using gamevault.Helper.Integrations; using AngleSharp.Dom; +using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.Wpf; +using gamevault.Windows; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace gamevault.UserControls { @@ -44,8 +48,8 @@ private void ClearImageCache_Clicked(object sender, RoutedEventArgs e) try { - Directory.Delete(AppFilePath.ImageCache, true); - Directory.CreateDirectory(AppFilePath.ImageCache); + Directory.Delete(LoginManager.Instance.GetUserProfile().ImageCacheDir, true); + Directory.CreateDirectory(LoginManager.Instance.GetUserProfile().ImageCacheDir); ViewModel.ImageCacheSize = 0; MainWindowViewModel.Instance.AppBarText = "Image cache cleared"; } @@ -55,27 +59,29 @@ private void ClearImageCache_Clicked(object sender, RoutedEventArgs e) } } - private void ClearOfflineCache_Clicked(object sender, RoutedEventArgs e) + private async void ClearOfflineCache_Clicked(object sender, RoutedEventArgs e) { - - try + MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync($"Are you sure you want delete the offline cache? \nThis can lead to games not being displayed correctly when you are offline.", "", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false }); + if (result == MessageDialogResult.Affirmative) { - if (File.Exists(AppFilePath.IgnoreList)) + try { - File.Delete(AppFilePath.IgnoreList); + if (File.Exists(LoginManager.Instance.GetUserProfile().IgnoreList)) + { + File.Delete(LoginManager.Instance.GetUserProfile().IgnoreList); + } + if (File.Exists(LoginManager.Instance.GetUserProfile().OfflineCache)) + { + File.Delete(LoginManager.Instance.GetUserProfile().OfflineCache); + } + ViewModel.OfflineCacheSize = 0; + MainWindowViewModel.Instance.AppBarText = "Offline cache cleared"; } - if (File.Exists(AppFilePath.OfflineCache)) + catch { - File.Delete(AppFilePath.OfflineCache); + MainWindowViewModel.Instance.AppBarText = "Something went wrong while the offline cache was cleared"; } - ViewModel.OfflineCacheSize = 0; - MainWindowViewModel.Instance.AppBarText = "Offline cache cleared"; - } - catch - { - MainWindowViewModel.Instance.AppBarText = "Something went wrong while the offline cache was cleared"; } - } private async void UserControl_Loaded(object sender, RoutedEventArgs e) { @@ -93,7 +99,7 @@ private async void UserControl_Loaded(object sender, RoutedEventArgs e) } uiAutostartToggle.Toggled += AppAutostart_Toggled; LoadThemes(); - uiPwExtraction.Password = Preferences.Get(AppConfigKey.ExtractionPassword, AppFilePath.UserFile, true); + uiPwExtraction.Password = Preferences.Get(AppConfigKey.ExtractionPassword, LoginManager.Instance.GetUserProfile().UserConfigFile, true); } private async void AppAutostart_Toggled(object sender, RoutedEventArgs e) { @@ -118,8 +124,8 @@ private async void TabControl_SelectionChanged(object sender, SelectionChangedEv { if (((TabControl)sender).SelectedIndex == 3) { - ViewModel.ImageCacheSize = await CalculateDirectorySize(new DirectoryInfo(AppFilePath.ImageCache)); - ViewModel.OfflineCacheSize = (File.Exists(AppFilePath.OfflineCache) ? new FileInfo(AppFilePath.OfflineCache).Length : 0); + ViewModel.ImageCacheSize = await CalculateDirectorySize(new DirectoryInfo(LoginManager.Instance.GetUserProfile().ImageCacheDir)); + ViewModel.OfflineCacheSize = (File.Exists(LoginManager.Instance.GetUserProfile().OfflineCache) ? new FileInfo(LoginManager.Instance.GetUserProfile().OfflineCache).Length : 0); } } private async Task CalculateDirectorySize(DirectoryInfo d) @@ -145,11 +151,68 @@ private async Task CalculateDirectorySize(DirectoryInfo d) }); } - private void Logout_Click(object sender, RoutedEventArgs e) + private void ChangeUserProfile_Click(object sender, RoutedEventArgs e) { - LoginManager.Instance.Logout(); - MainWindowViewModel.Instance.UserAvatar = null; - MainWindowViewModel.Instance.AppBarText = "Successfully logged out"; + ((FrameworkElement)sender).IsEnabled = false; + Preferences.DeleteKey(AppConfigKey.LastUserProfile, ProfileManager.ProfileConfigFile); + ((MainWindow)App.Current.MainWindow).Dispose(); + App.Current.MainWindow = new LoginWindow(true); + App.Current.MainWindow.Show(); + ((FrameworkElement)sender).IsEnabled = true; + } + private async void Logout_Click(object sender, RoutedEventArgs e) + { + ((FrameworkElement)sender).IsEnabled = false; + MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync($"Are you sure you want to log out?", "", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false }); + if (result == MessageDialogResult.Affirmative) + { + try + { + bool isLoggedInWithSSO = Preferences.Get(AppConfigKey.IsLoggedInWithSSO, LoginManager.Instance.GetUserProfile().UserConfigFile) == "1"; + Preferences.DeleteKey(AppConfigKey.SessionToken, LoginManager.Instance.GetUserProfile().UserConfigFile); + await WebHelper.PostAsync($"{SettingsViewModel.Instance.ServerUrl}/api/auth/revoke", "{" + $"\"refresh_token\": \"{WebHelper.GetRefreshToken()}\"" + "}"); + if (!isLoggedInWithSSO) + { + Preferences.DeleteKey(AppConfigKey.Password, LoginManager.Instance.GetUserProfile().UserConfigFile); + } + Preferences.DeleteKey(AppConfigKey.LastUserProfile, ProfileManager.ProfileConfigFile); + ((MainWindow)App.Current.MainWindow).Dispose(); + App.Current.MainWindow = new LoginWindow(true); + App.Current.MainWindow.Show(); + } + catch (Exception ex) + { + MainWindowViewModel.Instance.AppBarText = ex.Message; + } + } + ((FrameworkElement)sender).IsEnabled = true; + } + private async void LogoutFromAllDevices_Click(object sender, RoutedEventArgs e) + { + ((FrameworkElement)sender).IsEnabled = false; + MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync($"Are you sure you want to log out from all devices?", "", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false }); + if (result == MessageDialogResult.Affirmative) + { + try + { + bool isLoggedInWithSSO = Preferences.Get(AppConfigKey.IsLoggedInWithSSO, LoginManager.Instance.GetUserProfile().UserConfigFile) == "1"; + Preferences.DeleteKey(AppConfigKey.SessionToken, LoginManager.Instance.GetUserProfile().UserConfigFile); + await WebHelper.PostAsync($"{SettingsViewModel.Instance.ServerUrl}/api/auth/revoke/all", ""); + if (!isLoggedInWithSSO) + { + Preferences.DeleteKey(AppConfigKey.Password, LoginManager.Instance.GetUserProfile().UserConfigFile); + } + Preferences.DeleteKey(AppConfigKey.LastUserProfile, ProfileManager.ProfileConfigFile); + ((MainWindow)App.Current.MainWindow).Dispose(); + App.Current.MainWindow = new LoginWindow(true); + App.Current.MainWindow.Show(); + } + catch (Exception ex) + { + MainWindowViewModel.Instance.AppBarText = ex.Message; + } + } + ((FrameworkElement)sender).IsEnabled = true; } private void DownloadLimit_InputValidation(object sender, EventArgs e) @@ -181,7 +244,7 @@ private void DownloadLimit_Save(object sender, RoutedEventArgs e) } } ViewModel.DownloadLimit = ViewModel.DownloadLimitUIValue; - Preferences.Set(AppConfigKey.DownloadLimit, ViewModel.DownloadLimit, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.DownloadLimit, ViewModel.DownloadLimit, LoginManager.Instance.GetUserProfile().UserConfigFile); MainWindowViewModel.Instance.AppBarText = "Successfully saved download limit"; } @@ -198,7 +261,9 @@ private async void PhalcodeLoginLogout_Click(object sender, RoutedEventArgs e) ((FrameworkElement)sender).IsEnabled = false; if (string.IsNullOrEmpty(SettingsViewModel.Instance.License.UserName)) { - await LoginManager.Instance.PhalcodeLogin(); + string phalcodeLoginMessage = await LoginManager.Instance.PhalcodeLogin(); + if (phalcodeLoginMessage != string.Empty) + MainWindowViewModel.Instance.AppBarText = phalcodeLoginMessage; } else { @@ -266,10 +331,8 @@ private void Themes_SelectionChanged(object sender, SelectionChangedEventArgs e) } try { - App.Current.Resources.MergedDictionaries[0] = new ResourceDictionary() { Source = new Uri(selectedTheme.Path) }; - //Reload Base Styles to apply new colors - App.Current.Resources.MergedDictionaries[1] = new ResourceDictionary() { Source = new Uri("pack://application:,,,/gamevault;component/Resources/Assets/Base.xaml") }; - Preferences.Set(AppConfigKey.Theme, JsonSerializer.Serialize(selectedTheme), AppFilePath.UserFile, true); + App.Instance.SetTheme(selectedTheme.Path); + Preferences.Set(AppConfigKey.Theme, JsonSerializer.Serialize(selectedTheme), LoginManager.Instance.GetUserProfile().UserConfigFile, true); } catch (Exception ex) { MainWindowViewModel.Instance.AppBarText = ex.Message; } } @@ -308,9 +371,9 @@ private void LoadThemes() res.Source = new Uri("pack://application:,,,/gamevault;component/Resources/Assets/Themes/ThemeChristmasDark.xaml"); ViewModel.Themes.Add(new ThemeItem() { DisplayName = (string)res["Theme.DisplayName"], Description = (string)res["Theme.Description"], Author = (string)res["Theme.Author"], IsPlus = true, Path = res.Source.OriginalString }); - if (Directory.Exists(AppFilePath.ThemesLoadDir)) + if (Directory.Exists(LoginManager.Instance.GetUserProfile().ThemesLoadDir)) { - foreach (var file in Directory.GetFiles(AppFilePath.ThemesLoadDir, "*.xaml", SearchOption.AllDirectories)) + foreach (var file in Directory.GetFiles(LoginManager.Instance.GetUserProfile().ThemesLoadDir, "*.xaml", SearchOption.AllDirectories)) { try { @@ -320,7 +383,7 @@ private void LoadThemes() catch { } } } - string currentThemeString = Preferences.Get(AppConfigKey.Theme, AppFilePath.UserFile, true); + string currentThemeString = Preferences.Get(AppConfigKey.Theme, LoginManager.Instance.GetUserProfile().UserConfigFile, true); ThemeItem currentTheme = JsonSerializer.Deserialize(currentThemeString); int themeIndex = ViewModel.Themes.ToList().FindIndex(i => i.Path == currentTheme.Path); if (themeIndex != -1 && (ViewModel.Themes[themeIndex].IsPlus == true ? ViewModel.License.IsActive() : true)) @@ -340,14 +403,14 @@ private void OpenThemeFolder_Click(object sender, RoutedEventArgs e) { try { - if (Directory.Exists(AppFilePath.ThemesLoadDir)) + if (Directory.Exists(LoginManager.Instance.GetUserProfile().ThemesLoadDir)) { - Directory.CreateDirectory(AppFilePath.ThemesLoadDir); + Directory.CreateDirectory(LoginManager.Instance.GetUserProfile().ThemesLoadDir); } Process.Start(new ProcessStartInfo { FileName = "explorer.exe", - Arguments = $"\"{AppFilePath.ThemesLoadDir}\"", + Arguments = $"\"{LoginManager.Instance.GetUserProfile().ThemesLoadDir}\"", UseShellExecute = true }); } @@ -370,14 +433,14 @@ private async void ReloadThemeList_Click(object sender, RoutedEventArgs e) } private async Task> LoadCommunityThemesHeader() { - string jsonResponse = await WebHelper.DownloadFileContentAsync("https://api.github.com/repos/phalcode/gamevault-community-themes/contents/v1"); + string jsonResponse = await WebHelper.BaseGetAsync("https://api.github.com/repos/phalcode/gamevault-community-themes/contents/v1"); return JsonSerializer.Deserialize>(jsonResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } private async Task LoadThemeItemFromUrl(string url) { try { - string result = await WebHelper.DownloadFileContentAsync(url); + string result = await WebHelper.BaseGetAsync(url); ResourceDictionary res = (ResourceDictionary)XamlReader.Parse(result); return new ThemeItem() { DisplayName = (string)res["Theme.DisplayName"], Description = (string)res["Theme.Description"], Author = (string)res["Theme.Author"], Path = url }; } @@ -435,8 +498,8 @@ private async void InstallCommunityTheme_Click(object sender, RoutedEventArgs e) try { ThemeItem theme = (ThemeItem)uiCBCommunityThemes.SelectedItem; - string installationPath = Path.Combine(AppFilePath.ThemesLoadDir, theme.DisplayName + ".xaml"); - string result = await WebHelper.DownloadFileContentAsync(theme.Path); + string installationPath = Path.Combine(LoginManager.Instance.GetUserProfile().ThemesLoadDir, theme.DisplayName + ".xaml"); + string result = await WebHelper.BaseGetAsync(theme.Path); File.WriteAllText(installationPath, result); LoadThemes(); try @@ -486,15 +549,15 @@ private void Awesome_Click(object sender, MouseButtonEventArgs e) private void ExtractionPasswordSave_Click(object sender, RoutedEventArgs e) { - Preferences.Set(AppConfigKey.ExtractionPassword, uiPwExtraction.Password, AppFilePath.UserFile, true); + Preferences.Set(AppConfigKey.ExtractionPassword, uiPwExtraction.Password, LoginManager.Instance.GetUserProfile().UserConfigFile, true); MainWindowViewModel.Instance.AppBarText = "Successfully saved extraction password"; } private async void IgnoredExecutablesReset_Click(object sender, RoutedEventArgs e) { try { - if (File.Exists(AppFilePath.IgnoreList)) - File.Delete(AppFilePath.IgnoreList); + if (File.Exists(LoginManager.Instance.GetUserProfile().IgnoreList)) + File.Delete(LoginManager.Instance.GetUserProfile().IgnoreList); await SettingsViewModel.Instance.InitIgnoreList(); } @@ -504,7 +567,7 @@ private void IgnoredExecutablesSave_Click(object sender, RoutedEventArgs e) { try { - Preferences.Set("IL", SettingsViewModel.Instance.IgnoreList, AppFilePath.IgnoreList); + Preferences.Set("IL", SettingsViewModel.Instance.IgnoreList, LoginManager.Instance.GetUserProfile().IgnoreList); } catch { } } @@ -543,15 +606,16 @@ private void DevMode_Click(object sender, MouseButtonEventArgs e) } private void RemoveCustomCloudSaveManifest_Click(object sender, RoutedEventArgs e) { - int index = ViewModel.CustomCloudSaveManifests.IndexOf(((LudusaviManifestEntry)((FrameworkElement)sender).DataContext)); + int index = ViewModel.CustomCloudSaveManifests.IndexOf(((DirectoryEntry)((FrameworkElement)sender).DataContext)); if (index >= 0) { ViewModel.CustomCloudSaveManifests.RemoveAt(index); } } + private void AddCustomCloudSaveManifest_Click(object sender, RoutedEventArgs e) { - ViewModel.CustomCloudSaveManifests.Add(new LudusaviManifestEntry()); + ViewModel.CustomCloudSaveManifests.Add(new DirectoryEntry()); } private void SaveCustomCloudSaveManifests_Click(object sender, RoutedEventArgs e) @@ -559,10 +623,82 @@ private void SaveCustomCloudSaveManifests_Click(object sender, RoutedEventArgs e try { string result = string.Join(";", ViewModel.CustomCloudSaveManifests.Where(entry => !string.IsNullOrWhiteSpace(entry.Uri)).Select(entry => entry.Uri)); - Preferences.Set(AppConfigKey.CustomCloudSaveManifests, result, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.CustomCloudSaveManifests, result, LoginManager.Instance.GetUserProfile().UserConfigFile); } catch { } MainWindowViewModel.Instance.AppBarText = "Successfully saved custom Ludusavi Manifests"; } + private async void AddRootDirectory_Click(object sender, RoutedEventArgs e) + { + ((FrameworkElement)sender).IsEnabled = false; + try + { + string selectedDirectory = await SettingsViewModel.Instance.SelectDownloadPath(); + if (Directory.Exists(selectedDirectory)) + { + ViewModel.RootDirectories.Add(new DirectoryEntry() { Uri = selectedDirectory }); + string result = string.Join(";", ViewModel.RootDirectories.Select(entry => entry.Uri)); + Preferences.Set(AppConfigKey.RootDirectories, result, LoginManager.Instance.GetUserProfile().UserConfigFile); + await MainWindowViewModel.Instance.Library.GetGameInstalls().RestoreInstalledGames(); + await MainWindowViewModel.Instance.Downloads.RestoreDownloadedGames(); + if (InstallViewModel.Instance.InstalledGamesDuplicates.Any()) + { + await ShowInstalledGameDuplicates(); + } + } + } + catch (Exception ex) + { + MainWindowViewModel.Instance.AppBarText = ex.Message; + } + ((FrameworkElement)sender).IsEnabled = true; + } + private async Task ShowInstalledGameDuplicates() + { + string duplicateMessage = ""; + foreach (var duplicate in InstallViewModel.Instance.InstalledGamesDuplicates) + { + var matchingGame = InstallViewModel.Instance.InstalledGames?.FirstOrDefault(game => game.Key.ID == duplicate.Key); + if (string.IsNullOrEmpty(matchingGame?.Key?.Title)) + continue; + + duplicateMessage += $"\n\n'{matchingGame?.Key?.Title}' is already installed at:\n{duplicate.Value}"; + } + await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync("Duplicate game installation detected", duplicateMessage, MessageDialogStyle.Affirmative, new MetroDialogSettings() { AffirmativeButtonText = "Ok", DialogTitleFontSize = 20, AnimateHide = false }); + } + private async void RemoveRootDirectory_Click(object sender, RoutedEventArgs e) + { + try + { + int index = ViewModel.RootDirectories.IndexOf(((DirectoryEntry)((FrameworkElement)sender).DataContext)); + if (index >= 0) + { + ViewModel.RootDirectories.RemoveAt(index); + string result = string.Join(";", ViewModel.RootDirectories.Select(entry => entry.Uri)); + Preferences.Set(AppConfigKey.RootDirectories, result, LoginManager.Instance.GetUserProfile().UserConfigFile); + + ((FrameworkElement)sender).IsEnabled = false;//Disable the add button to block async restoring installed games + await MainWindowViewModel.Instance.Library.GetGameInstalls().RestoreInstalledGames(); + await MainWindowViewModel.Instance.Downloads.RestoreDownloadedGames(); + } + } + catch (Exception ex) + { + MainWindowViewModel.Instance.AppBarText = ex.Message; + } + ((FrameworkElement)sender).IsEnabled = true; + } + private void OpenUserCacheFolder_Click(object sender, RoutedEventArgs e) + { + if (Directory.Exists(LoginManager.Instance.GetUserProfile().RootDir)) + { + Process.Start(new ProcessStartInfo + { + FileName = "explorer.exe", + Arguments = $"\"{LoginManager.Instance.GetUserProfile().RootDir}\"", + UseShellExecute = true + }); + } + } } } \ No newline at end of file diff --git a/gamevault/UserControls/UserSettingsUserControl.xaml b/gamevault/UserControls/UserSettingsUserControl.xaml index 03bfab5..16ea153 100644 --- a/gamevault/UserControls/UserSettingsUserControl.xaml +++ b/gamevault/UserControls/UserSettingsUserControl.xaml @@ -144,7 +144,7 @@ + + diff --git a/gamevault/UserControls/UserSettingsUserControl.xaml.cs b/gamevault/UserControls/UserSettingsUserControl.xaml.cs index fa88a15..811dd60 100644 --- a/gamevault/UserControls/UserSettingsUserControl.xaml.cs +++ b/gamevault/UserControls/UserSettingsUserControl.xaml.cs @@ -13,6 +13,7 @@ using System.Windows.Input; using System.Windows.Media.Imaging; using Windows.ApplicationModel.VoiceCommands; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace gamevault.UserControls @@ -291,35 +292,32 @@ private async Task SaveImage(string tag) string resp = await WebHelper.UploadFileAsync($"{SettingsViewModel.Instance.ServerUrl}/api/media", ms, filename, null); ms.Dispose(); var newImageId = JsonSerializer.Deserialize(resp).ID; - await Task.Run(() => + try { - try + UpdateUserDto updateObject = new UpdateUserDto(); + if (tag == "avatar") { - UpdateUserDto updateObject = new UpdateUserDto(); - if (tag == "avatar") - { - updateObject.AvatarId = newImageId; - } - else - { - updateObject.BackgroundId = newImageId; - } - string url = $"{SettingsViewModel.Instance.ServerUrl}/api/users/{ViewModel.OriginUser.ID}"; - if (LoginManager.Instance.GetCurrentUser().ID == ViewModel.OriginUser.ID) - { - url = @$"{SettingsViewModel.Instance.ServerUrl}/api/users/me"; - } - string updatedUser = WebHelper.Put(url, JsonSerializer.Serialize(updateObject), true); - ViewModel.OriginUser = JsonSerializer.Deserialize(updatedUser); - success = true; - MainWindowViewModel.Instance.AppBarText = "Successfully updated image"; + updateObject.AvatarId = newImageId; } - catch (Exception ex) + else { - string msg = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = msg; + updateObject.BackgroundId = newImageId; } - }); + string url = $"{SettingsViewModel.Instance.ServerUrl}/api/users/{ViewModel.OriginUser.ID}"; + if (LoginManager.Instance.GetCurrentUser().ID == ViewModel.OriginUser.ID) + { + url = @$"{SettingsViewModel.Instance.ServerUrl}/api/users/me"; + } + string updatedUser = await WebHelper.PutAsync(url, JsonSerializer.Serialize(updateObject)); + ViewModel.OriginUser = JsonSerializer.Deserialize(updatedUser); + success = true; + MainWindowViewModel.Instance.AppBarText = "Successfully updated image"; + } + catch (Exception ex) + { + string msg = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = msg; + } //Update Data Context for Community Page. So that the images are also refreshed there directly if (success) { @@ -358,35 +356,36 @@ private async void SaveUserDetails_Click(object sender, RoutedEventArgs e) if (newPassword != "") selectedUser.Password = newPassword; - if(selectedUser.BirthDate == ViewModel.OriginUser.BirthDate)//Set birthday to null, so a underage user can edit the rest of its data + if (selectedUser.BirthDate == ViewModel.OriginUser.BirthDate)//Set birthday to null, so a underage user can edit the rest of its data { selectedUser.BirthDate = null; } bool error = false; - await Task.Run(() => + try { - try + string url = $"{SettingsViewModel.Instance.ServerUrl}/api/users/{ViewModel.OriginUser.ID}"; + if (LoginManager.Instance.GetCurrentUser().ID == ViewModel.OriginUser.ID) { - string url = $"{SettingsViewModel.Instance.ServerUrl}/api/users/{ViewModel.OriginUser.ID}"; - if (LoginManager.Instance.GetCurrentUser().ID == ViewModel.OriginUser.ID) - { - url = @$"{SettingsViewModel.Instance.ServerUrl}/api/users/me"; - } - string result = WebHelper.Put(url, JsonSerializer.Serialize(selectedUser), true); - ViewModel.OriginUser = JsonSerializer.Deserialize(result); - MainWindowViewModel.Instance.AppBarText = "Successfully saved user changes"; + url = @$"{SettingsViewModel.Instance.ServerUrl}/api/users/me"; } - catch (Exception ex) + string result = await WebHelper.PutAsync(url, JsonSerializer.Serialize(selectedUser)); + ViewModel.OriginUser = JsonSerializer.Deserialize(result); + if (LoginManager.Instance.GetCurrentUser().ID == ViewModel.OriginUser.ID) { - ConvertToUpdateUser();//Reset to Origin User - error = true; - string msg = WebExceptionHelper.TryGetServerMessage(ex); - MainWindowViewModel.Instance.AppBarText = msg; + WebHelper.OverrideCredentials(selectedUser.Username, selectedUser.Password); } - }); + MainWindowViewModel.Instance.AppBarText = "Successfully saved user changes"; + } + catch (Exception ex) + { + ConvertToUpdateUser();//Reset to Origin User + error = true; + string msg = WebExceptionHelper.TryGetServerMessage(ex); + MainWindowViewModel.Instance.AppBarText = msg; + } if (!error) - { + { try { ViewModel.OriginUser.Password = newPassword; @@ -404,7 +403,16 @@ private async Task HandleChangesOnCurrentUser(User selectedUser) { if (LoginManager.Instance.GetCurrentUser().ID == selectedUser.ID) { - await LoginManager.Instance.ManualLogin(selectedUser.Username, string.IsNullOrEmpty(selectedUser.Password) ? WebHelper.GetCredentials()[1] : selectedUser.Password); + UserProfile profile = LoginManager.Instance.GetUserProfile(); + bool isLoggedInWithSSO = Preferences.Get(AppConfigKey.IsLoggedInWithSSO, profile.UserConfigFile) == "1"; + if (isLoggedInWithSSO) + { + await LoginManager.Instance.SSOLogin(profile); + } + else + { + await LoginManager.Instance.Login(profile, WebHelper.GetCredentials()[0], WebHelper.GetCredentials()[1]); + } MainWindowViewModel.Instance.UserAvatar = LoginManager.Instance.GetCurrentUser(); } @@ -412,7 +420,15 @@ private async Task HandleChangesOnCurrentUser(User selectedUser) await MainWindowViewModel.Instance.Community.InitUserList(); } - + private void CopyUserApiKey_Click(object sender, RoutedEventArgs e) + { + try + { + Clipboard.SetText(ViewModel.OriginUser.ApiKey); + MainWindowViewModel.Instance.AppBarText = "Copied API Key to Clipboard"; + } + catch { } + } private void Help_Click(object sender, MouseButtonEventArgs e) { try @@ -422,7 +438,7 @@ private void Help_Click(object sender, MouseButtonEventArgs e) { case 0: { - url = "https://gamevau.lt/docs/client-docs/gui#edit-images-1"; + url = "https://gamevau.lt/docs/client-docs/gui/#edit-user-images"; break; } case 1: @@ -441,7 +457,7 @@ private void Help_Click(object sender, MouseButtonEventArgs e) { MainWindowViewModel.Instance.AppBarText = ex.Message; } - } + } } } diff --git a/gamevault/UserControls/Wizard.xaml b/gamevault/UserControls/Wizard.xaml deleted file mode 100644 index 9a665bc..0000000 --- a/gamevault/UserControls/Wizard.xaml +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - Before we can start we need some important configuration. - - Everything in this setup can be changed manually later in the settings menu. - - - - - - - - - - - - - Please choose the path where you would like all your downloaded and installed games to be stored. This folder will serve as the central location for your game library. - - - Click the "Select root path" button to navigate to the desired location on your computer. Once you've selected the folder, click "Next" to proceed. - - - Remember, it's important to choose a location with sufficient storage space to store your game collection. - - - - - - - - - - - - - - - - - - - - To connect your GameVault application to the backend, we need to know the URL of the server. If you have your own selfhosted GameVault server, please enter its URL in the provided field. - - - If you want to try out GameVault without setting up your own server, you can use the demo server by entering "demo.gamevau.lt" as the URL. This will allow you to explore the features and functionality of GameVault. - - - Once you've entered the server URL, click "Next" to proceed. - - - - - - - - - - - - - - - - - - - - You'll need to log in to your GameVault account on the selected backend server. - - - If you already have a GameVault account on this server, select Login and enter your login credentials - (username and password) in the provided fields. These credentials are specific to the GameVault backend server you've chosen. - Once you've entered your information, click "Login" to proceed. - - - If you don't have an account on the selected GameVault backend server, you'll need to register a new account. Click - the "Register" button and follow the instructions to create your account. - - - Demo server credentials: Username: "demo", Password: "demodemo" - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/gamevault/UserControls/Wizard.xaml.cs b/gamevault/UserControls/Wizard.xaml.cs deleted file mode 100644 index 0eb3625..0000000 --- a/gamevault/UserControls/Wizard.xaml.cs +++ /dev/null @@ -1,81 +0,0 @@ -using gamevault.UserControls.SettingsComponents; -using gamevault.ViewModels; -using MahApps.Metro.Controls.Dialogs; -using MahApps.Metro.Controls; -using System; -using System.Collections.Generic; -using System.Diagnostics; -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.Forms; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using UserControl = System.Windows.Controls.UserControl; - -namespace gamevault.UserControls -{ - /// - /// Interaction logic for Wizard.xaml - /// - public partial class Wizard : UserControl - { - public Wizard() - { - InitializeComponent(); - this.DataContext = SettingsViewModel.Instance; - } - - private void Next_Clicked(object sender, RoutedEventArgs e) - { - uiTabControl.SelectedIndex += 1; - } - - private void Back_Clicked(object sender, RoutedEventArgs e) - { - uiTabControl.SelectedIndex -= 1; - } - - private void Login_Clicked(object sender, RoutedEventArgs e) - { - uiLoginRegisterPopup.Child = new LoginUserControl(); - uiLoginRegisterPopup.IsOpen = true; - } - - private void Register_Clicked(object sender, RoutedEventArgs e) - { - uiLoginRegisterPopup.Child = new RegisterUserControl(); - uiLoginRegisterPopup.IsOpen = true; - } - - private void Help_Clicked(object sender, MouseButtonEventArgs e) - { - try - { - string? url = (string)((FrameworkElement)sender).Tag; - if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) - { - Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); - } - } - catch { } - } - - private async void Finish_Clicked(object sender, RoutedEventArgs e) - { - MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync("", $"Do you want to leave the setup wizard?", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false, DialogMessageFontSize = 50 }); - if (result == MessageDialogResult.Affirmative) - { - MainWindowViewModel.Instance.SetActiveControl(null); - MainWindowViewModel.Instance.SetActiveControl(MainControl.Library); - } - } - } -} diff --git a/gamevault/ViewModels/CommunityViewModel.cs b/gamevault/ViewModels/CommunityViewModel.cs index 9f68290..4dea709 100644 --- a/gamevault/ViewModels/CommunityViewModel.cs +++ b/gamevault/ViewModels/CommunityViewModel.cs @@ -15,6 +15,7 @@ internal class CommunityViewModel : ViewModelBase private User[]? m_Users { get; set; } private User? m_CurrentShownUser { get; set; } private List m_UserProgresses { get; set; } + private bool loadingUser { get; set; } #endregion public User[]? Users @@ -30,15 +31,15 @@ public User? CurrentShownUser m_CurrentShownUser = value; UserProgresses = new List(m_CurrentShownUser.Progresses); m_CurrentShownUser.Progresses = null; - OnPropertyChanged(); + OnPropertyChanged(); } } - + public List UserProgresses { get { - if(m_UserProgresses== null) + if (m_UserProgresses == null) { m_UserProgresses = new List(); } @@ -49,6 +50,11 @@ public List UserProgresses m_UserProgresses = value; OnPropertyChanged(); } } + public bool LoadingUser + { + get { return loadingUser; } + set { loadingUser = value; OnPropertyChanged(); } + } public string[] SortBy { get diff --git a/gamevault/ViewModels/GameDownloadViewModel.cs b/gamevault/ViewModels/GameDownloadViewModel.cs index 128d165..c454bca 100644 --- a/gamevault/ViewModels/GameDownloadViewModel.cs +++ b/gamevault/ViewModels/GameDownloadViewModel.cs @@ -1,4 +1,5 @@ using gamevault.Converter; +using gamevault.Helper; using gamevault.Models; using gamevault.UserControls; using System; @@ -109,7 +110,7 @@ public bool? CreateShortcut { if (createShortcut == null) { - createShortcut = Preferences.Get(AppConfigKey.CreateDesktopShortcut, AppFilePath.UserFile) == "1"; + createShortcut = Preferences.Get(AppConfigKey.CreateDesktopShortcut, LoginManager.Instance.GetUserProfile().UserConfigFile) == "1"; } return createShortcut; } @@ -117,7 +118,7 @@ public bool? CreateShortcut set { createShortcut = value; OnPropertyChanged(); - Preferences.Set(AppConfigKey.CreateDesktopShortcut, createShortcut == true ? "1" : "0", AppFilePath.UserFile); + Preferences.Set(AppConfigKey.CreateDesktopShortcut, createShortcut == true ? "1" : "0", LoginManager.Instance.GetUserProfile().UserConfigFile); } } public string[] SupportedArchives diff --git a/gamevault/ViewModels/GameSettingsViewModel.cs b/gamevault/ViewModels/GameSettingsViewModel.cs index 5069906..e2b6112 100644 --- a/gamevault/ViewModels/GameSettingsViewModel.cs +++ b/gamevault/ViewModels/GameSettingsViewModel.cs @@ -28,6 +28,7 @@ internal class GameSettingsViewModel : ViewModelBase private MetadataProviderDto[]? metadataProviders { get; set; } private bool metadataProvidersLoaded { get; set; } private int selectedMetadataProviderIndex { get; set; } + private string? installedGameVersion { get; set; } #endregion public Game Game { @@ -126,5 +127,10 @@ public GameMetadata? CurrentShownMappedGame OnPropertyChanged(); } } + public string? InstalledGameVersion + { + get { return installedGameVersion; } + set { installedGameVersion = value; OnPropertyChanged(); } + } } } diff --git a/gamevault/ViewModels/InstallViewModel.cs b/gamevault/ViewModels/InstallViewModel.cs index f59f65b..1d63812 100644 --- a/gamevault/ViewModels/InstallViewModel.cs +++ b/gamevault/ViewModels/InstallViewModel.cs @@ -49,6 +49,7 @@ public ObservableCollection> InstalledGames } set { m_InstalledGames = value; OnPropertyChanged(); } } + public Dictionary InstalledGamesDuplicates= new Dictionary(); public ICollectionView? InstalledGamesFilter { get { return installedGamesFilter; } diff --git a/gamevault/ViewModels/LoginWindowViewModel.cs b/gamevault/ViewModels/LoginWindowViewModel.cs new file mode 100644 index 0000000..0564b47 --- /dev/null +++ b/gamevault/ViewModels/LoginWindowViewModel.cs @@ -0,0 +1,121 @@ +using gamevault.Helper; +using gamevault.Models; +using gamevault.Windows; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; + +namespace gamevault.ViewModels +{ + internal class LoginWindowViewModel : ViewModelBase + { + public ObservableCollection UserProfiles { get; set; } = new ObservableCollection(); + public LoginWindowViewModel() + { + } + + private int loginStepIndex { get; set; } = 0; + public int LoginStepIndex + { + get { return loginStepIndex; } + set { loginStepIndex = value; OnPropertyChanged(); } + } + private string statusText { get; set; } + public string StatusText + { + get { return statusText; } + set { statusText = value; OnPropertyChanged(); } + } + private BindableServerInfo loginServerInfo { get; set; } = new BindableServerInfo(); + public BindableServerInfo LoginServerInfo + { + get { return loginServerInfo; } + set { loginServerInfo = value; OnPropertyChanged(); } + } + private LoginUser loginUser { get; set; } + public LoginUser LoginUser + { + get + { + if (loginUser == null) + { + loginUser = new LoginUser(); + } + return loginUser; + } + set { loginUser = value; OnPropertyChanged(); } + } + private BindableServerInfo signUpServerInfo { get; set; } = new BindableServerInfo(); + public BindableServerInfo SignUpServerInfo + { + get { return signUpServerInfo; } + set { signUpServerInfo = value; OnPropertyChanged(); } + } + private LoginUser signupUser { get; set; } + public LoginUser SignupUser + { + get + { + if (signupUser == null) + { + signupUser = new LoginUser(); + } + return signupUser; + } + set { signupUser = value; OnPropertyChanged(); } + } + private LoginUser editUser { get; set; } + public LoginUser EditUser + { + get + { + if (editUser == null) + { + editUser = new LoginUser(); + } + return editUser; + } + set { editUser = value; OnPropertyChanged(); } + } + private bool m_IsAppBarOpen { get; set; } + public bool IsAppBarOpen + { + get { return m_IsAppBarOpen; } + set { m_IsAppBarOpen = value; OnPropertyChanged(); } + } + private string m_AppBarText { get; set; } + public string AppBarText + { + get { return m_AppBarText; } + set { m_AppBarText = value; OnPropertyChanged(); IsAppBarOpen = true; } + } + private bool rememberMe { get; set; } + public bool RememberMe + { + get { return rememberMe; } + set + { + rememberMe = value; OnPropertyChanged(); + Preferences.Set(AppConfigKey.LoginRememberMe, rememberMe ? "1" : "0", ProfileManager.ProfileConfigFile); + } + } + private ObservableCollection additionalRequestHeaders; + public ObservableCollection AdditionalRequestHeaders + { + get + { + if (additionalRequestHeaders == null) + { + additionalRequestHeaders = new ObservableCollection(); + } + return additionalRequestHeaders; + } + set { additionalRequestHeaders = value; OnPropertyChanged(); } + } + } +} diff --git a/gamevault/ViewModels/SettingsViewModel.cs b/gamevault/ViewModels/SettingsViewModel.cs index 90cc154..e76e69b 100644 --- a/gamevault/ViewModels/SettingsViewModel.cs +++ b/gamevault/ViewModels/SettingsViewModel.cs @@ -36,8 +36,6 @@ public static SettingsViewModel Instance #endregion #region PrivateMembers private string m_UserName { get; set; } - private string m_RootPath { get; set; } - private bool m_BackgroundStart { get; set; } private bool m_LibStartup { get; set; } private bool m_AutoExtract { get; set; } @@ -51,7 +49,6 @@ public static SettingsViewModel Instance private long m_DownloadLimit { get; set; } private long m_DownloadLimitUIValue { get; set; } private string[] ignoreList { get; set; } - private User m_RegistrationUser = new User() { Avatar = new Media(), Background = new Media() }; private PhalcodeProduct license { get; set; } private ObservableCollection themes { get; set; } private ObservableCollection communityThemes { get; set; } @@ -61,7 +58,8 @@ public static SettingsViewModel Instance private bool cloudSaves { get; set; } private bool isCommunityThemeSelected { get; set; } private bool usePrimaryCloudSaveManifest { get; set; } - private ObservableCollection customCloudSaveManifests; + private ObservableCollection customCloudSaveManifests; + private ObservableCollection rootDirectories; private bool mountIso { get; set; } //DevMode private bool devModeEnabled { get; set; } @@ -72,32 +70,41 @@ public static SettingsViewModel Instance public SettingsViewModel() { - UserName = Preferences.Get(AppConfigKey.Username, AppFilePath.UserFile); - RootPath = Preferences.Get(AppConfigKey.RootPath, AppFilePath.UserFile); - ServerUrl = Preferences.Get(AppConfigKey.ServerUrl, AppFilePath.UserFile, true); - string showMappedTitleString = Preferences.Get(AppConfigKey.ShowMappedTitle, AppFilePath.UserFile); + } + private string userConfigFile; + public void Init() + { + userConfigFile = LoginManager.Instance.GetUserProfile().UserConfigFile; + + UserName = Preferences.Get(AppConfigKey.Username, userConfigFile); + ServerUrl = Preferences.Get(AppConfigKey.ServerUrl, userConfigFile, true); + + string rootDirectoriesString = Preferences.Get(AppConfigKey.RootDirectories, userConfigFile); + RootDirectories = string.IsNullOrWhiteSpace(rootDirectoriesString) ? null! : new ObservableCollection(rootDirectoriesString.Split(';').Select(part => new DirectoryEntry { Uri = part }).ToList()); + + string showMappedTitleString = Preferences.Get(AppConfigKey.ShowMappedTitle, userConfigFile); showMappedTitle = showMappedTitleString == "1" || showMappedTitleString == ""; //Setting the private members to avoid writing to the user config file over and over again - m_BackgroundStart = (Preferences.Get(AppConfigKey.BackgroundStart, AppFilePath.UserFile) == "1"); OnPropertyChanged(nameof(BackgroundStart)); - m_AutoExtract = (Preferences.Get(AppConfigKey.AutoExtract, AppFilePath.UserFile) == "1"); OnPropertyChanged(nameof(AutoExtract)); - autoDeletePortableGameFiles = Preferences.Get(AppConfigKey.AutoDeletePortable, AppFilePath.UserFile) == "1"; OnPropertyChanged(nameof(AutoDeletePortableGameFiles)); - retainLibarySortByAndOrderBy = Preferences.Get(AppConfigKey.RetainLibarySortByAndOrderBy, AppFilePath.UserFile) == "1"; OnPropertyChanged(nameof(RetainLibarySortByAndOrderBy)); + m_BackgroundStart = (Preferences.Get(AppConfigKey.BackgroundStart, userConfigFile) == "1"); OnPropertyChanged(nameof(BackgroundStart)); + m_AutoExtract = (Preferences.Get(AppConfigKey.AutoExtract, userConfigFile) == "1"); OnPropertyChanged(nameof(AutoExtract)); + autoDeletePortableGameFiles = Preferences.Get(AppConfigKey.AutoDeletePortable, userConfigFile) == "1"; OnPropertyChanged(nameof(AutoDeletePortableGameFiles)); + retainLibarySortByAndOrderBy = Preferences.Get(AppConfigKey.RetainLibarySortByAndOrderBy, userConfigFile) == "1"; OnPropertyChanged(nameof(RetainLibarySortByAndOrderBy)); - string analyticsPreference = Preferences.Get(AppConfigKey.SendAnonymousAnalytics, AppFilePath.UserFile); + string analyticsPreference = Preferences.Get(AppConfigKey.SendAnonymousAnalytics, userConfigFile); sendAnonymousAnalytics = (analyticsPreference == "" || analyticsPreference == "1"); OnPropertyChanged(nameof(SendAnonymousAnalytics)); - syncSteamShortcuts = Preferences.Get(AppConfigKey.SyncSteamShortcuts, AppFilePath.UserFile) == "1"; OnPropertyChanged(nameof(SyncSteamShortcuts)); - syncDiscordPresence = Preferences.Get(AppConfigKey.SyncDiscordPresence, AppFilePath.UserFile) == "1"; OnPropertyChanged(nameof(SyncDiscordPresence)); - cloudSaves = Preferences.Get(AppConfigKey.CloudSaves, AppFilePath.UserFile) == "1"; OnPropertyChanged(nameof(CloudSaves)); + syncSteamShortcuts = Preferences.Get(AppConfigKey.SyncSteamShortcuts, userConfigFile) == "1"; OnPropertyChanged(nameof(SyncSteamShortcuts)); + syncDiscordPresence = Preferences.Get(AppConfigKey.SyncDiscordPresence, userConfigFile) == "1"; OnPropertyChanged(nameof(SyncDiscordPresence)); + cloudSaves = Preferences.Get(AppConfigKey.CloudSaves, userConfigFile) == "1"; OnPropertyChanged(nameof(CloudSaves)); - string autoInstallPortableStr = Preferences.Get(AppConfigKey.AutoInstallPortable, AppFilePath.UserFile); + string autoInstallPortableStr = Preferences.Get(AppConfigKey.AutoInstallPortable, userConfigFile); if (string.IsNullOrWhiteSpace(autoInstallPortableStr) || autoInstallPortableStr == "1") { autoInstallPortable = true; OnPropertyChanged(nameof(AutoInstallPortable)); } - string libstartupStr = Preferences.Get(AppConfigKey.LibStartup, AppFilePath.UserFile); + string libstartupStr = Preferences.Get(AppConfigKey.LibStartup, userConfigFile); if (libstartupStr == string.Empty) { LibStartup = true; @@ -106,7 +113,7 @@ public SettingsViewModel() { m_LibStartup = (libstartupStr == "1"); OnPropertyChanged(nameof(LibStartup)); } - if (long.TryParse(Preferences.Get(AppConfigKey.DownloadLimit, AppFilePath.UserFile), out long downloadLimitResult)) + if (long.TryParse(Preferences.Get(AppConfigKey.DownloadLimit, userConfigFile), out long downloadLimitResult)) { DownloadLimit = downloadLimitResult; DownloadLimitUIValue = DownloadLimit; @@ -116,52 +123,50 @@ public SettingsViewModel() DownloadLimit = 0; DownloadLimitUIValue = 0; } - string usePrimaryCloudSaveManifestString = Preferences.Get(AppConfigKey.UsePrimaryCloudSaveManifest, AppFilePath.UserFile); + string usePrimaryCloudSaveManifestString = Preferences.Get(AppConfigKey.UsePrimaryCloudSaveManifest, userConfigFile); usePrimaryCloudSaveManifest = usePrimaryCloudSaveManifestString == "1" || usePrimaryCloudSaveManifestString == ""; - string customCloudSaveManifestsString = Preferences.Get(AppConfigKey.CustomCloudSaveManifests, AppFilePath.UserFile); - customCloudSaveManifests = string.IsNullOrWhiteSpace(customCloudSaveManifestsString) ? null! : new ObservableCollection(customCloudSaveManifestsString.Split(';').Select(part => new LudusaviManifestEntry { Uri = part }).ToList()); + string customCloudSaveManifestsString = Preferences.Get(AppConfigKey.CustomCloudSaveManifests, userConfigFile); + customCloudSaveManifests = string.IsNullOrWhiteSpace(customCloudSaveManifestsString) ? null! : new ObservableCollection(customCloudSaveManifestsString.Split(';').Select(part => new DirectoryEntry { Uri = part }).ToList()); - string mountIsoString = Preferences.Get(AppConfigKey.MountIso, AppFilePath.UserFile); + string mountIsoString = Preferences.Get(AppConfigKey.MountIso, userConfigFile); mountIso = mountIsoString == "1"; //DevMode - devModeEnabled = Preferences.Get(AppConfigKey.DevModeEnabled, AppFilePath.UserFile) == "1"; OnPropertyChanged(nameof(DevModeEnabled)); - devTargetPhalcodeTestBackend = Preferences.Get(AppConfigKey.DevTargetPhalcodeTestBackend, AppFilePath.UserFile) == "1"; OnPropertyChanged(nameof(DevTargetPhalcodeTestBackend)); - // + devModeEnabled = Preferences.Get(AppConfigKey.DevModeEnabled, userConfigFile) == "1"; OnPropertyChanged(nameof(DevModeEnabled)); + devTargetPhalcodeTestBackend = Preferences.Get(AppConfigKey.DevTargetPhalcodeTestBackend, userConfigFile) == "1"; OnPropertyChanged(nameof(DevTargetPhalcodeTestBackend)); + // } public async Task InitIgnoreList() { - await Task.Run(() => + string ignoreListFile = LoginManager.Instance.GetUserProfile().IgnoreList; + try { - try + if (!File.Exists(ignoreListFile)) { - if (!File.Exists(AppFilePath.IgnoreList)) - { - string response = WebHelper.GetRequest(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/ignorefile"); - string[] ignoreList = JsonSerializer.Deserialize(response); - if (ignoreList != null || ignoreList?.Length > 0) - { - IgnoreList = ignoreList.Where(s => !string.IsNullOrEmpty(s)).ToArray(); //Make sure server ignore list don't contain empty strings, because this will exclude any file which is compared to the ignore list - Preferences.Set("IL", response.Replace("\n", ""), AppFilePath.IgnoreList); - } - } - else + string response = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/ignorefile"); + string[] ignoreList = JsonSerializer.Deserialize(response); + if (ignoreList != null || ignoreList?.Length > 0) { - string result = Preferences.Get("IL", AppFilePath.IgnoreList); - IgnoreList = JsonSerializer.Deserialize(result); + IgnoreList = ignoreList.Where(s => !string.IsNullOrEmpty(s)).ToArray(); //Make sure server ignore list don't contain empty strings, because this will exclude any file which is compared to the ignore list + Preferences.Set("IL", response.Replace("\n", ""), ignoreListFile); } } - catch + else { - try - { - string result = Preferences.Get("IL", AppFilePath.IgnoreList); - IgnoreList = JsonSerializer.Deserialize(result); - } - catch { } + string result = Preferences.Get("IL", ignoreListFile); + IgnoreList = JsonSerializer.Deserialize(result); } - }); + } + catch + { + try + { + string result = Preferences.Get("IL", ignoreListFile); + IgnoreList = JsonSerializer.Deserialize(result); + } + catch { } + } } public string UserName @@ -169,11 +174,6 @@ public string UserName get { return m_UserName; } set { m_UserName = value; OnPropertyChanged(); } } - public string RootPath - { - get { return m_RootPath; } - set { m_RootPath = value; OnPropertyChanged(); } - } public bool BackgroundStart { get { return m_BackgroundStart; } @@ -186,7 +186,7 @@ public bool BackgroundStart { stringValue = "0"; } - Preferences.Set(AppConfigKey.BackgroundStart, stringValue, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.BackgroundStart, stringValue, userConfigFile); } } public bool LibStartup @@ -201,7 +201,7 @@ public bool LibStartup { stringValue = "0"; } - Preferences.Set(AppConfigKey.LibStartup, stringValue, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.LibStartup, stringValue, userConfigFile); } } public bool AutoExtract @@ -216,7 +216,7 @@ public bool AutoExtract { stringValue = "0"; } - Preferences.Set(AppConfigKey.AutoExtract, stringValue, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.AutoExtract, stringValue, userConfigFile); } } public bool AutoInstallPortable @@ -231,7 +231,7 @@ public bool AutoInstallPortable { stringValue = "0"; } - Preferences.Set(AppConfigKey.AutoInstallPortable, stringValue, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.AutoInstallPortable, stringValue, userConfigFile); } } public bool AutoDeletePortableGameFiles @@ -246,7 +246,7 @@ public bool AutoDeletePortableGameFiles { stringValue = "0"; } - Preferences.Set(AppConfigKey.AutoDeletePortable, stringValue, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.AutoDeletePortable, stringValue, userConfigFile); } } public bool RetainLibarySortByAndOrderBy @@ -261,7 +261,7 @@ public bool RetainLibarySortByAndOrderBy { stringValue = "0"; } - Preferences.Set(AppConfigKey.RetainLibarySortByAndOrderBy, stringValue, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.RetainLibarySortByAndOrderBy, stringValue, userConfigFile); } } public bool SendAnonymousAnalytics @@ -276,7 +276,7 @@ public bool SendAnonymousAnalytics { stringValue = "0"; } - Preferences.Set(AppConfigKey.SendAnonymousAnalytics, stringValue, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.SendAnonymousAnalytics, stringValue, userConfigFile); } } @@ -323,7 +323,7 @@ public bool ShowMappedTitle { return showMappedTitle; } - set { showMappedTitle = value; Preferences.Set(AppConfigKey.ShowMappedTitle, showMappedTitle ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { showMappedTitle = value; Preferences.Set(AppConfigKey.ShowMappedTitle, showMappedTitle ? "1" : "0", userConfigFile); OnPropertyChanged(); } } public bool SyncSteamShortcuts { @@ -331,7 +331,7 @@ public bool SyncSteamShortcuts { return syncSteamShortcuts; } - set { syncSteamShortcuts = value; Preferences.Set(AppConfigKey.SyncSteamShortcuts, syncSteamShortcuts ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { syncSteamShortcuts = value; Preferences.Set(AppConfigKey.SyncSteamShortcuts, syncSteamShortcuts ? "1" : "0", userConfigFile); OnPropertyChanged(); } } public bool SyncDiscordPresence { @@ -339,7 +339,7 @@ public bool SyncDiscordPresence { return syncDiscordPresence; } - set { syncDiscordPresence = value; Preferences.Set(AppConfigKey.SyncDiscordPresence, syncDiscordPresence ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { syncDiscordPresence = value; Preferences.Set(AppConfigKey.SyncDiscordPresence, syncDiscordPresence ? "1" : "0", userConfigFile); OnPropertyChanged(); } } public bool CloudSaves { @@ -347,7 +347,7 @@ public bool CloudSaves { return cloudSaves; } - set { cloudSaves = value; Preferences.Set(AppConfigKey.CloudSaves, cloudSaves ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { cloudSaves = value; Preferences.Set(AppConfigKey.CloudSaves, cloudSaves ? "1" : "0", userConfigFile); OnPropertyChanged(); } } public bool IsCommunityThemeSelected { @@ -357,11 +357,6 @@ public bool IsCommunityThemeSelected } set { isCommunityThemeSelected = value; OnPropertyChanged(); } } - public User RegistrationUser - { - get { return m_RegistrationUser; } - set { m_RegistrationUser = value; OnPropertyChanged(); } - } public ObservableCollection Themes { get { return themes; } @@ -388,55 +383,67 @@ public bool UsePrimaryCloudSaveManifest { return usePrimaryCloudSaveManifest; } - set { usePrimaryCloudSaveManifest = value; Preferences.Set(AppConfigKey.UsePrimaryCloudSaveManifest, usePrimaryCloudSaveManifest ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { usePrimaryCloudSaveManifest = value; Preferences.Set(AppConfigKey.UsePrimaryCloudSaveManifest, usePrimaryCloudSaveManifest ? "1" : "0", userConfigFile); OnPropertyChanged(); } } - public ObservableCollection CustomCloudSaveManifests + public ObservableCollection CustomCloudSaveManifests { get { if (customCloudSaveManifests == null) { - customCloudSaveManifests = new ObservableCollection(); + customCloudSaveManifests = new ObservableCollection(); } return customCloudSaveManifests; } set { customCloudSaveManifests = value; OnPropertyChanged(); } } + public ObservableCollection RootDirectories + { + get + { + if (rootDirectories == null) + { + rootDirectories = new ObservableCollection(); + } + return rootDirectories; + } + set { rootDirectories = value; OnPropertyChanged(); } + } public bool MountIso { get { return mountIso; } - set { mountIso = value; Preferences.Set(AppConfigKey.MountIso, mountIso ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { mountIso = value; Preferences.Set(AppConfigKey.MountIso, mountIso ? "1" : "0", userConfigFile); OnPropertyChanged(); } } - public System.Windows.Forms.DialogResult SelectDownloadPath() + public async Task SelectDownloadPath() { - using (var dialog = new System.Windows.Forms.FolderBrowserDialog()) + return await Task.Run(() => { - System.Windows.Forms.DialogResult result = dialog.ShowDialog(); - if (result == System.Windows.Forms.DialogResult.OK && Directory.Exists(dialog.SelectedPath)) + string selectedDirectory = ""; + App.Current.Dispatcher.Invoke(() => { - try - { - File.Create(@$"{dialog.SelectedPath}\accesscheck.file").Close(); - File.Delete(@$"{dialog.SelectedPath}\accesscheck.file"); - } - catch (Exception ex) + using (var dialog = new System.Windows.Forms.FolderBrowserDialog()) { - MainWindowViewModel.Instance.AppBarText = $"Access to the path {dialog.SelectedPath} is denied"; - return System.Windows.Forms.DialogResult.Cancel; + System.Windows.Forms.DialogResult result = dialog.ShowDialog(); + if (result == System.Windows.Forms.DialogResult.OK && Directory.Exists(dialog.SelectedPath)) + { + try + { + File.Create(@$"{dialog.SelectedPath}\accesscheck.file").Close(); + File.Delete(@$"{dialog.SelectedPath}\accesscheck.file"); + } + catch (Exception ex) + { + MainWindowViewModel.Instance.AppBarText = $"Access to the path {dialog.SelectedPath} is denied"; + } + selectedDirectory = dialog.SelectedPath.Replace(@"\\", @"\"); + } } - Preferences.Set(AppConfigKey.RootPath, dialog.SelectedPath, AppFilePath.UserFile); - RootPath = dialog.SelectedPath.Replace(@"\\", @"\"); - return System.Windows.Forms.DialogResult.OK; - } - return System.Windows.Forms.DialogResult.Cancel; - } - } - public bool SetupCompleted() - { - return !((m_RootPath == string.Empty) || (m_ServerUrl == string.Empty) || (m_UserName == string.Empty)); + }); + return selectedDirectory; + }); } public string Version { @@ -452,7 +459,7 @@ public bool DevModeEnabled { return devModeEnabled; } - set { devModeEnabled = value; Preferences.Set(AppConfigKey.DevModeEnabled, devModeEnabled ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { devModeEnabled = value; Preferences.Set(AppConfigKey.DevModeEnabled, devModeEnabled ? "1" : "0", userConfigFile); OnPropertyChanged(); } } public bool DevTargetPhalcodeTestBackend { @@ -460,7 +467,7 @@ public bool DevTargetPhalcodeTestBackend { return devTargetPhalcodeTestBackend; } - set { devTargetPhalcodeTestBackend = value; Preferences.Set(AppConfigKey.DevTargetPhalcodeTestBackend, devTargetPhalcodeTestBackend ? "1" : "0", AppFilePath.UserFile); OnPropertyChanged(); } + set { devTargetPhalcodeTestBackend = value; Preferences.Set(AppConfigKey.DevTargetPhalcodeTestBackend, devTargetPhalcodeTestBackend ? "1" : "0", userConfigFile); OnPropertyChanged(); } } // } diff --git a/gamevault/Windows/ExceptionWindow.xaml.cs b/gamevault/Windows/ExceptionWindow.xaml.cs index e33997d..75a1393 100644 --- a/gamevault/Windows/ExceptionWindow.xaml.cs +++ b/gamevault/Windows/ExceptionWindow.xaml.cs @@ -41,11 +41,11 @@ private void OpenLog_Click(object sender, RoutedEventArgs e) } else { - path = AppFilePath.ErrorLog.Replace(@"\\", @"\").Replace("/", @"\"); + path = ProfileManager.ErrorLogDir.Replace(@"\\", @"\").Replace("/", @"\"); } if (!Directory.Exists(path)) { - path = AppFilePath.ErrorLog.Replace(@"\\", @"\").Replace("/", @"\"); + path = ProfileManager.ErrorLogDir.Replace(@"\\", @"\").Replace("/", @"\"); } if (Directory.Exists(path)) { diff --git a/gamevault/Windows/LoginWindow.xaml b/gamevault/Windows/LoginWindow.xaml new file mode 100644 index 0000000..66b9e3a --- /dev/null +++ b/gamevault/Windows/LoginWindow.xaml @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gamevault/Windows/LoginWindow.xaml.cs b/gamevault/Windows/LoginWindow.xaml.cs new file mode 100644 index 0000000..006a574 --- /dev/null +++ b/gamevault/Windows/LoginWindow.xaml.cs @@ -0,0 +1,676 @@ +using gamevault.Helper; +using gamevault.Models; +using gamevault.ViewModels; +using MahApps.Metro.Controls.Dialogs; +using MahApps.Metro.Controls; +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.Json.Nodes; +using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Text.Json; +using System.Collections.ObjectModel; + + +namespace gamevault.Windows +{ + public class LoginUser + { + public string ID { get; set; } + public string ServerUrl { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string RepeatPassword { get; set; } + public DateTime BirthDate { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string EMail { get; set; } + public bool IsLoggedInWithSSO { get; set; } + } + public enum LoginStep + { + LoadingAction = 0, + ChooseProfile = 1, + SignInOrSignUp = 2, + SignIn = 3, + SignUp = 4, + EditProfile = 5, + PendingActivation = 6, + Settings = 7 + } + public partial class LoginWindow + { + + private LoginWindowViewModel ViewModel { get; set; } + private StoreHelper StoreHelper; + private bool SkipBootTasks = false; + private InputTimer InputTimer { get; set; } + public LoginWindow(bool skipBootTasks = false) + { + InitializeComponent(); + ViewModel = new LoginWindowViewModel(); + this.DataContext = ViewModel; + ProfileManager.EnsureRootDirectory(); + this.SkipBootTasks = skipBootTasks; + + InputTimer = new InputTimer(); + InputTimer.Interval = TimeSpan.FromMilliseconds(400); + InputTimer.Tick += ServerUrlInput_Tick; + } + + private async void LoginWindow_Loaded(object sender, RoutedEventArgs e) + { + VisualHelper.AdjustWindowChrome(this); + ViewModel.RememberMe = Preferences.Get(AppConfigKey.LoginRememberMe, ProfileManager.ProfileConfigFile) == "1"; + try + { + string result = Preferences.Get(AppConfigKey.AdditionalRequestHeaders, ProfileManager.ProfileConfigFile); + var objResult = JsonSerializer.Deserialize>(result); + ViewModel.AdditionalRequestHeaders = objResult; + WebHelper.SetAdditionalRequestHeaders(ViewModel.AdditionalRequestHeaders?.ToArray()); + } + catch { } + if (!SkipBootTasks) + { + await CheckForUpdates(this); + ViewModel.StatusText = "Checking License..."; + string phalcodeLoginMessage = await LoginManager.Instance.PhalcodeLogin(true); + if (phalcodeLoginMessage != string.Empty) + ViewModel.AppBarText = phalcodeLoginMessage; + + if (ViewModel.RememberMe) + { + try + { + string lastUserProfileIdentifier = Preferences.Get(AppConfigKey.LastUserProfile, ProfileManager.ProfileConfigFile); + UserProfile lastUserProfile = ProfileManager.GetUserProfiles().First(up => up.RootDir == lastUserProfileIdentifier); + ProfileManager.EnsureUserProfileFileTree(lastUserProfile); + await Login(lastUserProfile); + } + catch { } + } + } + + foreach (UserProfile userProfile in ProfileManager.GetUserProfiles()) + { + ViewModel.UserProfiles.Add(userProfile); + } + if (ViewModel.UserProfiles.Count == 0) + { + CreateDemoUser(); + } + ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; + } + private void NewProfile_Click(object sender, RoutedEventArgs e) + { + if (!SettingsViewModel.Instance.License.IsActive() && ViewModel.UserProfiles.Count >= 1) + { + bool isDemoUserException = ViewModel.UserProfiles.Count == 1 && ViewModel.UserProfiles[0].ServerUrl == "https://demo.gamevau.lt"; + if (!isDemoUserException) + { + ViewModel.AppBarText = "Oops! You just reached a premium feature of GameVault - Upgrade now and support the devs!"; + return; + } + } + ViewModel.LoginStepIndex = (int)LoginStep.SignInOrSignUp; + } + private void SignIn_Click(object sender, RoutedEventArgs e) + { + ViewModel.LoginStepIndex = (int)LoginStep.SignIn; + } + private void SignUp_Click(object sender, RoutedEventArgs e) + { + ViewModel.LoginStepIndex = (int)LoginStep.SignUp; + } + + private void Back_Click(object sender, RoutedEventArgs e) + { + int loginStep = (int)((FrameworkElement)sender).Tag; + ViewModel.LoginStepIndex = loginStep; + } + private async void ServerUrlInput_Tick(object sender, EventArgs e) + { + try + { + InputTimer.Stop(); + string result = await WebHelper.BaseGetAsync($"{ValidateUriScheme(InputTimer?.Data)}/api/status"); + ServerInfo serverInfo = JsonSerializer.Deserialize(result); + if (ViewModel.LoginStepIndex == (int)LoginStep.SignIn) + { + ViewModel.LoginServerInfo = new BindableServerInfo(serverInfo); + } + else if (ViewModel.LoginStepIndex == (int)LoginStep.SignUp) + { + ViewModel.SignUpServerInfo = new BindableServerInfo(serverInfo); + } + } + catch (Exception ex) + { + string message = WebExceptionHelper.TryGetServerMessage(ex); + if (ViewModel.LoginStepIndex == (int)LoginStep.SignIn) + { + ViewModel.LoginServerInfo = new BindableServerInfo(message == "" ? "Could not connect to server" : message); + } + else if (ViewModel.LoginStepIndex == (int)LoginStep.SignUp) + { + ViewModel.SignUpServerInfo = new BindableServerInfo(message == "" ? "Could not connect to server" : message); + } + } + } + private void ServerUrlInput_TextChanged(object sender, RoutedEventArgs e) + { + InputTimer.Stop(); + InputTimer.Data = ((TextBox)sender).Text; + InputTimer.Start(); + } + private UserProfile SetupUserProfile(LoginUser user) + { + string cleanedServerUrl = WebHelper.RemoveSpecialCharactersFromUrl(user.ServerUrl); + UserProfile profile = ProfileManager.CreateUserProfile(cleanedServerUrl); + profile.ServerUrl = user.ServerUrl; + Preferences.Set(AppConfigKey.ServerUrl, user.ServerUrl, profile.UserConfigFile, true); + if (!user.IsLoggedInWithSSO) + { + profile.Name = user.Username; + ViewModel.UserProfiles.Add(profile); + Preferences.Set(AppConfigKey.Username, user.Username, profile.UserConfigFile); + Preferences.Set(AppConfigKey.Password, user.Password, profile.UserConfigFile, true); + } + else + { + Preferences.Set(AppConfigKey.IsLoggedInWithSSO, "1", profile.UserConfigFile); + } + return profile; + } + private async void SaveWithoutLogin_Click(object sender, RoutedEventArgs e) + { + await SaveAndLogin(true); + } + private async void SaveAndLogin_Click(object sender, RoutedEventArgs e) + { + await SaveAndLogin(); + } + private async Task SaveAndLogin(bool saveOnly = false) + { + try + { + ValidateSignInData(ViewModel.LoginUser, true); + UserProfile profile = SetupUserProfile(ViewModel.LoginUser); + ViewModel.LoginUser = new LoginUser();//Reset + ViewModel.LoginServerInfo = new BindableServerInfo();//Reset + RemoveDemoUserIfExists(); + if (saveOnly) + { + ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; + return; + } + await Login(profile, true); + } + catch (Exception ex) + { + ViewModel.AppBarText = ex.Message; + } + } + private async void ProfileLogin_Click(object sender, RoutedEventArgs e) + { + UserProfile selectedProfile = (UserProfile)((FrameworkElement)sender).DataContext; + ProfileManager.EnsureUserProfileFileTree(selectedProfile); + await Login(selectedProfile); + } + private string ValidateUriScheme(string uri) + { + if (uri.EndsWith("/")) + { + uri = uri.Substring(0, uri.Length - 1); + } + if (!uri.Contains(System.Uri.UriSchemeHttp)) + { + uri = $"{System.Uri.UriSchemeHttps}://{uri}"; + } + return uri; + } + private void ValidateSignInData(LoginUser loginUser, bool isLogin) + { + if (ViewModel.UserProfiles.Any(user => user.Name == loginUser.Username)) + throw new ArgumentException("Profile with this name already exists"); + + if (string.IsNullOrWhiteSpace(loginUser.ServerUrl)) + throw new ArgumentException("ServerUrl is not set"); + + if (isLogin && !ViewModel.LoginServerInfo.IsAvailable) + { + throw new ArgumentException("Server could not be reached"); + } + + loginUser.ServerUrl = ValidateUriScheme(loginUser.ServerUrl); + + if (!ViewModel.LoginUser.IsLoggedInWithSSO) + { + if (string.IsNullOrWhiteSpace(loginUser.Username)) + throw new ArgumentException("Username is not set"); + + if (string.IsNullOrWhiteSpace(loginUser.Password)) + throw new ArgumentException("Password is not set"); + } + } + + private async Task Login(UserProfile profile, bool firstTimeLogin = false, bool calledByActivationLoop = false) + { + if (!calledByActivationLoop) + { + ViewModel.StatusText = "Logging in..."; + ViewModel.LoginStepIndex = (int)LoginStep.LoadingAction; + + if (await CheckIfServerIsOutdated(profile.ServerUrl)) + { + ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; + return; + } + } + + bool isLoggedInWithSSO = Preferences.Get(AppConfigKey.IsLoggedInWithSSO, profile.UserConfigFile) == "1"; + LoginState state = LoginState.Success; + if (!isLoggedInWithSSO) + { + string username = Preferences.Get(AppConfigKey.Username, profile.UserConfigFile); + string password = Preferences.Get(AppConfigKey.Password, profile.UserConfigFile, true); + state = await LoginManager.Instance.Login(profile, username, password); + } + else + { + state = await LoginManager.Instance.SSOLogin(profile); + if (state == LoginState.Success) + { + Preferences.Set(AppConfigKey.Username, LoginManager.Instance.GetCurrentUser()?.Username, profile.UserConfigFile); + } + else if (state != LoginState.Success && firstTimeLogin) + { + try + { + await Task.Delay(500); + ProfileManager.DeleteUserProfile(profile); + } + catch + { + await Task.Delay(1500);//For slower machines, we have to wait for the webview of SSOLogin to free the web cache + ProfileManager.DeleteUserProfile(profile); + } + } + } + if (state == LoginState.Success) + { + Preferences.Set(AppConfigKey.UserID, LoginManager.Instance.GetCurrentUser()?.ID, profile.UserConfigFile); + await LoadMainWindow(profile); + } + else if (state == LoginState.NotActivated) + { + ViewModel.LoginStepIndex = (int)LoginStep.PendingActivation; + await Task.Delay(5000); + if (ViewModel.LoginStepIndex == (int)LoginStep.PendingActivation) + await Login(profile, firstTimeLogin, true); + return; + } + else + { + if (firstTimeLogin) + { + ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; + ViewModel.AppBarText = LoginManager.Instance.GetServerLoginResponseMessage(); + } + else + { + try + {//Check if the user was ever connected and is properly set up + string result = Preferences.Get(AppConfigKey.UserID, profile.UserConfigFile); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception("NOID"); + } + await LoadMainWindow(profile); + } + catch (Exception ex) + { + ViewModel.AppBarText = ex.Message == "NOID" ? LoginManager.Instance.GetServerLoginResponseMessage() : "Can not load user profile in offline mode"; + ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; + } + } + } + } + private async Task LoadMainWindow(UserProfile profile) + { + LoginManager.Instance.SetUserProfile(profile); + SettingsViewModel.Instance.Init(); + + ViewModel.StatusText = "Optimizing Cache..."; + await CacheHelper.OptimizeCache(); + if (ViewModel.RememberMe) + { + Preferences.Set(AppConfigKey.LastUserProfile, profile.RootDir, ProfileManager.ProfileConfigFile); + } + App.Current.MainWindow = new MainWindow(); + if (!PipeServiceHandler.Instance.IsAppStartup && !SettingsViewModel.Instance.BackgroundStart) + { + App.Current.MainWindow.Show(); + } + //First Startup Window Visibility will be determined by the protocol handler + try + { + this.DialogResult = true;//will throw error, if its called from the MainWindow + } + catch { } + this.Close(); + } + private async void SaveAndSignUp_Click(object sender, RoutedEventArgs e) + { + await SaveAndSignUp(); + } + private async Task SaveAndSignUp() + { + try + { + ValidateSignUpData(); + LoginState state = LoginState.Success; + if (!ViewModel.SignupUser.IsLoggedInWithSSO) + { + state = await LoginManager.Instance.Register(ViewModel.SignupUser);//Only non-provider login has a additional call. Else its just a login, because the server will create the account internally + } + UserProfile profile = null; + if (state != LoginState.Error) + { + profile = SetupUserProfile(ViewModel.SignupUser); + if (profile == null) + { + ViewModel.AppBarText = "Failed to setup User Profile"; + return; + } + await Login(profile, true); + return; + } + ViewModel.AppBarText = LoginManager.Instance.GetServerLoginResponseMessage(); + } + catch (Exception ex) + { + ViewModel.AppBarText = ex.Message; + } + } + private void ValidateSignUpData() + { + if (string.IsNullOrWhiteSpace(ViewModel.SignupUser.ServerUrl)) + { + throw new Exception("Server URL is not set"); + } + ViewModel.SignupUser.ServerUrl = ValidateUriScheme(ViewModel.SignupUser.ServerUrl); + if (!ViewModel.SignupUser.IsLoggedInWithSSO) + { + if (string.IsNullOrWhiteSpace(ViewModel.SignupUser.Password) || string.IsNullOrWhiteSpace(ViewModel.SignupUser.RepeatPassword)) + { + throw new Exception("Password is not set"); + } + if (ViewModel.SignupUser.Password != ViewModel.SignupUser.RepeatPassword) + { + throw new Exception("Password must be equal"); + } + if (string.IsNullOrWhiteSpace(ViewModel.SignupUser.Username)) + { + throw new Exception("Username is not set"); + } + } + } + + + private void UserProfileContextMenu_Click(object sender, RoutedEventArgs e) + { + e.Handled = true; + if (sender is Button button && button.ContextMenu != null) + { + button.ContextMenu.IsOpen = true; + } + } + + private void EditUserProfile_Click(object sender, RoutedEventArgs e) + { + try + { + if (sender is MenuItem menuItem) + { + UserProfile profileToEdit = (UserProfile)((FrameworkElement)((ContextMenu)menuItem.Parent).TemplatedParent).DataContext; + ViewModel.EditUser = new LoginUser() { ID = profileToEdit.RootDir, ServerUrl = profileToEdit.ServerUrl, Username = profileToEdit.Name, Password = Preferences.Get(AppConfigKey.Password, profileToEdit.UserConfigFile, true) }; + ViewModel.LoginStepIndex = (int)LoginStep.EditProfile; + } + } + catch (Exception ex) + { + ViewModel.AppBarText = ex.Message; + } + } + + private async void DeleteUserProfile_Click(object sender, RoutedEventArgs e) + { + if (sender is MenuItem menuItem) + { + UserProfile profileToDelete = (UserProfile)((FrameworkElement)((ContextMenu)menuItem.Parent).TemplatedParent).DataContext; + MessageDialogResult result = await ((MetroWindow)this).ShowMessageAsync($"Are you sure you want to delete Profile '{profileToDelete.Name}'?", "", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false }); + if (result == MessageDialogResult.Affirmative) + { + ProfileManager.DeleteUserProfile(profileToDelete); + ViewModel.UserProfiles.Remove(profileToDelete); + } + } + } + + private void UserProfileEditSave_Click(object sender, RoutedEventArgs e) + { + try + { + UserProfile? profileToEdit = ViewModel.UserProfiles.FirstOrDefault(x => x.RootDir == ViewModel.EditUser.ID); + if (profileToEdit == null) + throw new Exception("User Profile not found"); + if (profileToEdit.ServerUrl != ViewModel.EditUser.ServerUrl) + { + ProfileManager.DeleteUserProfile(profileToEdit); + ViewModel.UserProfiles.Remove(profileToEdit); + string cleanedServerUrl = WebHelper.RemoveSpecialCharactersFromUrl(ViewModel.EditUser.ServerUrl); + UserProfile profile = ProfileManager.CreateUserProfile(cleanedServerUrl); + profile.Name = ViewModel.EditUser.Username; + profile.ServerUrl = ViewModel.EditUser.ServerUrl; + ViewModel.UserProfiles.Add(profile); + Preferences.Set(AppConfigKey.ServerUrl, ViewModel.EditUser.ServerUrl, profile.UserConfigFile, true); + Preferences.Set(AppConfigKey.Username, ViewModel.EditUser.Username, profile.UserConfigFile); + Preferences.Set(AppConfigKey.Password, ViewModel.EditUser.Password, profile.UserConfigFile, true); + } + else + { + profileToEdit.Name = ViewModel.EditUser.Username; + Preferences.Set(AppConfigKey.Username, ViewModel.EditUser.Username, profileToEdit.UserConfigFile); + Preferences.Set(AppConfigKey.Password, ViewModel.EditUser.Password, profileToEdit.UserConfigFile, true); + } + ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; + } + catch (Exception ex) + { + ViewModel.AppBarText = ex.Message; + } + } + private void CreateDemoUser() + { + try + { + string demoServerUrl = "https://demo.gamevau.lt"; + string demoUsername = "demo"; + string demoPassword = "demodemo"; + + UserProfile demoProfile = ProfileManager.CreateUserProfile(WebHelper.RemoveSpecialCharactersFromUrl(demoServerUrl)); + demoProfile.Name = demoUsername; + demoProfile.ServerUrl = demoServerUrl; + + ViewModel.UserProfiles.Add(demoProfile); + + Preferences.Set(AppConfigKey.ServerUrl, demoServerUrl, demoProfile.UserConfigFile, true); + Preferences.Set(AppConfigKey.Username, demoUsername, demoProfile.UserConfigFile); + Preferences.Set(AppConfigKey.Password, demoPassword, demoProfile.UserConfigFile, true); + } + catch (Exception ex) + { + ViewModel.AppBarText = $"Failed to create demo user: {ex.Message}"; + } + } + + private void RemoveDemoUserIfExists() + { + try + { + UserProfile? demoProfile = ViewModel.UserProfiles.FirstOrDefault(p => p.ServerUrl == "https://demo.gamevau.lt"); + + if (demoProfile != null && ViewModel.UserProfiles.Count > 1) + { + ProfileManager.DeleteUserProfile(demoProfile); + ViewModel.UserProfiles.Remove(demoProfile); + } + } + catch (Exception ex) { } + } + public async Task CheckForUpdates(Window root) + { + try + { + ViewModel.StatusText = "Searching for Updates..."; + StoreHelper = new StoreHelper(); + if (true == await StoreHelper.UpdatesAvailable()) + { + ViewModel.StatusText = "Updating..."; + await StoreHelper.DownloadAndInstallAllUpdatesAsync(root); + } + App.IsWindowsPackage = true; + } + catch (COMException comEx) + { + //Is no MSIX package + } + catch (Exception ex) + { + //rest of the cases + } + try + { + if (App.IsWindowsPackage == false) + { + var response = await WebHelper.BaseGetAsync("https://api.github.com/repos/Phalcode/gamevault-app/releases"); + dynamic obj = JsonNode.Parse(response); + string version = (string)obj[0]["tag_name"]; + if (Convert.ToInt32(version.Replace(".", "")) > Convert.ToInt32(SettingsViewModel.Instance.Version.Replace(".", ""))) + { + MessageBoxResult result = MessageBox.Show($"A new version of GameVault is now available on GitHub.\nCurrent Version '{SettingsViewModel.Instance.Version}' -> new Version '{version}'\nWould you like to download it? (No automatic installation)", "Info", MessageBoxButton.YesNo, MessageBoxImage.Information); + if (result == MessageBoxResult.Yes) + { + string downloadUrl = (string)obj[0]["assets"][0]["browser_download_url"]; + Process.Start(new ProcessStartInfo(downloadUrl) { UseShellExecute = true }); + App.Current.Shutdown(); + } + } + } + } + catch { } + } + + private async Task CheckIfServerIsOutdated(string serverUrl) + { + //User Notification for major client/server update + bool isServerOutdated = false; + try + { + string serverResonse = await WebHelper.BaseGetAsync(@$"{serverUrl}/api/status"); + string currentServerVersion = System.Text.Json.JsonSerializer.Deserialize(serverResonse).Version; + if (currentServerVersion == null || currentServerVersion == "") + { + isServerOutdated = true; + } + isServerOutdated = new Version(currentServerVersion) < new Version("15.0.0"); + } + catch { } + if (isServerOutdated) + { + try + { + MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync("CLIENT-SERVER-INCOMPABILITY DETECTED", + $"Your GameVault Client is not compatible with the GameVault Server you are using (<15.0.0). This server is too old for your client.\r\n\r\nYou have the following options:\r\n", + MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() + { + AffirmativeButtonText = "Get older client version from GitHub", + NegativeButtonText = "Update the server", + AnimateHide = false, + DialogMessageFontSize = 20, + DialogTitleFontSize = 25 + }); + if (result == MessageDialogResult.Affirmative) + { + Process.Start(new ProcessStartInfo("https://github.com/Phalcode/gamevault-app/releases") { UseShellExecute = true }); + } + else + { + Process.Start(new ProcessStartInfo("https://github.com/Phalcode/gamevault-backend/releases/tag/12.2.0") { UseShellExecute = true }); + } + } + catch { } + } + return isServerOutdated; + } + + private void MetroWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + if (!App.HideToSystemTray) + { + App.Current.Shutdown(); + } + } + + private void AddAdditionalRequestHeader_Click(object sender, RoutedEventArgs e) + { + ViewModel.AdditionalRequestHeaders.Add(new RequestHeader()); + } + + private void RemoveAdditionalRequestHeader_Click(object sender, RoutedEventArgs e) + { + int index = ViewModel.AdditionalRequestHeaders.IndexOf(((RequestHeader)((FrameworkElement)sender).DataContext)); + if (index >= 0) + { + ViewModel.AdditionalRequestHeaders.RemoveAt(index); + } + } + private void SaveAdditionalHeaders_Click(object sender, RoutedEventArgs e) + { + try + { + Preferences.Set(AppConfigKey.AdditionalRequestHeaders, ViewModel.AdditionalRequestHeaders.Where(rh => !string.IsNullOrWhiteSpace(rh.Name) && !string.IsNullOrWhiteSpace(rh.Value)), ProfileManager.ProfileConfigFile); + WebHelper.SetAdditionalRequestHeaders(ViewModel.AdditionalRequestHeaders?.ToArray()); + ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; + ViewModel.AppBarText = "Successfully saved additional request headers"; + } + catch (Exception ex) { ViewModel.AppBarText = ex.Message; } + } + private void LoginWindowSettings_Click(object sender, RoutedEventArgs e) + { + ViewModel.LoginStepIndex = (int)LoginStep.Settings; + } + + private async void UserLoginTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == System.Windows.Input.Key.Enter) + { + await SaveAndLogin(); + } + } + private async void UserRegistrationTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == System.Windows.Input.Key.Enter) + { + await SaveAndSignUp(); + } + } + + } +} diff --git a/gamevault/Windows/MainWindow.xaml.cs b/gamevault/Windows/MainWindow.xaml.cs index 7b43ba8..5e2a8ec 100644 --- a/gamevault/Windows/MainWindow.xaml.cs +++ b/gamevault/Windows/MainWindow.xaml.cs @@ -17,29 +17,39 @@ using System.Threading.Tasks; using System.Windows.Threading; using System.Windows.Controls; +using System.Text.Json; namespace gamevault.Windows { /// /// Interaction logic for MainWindow.xaml /// - public partial class MainWindow + public partial class MainWindow : IDisposable { + private GameTimeTracker GameTimeTracker; public MainWindow() { InitializeComponent(); this.DataContext = MainWindowViewModel.Instance; + InitBootTasks(); } - private async void HamburgerMenuControl_OnItemInvoked(object sender, HamburgerMenuItemInvokedEventArgs args) + private void InitBootTasks() { - if (MainWindowViewModel.Instance.ActiveControl != null && MainWindowViewModel.Instance.ActiveControl.GetType() == typeof(Wizard)) + App.HideToSystemTray = true; + RestoreTheme(); + Task.Run(async () => { - MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync("", $"Do you want to leave the setup wizard?", MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Yes", NegativeButtonText = "No", AnimateHide = false, DialogMessageFontSize = 50 }); - if (result == MessageDialogResult.Negative) + if (GameTimeTracker == null) { - return; + GameTimeTracker = new GameTimeTracker(); + await GameTimeTracker.Start(); } - } + }); + AnalyticsHelper.Instance.SendCustomEvent(CustomAnalyticsEventKeys.USER_SETTINGS, AnalyticsHelper.Instance.PrepareSettingsForAnalytics()); + PipeServiceHandler.Instance.IsReadyForCommands = true; + } + private async void HamburgerMenuControl_OnItemInvoked(object sender, HamburgerMenuItemInvokedEventArgs args) + { MainControl activeControlIndex = (MainControl)MainWindowViewModel.Instance.ActiveControlIndex; switch (activeControlIndex) @@ -75,19 +85,12 @@ private async void HamburgerMenuControl_OnItemInvoked(object sender, HamburgerMe private async void MetroWindow_Loaded(object sender, System.Windows.RoutedEventArgs e) { - AdjustWindowChrome(); - if (SettingsViewModel.Instance.SetupCompleted()) - { - MainWindowViewModel.Instance.SetActiveControl(MainControl.Library); - } - else - { - MainWindowViewModel.Instance.SetActiveControl(new Wizard()); - } + VisualHelper.AdjustWindowChrome(this); + MainWindowViewModel.Instance.SetActiveControl(MainControl.Library); LoginState state = LoginManager.Instance.GetState(); if (LoginState.Success == state) { - if (Preferences.Get(AppConfigKey.LibStartup, AppFilePath.UserFile) == "1") + if (Preferences.Get(AppConfigKey.LibStartup, LoginManager.Instance.GetUserProfile().UserConfigFile) == "1") { await MainWindowViewModel.Instance.Library.LoadLibrary(); } @@ -98,67 +101,27 @@ private async void MetroWindow_Loaded(object sender, System.Windows.RoutedEventA } else if (LoginState.Error == state) { - MainWindowViewModel.Instance.AppBarText = LoginManager.Instance.GetLoginMessage(); + MainWindowViewModel.Instance.AppBarText = LoginManager.Instance.GetServerLoginResponseMessage(); MainWindowViewModel.Instance.Library.ShowLibraryError(); } await MainWindowViewModel.Instance.Library.GetGameInstalls().RestoreInstalledGames(); await MainWindowViewModel.Instance.Downloads.RestoreDownloadedGames(); + LoginManager.Instance.InitOnlineTimer(); MainWindowViewModel.Instance.UserAvatar = LoginManager.Instance.GetCurrentUser(); uiNewsBadge.Badge = await CheckForNews() ? "!" : ""; InitNewsTimer(); - if (await IsServerTooOutdated()) - { - try - { - MessageDialogResult result = await ((MetroWindow)App.Current.MainWindow).ShowMessageAsync("CLIENT-SERVER-INCOMPABILITY DETECTED", - $"Your GameVault Client is not compatible with the GameVault Server you are using (<13.0.0). This server is too old for your client.\r\n\r\nYou have the following options:\r\n", - MessageDialogStyle.AffirmativeAndNegative, new MetroDialogSettings() { AffirmativeButtonText = "Install the older version of the client from GitHub", NegativeButtonText = "Update the server to version 13", AnimateHide = false, DialogMessageFontSize = 25, DialogTitleFontSize = 30 }); - if (result == MessageDialogResult.Affirmative) - { - Process.Start(new ProcessStartInfo("https://github.com/Phalcode/gamevault-app/releases") { UseShellExecute = true }); - } - else - { - Process.Start(new ProcessStartInfo("https://github.com/Phalcode/gamevault-backend/releases/tag/12.2.0") { UseShellExecute = true }); - } - } - catch { } - } - } - //User Notification for major client/serveWPFr update - private async Task IsServerTooOutdated() - { - try - { - using (System.Net.Http.HttpClient httpClient = new System.Net.Http.HttpClient()) - { - - httpClient.DefaultRequestHeaders.Add("User-Agent", "Other"); - string serverResonse = await WebHelper.GetRequestAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/health"); - string currentServerVersion = System.Text.Json.JsonSerializer.Deserialize(serverResonse).Version; - if (currentServerVersion == null || currentServerVersion == "") - { - return true; - } - return new Version(currentServerVersion) < new Version("13.0.0"); - } - } - catch { } - return false; } - //###### - private void MetroWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) { - if (App.ShowToastMessage) + if (App.HideToSystemTray) { e.Cancel = true; this.Hide(); - if (Preferences.Get(AppConfigKey.RunningInTrayMessage, AppFilePath.UserFile) != "1") + if (Preferences.Get(AppConfigKey.RunningInTrayMessage, LoginManager.Instance.GetUserProfile().UserConfigFile) != "1") { - Preferences.Set(AppConfigKey.RunningInTrayMessage, "1", AppFilePath.UserFile); + Preferences.Set(AppConfigKey.RunningInTrayMessage, "1", LoginManager.Instance.GetUserProfile().UserConfigFile); ToastMessageHelper.CreateToastMessage("Information", "GameVault is still running in the background"); } } @@ -180,18 +143,18 @@ private async Task CheckForNews() { try { - if (Preferences.Get(AppConfigKey.UnreadNews, AppFilePath.UserFile) == "1") + if (Preferences.Get(AppConfigKey.UnreadNews, LoginManager.Instance.GetUserProfile().UserConfigFile) == "1") { return true; } - string gameVaultNews = await WebHelper.DownloadFileContentAsync("https://gamevau.lt/news.md"); - string serverNews = await WebHelper.GetRequestAsync($"{SettingsViewModel.Instance.ServerUrl}/api/config/news"); + string gameVaultNews = await WebHelper.GetAsync("https://gamevau.lt/news.md"); + string serverNews = await WebHelper.GetAsync($"{SettingsViewModel.Instance.ServerUrl}/api/config/news"); string hash = await CacheHelper.CreateHashAsync(gameVaultNews + serverNews); - if (Preferences.Get(AppConfigKey.NewsHash, AppFilePath.UserFile) != hash) + if (Preferences.Get(AppConfigKey.NewsHash, LoginManager.Instance.GetUserProfile().UserConfigFile) != hash) { - Preferences.Set(AppConfigKey.UnreadNews, "1", AppFilePath.UserFile); - Preferences.Set(AppConfigKey.NewsHash, hash, AppFilePath.UserFile); + Preferences.Set(AppConfigKey.UnreadNews, "1", LoginManager.Instance.GetUserProfile().UserConfigFile); + Preferences.Set(AppConfigKey.NewsHash, hash, LoginManager.Instance.GetUserProfile().UserConfigFile); return true; } return false; @@ -212,31 +175,16 @@ private void InitNewsTimer() } private void News_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) - { + { MainWindowViewModel.Instance.OpenPopup(new NewsPopup()); try { uiNewsBadge.Badge = ""; - Preferences.Set(AppConfigKey.UnreadNews, "0", AppFilePath.UserFile); + Preferences.Set(AppConfigKey.UnreadNews, "0", LoginManager.Instance.GetUserProfile().UserConfigFile); } catch { } } - private void AdjustWindowChrome() - { - try - { - //var root = this.Template.FindName("PART_Content", this); - //System.Windows.Controls.Panel.SetZIndex((MetroContentControl)root, 6); - var thumb = (FrameworkElement)this.Template.FindName("PART_WindowTitleThumb", this); - thumb.Margin = new Thickness(50, 0, 0, 0); - System.Windows.Controls.Panel.SetZIndex(thumb, 7); - var btnCommands = (FrameworkElement)this.Template.FindName("PART_WindowButtonCommands", this); - System.Windows.Controls.Panel.SetZIndex(btnCommands, 8); - } - catch { } - } - private void Shortlink_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { try @@ -259,5 +207,37 @@ private void CopyMessage_Click(object sender, RoutedEventArgs e) } catch { } } + private void RestoreTheme() + { + try + { + string currentThemeString = Preferences.Get(AppConfigKey.Theme, LoginManager.Instance.GetUserProfile().UserConfigFile, true); + if (currentThemeString != string.Empty) + { + ThemeItem currentTheme = JsonSerializer.Deserialize(currentThemeString)!; + + if (App.Current.Resources.MergedDictionaries[0].Source.OriginalString != currentTheme.Path) + { + App.Instance.SetTheme(currentTheme.Path); + } + } + } + catch { } + } + public void Dispose() + { + GameTimeTracker.Stop(); + MainWindowViewModel.Instance.Downloads.CancelAllDownloads(); + InstallViewModel.Instance.InstalledGames.Clear(); + DownloadsViewModel.Instance.DownloadedGames.Clear(); + ProcessShepherd.Instance.KillAllChildProcesses(); + App.HideToSystemTray = false; + App.Instance.ResetToDefaultTheme(); + LoginManager.Instance.StopOnlineTimer(); + App.Instance.ResetJumpListGames(); + PipeServiceHandler.Instance.IsReadyForCommands = false; + MainWindowViewModel.Instance.UserAvatar = null; + this.Close(); + } } } diff --git a/gamevault/Windows/UpdateWindow.xaml b/gamevault/Windows/UpdateWindow.xaml deleted file mode 100644 index 9684180..0000000 --- a/gamevault/Windows/UpdateWindow.xaml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/gamevault/Windows/UpdateWindow.xaml.cs b/gamevault/Windows/UpdateWindow.xaml.cs deleted file mode 100644 index 849b720..0000000 --- a/gamevault/Windows/UpdateWindow.xaml.cs +++ /dev/null @@ -1,87 +0,0 @@ -using gamevault.Helper; -using gamevault.ViewModels; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.Json.Nodes; -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.Shapes; - -namespace gamevault.Windows -{ - /// - /// Interaction logic for UpdateWindow.xaml - /// - public partial class UpdateWindow - { - private StoreHelper m_StoreHelper; - public UpdateWindow() - { - InitializeComponent(); - } - - private async void MetroWindow_Loaded(object sender, RoutedEventArgs e) - { - try - { - m_StoreHelper = new StoreHelper(); - if (true == await m_StoreHelper.UpdatesAvailable()) - { - uiTxtStatus.Text = "Updating..."; - await m_StoreHelper.DownloadAndInstallAllUpdatesAsync(this); - } - App.IsWindowsPackage = true; - } - catch (COMException comEx) - { - //Is no MSIX package - } - catch (Exception ex) - { - //rest of the cases - } - try - { - if (App.IsWindowsPackage == false) - { - using (HttpClient httpClient = new HttpClient()) - { - httpClient.DefaultRequestHeaders.Add("User-Agent", "Other"); - var response = await httpClient.GetStringAsync("https://api.github.com/repos/Phalcode/gamevault-app/releases"); - dynamic obj = JsonNode.Parse(response); - string version = (string)obj[0]["tag_name"]; - if (Convert.ToInt32(version.Replace(".", "")) > Convert.ToInt32(SettingsViewModel.Instance.Version.Replace(".", ""))) - { - MessageBoxResult result = MessageBox.Show($"A new version of GameVault is now available on GitHub.\nCurrent Version '{SettingsViewModel.Instance.Version}' -> new Version '{version}'\nWould you like to download it? (No automatic installation)", "Info", MessageBoxButton.YesNo, MessageBoxImage.Information); - if (result == MessageBoxResult.Yes) - { - string downloadUrl = (string)obj[0]["assets"][0]["browser_download_url"]; - Process.Start(new ProcessStartInfo(downloadUrl) { UseShellExecute = true }); - App.Current.Shutdown(); - } - } - } - } - } - catch { } - try - { - uiTxtStatus.Text = "Optimizing cache..."; - await CacheHelper.OptimizeCache(); - } - catch { } - this.Close(); - } - } -} diff --git a/gamevault/gamevault.csproj b/gamevault/gamevault.csproj index ccb830a..34af929 100644 --- a/gamevault/gamevault.csproj +++ b/gamevault/gamevault.csproj @@ -53,21 +53,21 @@ - + - + - + - +