diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj b/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj
new file mode 100644
index 0000000..fa02bbb
--- /dev/null
+++ b/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/NebulaAuth.LegacyConverter/Program.cs b/NebulaAuth.LegacyConverter/Program.cs
new file mode 100644
index 0000000..a1076ad
--- /dev/null
+++ b/NebulaAuth.LegacyConverter/Program.cs
@@ -0,0 +1,51 @@
+using Newtonsoft.Json;
+using SteamLib.Utility.MaFiles;
+
+var currentPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
+if (currentPath != null)
+ Environment.CurrentDirectory = currentPath;
+
+
+Console.WriteLine(currentPath);
+foreach (var path in args)
+{
+
+ if (Path.Exists(path) == false)
+ {
+ Console.WriteLine($"NOT VALID PATH: '{path}'");
+ continue;
+ }
+ Console.WriteLine("Reading: " + path);
+ try
+ {
+ var text = File.ReadAllText(path);
+ var maf = MafileSerializer.Deserialize(text, out _);
+ var legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented);
+ var name = Path.GetFileNameWithoutExtension(path);
+ Write(legacy, name);
+
+ Console.WriteLine("DONE: " + name);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"ERROR: {ex.Message}");
+ Console.WriteLine(ex);
+ }
+ finally
+ {
+ Console.WriteLine("-----------------------------------------");
+ }
+
+
+}
+
+Console.WriteLine("Press any key to exit...");
+Console.ReadKey();
+
+
+void Write(string maf, string name)
+{
+
+ var path = Path.Combine(name + "_legacy.mafile");
+ File.WriteAllText(path, maf);
+}
\ No newline at end of file
diff --git a/NebulaAuth.sln b/NebulaAuth.sln
new file mode 100644
index 0000000..9f72133
--- /dev/null
+++ b/NebulaAuth.sln
@@ -0,0 +1,40 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33205.214
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NebulaAuth", "NebulaAuth\NebulaAuth.csproj", "{0FD01700-6D5C-451B-93BA-0860647E8F13}"
+ ProjectSection(ProjectDependencies) = postProject
+ {09F02072-F91D-4DAA-87BC-A34D3E150570} = {09F02072-F91D-4DAA-87BC-A34D3E150570}
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NebulaAuth.LegacyConverter", "NebulaAuth.LegacyConverter\NebulaAuth.LegacyConverter.csproj", "{2D78A7D9-986A-4890-8A91-7ABD57A91830}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamLibForked", "SteamLibForked\SteamLibForked.csproj", "{09F02072-F91D-4DAA-87BC-A34D3E150570}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {0FD01700-6D5C-451B-93BA-0860647E8F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0FD01700-6D5C-451B-93BA-0860647E8F13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0FD01700-6D5C-451B-93BA-0860647E8F13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0FD01700-6D5C-451B-93BA-0860647E8F13}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09F02072-F91D-4DAA-87BC-A34D3E150570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09F02072-F91D-4DAA-87BC-A34D3E150570}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09F02072-F91D-4DAA-87BC-A34D3E150570}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {09F02072-F91D-4DAA-87BC-A34D3E150570}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {FA34DDD5-BDC0-4C0A-B347-4ECDBF446900}
+ EndGlobalSection
+EndGlobal
diff --git a/NebulaAuth.sln.DotSettings b/NebulaAuth.sln.DotSettings
new file mode 100644
index 0000000..5b88222
--- /dev/null
+++ b/NebulaAuth.sln.DotSettings
@@ -0,0 +1,2 @@
+
+ DO_NOT_SHOW
\ No newline at end of file
diff --git a/NebulaAuth/App.xaml b/NebulaAuth/App.xaml
new file mode 100644
index 0000000..8f47884
--- /dev/null
+++ b/NebulaAuth/App.xaml
@@ -0,0 +1,50 @@
+
+
+
+ #1E2025
+ pack://application:,,,/Fonts/Roboto/#Roboto
+ pack://application:,,,/Fonts/Roboto/#Roboto Symbols
+
+
+
+
+
+
+
+
+
+
+
+ True
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NebulaAuth/App.xaml.cs b/NebulaAuth/App.xaml.cs
new file mode 100644
index 0000000..bf9d06f
--- /dev/null
+++ b/NebulaAuth/App.xaml.cs
@@ -0,0 +1,35 @@
+using NebulaAuth.Core;
+using NebulaAuth.Model;
+using NebulaAuth.Model.Exceptions;
+using System;
+using System.Windows;
+using CodingSeb.Localization;
+
+namespace NebulaAuth;
+
+
+public partial class App : Application
+{
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ base.OnStartup(e);
+
+ LocManager.Init();
+ LocManager.SetApplicationLocalization(Settings.Instance.Language);
+ try
+ {
+ Shell.Initialize();
+ }
+ catch (Exception ex)
+ {
+ var msg = ex.Message;
+ if (ex is CantAlignTimeException)
+ {
+ msg = Loc.Tr(LocManager.GetCodeBehind("CantAlignTimeError"));
+ }
+
+ MessageBox.Show(msg);
+ throw;
+ }
+ }
+}
diff --git a/NebulaAuth/AssemblyInfo.cs b/NebulaAuth/AssemblyInfo.cs
new file mode 100644
index 0000000..8b5504e
--- /dev/null
+++ b/NebulaAuth/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs b/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs
new file mode 100644
index 0000000..d886d19
--- /dev/null
+++ b/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs
@@ -0,0 +1,20 @@
+using NebulaAuth.Model;
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace NebulaAuth.Converters.Background;
+
+public class BackgroundImageVisibleConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not BackgroundMode.Color ? Visibility.Visible : Visibility.Hidden;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs b/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs
new file mode 100644
index 0000000..b66349a
--- /dev/null
+++ b/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Windows.Data;
+using System.Windows.Media.Imaging;
+using NebulaAuth.Model;
+
+namespace NebulaAuth.Converters.Background;
+
+public class BackgroundSourceConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is BackgroundMode.Custom)
+ {
+ if(File.Exists("Background.png"))
+ return new BitmapImage(new Uri(Path.GetFullPath("Background.png")));
+ }
+
+
+ return new BitmapImage(new Uri("pack://application:,,,/Theme/Background.jpg"));
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/CoefficientConverter.cs b/NebulaAuth/Converters/CoefficientConverter.cs
new file mode 100644
index 0000000..e79ddc8
--- /dev/null
+++ b/NebulaAuth/Converters/CoefficientConverter.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NebulaAuth.Converters;
+
+[ValueConversion(typeof(double), typeof(double))]
+public class CoefficientConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return System.Convert.ToDouble(value) * System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return (double)value / (double)parameter;
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/ColorToBrushConverter.cs b/NebulaAuth/Converters/ColorToBrushConverter.cs
new file mode 100644
index 0000000..c84794b
--- /dev/null
+++ b/NebulaAuth/Converters/ColorToBrushConverter.cs
@@ -0,0 +1,30 @@
+using System.Globalization;
+using System.Windows.Data;
+using System.Windows.Media;
+using System;
+
+namespace NebulaAuth.Converters;
+
+[ValueConversion(typeof(Color), typeof(Brush))]
+public class ColorToBrushConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is Color color)
+ {
+ SolidColorBrush rv = new(color);
+ rv.Freeze();
+ return rv;
+ }
+ return Binding.DoNothing;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is SolidColorBrush brush)
+ {
+ return brush.Color;
+ }
+ return default(Color);
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/MultiCommandParamaterConverter.cs b/NebulaAuth/Converters/MultiCommandParamaterConverter.cs
new file mode 100644
index 0000000..d228e06
--- /dev/null
+++ b/NebulaAuth/Converters/MultiCommandParamaterConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NebulaAuth.Converters;
+
+public class MultiCommandParameterConverter : IMultiValueConverter
+{
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ return values.Clone();
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/OnOffBoolTextConverter.cs b/NebulaAuth/Converters/OnOffBoolTextConverter.cs
new file mode 100644
index 0000000..73e4b47
--- /dev/null
+++ b/NebulaAuth/Converters/OnOffBoolTextConverter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NebulaAuth.Converters;
+
+public class OnOffBoolTextConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not bool b)
+ {
+ throw new InvalidCastException($"Can't cast value {value} to 'bool'");
+ }
+
+ return b ? "вкл" : "выкл";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/ProxyTextConverter.cs b/NebulaAuth/Converters/ProxyTextConverter.cs
new file mode 100644
index 0000000..889a351
--- /dev/null
+++ b/NebulaAuth/Converters/ProxyTextConverter.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using NebulaAuth.Model.Entities;
+
+namespace NebulaAuth.Converters;
+
+public class ProxyTextConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not MaProxy p)
+ {
+ return string.Empty;
+ }
+
+ return $"{p.Id}: {p.Data.Address}";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/ReverseBooleanConverter.cs b/NebulaAuth/Converters/ReverseBooleanConverter.cs
new file mode 100644
index 0000000..020a17b
--- /dev/null
+++ b/NebulaAuth/Converters/ReverseBooleanConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NebulaAuth.Converters;
+
+public class ReverseBooleanConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return !(bool)value;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return !(bool)value;
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/SelectedProxyTextConverter.cs b/NebulaAuth/Converters/SelectedProxyTextConverter.cs
new file mode 100644
index 0000000..5885ce8
--- /dev/null
+++ b/NebulaAuth/Converters/SelectedProxyTextConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using NebulaAuth.Model.Entities;
+
+namespace NebulaAuth.Converters;
+
+public class SelectedProxyTextConverter : IMultiValueConverter
+{
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values[0] is not MaProxy proxy)
+ {
+ return string.Empty;
+ }
+
+ var proxyExist = (bool)values[1];
+ return proxyExist ? $"{proxy.Id}: {proxy.Data.Address}" : "";
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Converters/ValueConverterGroup.cs b/NebulaAuth/Converters/ValueConverterGroup.cs
new file mode 100644
index 0000000..4d7b5fd
--- /dev/null
+++ b/NebulaAuth/Converters/ValueConverterGroup.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Data;
+
+namespace NebulaAuth.Converters;
+
+public class ValueConverterGroup : List, IValueConverter
+{
+ #region IValueConverter Members
+
+ public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+ {
+ return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/NebulaAuth/Core/DialogsController.cs b/NebulaAuth/Core/DialogsController.cs
new file mode 100644
index 0000000..349f7d8
--- /dev/null
+++ b/NebulaAuth/Core/DialogsController.cs
@@ -0,0 +1,76 @@
+using System.Threading.Tasks;
+using MaterialDesignThemes.Wpf;
+using NebulaAuth.Model.Entities;
+using NebulaAuth.View;
+using NebulaAuth.View.Dialogs;
+using NebulaAuth.ViewModel.Other;
+
+namespace NebulaAuth.Core;
+
+public static class DialogsController
+{
+
+ #region CommonDialogs
+
+ public static async Task ShowConfirmCancelDialog(string? msg = null)
+ {
+
+ var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
+
+ var result = await DialogHost.Show(content);
+ return result != null && (bool) result;
+ }
+
+ //public static async Task ShowTextFieldDialog(string? msg = null)
+ //{
+ // var content = msg == null ? new TextFieldDialog() : new TextFieldDialog(msg);
+ // var result = await DialogHost.Show(content);
+ // return result as string;
+ //}
+
+ #endregion
+
+ public static async Task ShowLoginAgainDialog(string username)
+ {
+ var vm = new LoginAgainVM
+ {
+ UserName = username
+ };
+ var content = new LoginAgainDialog()
+ {
+ DataContext = vm
+ };
+ var result = await DialogHost.Show(content);
+ if (result is true)
+ {
+ return vm;
+ }
+
+ return null;
+ }
+
+ public static async Task ShowProxyManager(MaProxy? currentProxy)
+ {
+ var vm = new ProxyManagerVM();
+ var view = new ProxyManagerView
+ {
+ DataContext = vm
+ };
+ await DialogHost.Show(view);
+ }
+ public static void CloseDialog()
+ {
+ DialogHost.Close(null);
+ }
+ public static async Task ShowLinkerDialog()
+ {
+ var vm = new LinkAccountVM();
+ var view = new LinkerView()
+ {
+ DataContext = vm
+ };
+ await DialogHost.Show(view);
+ }
+
+
+}
\ No newline at end of file
diff --git a/NebulaAuth/Core/LocalizationManager.cs b/NebulaAuth/Core/LocalizationManager.cs
new file mode 100644
index 0000000..7b30221
--- /dev/null
+++ b/NebulaAuth/Core/LocalizationManager.cs
@@ -0,0 +1,105 @@
+using CodingSeb.Localization;
+using CodingSeb.Localization.Loaders;
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text;
+
+namespace NebulaAuth.Core;
+
+public static class LocManager
+{
+ public const string CODE_BEHIND_PATH_PART = "CodeBehind";
+ public const string COMMON_PATH_PART = "Common";
+ public static void SetApplicationLocalization(LocalizationLanguage language)
+ {
+ Loc.Instance.CurrentLanguage = GetLanguageCode(language);
+ //Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(GetLanguageCode(language));
+ //Thread.CurrentThread.CurrentCulture= CultureInfo.GetCultureInfo(GetLanguageCode(language));
+ }
+
+
+ public static string GetLanguageCode(LocalizationLanguage language)
+ {
+ return language switch
+ {
+ LocalizationLanguage.English => "en",
+ LocalizationLanguage.Russian => "ru",
+ LocalizationLanguage.Ukrainian => "ua",
+ _ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
+ };
+ }
+
+
+
+ public static void Init()
+ {
+ Loc.LogOutMissingTranslations = true;
+
+ LocalizationLoader.Instance.FileLanguageLoaders.Add(new JsonFileLoader());
+
+ ReloadFiles();
+ }
+
+ public static void ReloadFiles()
+ {
+ string exampleFileFileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "localization.loc.json");
+ LocalizationLoader.Instance.ClearAllTranslations();
+ LocalizationLoader.Instance.AddFile(exampleFileFileName);
+ }
+
+ public static string? GetCodeBehind(params string[] path)
+ {
+ return GetConcat(CODE_BEHIND_PATH_PART, path);
+ }
+
+ public static string GetCodeBehindOrDefault(string def, params string[] path)
+ {
+ return GetCodeBehind(path) ?? def;
+ }
+
+ public static string? GetCommon(params string[] path)
+ {
+ return GetConcat(COMMON_PATH_PART, path);
+ }
+
+ public static string GetCommonOrDefault(string def, params string[] path)
+ {
+ return GetCommon(path) ?? def;
+ }
+
+
+ public static string? Get(params string[] path)
+ {
+ var sb = new StringBuilder();
+ foreach (var part in path)
+ {
+ sb.Append('.');
+ sb.Append(part);
+ }
+
+ sb.Remove(0, 1);
+ var fullPath = sb.ToString();
+ var message = Loc.Tr(fullPath, "~|*ABSENT|~");
+ return message == "~|*ABSENT|~" ? null : message;
+ }
+
+ public static string GetOrDefault(string def, params string[] path)
+ {
+ return Get(path) ?? def;
+ }
+ private static string? GetConcat(string first, string[] path)
+ {
+ string[] newArray = new string[path.Length + 1];
+ newArray[0] = first;
+ Array.Copy(path, 0, newArray, 1, path.Length);
+ return Get(newArray);
+ }
+}
+
+public enum LocalizationLanguage
+{
+ English,
+ Russian,
+ Ukrainian
+}
\ No newline at end of file
diff --git a/NebulaAuth/Core/MyUpdateManager.cs b/NebulaAuth/Core/MyUpdateManager.cs
new file mode 100644
index 0000000..dbe48de
--- /dev/null
+++ b/NebulaAuth/Core/MyUpdateManager.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+
+namespace NebulaAuth.Core;
+
+public static class UpdateManager
+{
+ public static async Task CheckForUpdates()
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/NebulaAuth/Core/SnackbarController.cs b/NebulaAuth/Core/SnackbarController.cs
new file mode 100644
index 0000000..fc0accb
--- /dev/null
+++ b/NebulaAuth/Core/SnackbarController.cs
@@ -0,0 +1,56 @@
+using System;
+using MaterialDesignThemes.Wpf;
+
+namespace NebulaAuth.Core;
+
+public class SnackbarController
+{
+
+ public static SnackbarMessageQueue MessageQueue { get; } = new() { DiscardDuplicates = true};
+ private const int MIN_SNACKBAR_TIME = 1000;
+ public SnackbarController()
+ {
+
+ }
+ ///
+ ///
+ ///
+ ///
+ /// Default duration is 1 second
+ public static void SendSnackbar(string text, TimeSpan? duration = null)
+ {
+ duration ??= GetSnackbarTime(text);
+ MessageQueue.Enqueue(text, null, null, null, false, false, duration.Value);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ /// Default duration is 1 second
+ public static void SendSnackbarWithButton(string text, string actionText = "OK", Action? action = null, TimeSpan? duration = null)
+ {
+ duration ??= GetSnackbarTime(text);
+ Action