mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-25 06:14:31 +00:00
Добавьте файлы проекта.
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SteamLibForked\SteamLibForked.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String></wpf:ResourceDictionary>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<Application x:Class="NebulaAuth.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:NebulaAuth"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||||
|
xmlns:converters="clr-namespace:NebulaAuth.Converters"
|
||||||
|
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||||
|
xmlns:background="clr-namespace:NebulaAuth.Converters.Background"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<SolidColorBrush x:Key="MaterialDesignPaper">#1E2025</SolidColorBrush>
|
||||||
|
<FontFamily x:Key="Roboto">pack://application:,,,/Fonts/Roboto/#Roboto</FontFamily>
|
||||||
|
<FontFamily x:Key="RobotoSymbols">pack://application:,,,/Fonts/Roboto/#Roboto Symbols</FontFamily>
|
||||||
|
<converters:CoefficientConverter x:Key="CoefficientConverter"/>
|
||||||
|
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter"/>
|
||||||
|
<converters:OnOffBoolTextConverter x:Key="OnOffBoolTextConverter"/>
|
||||||
|
<converters:SelectedProxyTextConverter x:Key="SelectedProxyTextConverter"/>
|
||||||
|
<converters:ProxyTextConverter x:Key="ProxyTextConverter"/>
|
||||||
|
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter"/>
|
||||||
|
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter"/>
|
||||||
|
<!-- Background converters-->
|
||||||
|
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter"/>
|
||||||
|
<background:BackgroundSourceConverter x:Key="BackgroundSourceConverter"/>
|
||||||
|
|
||||||
|
<system:Boolean x:Key="True">True</system:Boolean>
|
||||||
|
<system:Boolean x:Key="False">False</system:Boolean>
|
||||||
|
|
||||||
|
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
|
||||||
|
<materialDesign:BundledTheme BaseTheme="Dark" PrimaryColor="LightGreen" SecondaryColor="Purple" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/MaterialDesignExtensions;component/Themes/Generic.xaml" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/materialdesigntheme.snackbar.xaml" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/materialdesigntheme.Dark.xaml" />
|
||||||
|
<!-- Theme-->
|
||||||
|
<ResourceDictionary Source="Theme/Brushes.xaml"/>
|
||||||
|
<ResourceDictionary Source="Theme/WindowStyle/WindowStyle.xaml"/>
|
||||||
|
|
||||||
|
<!--Controls-->
|
||||||
|
<ResourceDictionary Source="View/ConfirmationTemplates.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
|
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
)]
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>, 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
|
||||||
|
}
|
||||||
@@ -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<bool> 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<string?> 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<LoginAgainVM?> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
||||||
|
namespace NebulaAuth.Core;
|
||||||
|
|
||||||
|
public static class UpdateManager
|
||||||
|
{
|
||||||
|
public static async Task CheckForUpdates()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="duration">Default duration is 1 second</param>
|
||||||
|
public static void SendSnackbar(string text, TimeSpan? duration = null)
|
||||||
|
{
|
||||||
|
duration ??= GetSnackbarTime(text);
|
||||||
|
MessageQueue.Enqueue(text, null, null, null, false, false, duration.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="duration">Default duration is 1 second</param>
|
||||||
|
public static void SendSnackbarWithButton(string text, string actionText = "OK", Action? action = null, TimeSpan? duration = null)
|
||||||
|
{
|
||||||
|
duration ??= GetSnackbarTime(text);
|
||||||
|
Action<object?> argAction;
|
||||||
|
if (action == null)
|
||||||
|
{
|
||||||
|
argAction = _ => { };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
argAction = _ => action();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageQueue.Enqueue(text, actionText, argAction, null, false, false, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan GetSnackbarTime(string str)
|
||||||
|
{
|
||||||
|
if (str.Length <= 100) return TimeSpan.FromMilliseconds(MIN_SNACKBAR_TIME);
|
||||||
|
|
||||||
|
var length = str.Length / 0.07;
|
||||||
|
return TimeSpan.FromMilliseconds(length);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using NebulaAuth.Model;
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using Color = System.Drawing.Color;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Core;
|
||||||
|
|
||||||
|
public static class ThemeManager
|
||||||
|
{
|
||||||
|
public static System.Windows.Media.Color DefaultBackgroundColor = System.Windows.Media.Color.FromRgb(30, 32, 37);
|
||||||
|
private static readonly Window MainWindow = Application.Current.MainWindow!;
|
||||||
|
static ThemeManager()
|
||||||
|
{
|
||||||
|
Settings.Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SettingsOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(Settings.BackgroundColor))
|
||||||
|
{
|
||||||
|
UpdateBackground();
|
||||||
|
}
|
||||||
|
else if (e.PropertyName == nameof(Settings.IconColor))
|
||||||
|
{
|
||||||
|
UpdateIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateIcon()
|
||||||
|
{
|
||||||
|
var color = Settings.Instance.IconColor;
|
||||||
|
if (color == null)
|
||||||
|
{
|
||||||
|
MainWindow.TaskbarItemInfo = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var c = color.Value;
|
||||||
|
var diameter = 14;
|
||||||
|
var bitmap = new Bitmap(diameter, diameter);
|
||||||
|
var graphics = Graphics.FromImage(bitmap);
|
||||||
|
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
||||||
|
var brush = new SolidBrush(Color.FromArgb(c.A, c.R, c.G, c.B));
|
||||||
|
graphics.FillEllipse(brush, 0, 0, diameter, diameter);
|
||||||
|
var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty,
|
||||||
|
BitmapSizeOptions.FromEmptyOptions());
|
||||||
|
|
||||||
|
MainWindow.TaskbarItemInfo = new();
|
||||||
|
MainWindow.TaskbarItemInfo.Overlay = bitmapSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateBackground()
|
||||||
|
{
|
||||||
|
var color = Settings.Instance.BackgroundColor ?? DefaultBackgroundColor;
|
||||||
|
Application.Current.Resources["WindowBackground"] = new SolidColorBrush(color);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void InitializeTheme()
|
||||||
|
{
|
||||||
|
UpdateIcon();
|
||||||
|
UpdateBackground();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using Application = System.Windows.Application;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Core;
|
||||||
|
|
||||||
|
public static class TrayManager
|
||||||
|
{
|
||||||
|
private static NotifyIcon _notifyIcon;
|
||||||
|
private static readonly Window MainWindow = Application.Current.MainWindow!;
|
||||||
|
public static bool IsEnabled => Settings.Instance.HideToTray;
|
||||||
|
|
||||||
|
|
||||||
|
public static void InitializeTray()
|
||||||
|
{
|
||||||
|
_notifyIcon = new NotifyIcon();
|
||||||
|
_notifyIcon.Text = "NebulaAuth";
|
||||||
|
|
||||||
|
Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/Theme/nebula lock.ico"))!.Stream;
|
||||||
|
_notifyIcon.Icon = new Icon(iconStream);
|
||||||
|
|
||||||
|
_notifyIcon.MouseDoubleClick += NotifyIcon_MouseDoubleClick;
|
||||||
|
|
||||||
|
var contextMenu = new ContextMenuStrip();
|
||||||
|
|
||||||
|
contextMenu.Items.Add("Выйти", null!, onClick: OnExitClick);
|
||||||
|
|
||||||
|
_notifyIcon.ContextMenuStrip = contextMenu;
|
||||||
|
|
||||||
|
MainWindow.StateChanged += MainWindow_StateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnExitClick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ExitApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NotifyIcon_MouseDoubleClick(object? sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Button == MouseButtons.Left)
|
||||||
|
{
|
||||||
|
ShowMainWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MainWindow_StateChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (MainWindow.WindowState == WindowState.Minimized)
|
||||||
|
{
|
||||||
|
HideMainWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ShowMainWindow()
|
||||||
|
{
|
||||||
|
MainWindow.Show();
|
||||||
|
MainWindow.WindowState = WindowState.Normal;
|
||||||
|
_notifyIcon.Visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HideMainWindow()
|
||||||
|
{
|
||||||
|
if(IsEnabled == false) return;
|
||||||
|
_notifyIcon.Visible = true;
|
||||||
|
MainWindow.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExitApplication()
|
||||||
|
{
|
||||||
|
_notifyIcon.Dispose();
|
||||||
|
Application.Current.Shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||||
|
<CodingSebLocalization />
|
||||||
|
</Weavers>
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<w:FontScaleWindow x:Class="NebulaAuth.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:w="clr-namespace:NebulaAuth.Theme"
|
||||||
|
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||||
|
xmlns:windowStyle="clr-namespace:NebulaAuth.Theme.WindowStyle"
|
||||||
|
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:converters="clr-namespace:NebulaAuth.Converters"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
|
||||||
|
xmlns:viewModel="clr-namespace:NebulaAuth.ViewModel"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
MinHeight="500" MinWidth="500" DefaultFontSize="18" ScaleCoefficient="0.4"
|
||||||
|
Title="NebulaAuth" Height="800" Width="730" Style="{StaticResource MainWindow}"
|
||||||
|
RenderOptions.BitmapScalingMode="HighQuality" Foreground="#FFF5F5F5"
|
||||||
|
FontFamily="{md:MaterialDesignFont}"
|
||||||
|
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DataContext="{d:DesignInstance viewModel:MainVM}"
|
||||||
|
|
||||||
|
|
||||||
|
>
|
||||||
|
<b:Interaction.Behaviors>
|
||||||
|
<windowStyle:WindowChromeRenderedBehavior />
|
||||||
|
</b:Interaction.Behaviors>
|
||||||
|
|
||||||
|
<Window.InputBindings>
|
||||||
|
<KeyBinding Command="{Binding Path=CopyMafileFromBufferCommand}"
|
||||||
|
Key="V"
|
||||||
|
Modifiers="Control"/>
|
||||||
|
</Window.InputBindings>
|
||||||
|
<Border Name="DragNDropBorder" Panel.ZIndex="3" Opacity="1" AllowDrop="True" DragDrop.DragEnter="Rectangle_DragEnter" DragLeave="Rectangle_DragLeave" Drop="Rectangle_Drop">
|
||||||
|
<md:DialogHost DialogOpened="DialogHost_DialogOpened" DialogClosed="DialogHost_DialogClosed">
|
||||||
|
<Grid ZIndex="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Image Grid.RowSpan="2" Opacity="0.6" Stretch="UniformToFill" RenderOptions.BitmapScalingMode="LowQuality" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding Settings.BackgroundMode, Converter={StaticResource BackgroundImageVisibleConverter}}" Source="{Binding Settings.BackgroundMode, Mode=OneWay, Converter={StaticResource BackgroundSourceConverter}}" />
|
||||||
|
<Rectangle Grid.Row="0" Grid.RowSpan="2" Opacity="0.5" Fill="#FF1F1D1D" Visibility="{Binding Settings.BackgroundMode, Converter={StaticResource BackgroundImageVisibleConverter}}" />
|
||||||
|
<Rectangle Name="DragNDropOverlay" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="1" Fill="#242123" Opacity="0.9" Visibility="Hidden" />
|
||||||
|
<StackPanel Name="DragNDropPanel" Grid.Row="0" ZIndex="2" w:FontScaleWindow.Scale="1" w:FontScaleWindow.ResizeFont="True" Grid.RowSpan="3" Visibility="Hidden" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<TextBlock FontWeight="Bold" Foreground="#9a65b8">Отпустите, чтобы добавить mafile</TextBlock>
|
||||||
|
<md:PackIcon Foreground="#9a65b8" HorizontalAlignment="Center" Width="36" Height="36" Kind="FileReplaceOutline" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToolBarTray IsLocked="True" Grid.Row="0">
|
||||||
|
<ToolBarTray.Background>
|
||||||
|
<SolidColorBrush Opacity="0.6" Color="#FF1A1C25" />
|
||||||
|
</ToolBarTray.Background>
|
||||||
|
<ToolBar Background="#00FFFFFF" ClipToBounds="False" Style="{StaticResource MaterialDesignToolBar}" w:FontScaleWindow.Scale="0.75" w:FontScaleWindow.ResizeFont="True">
|
||||||
|
<Menu>
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.File.Caption}">
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.File.Import}" Command="{Binding AddMafileCommand}" />
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.File.Remove}" Command="{Binding RemoveMafileCommand}" />
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.File.OpenFolder}" Command="{Binding OpenMafileFolderCommand}" />
|
||||||
|
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.File.Settings}" Command="{Binding OpenSettingsDialogCommand}" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<Menu>
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.Account.Caption}">
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.Account.Link}" Command="{Binding LinkAccountCommand}" />
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.Account.Unlink}" Command="{Binding RemoveAuthenticatorCommand}"/>
|
||||||
|
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.Account.RefreshSession}" Command="{Binding RefreshSessionCommand}" />
|
||||||
|
<MenuItem Header="{Tr MainWindow.Menu.Account.LoginAgain}" Command="{Binding LoginAgainCommand}" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<Separator />
|
||||||
|
<ComboBox MinWidth="100" Margin="8,0,8,0" VerticalAlignment="Center" md:HintAssist.Hint="{Tr MainWindow.AppBar.GroupsHint}" md:TextFieldAssist.HasClearButton="True" IsEditable="True" ItemsSource="{Binding Groups}" SelectedValue="{Binding SelectedGroup}">
|
||||||
|
<FrameworkElement.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<Style TargetType="{x:Type md:PackIcon}">
|
||||||
|
<Setter Property="Width" Value="15" />
|
||||||
|
<Setter Property="Height" Value="15" />
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</FrameworkElement.Resources>
|
||||||
|
<UIElement.InputBindings>
|
||||||
|
<KeyBinding Key="Enter" Command="{Binding AddGroupCommand}" CommandParameter="{Binding Path=Text, RelativeSource={RelativeSource AncestorType=ComboBox}}" />
|
||||||
|
</UIElement.InputBindings>
|
||||||
|
</ComboBox>
|
||||||
|
<ComboBox MinWidth="100" Margin="8,0,8,0" VerticalAlignment="Center" md:HintAssist.Hint="{Tr MainWindow.AppBar.Proxy.ProxyHint}" md:ComboBoxAssist.ShowSelectedItem="False" SelectedItem="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type entities:MaProxy}">
|
||||||
|
<TextBlock Text="{Binding Converter={StaticResource ProxyTextConverter}, Mode=OneWay}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
<FrameworkElement.ContextMenu>
|
||||||
|
<ContextMenu>
|
||||||
|
<MenuItem Header="{Tr MainWindow.AppBar.Proxy.ProxyOpenManagerHint}" Command="{Binding OpenProxyManagerCommand}" />
|
||||||
|
</ContextMenu>
|
||||||
|
</FrameworkElement.ContextMenu>
|
||||||
|
<UIElement.InputBindings>
|
||||||
|
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
|
||||||
|
</UIElement.InputBindings>
|
||||||
|
</ComboBox>
|
||||||
|
<md:PackIcon Kind="StopAlert" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#FFFF0000" Margin="3" ToolTip="{Tr MainWindow.AppBar.Proxy.ProxyAlert.MafileProxyInUse}" ToolTipService.InitialShowDelay="600">
|
||||||
|
<UIElement.Visibility>
|
||||||
|
<Binding Path="ProxyExist">
|
||||||
|
<Binding.Converter>
|
||||||
|
<converters:ValueConverterGroup>
|
||||||
|
<converters:ReverseBooleanConverter />
|
||||||
|
<BooleanToVisibilityConverter />
|
||||||
|
</converters:ValueConverterGroup>
|
||||||
|
</Binding.Converter>
|
||||||
|
</Binding>
|
||||||
|
</UIElement.Visibility>
|
||||||
|
</md:PackIcon>
|
||||||
|
<md:PackIcon Kind="StopAlert" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#FFFFA500" Margin="3" ToolTip="{Tr MainWindow.AppBar.Proxy.ProxyAlert.DefaultInUse}" ToolTipService.InitialShowDelay="600" Visibility="{Binding IsDefaultProxy, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
|
<CheckBox FontSize="14" Style="{StaticResource MaterialDesignFilterChipAccentCheckBox}" IsChecked="{Binding TradeTimerEnabled}" Content="{Tr MainWindow.AppBar.TradeTimerHint}"/>
|
||||||
|
<CheckBox FontSize="14" Style="{StaticResource MaterialDesignFilterChipAccentCheckBox}" IsChecked="{Binding MarketTimerEnabled}" Content="{Tr MainWindow.AppBar.MarketTimerHint}"/>
|
||||||
|
<TextBox w:FontScaleWindow.Scale="0.8" w:FontScaleWindow.ResizeFont="True" md:HintAssist.Hint="{Tr Common.Abbreviations.Time.Seconds}" Margin="10,0,0,0" MinWidth="30" Style="{StaticResource MaterialDesignFloatingHintTextBox}" Text="{Binding TimerCheckSeconds, UpdateSourceTrigger=PropertyChanged, Delay=700}" PreviewTextInput="UIElement_OnPreviewTextInput" />
|
||||||
|
</ToolBar>
|
||||||
|
</ToolBarTray>
|
||||||
|
<Grid Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="0.4*" />
|
||||||
|
<ColumnDefinition Width="0.6*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ListBox w:FontScaleWindow.Scale="0.8" w:FontScaleWindow.ResizeFont="True" Margin="10,15,10,15" DisplayMemberPath="AccountName" ItemsSource="{Binding MaFiles}" SelectedValue="{Binding SelectedMafile}">
|
||||||
|
<FrameworkElement.ContextMenu>
|
||||||
|
<ContextMenu>
|
||||||
|
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AddToGroup}" ItemsSource="{Binding Groups}">
|
||||||
|
<ItemsControl.ItemContainerStyle>
|
||||||
|
<Style BasedOn="{StaticResource MaterialDesignMenuItem}" TargetType="{x:Type MenuItem}">
|
||||||
|
<Setter Property="MenuItem.Command" Value="{Binding DataContext.AddToGroupCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
|
<Setter Property="MenuItem.CommandParameter">
|
||||||
|
<Setter.Value>
|
||||||
|
<MultiBinding Converter="{StaticResource MultiCommandParameterConverter}">
|
||||||
|
<Binding />
|
||||||
|
<Binding Path="PlacementTarget.SelectedValue" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ItemsControl.ItemContainerStyle>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}" Command="{Binding Path=RemoveGroupCommand}" CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
|
||||||
|
</ContextMenu>
|
||||||
|
</FrameworkElement.ContextMenu>
|
||||||
|
</ListBox>
|
||||||
|
<TextBox Style="{StaticResource MaterialDesignFloatingHintTextBox}" md:TextFieldAssist.HasClearButton="True" w:FontScaleWindow.Scale="0.7" w:FontScaleWindow.ResizeFont="True" Grid.Row="1" Margin="10" md:HintAssist.Hint="{Tr MainWindow.LeftPart.SearchBoxHint}" Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
</Grid>
|
||||||
|
<md:Card Grid.Column="1" Margin="10,10,15,10" UniformCornerRadius="15">
|
||||||
|
<Control.Background>
|
||||||
|
<SolidColorBrush Color="#FF1A1D25" Opacity="0.6" />
|
||||||
|
</Control.Background>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid Row="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBox w:FontScaleWindow.Scale="1.1" w:FontScaleWindow.ResizeFont="True" IsReadOnly="True" HorizontalContentAlignment="Center" Background="#00FFFFFF" Style="{StaticResource MaterialDesignFilledTextBox}" Text="{Binding Code, FallbackValue=Code}" PreviewMouseDown="SteamGuard_DoubleClick" />
|
||||||
|
<Grid Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ProgressBar Margin="5" Foreground="#FF9932CC" Height="15" md:TransitionAssist.DisableTransitions="True" Value="{Binding CodeProgress}" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Button w:FontScaleWindow.Scale="0.7" w:FontScaleWindow.ResizeFont="True" Grid.Row="1" Style="{StaticResource MaterialDesignOutlinedButton}" Margin="10" Command="{Binding GetConfirmationsCommand}" Width="{Binding Path=ActualWidth, Converter={StaticResource CoefficientConverter}, ConverterParameter=0.6, RelativeSource={RelativeSource AncestorType=md:Card}}">
|
||||||
|
<TextBlock TextWrapping="Wrap" TextTrimming="WordEllipsis" Text="{Tr MainWindow.RightPart.LoadConfirmations}"/>
|
||||||
|
</Button>
|
||||||
|
<ListBox md:RippleAssist.IsDisabled="True" Focusable="False" Grid.Row="2" w:FontScaleWindow.Scale="0.72" w:FontScaleWindow.ResizeFont="True" Margin="10" Visibility="{Binding ConfirmationsVisible, Converter={StaticResource BooleanToVisibilityConverter}}" ItemsSource="{Binding Confirmations}" SelectionChanged="Selector_OnSelectionChanged">
|
||||||
|
<ItemsControl.ItemContainerStyle>
|
||||||
|
<Style BasedOn="{StaticResource MaterialDesignListBoxItem}" TargetType="{x:Type ListBoxItem}">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
</Style>
|
||||||
|
</ItemsControl.ItemContainerStyle>
|
||||||
|
</ListBox>
|
||||||
|
<Button Grid.Row="3" Margin="1,0,1,3" w:FontScaleWindow.Scale="0.8" w:FontScaleWindow.ResizeFont="True" Command="{Binding ConfirmLoginCommand}" Content="{Tr MainWindow.RightPart.ConfirmLogin}"/>
|
||||||
|
</Grid>
|
||||||
|
</md:Card>
|
||||||
|
</Grid>
|
||||||
|
<Border Grid.Row="2" BorderBrush="#FF696969">
|
||||||
|
<Border.Background>
|
||||||
|
<SolidColorBrush Opacity="0.5" Color="DimGray" />
|
||||||
|
</Border.Background>
|
||||||
|
<Grid>
|
||||||
|
<ToolBarPanel Orientation="Horizontal" Margin="5" w:FontScaleWindow.Scale="0.73" w:FontScaleWindow.ResizeFont="True">
|
||||||
|
<TextBlock FontWeight="Bold" Text="{Tr MainWindow.Footer.Account}" />
|
||||||
|
<TextBlock Text="{Binding SelectedMafile.AccountName}" />
|
||||||
|
<TextBlock Text="|" Margin="10,0,10,0" />
|
||||||
|
<TextBlock FontWeight="Bold" Text="{Tr MainWindow.Footer.Group}" />
|
||||||
|
<TextBlock FontWeight="Normal" Text="{Binding SelectedMafile.Group, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
|
||||||
|
</ToolBarPanel>
|
||||||
|
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" w:FontScaleWindow.Scale="0.7" w:FontScaleWindow.ResizeFont="True" Margin="0,0,10,0">
|
||||||
|
<Hyperlink NavigateUri="https://zelenka.guru/achies/" Foreground="{DynamicResource PrimaryHueMidBrush}" RequestNavigate="Hyperlink_OnRequestNavigate">by achies</Hyperlink>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<md:Snackbar Grid.Row="1" FontSize="20" MessageQueue="{Binding MessageQueue}" />
|
||||||
|
</Grid>
|
||||||
|
</md:DialogHost>
|
||||||
|
</Border>
|
||||||
|
</w:FontScaleWindow>
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
using MaterialDesignThemes.Wpf;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.View.Dialogs;
|
||||||
|
using NebulaAuth.ViewModel;
|
||||||
|
using NebulaAuth.ViewModel.Other;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace NebulaAuth;
|
||||||
|
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
|
||||||
|
private static string CodeCopiedString => LocManager.GetOrDefault("CodeCopied", "MainWindow", "CodeCopied");
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
base.DataContext = new MainVM();
|
||||||
|
Application.Current.MainWindow = this;
|
||||||
|
TrayManager.InitializeTray();
|
||||||
|
ThemeManager.InitializeTheme();
|
||||||
|
base.Title = base.Title + " " + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3);
|
||||||
|
this.Loaded += OnApplicationStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnApplicationStarted(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (Settings.Instance.IsPasswordSet == false) return;
|
||||||
|
await Dispatcher.BeginInvoke(ShowSetPasswordDialog, DispatcherPriority.ContextIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowSetPasswordDialog()
|
||||||
|
{
|
||||||
|
var vm = new SetEncryptPasswordVM();
|
||||||
|
var dialog = new SetCryptPasswordDialog()
|
||||||
|
{
|
||||||
|
DataContext = vm
|
||||||
|
};
|
||||||
|
var result = await DialogHost.Show(dialog);
|
||||||
|
var pass = vm.Password;
|
||||||
|
if (result is true && string.IsNullOrWhiteSpace(pass) == false)
|
||||||
|
{
|
||||||
|
PHandler.SetPassword(pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var lb = (ListBox)sender;
|
||||||
|
lb.SelectedValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SteamGuard_DoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
var tb = (TextBox)sender;
|
||||||
|
if (tb.Text == CodeCopiedString) return;
|
||||||
|
var code = tb.Text;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Clipboard.SetText(code);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Error(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tb.Text = CodeCopiedString;
|
||||||
|
await Task.Delay(200);
|
||||||
|
if (tb.Text == CodeCopiedString)
|
||||||
|
{
|
||||||
|
tb.Text = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region Dran'n'Drop
|
||||||
|
private void UIElement_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
|
||||||
|
{
|
||||||
|
if (int.TryParse(e.Text, out _) == false) e.Handled = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rectangle_DragEnter(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DragNDropPanel.Visibility = Visibility.Visible;
|
||||||
|
DragNDropOverlay.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Rectangle_Drop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
|
||||||
|
string[] filePaths = (string[])e.Data.GetData(DataFormats.FileDrop)!;
|
||||||
|
if (filePaths.Length == 0) return;
|
||||||
|
if (this.DataContext is MainVM mainVm)
|
||||||
|
{
|
||||||
|
await mainVm.AddMafile(filePaths);
|
||||||
|
}
|
||||||
|
DragNDropPanel.Visibility = Visibility.Hidden;
|
||||||
|
DragNDropOverlay.Visibility = Visibility.Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rectangle_DragLeave(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
DragNDropPanel.Visibility = Visibility.Hidden;
|
||||||
|
DragNDropOverlay.Visibility = Visibility.Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void DialogHost_DialogOpened(object sender, DialogOpenedEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
DragNDropBorder.AllowDrop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DialogHost_DialogClosed(object sender, DialogClosedEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
DragNDropBorder.AllowDrop = true;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(e.Uri.ToString())
|
||||||
|
{
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model.Comparers;
|
||||||
|
|
||||||
|
public class ProxyComparer : IEqualityComparer<MaProxy>
|
||||||
|
{
|
||||||
|
public bool Equals(MaProxy? x, MaProxy? y)
|
||||||
|
{
|
||||||
|
return Equal(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Equal(MaProxy? x, MaProxy? y)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(x, y)) return true;
|
||||||
|
if (ReferenceEquals(x, null)) return false;
|
||||||
|
if (ReferenceEquals(y, null)) return false;
|
||||||
|
return x.Id == y.Id && x.Data.Equals(y.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(MaProxy obj)
|
||||||
|
{
|
||||||
|
return HashCode.Combine(obj.Id, obj.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AchiesUtilities.Web.Proxy;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model.Comparers;
|
||||||
|
|
||||||
|
public class ProxyDataComparer : IEqualityComparer<ProxyData>
|
||||||
|
{
|
||||||
|
public bool Equals(ProxyData? x, ProxyData? y)
|
||||||
|
{
|
||||||
|
return Equal(x, y);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Equal(ProxyData? x, ProxyData? y)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(x, y)) return true;
|
||||||
|
if (ReferenceEquals(x, null)) return false;
|
||||||
|
if (ReferenceEquals(y, null)) return false;
|
||||||
|
return x.Address == y.Address
|
||||||
|
&& x.Port == y.Port
|
||||||
|
&& x.Username == y.Username
|
||||||
|
&& x.Password == y.Password;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(ProxyData obj)
|
||||||
|
{
|
||||||
|
return HashCode.Combine(obj.Address, obj.Port, obj.Username, obj.Password);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model.Entities;
|
||||||
|
|
||||||
|
public class LoginConfirmationResult
|
||||||
|
{
|
||||||
|
[MemberNotNullWhen(false, nameof(Error))]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string IP { get; set; }
|
||||||
|
public string Country { get; set; }
|
||||||
|
public LoginConfirmationError? Error { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum LoginConfirmationError
|
||||||
|
{
|
||||||
|
NoRequests,
|
||||||
|
MoreThanOneRequest
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using AchiesUtilities.Web.Proxy;
|
||||||
|
using NebulaAuth.Model.Comparers;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model.Entities;
|
||||||
|
|
||||||
|
public class MaProxy
|
||||||
|
{
|
||||||
|
public int Id { get; }
|
||||||
|
public ProxyData Data { get; }
|
||||||
|
|
||||||
|
public MaProxy(int id, ProxyData data)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return obj is MaProxy p && ProxyComparer.Equal(this, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool Equals(MaProxy other)
|
||||||
|
{
|
||||||
|
return Id == other.Id && Data.Equals(other.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(Id, Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using SteamLib;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model.Entities;
|
||||||
|
|
||||||
|
public class Mafile : MobileDataExtended
|
||||||
|
{
|
||||||
|
public MaProxy? Proxy { get; set; }
|
||||||
|
public string? Group { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
public static Mafile FromMobileDataExtended(MobileDataExtended data)
|
||||||
|
{
|
||||||
|
return new Mafile
|
||||||
|
{
|
||||||
|
AccountName = data.AccountName,
|
||||||
|
DeviceId = data.DeviceId,
|
||||||
|
IdentitySecret = data.IdentitySecret,
|
||||||
|
RevocationCode = data.RevocationCode,
|
||||||
|
Secret1 = data.Secret1,
|
||||||
|
SerialNumber = data.SerialNumber,
|
||||||
|
SessionData = data.SessionData,
|
||||||
|
SharedSecret = data.SharedSecret,
|
||||||
|
ServerTime = data.ServerTime,
|
||||||
|
TokenGid = data.TokenGid,
|
||||||
|
Uri = data.Uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Mafile FromMobileDataExtended(MobileDataExtended data, MaProxy? proxy, string? group, string? password)
|
||||||
|
{
|
||||||
|
var result = FromMobileDataExtended(data);
|
||||||
|
result.Proxy = proxy;
|
||||||
|
result.Group = group;
|
||||||
|
result.Password = password;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using SteamLib.SteamMobile.Confirmations;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model.Entities;
|
||||||
|
|
||||||
|
public class MarketMultiConfirmation : Confirmation
|
||||||
|
{
|
||||||
|
public ObservableCollection<MarketConfirmation> Confirmations { get; }
|
||||||
|
public MarketMultiConfirmation(IEnumerable<MarketConfirmation> confirmations) : base(0, 0, 0, 0, ConfirmationType.Unknown, "")
|
||||||
|
{
|
||||||
|
Confirmations = new(confirmations);
|
||||||
|
Time = Confirmations.FirstOrDefault()?.Time ?? default;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model.Exceptions;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class CantAlignTimeException : Exception
|
||||||
|
{
|
||||||
|
public CantAlignTimeException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CantAlignTimeException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CantAlignTimeException(string message, Exception inner) : base(message, inner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CantAlignTimeException(
|
||||||
|
SerializationInfo info,
|
||||||
|
StreamingContext context) : base(info, context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
using AchiesUtilities.Web.Proxy;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using SteamLib.Account;
|
||||||
|
using SteamLib.Api.Mobile;
|
||||||
|
using SteamLib.Authentication;
|
||||||
|
using SteamLib.Authentication.LoginV2;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.ProtoCore;
|
||||||
|
using SteamLib.ProtoCore.Services;
|
||||||
|
using SteamLib.SteamMobile;
|
||||||
|
using SteamLib.SteamMobile.Confirmations;
|
||||||
|
using SteamLib.Web;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SteamLib.Api;
|
||||||
|
using SteamLib.Core.Enums;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
|
||||||
|
public static class MaClient
|
||||||
|
{
|
||||||
|
private static HttpClientHandler ClientHandler { get; }
|
||||||
|
|
||||||
|
private static HttpClient Client { get; }
|
||||||
|
|
||||||
|
private static DynamicProxy Proxy { get; }
|
||||||
|
|
||||||
|
public static ProxyData? DefaultProxy { get; set; }
|
||||||
|
|
||||||
|
static MaClient()
|
||||||
|
{
|
||||||
|
Proxy = new DynamicProxy(null);
|
||||||
|
var pair = ClientBuilder.BuildMobileClient(Proxy, null);
|
||||||
|
Client = pair.Client;
|
||||||
|
ClientHandler = pair.Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearCookies()
|
||||||
|
{
|
||||||
|
foreach (Cookie allCookie in ClientHandler.CookieContainer.GetAllCookies())
|
||||||
|
{
|
||||||
|
allCookie.Expired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetAccount(Mafile? account)
|
||||||
|
{
|
||||||
|
ClearCookies();
|
||||||
|
if (account != null)
|
||||||
|
{
|
||||||
|
if (account.SessionData != null)
|
||||||
|
{
|
||||||
|
ClientHandler.CookieContainer.SetSteamMobileCookies(account.SessionData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClientHandler.CookieContainer.ClearSteamCookies();
|
||||||
|
ClientHandler.CookieContainer.AddMinimalMobileCookies();
|
||||||
|
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
|
||||||
|
}
|
||||||
|
Proxy.SetData(account.Proxy?.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<IEnumerable<Confirmation>> GetConfirmations(Mafile mafile)
|
||||||
|
{
|
||||||
|
ValidateMafile(mafile);
|
||||||
|
SetProxy(mafile);
|
||||||
|
return SteamMobileConfirmationsApi.GetConfirmations(Client, mafile, mafile.SessionData!.SteamId.Steam64);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task LoginAgain(Mafile mafile, string password, bool savePassword, ICaptchaResolver? resolver)
|
||||||
|
{
|
||||||
|
SetProxy(mafile);
|
||||||
|
var sgGenerator = new SteamGuardCodeGenerator(mafile.SharedSecret);
|
||||||
|
var options = new LoginV2ExecutorOptions(LoginV2Executor.NullConsumer, Client)
|
||||||
|
{
|
||||||
|
Logger = Shell.ExtensionsLogger,
|
||||||
|
SteamGuardProvider = sgGenerator,
|
||||||
|
DeviceDetails = LoginV2ExecutorOptions.GetMobileDefaultDevice(),
|
||||||
|
WebsiteId = "Mobile"
|
||||||
|
};
|
||||||
|
ClientHandler.CookieContainer.ClearMobileSessionCookies();
|
||||||
|
var result = await LoginV2Executor.DoLogin(options, mafile.AccountName, password);
|
||||||
|
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
|
||||||
|
mafile.SessionData = (MobileSessionData)result;
|
||||||
|
if(PHandler.IsPasswordSet)
|
||||||
|
mafile.Password = (savePassword ? PHandler.Encrypt(password) : null);
|
||||||
|
Storage.UpdateMafile(mafile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RefreshSession(Mafile mafile)
|
||||||
|
{
|
||||||
|
ValidateMafile(mafile, true);
|
||||||
|
SetProxy(mafile);
|
||||||
|
var token = mafile.SessionData!.GetMobileToken();
|
||||||
|
if (token == null || token.Value.IsExpired)
|
||||||
|
{
|
||||||
|
var sessionToken = await SteamMobileApi.RefreshJwt(Client, mafile.SessionData!.RefreshToken.Token, mafile.SessionData.SteamId.Steam64);
|
||||||
|
var newToken = SteamTokenHelper.Parse(sessionToken);
|
||||||
|
mafile.SessionData.SetMobileToken(newToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var communityToken = mafile.SessionData!.GetToken(SteamDomain.Community);
|
||||||
|
if (communityToken == null || communityToken.Value.IsExpired)
|
||||||
|
{
|
||||||
|
var communityTokenString = await SteamGlobalApi.RefreshJwt(Client, SteamDomain.Community);
|
||||||
|
var newToken = SteamTokenHelper.Parse(communityTokenString);
|
||||||
|
mafile.SessionData.SetToken(SteamDomain.Community, newToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.UpdateMafile(mafile);
|
||||||
|
ClientHandler.CookieContainer.SetSteamMobileCookies(mafile.SessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<bool> SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
|
||||||
|
{
|
||||||
|
ValidateMafile(mafile);
|
||||||
|
SetProxy(mafile);
|
||||||
|
return SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId.Steam64, mafile, confirm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations, bool confirm)
|
||||||
|
{
|
||||||
|
var enumerable = confirmations.ToList();
|
||||||
|
if (!enumerable.Any())
|
||||||
|
{
|
||||||
|
return Task.FromResult(result: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateMafile(mafile);
|
||||||
|
SetProxy(mafile);
|
||||||
|
return SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, enumerable, mafile.SessionData!.SteamId.Steam64, mafile, confirm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<RemoveAuthenticator_Response> RemoveAuthenticator(Mafile mafile)
|
||||||
|
{
|
||||||
|
ValidateMafile(mafile);
|
||||||
|
SetProxy(mafile);
|
||||||
|
if (mafile.RevocationCode == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("This mafile does not have R-Code");
|
||||||
|
}
|
||||||
|
var token = mafile.SessionData!.GetMobileToken()!;
|
||||||
|
return SteamMobileApi.RemoveAuthenticator(Client, token.Value.Token, mafile.RevocationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetProxy(Mafile mafile)
|
||||||
|
{
|
||||||
|
Proxy.SetData(mafile.Proxy?.Data ?? DefaultProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateMafile(Mafile mafile, bool ignoreAccessToken = false)
|
||||||
|
{
|
||||||
|
if (mafile.SessionData == null) throw new SessionInvalidException();
|
||||||
|
if (mafile.SessionData.RefreshToken.IsExpired)
|
||||||
|
throw new SessionExpiredException();
|
||||||
|
|
||||||
|
if (ignoreAccessToken == false)
|
||||||
|
{
|
||||||
|
var access = mafile.SessionData.GetMobileToken();
|
||||||
|
if (access == null || access.Value.IsExpired)
|
||||||
|
throw new SessionExpiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<LoginConfirmationResult> ConfirmLoginRequest(Mafile mafile)
|
||||||
|
{
|
||||||
|
if (mafile.SessionData == null)
|
||||||
|
{
|
||||||
|
throw new SessionExpiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateMafile(mafile);
|
||||||
|
var token = mafile.SessionData.GetMobileToken()!.Value;
|
||||||
|
SetProxy(mafile);
|
||||||
|
|
||||||
|
var uri = "https://api.steampowered.com/IAuthenticationService/GetAuthSessionsForAccount/v1?access_token=" + token.Token;
|
||||||
|
GetAuthSessionsForAccount_Response getsess;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getsess = await Client.GetProto<GetAuthSessionsForAccount_Response>(uri, new EmptyMessage());
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
when (ex.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new SessionExpiredException(string.Empty, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getsess.ClientIds.Count == 0)
|
||||||
|
{
|
||||||
|
return new LoginConfirmationResult
|
||||||
|
{
|
||||||
|
Error = LoginConfirmationError.NoRequests
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (getsess.ClientIds.Count > 1)
|
||||||
|
{
|
||||||
|
return new LoginConfirmationResult
|
||||||
|
{
|
||||||
|
Error = LoginConfirmationError.MoreThanOneRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var clientId = getsess.ClientIds.Single();
|
||||||
|
var infoUri = "https://api.steampowered.com/IAuthenticationService/GetAuthSessionInfo/v1?access_token=" + token.Token;
|
||||||
|
var infoReq = new GetAuthSessionInfo_Request
|
||||||
|
{
|
||||||
|
ClientId = clientId
|
||||||
|
};
|
||||||
|
var infoResp = await Client.PostProto<GetAuthSessionInfo_Response>(infoUri, infoReq);
|
||||||
|
var updateUri = "https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithMobileConfirmation/v1?access_token=" + token.Token;
|
||||||
|
var updateReq = new UpdateAuthSessionWithMobileConfirmation_Request
|
||||||
|
{
|
||||||
|
ClientId = clientId,
|
||||||
|
Confirm = true,
|
||||||
|
Persistence = 1,
|
||||||
|
Steamid = mafile.SessionData.SteamId.Steam64.ToUlong(),
|
||||||
|
Version = 1
|
||||||
|
};
|
||||||
|
updateReq.ComputeSignature(mafile.SharedSecret);
|
||||||
|
await Client.PostProtoEnsureSuccess(updateUri, updateReq);
|
||||||
|
return new LoginConfirmationResult
|
||||||
|
{
|
||||||
|
Country = infoResp.Country,
|
||||||
|
IP = infoResp.IP,
|
||||||
|
Success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
public static class PHandler //RETHINK: Use SecureString?
|
||||||
|
{
|
||||||
|
public static bool IsPasswordSet => _k.Length > 0;
|
||||||
|
private static byte[] _k = Array.Empty<byte>();
|
||||||
|
|
||||||
|
|
||||||
|
public static void SetPassword(string? password)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
|
{
|
||||||
|
_k = Array.Empty<byte>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyBytes = Encoding.UTF8.GetBytes(password);
|
||||||
|
_k = SHA256.HashData(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Encrypt(string plainText)
|
||||||
|
{
|
||||||
|
if (_k.Length == 0) throw new Exception("Password not set");
|
||||||
|
byte[] encryptedBytes;
|
||||||
|
|
||||||
|
using (var aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = _k;
|
||||||
|
aes.IV = new byte[16];
|
||||||
|
|
||||||
|
var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||||
|
|
||||||
|
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
|
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
|
||||||
|
{
|
||||||
|
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
|
||||||
|
cryptoStream.FlushFinalBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedBytes = memoryStream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert.ToBase64String(encryptedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Decrypt(string encryptedText)
|
||||||
|
{
|
||||||
|
if (_k.Length == 0) throw new Exception("Password not set");
|
||||||
|
var encryptedBytes = Convert.FromBase64String(encryptedText);
|
||||||
|
string decryptedText = null;
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
var keyBytes = _k;
|
||||||
|
var iv = new byte[aes.IV.Length];
|
||||||
|
|
||||||
|
aes.Key = keyBytes;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor(aes.Key, iv);
|
||||||
|
using var ms = new MemoryStream(encryptedBytes);
|
||||||
|
using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read);
|
||||||
|
using var reader = new StreamReader(cs);
|
||||||
|
decryptedText = reader.ReadToEnd();
|
||||||
|
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using AchiesUtilities.Collections;
|
||||||
|
using AchiesUtilities.Web.Proxy;
|
||||||
|
using System.Linq;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
public static class ProxyStorage
|
||||||
|
{
|
||||||
|
public const string FORMAT = ADDRESS_FORMAT + ":{USER}:{PASS}";
|
||||||
|
public const string ADDRESS_FORMAT = "{IP}:{PORT}";
|
||||||
|
public static ObservableDictionary<int, ProxyData> Proxies { get; } = new();
|
||||||
|
|
||||||
|
|
||||||
|
static ProxyStorage()
|
||||||
|
{
|
||||||
|
|
||||||
|
if(File.Exists("proxies.json") == false)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText("proxies.json");
|
||||||
|
var proxies = JsonConvert.DeserializeObject<Proxies>(json) ?? throw new NullReferenceException();
|
||||||
|
Proxies = proxies.ProxiesData;
|
||||||
|
Proxies = new ObservableDictionary<int, ProxyData>(
|
||||||
|
Proxies.OrderBy(p => p.Key)
|
||||||
|
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
|
||||||
|
|
||||||
|
if (proxies.DefaultProxy != null)
|
||||||
|
{
|
||||||
|
MaClient.DefaultProxy = Proxies[proxies.DefaultProxy.Value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar("Ошибка при загрузке прокси");
|
||||||
|
SnackbarController.SendSnackbar(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetProxy(int? id, ProxyData proxyData)
|
||||||
|
{
|
||||||
|
if (id == null)
|
||||||
|
{
|
||||||
|
if (Proxies.Any() == false)
|
||||||
|
{
|
||||||
|
id = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
id = Proxies.Keys.Max() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Proxies[id] = proxyData;
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
public static void RemoveProxy(int id)
|
||||||
|
{
|
||||||
|
Proxies.Remove(id);
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
public static bool CompareProxy(ProxyData proxyData1, ProxyData proxyData2)
|
||||||
|
{
|
||||||
|
return proxyData1.ToString(FORMAT) == proxyData2.ToString(FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void Save()
|
||||||
|
{
|
||||||
|
var p = Create();
|
||||||
|
var json = JsonConvert.SerializeObject(p, Formatting.Indented);
|
||||||
|
File.WriteAllText("proxies.json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Proxies Create()
|
||||||
|
{
|
||||||
|
int? def = null;
|
||||||
|
if (MaClient.DefaultProxy != null)
|
||||||
|
{
|
||||||
|
var search = Proxies.FirstOrDefault(p => p.Value.Equals(MaClient.DefaultProxy));
|
||||||
|
if (search.Value != null!)
|
||||||
|
{
|
||||||
|
def = search.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Proxies
|
||||||
|
{
|
||||||
|
ProxiesData = Proxies,
|
||||||
|
DefaultProxy = def
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Proxies
|
||||||
|
{
|
||||||
|
public ObservableDictionary<int, ProxyData> ProxiesData { get; set; }
|
||||||
|
public int? DefaultProxy { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
public static class SessionHandler
|
||||||
|
{
|
||||||
|
public static event EventHandler? LoginStarted;
|
||||||
|
public static event EventHandler? LoginCompleted;
|
||||||
|
|
||||||
|
public static async Task<T> Handle<T>(Func<Task<T>> func, Mafile mafile)
|
||||||
|
{
|
||||||
|
string? password = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(mafile.Password))
|
||||||
|
{
|
||||||
|
password = PHandler.Decrypt(mafile.Password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
var refreshed = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await func();
|
||||||
|
}
|
||||||
|
catch (SessionExpiredException) when (mafile.SessionData is not { RefreshToken.IsExpired: true})
|
||||||
|
{
|
||||||
|
refreshed = await TryRefresh(mafile);
|
||||||
|
}
|
||||||
|
catch (SessionInvalidException)
|
||||||
|
when (password != null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (refreshed)
|
||||||
|
{
|
||||||
|
Shell.Logger.Debug("Token on mafile {name} {steamid} refreshed", mafile.AccountName,
|
||||||
|
mafile.SessionData?.SteamId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await func();
|
||||||
|
}
|
||||||
|
catch (Exception ex3)
|
||||||
|
when (password != null && ex3 is SessionExpiredException or SessionInvalidException)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (password == null)
|
||||||
|
{
|
||||||
|
throw new SessionInvalidException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LoginStarted?.Invoke(null, EventArgs.Empty);
|
||||||
|
await MaClient.LoginAgain(mafile, password, savePassword: true, null);
|
||||||
|
Shell.Logger.Debug("Mafile {name} {steamid} succesfully auto-relogined", mafile.AccountName,
|
||||||
|
mafile.SessionData?.SteamId);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LoginCompleted?.Invoke(null, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await func();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> TryRefresh(Mafile mafile)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MaClient.RefreshSession(mafile);
|
||||||
|
SnackbarController.SendSnackbar("Сессия была обновлена автоматически");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (SessionInvalidException)
|
||||||
|
{
|
||||||
|
Shell.Logger.Debug("Token on mafile {name} {steamid} not refreshed", mafile.AccountName, mafile.SessionData?.SteamId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using NebulaAuth.Core;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
public partial class Settings : ObservableObject
|
||||||
|
{
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _disableTimersOnChange = true;
|
||||||
|
[ObservableProperty] private BackgroundMode _backgroundMode = BackgroundMode.Default;
|
||||||
|
[ObservableProperty] private bool _hideToTray;
|
||||||
|
[ObservableProperty] private int _timerSeconds = 60;
|
||||||
|
[ObservableProperty] private Color? _backgroundColor;
|
||||||
|
[ObservableProperty] private Color? _iconColor;
|
||||||
|
[ObservableProperty] private bool _isPasswordSet;
|
||||||
|
[ObservableProperty] private LocalizationLanguage _language = LocalizationLanguage.English;
|
||||||
|
[ObservableProperty] private bool _legacyMode = true;
|
||||||
|
[ObservableProperty] private bool _allowAutoUpdate;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public static Settings Instance { get; }
|
||||||
|
static Settings()
|
||||||
|
{
|
||||||
|
if (File.Exists("settings.json") == false)
|
||||||
|
{
|
||||||
|
Instance = new();
|
||||||
|
Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText("settings.json");
|
||||||
|
var settings = JsonConvert.DeserializeObject<Settings>(json) ?? throw new NullReferenceException();
|
||||||
|
Instance = settings;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar("Ошибка при загрузке настроек. Настройки были сброшены");
|
||||||
|
SnackbarController.SendSnackbar(ex.Message);
|
||||||
|
Instance = new();
|
||||||
|
}
|
||||||
|
Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SettingsOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save()
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(Instance, Formatting.Indented);
|
||||||
|
File.WriteAllText("settings.json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum BackgroundMode
|
||||||
|
{
|
||||||
|
Default, Custom, Color
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using NebulaAuth.Model.Exceptions;
|
||||||
|
using NLog;
|
||||||
|
using SteamLib.Core;
|
||||||
|
using SteamLib.SteamMobile;
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
public static class Shell
|
||||||
|
{
|
||||||
|
public static Logger Logger { get; } = LogManager.GetLogger("Logger");
|
||||||
|
public static ILogger ExtensionsLogger { get; private set; } = null!;
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
|
||||||
|
var lp = new NLog.Extensions.Logging.NLogLoggerProvider();
|
||||||
|
var logger = lp.CreateLogger("SteamLib");
|
||||||
|
HealthMonitor.FatalLogger = logger;
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException;
|
||||||
|
|
||||||
|
var loggerFactory = new NLog.Extensions.Logging.NLogLoggerFactory();
|
||||||
|
ExtensionsLogger = loggerFactory.CreateLogger("Logger");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TimeAligner.AlignTime();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new CantAlignTimeException("", ex);
|
||||||
|
}
|
||||||
|
ExtensionsLogger.LogDebug("Application started");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.Fatal((Exception)e.ExceptionObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using SteamLib;
|
||||||
|
using SteamLib.SteamMobile;
|
||||||
|
using SteamLib.Utility.MaFiles;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
public static class Storage
|
||||||
|
{
|
||||||
|
public const string MAFILE_F = "maFiles";
|
||||||
|
public const string REMOVED_F = "maFiles_removed";
|
||||||
|
|
||||||
|
public static string MafileFolder { get; } = Path.GetFullPath(MAFILE_F);
|
||||||
|
public static string RemovedMafileFolder { get; } = Path.GetFullPath(REMOVED_F);
|
||||||
|
|
||||||
|
public static ObservableCollection<Mafile> MaFiles { get; } = new();
|
||||||
|
|
||||||
|
static Storage()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(MafileFolder) == false)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(MafileFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(RemovedMafileFolder) == false)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(RemovedMafileFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(MafileFolder);
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (Path.GetExtension(file).Equals(".mafile", StringComparison.InvariantCultureIgnoreCase) == false) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = ReadMafile(file);
|
||||||
|
MaFiles.Add(data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Error(ex, "Can't load mafile {file}", Path.GetFileName(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaFiles = new ObservableCollection<Mafile>(MaFiles.OrderBy(m => m.AccountName));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void AddNewMafile(string path, bool overwrite)
|
||||||
|
{
|
||||||
|
Mafile data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = ReadMafile(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Warn(ex, "Can't load mafile");
|
||||||
|
throw new FormatException("File data is not valid", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(data.AccountName))
|
||||||
|
throw new FormatException("File data is not valid. Missing AccountName");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var code = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new FormatException("Can't generate code on this mafile", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (data.SessionData == null)
|
||||||
|
throw new SessionInvalidException("File data is not valid. Missing SessionData")
|
||||||
|
{
|
||||||
|
Data = { { "mafile", data } }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (overwrite == false && File.Exists(GetMafilePath(data)))
|
||||||
|
{
|
||||||
|
throw new IOException("File already exist and overwrite is False");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SaveMafile(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Mafile ReadMafile(string path)
|
||||||
|
{
|
||||||
|
var str = File.ReadAllText(path);
|
||||||
|
var mafile = MafileSerializer.Deserialize(str, out var mafileData);
|
||||||
|
if (mafileData.IsExtended == false)
|
||||||
|
throw new FormatException("Mafile is not extended data");
|
||||||
|
|
||||||
|
|
||||||
|
var props = mafileData.UnusedProperties ?? new Dictionary<string, JProperty>();
|
||||||
|
var proxy = GetPropertyValue<MaProxy>("Proxy", props);
|
||||||
|
var group = GetPropertyValue<string>("Group", props);
|
||||||
|
var password = GetPropertyValue<string>("Password", props);
|
||||||
|
return Mafile.FromMobileDataExtended((MobileDataExtended)mafile, proxy, group, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SerializeMafile(Mafile data)
|
||||||
|
{
|
||||||
|
if (Settings.Instance.LegacyMode)
|
||||||
|
{
|
||||||
|
var props = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
{nameof(Mafile.Proxy), data.Proxy},
|
||||||
|
{nameof(Mafile.Group), data.Group},
|
||||||
|
{nameof(Mafile.Password), data.Password}
|
||||||
|
};
|
||||||
|
return MafileSerializer.SerializeLegacy(data, Formatting.Indented, props);
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MafileSerializer.Serialize(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
|
||||||
|
{
|
||||||
|
if (dictionary.TryGetValue(name, out var prop) == false) return default;
|
||||||
|
var value = prop.Value;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return value.ToObject<T>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Warn(ex, "Can't deserialize property {name}", name);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveMafile(Mafile data)
|
||||||
|
{
|
||||||
|
|
||||||
|
var path = GetMafilePath(data);
|
||||||
|
var str = SerializeMafile(data);
|
||||||
|
File.WriteAllText(Path.GetFullPath(path), str);
|
||||||
|
|
||||||
|
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
|
||||||
|
if (existed != null)
|
||||||
|
{
|
||||||
|
var index = MaFiles.IndexOf(existed);
|
||||||
|
MaFiles[index] = data;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MaFiles.Add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UpdateMafile(Mafile data)
|
||||||
|
{
|
||||||
|
var path = GetMafilePath(data);
|
||||||
|
var str = SerializeMafile(data);
|
||||||
|
File.WriteAllText(Path.GetFullPath(path), str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RemoveMafile(Mafile data)
|
||||||
|
{
|
||||||
|
var path = GetMafilePath(data);
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
MaFiles.Remove(data);
|
||||||
|
}
|
||||||
|
public static void MoveToRemoved(Mafile data)
|
||||||
|
{
|
||||||
|
var path = GetMafilePath(data);
|
||||||
|
var copyPath = Path.Combine(REMOVED_F, data.SessionData!.SteamId + ".mafile");
|
||||||
|
var copyPathCompleted = copyPath;
|
||||||
|
var i = 0;
|
||||||
|
while (File.Exists(copyPathCompleted))
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
copyPathCompleted = copyPath + $" ({i})";
|
||||||
|
}
|
||||||
|
File.Copy(path, copyPathCompleted, false);
|
||||||
|
File.Delete(path);
|
||||||
|
MaFiles.Remove(data);
|
||||||
|
}
|
||||||
|
private static string GetMafilePath(Mafile data)
|
||||||
|
{
|
||||||
|
if (data.SessionData == null)
|
||||||
|
throw new NullReferenceException("SessionData was null can't retrieve SteamId");
|
||||||
|
var fileName = data.SessionData.SteamId + ".mafile";
|
||||||
|
return Path.Combine(MafileFolder, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
throwExceptions="true">
|
||||||
|
|
||||||
|
<targets>
|
||||||
|
<target xsi:type="File" name="File" fileName="log.log"
|
||||||
|
deleteOldFileOnStartup="true"
|
||||||
|
>
|
||||||
|
<layout xsi:type='CompoundLayout'>
|
||||||
|
<layout xsi:type="JsonLayout" includeEventProperties="true" IncludeScopeProperties="true">
|
||||||
|
<attribute name="time" layout="${longdate}" />
|
||||||
|
<attribute name="level" layout="${level:upperCase=true}"/>
|
||||||
|
<attribute name="message" layout="${message}" />
|
||||||
|
<attribute name="exception" layout="${exception:format=@}" encode="false"/>
|
||||||
|
</layout>
|
||||||
|
<layout type='SimpleLayout' text="," />
|
||||||
|
</layout>
|
||||||
|
|
||||||
|
</target>
|
||||||
|
</targets>
|
||||||
|
|
||||||
|
<rules>
|
||||||
|
<logger name="*" minlevel="Trace" writeTo="File" />
|
||||||
|
</rules>
|
||||||
|
</nlog>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<SatelliteResourceLanguages>ru</SatelliteResourceLanguages>
|
||||||
|
<ApplicationIcon>Theme\nebula lock.ico</ApplicationIcon>
|
||||||
|
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
|
||||||
|
<AssemblyVersion>1.4.0</AssemblyVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Theme\Background.jpg" />
|
||||||
|
<None Remove="Theme\nebula lock.ico" />
|
||||||
|
<None Remove="Theme\nebula.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
|
||||||
|
<PackageReference Include="MaterialDesignColors" Version="2.1.4" />
|
||||||
|
<PackageReference Include="MaterialDesignExtensions" Version="3.3.0" />
|
||||||
|
<PackageReference Include="MaterialDesignThemes" Version="4.9.0" />
|
||||||
|
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||||
|
<PackageReference Include="NLog" Version="5.1.2" />
|
||||||
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="Theme\Background.jpg">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Resource>
|
||||||
|
<Resource Include="Theme\nebula lock.ico" />
|
||||||
|
<Resource Include="Theme\nebula.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Model\Utility\" />
|
||||||
|
<Folder Include="Model\Exceptions\" />
|
||||||
|
<Folder Include="Theme\Fonts\Новая папка\" />
|
||||||
|
<Folder Include="ViewModel\Other\" />
|
||||||
|
<Folder Include="Utility\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\SteamLib\SteamLib\SteamLib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="NLog.config">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
|
||||||
|
<ApplicationIcon>Theme\nebula lock.ico</ApplicationIcon>
|
||||||
|
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
|
||||||
|
<AssemblyVersion>1.4.3</AssemblyVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Theme\Background.jpg" />
|
||||||
|
<None Remove="Theme\nebula lock.ico" />
|
||||||
|
<None Remove="Theme\nebula.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Autoupdater.NET.Official" Version="1.8.4" />
|
||||||
|
<PackageReference Include="CodingSeb.Localization.JsonFileLoader" Version="1.3.0" />
|
||||||
|
<PackageReference Include="CodingSeb.Localization.WPF" Version="1.3.0" />
|
||||||
|
<PackageReference Include="CodingSebLocalization.Fody" Version="1.3.0" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||||
|
<PackageReference Include="MaterialDesignColors" Version="2.1.4" />
|
||||||
|
<PackageReference Include="MaterialDesignExtensions" Version="3.3.0" />
|
||||||
|
<PackageReference Include="MaterialDesignThemes" Version="4.9.0" />
|
||||||
|
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.77" />
|
||||||
|
<PackageReference Include="NLog" Version="5.2.8" />
|
||||||
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="Theme\Background.jpg">
|
||||||
|
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||||
|
</Resource>
|
||||||
|
<Resource Include="Theme\nebula lock.ico" />
|
||||||
|
<Resource Include="Theme\nebula.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Model\Exceptions\" />
|
||||||
|
<Folder Include="ViewModel\Other\" />
|
||||||
|
<Folder Include="Utility\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SteamLibForked\SteamLibForked.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="NLog.config">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="localization.loc.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 713 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!--Card-->
|
||||||
|
<SolidColorBrush x:Key="MaterialDesignCardBackground" Color="#26292E"></SolidColorBrush>
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueMidBrush" Color="#FF9056B1"></SolidColorBrush>
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueLightBrush" Color="#FF9C70B5"></SolidColorBrush>
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueDarkForegroundBrush" Color="#FF851BC1"></SolidColorBrush>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Theme;
|
||||||
|
|
||||||
|
public partial class FontScaleWindow : Window
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly Func<double, double, double> _diagonal = (h, w) => Math.Sqrt(h * h + w * w);
|
||||||
|
private double _currentDiagonal;
|
||||||
|
private double _defaultDiagonal = 1;
|
||||||
|
private readonly HashSet<FrameworkElement> _cachedObjects = new();
|
||||||
|
private bool _loaded = false;
|
||||||
|
|
||||||
|
public FontScaleWindow()
|
||||||
|
{
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var w = (FontScaleWindow)sender;
|
||||||
|
w._defaultDiagonal = _diagonal(MinHeight, MinWidth);
|
||||||
|
w.SizeChanged += OnSizeChanged;
|
||||||
|
w.Loaded -= OnLoaded;
|
||||||
|
_loaded = true;
|
||||||
|
w.Width += 1;
|
||||||
|
w.Width -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//<-------------------Window-------------------->
|
||||||
|
public double DefaultFontSize
|
||||||
|
{
|
||||||
|
get => (double)GetValue(DefaultFontSizeProperty);
|
||||||
|
set => SetValue(DefaultFontSizeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ScaleCoefficient
|
||||||
|
{
|
||||||
|
get => (double)GetValue(ScaleCoefficientProperty);
|
||||||
|
set => SetValue(ScaleCoefficientProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using a DependencyProperty as the backing store for ScaleCoefficient. This enables animation, styling, binding, etc...
|
||||||
|
public static readonly DependencyProperty ScaleCoefficientProperty =
|
||||||
|
DependencyProperty.Register("ScaleCoefficient", typeof(double), typeof(FontScaleWindow), new PropertyMetadata(1d));
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly DependencyProperty DefaultFontSizeProperty =
|
||||||
|
DependencyProperty.Register("DefaultFontSize", typeof(double), typeof(FontScaleWindow), new PropertyMetadata(20d));
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly DependencyPropertyKey ParentScaleWindowKey
|
||||||
|
= DependencyProperty.RegisterAttachedReadOnly(
|
||||||
|
"ParentScaleWindow",
|
||||||
|
typeof(FontScaleWindow), typeof(FontScaleWindow),
|
||||||
|
new FrameworkPropertyMetadata(default(FontScaleWindow),
|
||||||
|
FrameworkPropertyMetadataOptions.None));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ParentScaleWindowProperty
|
||||||
|
= ParentScaleWindowKey.DependencyProperty;
|
||||||
|
|
||||||
|
private static FontScaleWindow? GetParentScaleWindow(DependencyObject element) =>
|
||||||
|
(FontScaleWindow)element.GetValue(ParentScaleWindowProperty);
|
||||||
|
private static void SetParentScaleWindow(DependencyObject element, FontScaleWindow value) =>
|
||||||
|
element.SetValue(ParentScaleWindowKey, value);
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ResizeFontProperty = DependencyProperty.RegisterAttached(
|
||||||
|
"ResizeFont", typeof(bool), typeof(FontScaleWindow), new FrameworkPropertyMetadata(false, ResizeCallBack));
|
||||||
|
|
||||||
|
public static void SetResizeFont(DependencyObject element, bool value)
|
||||||
|
=> element.SetValue(ResizeFontProperty, value);
|
||||||
|
public static bool GetResizeFont(DependencyObject element)
|
||||||
|
=> (bool)element.GetValue(ResizeFontProperty);
|
||||||
|
|
||||||
|
private static bool SetWindow(FrameworkElement el)
|
||||||
|
{
|
||||||
|
var w = GetWindow(el);
|
||||||
|
if (w is not FontScaleWindow fsWindow) return false;
|
||||||
|
SetParentScaleWindow(el, fsWindow);
|
||||||
|
if (fsWindow._cachedObjects.Contains(el)) return true;
|
||||||
|
fsWindow._cachedObjects.Add(el);
|
||||||
|
el.Unloaded += ObjOnUnloaded;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ObjOnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var el = (FrameworkElement)sender;
|
||||||
|
if (GetParentScaleWindow(el) == null)
|
||||||
|
{
|
||||||
|
if (!SetWindow(el))
|
||||||
|
{
|
||||||
|
el.Loaded -= ObjOnLoaded;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResizeCallBack(sender, new DependencyPropertyChangedEventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResizeCallBack(object? sender, DependencyPropertyChangedEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (sender is not FrameworkElement obj) return;
|
||||||
|
var window = GetParentScaleWindow(obj);
|
||||||
|
if (window != null && !window._cachedObjects.Contains(obj))
|
||||||
|
{
|
||||||
|
if (!SetWindow(obj)) return;
|
||||||
|
}
|
||||||
|
else if (window == null)
|
||||||
|
{
|
||||||
|
obj.Loaded += ObjOnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window is not { _loaded: true }) return;
|
||||||
|
CalculateFontSizeResized(obj);
|
||||||
|
}
|
||||||
|
private static void ObjOnUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var obj = (FrameworkElement)sender;
|
||||||
|
var w = GetParentScaleWindow(obj)!;
|
||||||
|
w._cachedObjects.Remove(obj);
|
||||||
|
obj.Unloaded -= ObjOnUnloaded;
|
||||||
|
obj.Loaded -= ObjOnLoaded;
|
||||||
|
}
|
||||||
|
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_currentDiagonal = _diagonal(e.NewSize.Width, e.NewSize.Height);
|
||||||
|
foreach (var cached in _cachedObjects)
|
||||||
|
{
|
||||||
|
CalculateFontSizeResized(cached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//<-------------------Window-------------------->
|
||||||
|
|
||||||
|
|
||||||
|
//<-------------------Scaling-------------------->
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ScaleProperty = DependencyProperty.RegisterAttached(
|
||||||
|
"Scale", typeof(double), typeof(FontScaleWindow), new FrameworkPropertyMetadata(0.9999d ,FrameworkPropertyMetadataOptions.Inherits, ScalePropertyCallback));
|
||||||
|
|
||||||
|
public static void SetScale(DependencyObject element, double value)
|
||||||
|
=> element.SetValue(ScaleProperty, value);
|
||||||
|
public static double GetScale(DependencyObject element)
|
||||||
|
=> (double)element.GetValue(ScaleProperty);
|
||||||
|
|
||||||
|
|
||||||
|
private static void ScalePropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var window = GetParentScaleWindow(d);
|
||||||
|
if (window == null)
|
||||||
|
{
|
||||||
|
var w = GetWindow(d);
|
||||||
|
if (w is FontScaleWindow fsWindow)
|
||||||
|
{
|
||||||
|
SetParentScaleWindow(d, fsWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResizeElement(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ResizeElement(DependencyObject d)
|
||||||
|
{
|
||||||
|
var set = d.ReadLocalValue(ScaleProperty) != DependencyProperty.UnsetValue;
|
||||||
|
if (set)
|
||||||
|
{
|
||||||
|
if (GetResizeFont(d))
|
||||||
|
{
|
||||||
|
CalculateFontSizeResized(d);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CalculateFontSize(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static void CalculateFontSize(DependencyObject d)
|
||||||
|
{
|
||||||
|
var window = GetParentScaleWindow(d);
|
||||||
|
if (window == null) return;
|
||||||
|
var fontSize = GetScale(d) * window._defaultDiagonal;
|
||||||
|
d.SetValue(FontSizeProperty, fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CalculateFontSizeResized(DependencyObject d)
|
||||||
|
{
|
||||||
|
var window = GetParentScaleWindow(d);
|
||||||
|
if (window == null) return;
|
||||||
|
var windowsScale = Math.Pow(window._currentDiagonal / window._defaultDiagonal, window.ScaleCoefficient);
|
||||||
|
var elScale = GetScale(d);
|
||||||
|
var fontSize = window.DefaultFontSize * elScale * windowsScale;
|
||||||
|
d.SetValue(FontSizeProperty, fontSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="LolzFucker.Theme.Palette"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="300"
|
||||||
|
d:DesignWidth="300">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="DemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="4"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="1*"/>
|
||||||
|
<RowDefinition Height="1*"/>
|
||||||
|
<RowDefinition Height="1*"/>
|
||||||
|
<RowDefinition Height="1*"/>
|
||||||
|
<RowDefinition Height="1*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="1*"/>
|
||||||
|
<ColumnDefinition Width="1*"/>
|
||||||
|
<ColumnDefinition Width="1*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource PrimaryHueMidBrush}"
|
||||||
|
Grid.ColumnSpan="3">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource PrimaryHueMidForegroundBrush}"
|
||||||
|
Text="Primary - Mid"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource PrimaryHueLightBrush}"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0">
|
||||||
|
<TextBlock
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource PrimaryHueLightForegroundBrush}"
|
||||||
|
Text="Light"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource PrimaryHueMidBrush}"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource PrimaryHueMidForegroundBrush}"
|
||||||
|
Text="Mid"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource PrimaryHueDarkBrush}"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="2">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource PrimaryHueDarkForegroundBrush}"
|
||||||
|
Text="Dark"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource SecondaryHueLightBrush}"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="0">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource SecondaryHueLightForegroundBrush}"
|
||||||
|
Text="Light"/>
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource SecondaryHueMidBrush}"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="1">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource SecondaryHueMidForegroundBrush}"
|
||||||
|
Text="Mid"/>
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource SecondaryHueDarkBrush}"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="2">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource SecondaryHueDarkForegroundBrush}"
|
||||||
|
Text="Dark"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource MaterialDesignBackground}"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="0">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource SecondaryHueDarkForegroundBrush}"
|
||||||
|
Text="Background"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource MaterialDesignCardBackground}"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="1">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource SecondaryHueMidForegroundBrush}"
|
||||||
|
Text="CardBack"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource WindowBackground}"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="2">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource SecondaryHueMidForegroundBrush}"
|
||||||
|
Text="Paper"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource MaterialDesignBody}"
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="0">
|
||||||
|
<TextBlock
|
||||||
|
|
||||||
|
Text="Body"/>
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource MaterialDesignBodyLight}"
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="1">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{DynamicResource SecondaryHueMidForegroundBrush}"
|
||||||
|
Text="BodyLight"/>
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource MaterialDesignToolTipBackground}"
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="2">
|
||||||
|
<TextBlock
|
||||||
|
Text="ToolTipBack"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace LolzFucker.Theme
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для Palette.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class Palette : UserControl
|
||||||
|
{
|
||||||
|
public Palette()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Theme.WindowStyle;
|
||||||
|
|
||||||
|
public static class NativeMethods
|
||||||
|
{
|
||||||
|
public const int WM_NCCALCSIZE = 0x83;
|
||||||
|
public const int WM_NCPAINT = 0x85;
|
||||||
|
|
||||||
|
[DllImport("kernel32", SetLastError = true)]
|
||||||
|
private static extern IntPtr LoadLibrary(string lpFileName);
|
||||||
|
|
||||||
|
[DllImport("dwmapi.dll", PreserveSig = false)]
|
||||||
|
public static extern bool DwmIsCompositionEnabled();
|
||||||
|
|
||||||
|
[DllImport("kernel32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
|
||||||
|
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
|
||||||
|
|
||||||
|
public enum DWMWINDOWATTRIBUTE : uint
|
||||||
|
{
|
||||||
|
NCRenderingEnabled = 1,
|
||||||
|
NCRenderingPolicy,
|
||||||
|
TransitionsForceDisabled,
|
||||||
|
AllowNCPaint,
|
||||||
|
CaptionButtonBounds,
|
||||||
|
NonClientRtlLayout,
|
||||||
|
ForceIconicRepresentation,
|
||||||
|
Flip3DPolicy,
|
||||||
|
ExtendedFrameBounds,
|
||||||
|
HasIconicBitmap,
|
||||||
|
DisallowPeek,
|
||||||
|
ExcludedFromPeek,
|
||||||
|
Cloak,
|
||||||
|
Cloaked,
|
||||||
|
FreezeRepresentation
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct MARGINS
|
||||||
|
{
|
||||||
|
public int leftWidth;
|
||||||
|
public int rightWidth;
|
||||||
|
public int topHeight;
|
||||||
|
public int bottomHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private delegate int DwmExtendFrameIntoClientAreaDelegate(IntPtr hwnd, ref MARGINS margins);
|
||||||
|
|
||||||
|
public static int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins)
|
||||||
|
{
|
||||||
|
var hModule = LoadLibrary("dwmapi");
|
||||||
|
|
||||||
|
if (hModule == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var procAddress = GetProcAddress(hModule, "DwmExtendFrameIntoClientArea");
|
||||||
|
|
||||||
|
if (procAddress == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var delegateForFunctionPointer = (DwmExtendFrameIntoClientAreaDelegate)Marshal.GetDelegateForFunctionPointer(procAddress, typeof(DwmExtendFrameIntoClientAreaDelegate));
|
||||||
|
|
||||||
|
return delegateForFunctionPointer(hwnd, ref margins);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsDwmAvailable()
|
||||||
|
{
|
||||||
|
if (LoadLibrary("dwmapi") == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum WVR
|
||||||
|
{
|
||||||
|
ALIGNTOP = 0x0010,
|
||||||
|
ALIGNLEFT = 0x0020,
|
||||||
|
ALIGNBOTTOM = 0x0040,
|
||||||
|
ALIGNRIGHT = 0x0080,
|
||||||
|
HREDRAW = 0x0100,
|
||||||
|
VREDRAW = 0x0200,
|
||||||
|
VALIDRECTS = 0x0400,
|
||||||
|
REDRAW = HREDRAW | VREDRAW
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Theme.WindowStyle;
|
||||||
|
|
||||||
|
public static class WindowChromeHelper
|
||||||
|
{
|
||||||
|
public static Thickness LayoutOffsetThickness => new Thickness(0d, 0d, 0d, SystemParameters.WindowResizeBorderThickness.Bottom);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the properly adjusted window resize border thickness from system parameters.
|
||||||
|
/// </summary>
|
||||||
|
public static Thickness WindowResizeBorderThickness
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var dpix = GetDpi(GetDeviceCapsIndex.LOGPIXELSX);
|
||||||
|
var dpiy = GetDpi(GetDeviceCapsIndex.LOGPIXELSY);
|
||||||
|
|
||||||
|
var dx = GetSystemMetrics(GetSystemMetricsIndex.CXFRAME);
|
||||||
|
var dy = GetSystemMetrics(GetSystemMetricsIndex.CYFRAME);
|
||||||
|
|
||||||
|
// This adjustment is needed since .NET 4.5
|
||||||
|
var d = GetSystemMetrics(GetSystemMetricsIndex.SM_CXPADDEDBORDER);
|
||||||
|
dx += d;
|
||||||
|
dy += d;
|
||||||
|
|
||||||
|
var leftBorder = dx / dpix;
|
||||||
|
var topBorder = dy / dpiy;
|
||||||
|
|
||||||
|
return new Thickness(leftBorder, topBorder, leftBorder, topBorder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr GetDC(IntPtr hwnd);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||||
|
|
||||||
|
private static float GetDpi(GetDeviceCapsIndex index)
|
||||||
|
{
|
||||||
|
var desktopWnd = IntPtr.Zero;
|
||||||
|
var dc = GetDC(desktopWnd);
|
||||||
|
float dpi;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dpi = GetDeviceCaps(dc, (int)index);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseDC(desktopWnd, dc);
|
||||||
|
}
|
||||||
|
return dpi / 96f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum GetDeviceCapsIndex
|
||||||
|
{
|
||||||
|
LOGPIXELSX = 88,
|
||||||
|
LOGPIXELSY = 90
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int GetSystemMetrics(GetSystemMetricsIndex nIndex);
|
||||||
|
|
||||||
|
private enum GetSystemMetricsIndex
|
||||||
|
{
|
||||||
|
CXFRAME = 32,
|
||||||
|
CYFRAME = 33,
|
||||||
|
SM_CXPADDEDBORDER = 92
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Shell;
|
||||||
|
using Microsoft.Xaml.Behaviors;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Theme.WindowStyle;
|
||||||
|
|
||||||
|
public class WindowChromeRenderedBehavior : Behavior<Window>
|
||||||
|
{
|
||||||
|
private Window window;
|
||||||
|
|
||||||
|
protected override void OnAttached()
|
||||||
|
{
|
||||||
|
base.OnAttached();
|
||||||
|
AssociatedObject.ContentRendered += OnContentRendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetaching()
|
||||||
|
{
|
||||||
|
base.OnDetaching();
|
||||||
|
AssociatedObject.ContentRendered -= OnContentRendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnContentRendered(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
window = Window.GetWindow(AssociatedObject);
|
||||||
|
|
||||||
|
if (window == null) return;
|
||||||
|
|
||||||
|
var oldWindowChrome = WindowChrome.GetWindowChrome(window);
|
||||||
|
|
||||||
|
if (oldWindowChrome == null) return;
|
||||||
|
|
||||||
|
var newWindowChrome = new WindowChrome
|
||||||
|
{
|
||||||
|
CaptionHeight = oldWindowChrome.CaptionHeight,
|
||||||
|
CornerRadius = oldWindowChrome.CornerRadius,
|
||||||
|
GlassFrameThickness = new Thickness(0, 0, 0, 1),
|
||||||
|
NonClientFrameEdges = NonClientFrameEdges.Bottom,
|
||||||
|
ResizeBorderThickness = oldWindowChrome.ResizeBorderThickness,
|
||||||
|
UseAeroCaptionButtons = oldWindowChrome.UseAeroCaptionButtons
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowChrome.SetWindowChrome(window, newWindowChrome);
|
||||||
|
|
||||||
|
var hWnd = new WindowInteropHelper(window).Handle;
|
||||||
|
HwndSource.FromHwnd(hWnd)?.AddHook(WndProc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
switch (msg)
|
||||||
|
{
|
||||||
|
case NativeMethods.WM_NCPAINT:
|
||||||
|
handled = false;
|
||||||
|
RemoveFrame();
|
||||||
|
break;
|
||||||
|
case NativeMethods.WM_NCCALCSIZE:
|
||||||
|
handled = true;
|
||||||
|
var rcClientArea = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT));
|
||||||
|
rcClientArea.Bottom += (int)(WindowChromeHelper.WindowResizeBorderThickness.Bottom / 2);
|
||||||
|
Marshal.StructureToPtr(rcClientArea, lParam, false);
|
||||||
|
|
||||||
|
return wParam == new IntPtr(1) ? new IntPtr((int)NativeMethods.WVR.REDRAW) : IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveFrame()
|
||||||
|
{
|
||||||
|
if (Environment.OSVersion.Version.Major >= 6 && NativeMethods.IsDwmAvailable())
|
||||||
|
{
|
||||||
|
if (NativeMethods.DwmIsCompositionEnabled() && SystemParameters.DropShadow)
|
||||||
|
{
|
||||||
|
// to get the aero shadow back, margins have to be set on at least one side
|
||||||
|
// don't use negative values for the margins because it causes flickering when restoring or maximizing the window
|
||||||
|
// setting the margin on the left or top sides seem best, since the window never appears to flicker there on resizing
|
||||||
|
// but the right and bottom sometimes do, otherwise there will occasionally be white flicker when resizing the window.
|
||||||
|
NativeMethods.MARGINS margins;
|
||||||
|
|
||||||
|
margins.bottomHeight = 0;
|
||||||
|
margins.leftWidth = 1;
|
||||||
|
margins.rightWidth = 0;
|
||||||
|
margins.topHeight = 0;
|
||||||
|
|
||||||
|
var helper = new WindowInteropHelper(window);
|
||||||
|
|
||||||
|
NativeMethods.DwmExtendFrameIntoClientArea(helper.Handle, ref margins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct RECT
|
||||||
|
{
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
public static RECT Empty;
|
||||||
|
|
||||||
|
public RECT(int left, int top, int right, int bottom)
|
||||||
|
{
|
||||||
|
Left = left;
|
||||||
|
Top = top;
|
||||||
|
Right = right;
|
||||||
|
Bottom = bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<ResourceDictionary x:Class="NebulaAuth.Theme.WindowStyle.WindowStyle"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:local="clr-namespace:NebulaAuth.Theme.WindowStyle">
|
||||||
|
|
||||||
|
<Style TargetType="Button" BasedOn="{StaticResource MaterialDesignFlatButton}">
|
||||||
|
<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<ControlTemplate x:Key="WindowTemplate" TargetType="{x:Type Window}" >
|
||||||
|
<!--<i:Interaction.Behaviors>
|
||||||
|
<local:WindowChromeLoadedBehavior />
|
||||||
|
</i:Interaction.Behaviors>-->
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="RootBorder"
|
||||||
|
Margin="{x:Static local:WindowChromeHelper.LayoutOffsetThickness}"
|
||||||
|
BorderBrush="Transparent"
|
||||||
|
BorderThickness="1"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid x:Name="TitleBar"
|
||||||
|
Grid.Row="0"
|
||||||
|
Height="28"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||||
|
Text="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Title}" FontSize="17"/>
|
||||||
|
<!-- Window Buttons -->
|
||||||
|
<StackPanel
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<StackPanel.Resources>
|
||||||
|
<Style TargetType="materialDesign:PackIcon">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Resources>
|
||||||
|
<Button x:Name="MinimizeButton"
|
||||||
|
Click="OnMinimizeClick"
|
||||||
|
>
|
||||||
|
<materialDesign:PackIcon Kind="WindowMinimize"/>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="MaximizeRestoreButton"
|
||||||
|
Click="OnMaximizeRestoreClick"
|
||||||
|
>
|
||||||
|
<materialDesign:PackIcon Kind="WindowMaximize"/>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="CloseButton"
|
||||||
|
Click="OnCloseClick">
|
||||||
|
<materialDesign:PackIcon Kind="WindowClose"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Main Window Content -->
|
||||||
|
<Grid x:Name="MainGrid"
|
||||||
|
Grid.Row="1"
|
||||||
|
Background="{DynamicResource MainBackgroundBrush}">
|
||||||
|
<AdornerDecorator>
|
||||||
|
<ContentPresenter />
|
||||||
|
</AdornerDecorator>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsActive" Value="True">
|
||||||
|
<Setter TargetName="RootBorder" Property="BorderBrush" Value="#1e1e24" />
|
||||||
|
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="WindowState" Value="Maximized">
|
||||||
|
<Setter TargetName="RootBorder" Property="BorderThickness" Value="{Binding Source={x:Static local:WindowChromeHelper.WindowResizeBorderThickness}}" />
|
||||||
|
<Setter TargetName="RootBorder" Property="BorderBrush" Value="Transparent" />
|
||||||
|
<Setter TargetName="MaximizeRestoreButton" Property="Content" >
|
||||||
|
<Setter.Value>
|
||||||
|
<materialDesign:PackIcon Kind="WindowRestore"/>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
|
||||||
|
<Style x:Key="MainWindow" TargetType="{x:Type Window}">
|
||||||
|
<Setter Property="UseLayoutRounding" Value="True" />
|
||||||
|
<Setter Property="TextOptions.TextFormattingMode" Value="Ideal" />
|
||||||
|
<Setter Property="TextOptions.TextRenderingMode" Value="ClearType" />
|
||||||
|
<Setter Property="WindowChrome.WindowChrome">
|
||||||
|
<Setter.Value>
|
||||||
|
<WindowChrome
|
||||||
|
CaptionHeight="38"
|
||||||
|
CornerRadius="0"
|
||||||
|
GlassFrameThickness="0"
|
||||||
|
NonClientFrameEdges="None"
|
||||||
|
ResizeBorderThickness="5"
|
||||||
|
UseAeroCaptionButtons="False" />
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="Template" Value="{StaticResource WindowTemplate}" />
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Theme.WindowStyle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для WindowStyle.xaml
|
||||||
|
/// </summary>
|
||||||
|
partial class WindowStyle
|
||||||
|
{
|
||||||
|
public WindowStyle()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCloseClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var window = (Window)((FrameworkElement)sender).TemplatedParent;
|
||||||
|
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMaximizeRestoreClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var window = (Window)((FrameworkElement)sender).TemplatedParent;
|
||||||
|
|
||||||
|
window.WindowState = window.WindowState == WindowState.Normal ? WindowState.Maximized : WindowState.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMinimizeClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var window = (Window)((FrameworkElement)sender).TemplatedParent;
|
||||||
|
|
||||||
|
window.WindowState = WindowState.Minimized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 213 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
using CodingSeb.Localization;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.Exceptions.Mobile;
|
||||||
|
using SteamLib.ProtoCore.Enums;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Utility;
|
||||||
|
|
||||||
|
public static class ErrorTranslatorHelper
|
||||||
|
{
|
||||||
|
|
||||||
|
public static string TranslateLoginError(LoginError error)
|
||||||
|
{
|
||||||
|
var result = GetMessage("Login", error.ToString());
|
||||||
|
return result ?? error.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static string TranslateEResult(EResult eResult)
|
||||||
|
{
|
||||||
|
var result = GetMessage("EResult", eResult.ToString());
|
||||||
|
return result ?? eResult.ToString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TranslateLinkerError(AuthenticatorLinkerError error)
|
||||||
|
{
|
||||||
|
return GetMessage("AuthenticatorLinkerError", error.ToString()) ?? error.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetMessage(string path, string name)
|
||||||
|
{
|
||||||
|
var fullPath = LocManager.CODE_BEHIND_PATH_PART + "." + "ErrorTranslator" + "." + path + "." + name;
|
||||||
|
var message = Loc.Tr(fullPath, "|ABSENT|");
|
||||||
|
if (message == "|ABSENT|") return null;
|
||||||
|
return message;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.Exceptions.General;
|
||||||
|
using SteamLib.ProtoCore.Exceptions;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NebulaAuth.Utility;
|
||||||
|
|
||||||
|
public static class ExceptionHandler
|
||||||
|
{
|
||||||
|
private const string EXCEPTION_HANDLER_LOC_PATH = "ExceptionHandler";
|
||||||
|
public static bool Handle(Exception ex, string? prefix = null, string? postfix = null, bool handleAllExceptions = false)
|
||||||
|
{
|
||||||
|
string msg;
|
||||||
|
Shell.Logger.Error(ex);
|
||||||
|
switch (ex)
|
||||||
|
{
|
||||||
|
case SessionExpiredException:
|
||||||
|
{
|
||||||
|
msg = "SessionExpiredException".GetCodeBehindLocalization();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SessionInvalidException:
|
||||||
|
{
|
||||||
|
msg = "SessionExpiredException".GetCodeBehindLocalization();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TaskCanceledException e:
|
||||||
|
{
|
||||||
|
msg = e.InnerException is TimeoutException
|
||||||
|
? "TimeoutException".GetCodeBehindLocalization()
|
||||||
|
: "TaskCanceledException".GetCodeBehindLocalization();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case HttpRequestException e:
|
||||||
|
{
|
||||||
|
var str = "RequestError".GetCommonLocalization() + ": ";
|
||||||
|
if (e.StatusCode != null)
|
||||||
|
{
|
||||||
|
msg = str + e.StatusCode;
|
||||||
|
}
|
||||||
|
else if (e.InnerException != null)
|
||||||
|
{
|
||||||
|
msg = (str + e.InnerException.Message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msg = (str + e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UnsupportedResponseException:
|
||||||
|
{
|
||||||
|
msg = "UnsupportedResponseException".GetCodeBehindLocalization();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CantLoadConfirmationsException e:
|
||||||
|
{
|
||||||
|
msg = e.Message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EResultException e:
|
||||||
|
{
|
||||||
|
msg = "Error".GetCommonLocalization() + ": " + ErrorTranslatorHelper.TranslateEResult(e.Result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LoginException e:
|
||||||
|
{
|
||||||
|
msg = "LoginException".GetCodeBehindLocalization() + ": " + ErrorTranslatorHelper.TranslateLoginError(e.Error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case not null when handleAllExceptions:
|
||||||
|
{
|
||||||
|
msg = "UnknownException".GetCodeBehindLocalization() + ex.Message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnackbarController.SendSnackbar(prefix + msg + postfix);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static string GetCommonLocalization(this string key)
|
||||||
|
{
|
||||||
|
return LocManager.GetCommonOrDefault(key, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCodeBehindLocalization(this string key)
|
||||||
|
{
|
||||||
|
return LocManager.GetCodeBehindOrDefault(key, EXCEPTION_HANDLER_LOC_PATH, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||||
|
xmlns:converters="clr-namespace:NebulaAuth.Converters"
|
||||||
|
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<DataTemplate DataType="{x:Type confirmations:Confirmation}">
|
||||||
|
<Grid >
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" DockPanel.Dock="Left" Kind="QuestionMark" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="1" Text="{Binding TypeName}"></TextBlock>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Right" Text="{Binding Time, StringFormat=t}"/>
|
||||||
|
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.ConfirmCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Check"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CancelCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Close" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type confirmations:TradeConfirmation}">
|
||||||
|
<Grid >
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="AccountArrowRight" Margin="0,0,10,0"></materialDesign:PackIcon>
|
||||||
|
<Image Grid.Column="1" VerticalAlignment="Center" DockPanel.Dock="Left" Width="24" Height="24" Source="{Binding UserAvatarUri}" Margin="0,0,10,0"></Image>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="2" TextWrapping="WrapWithOverflow" >
|
||||||
|
<Run Text="{Tr MainWindow.ConfirmationTemplates.TradeWith, IsDynamic=False}"/>
|
||||||
|
<Run Text="{Binding UserName}" FontWeight="Bold"></Run>
|
||||||
|
</TextBlock>
|
||||||
|
<materialDesign:PackIcon Grid.Column="3" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5,0,5,0" Kind="Gift" Visibility="{Binding IsReceiveNothing, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="4" HorizontalAlignment="Right" Text="{Binding Time, StringFormat=t}"/>
|
||||||
|
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.ConfirmCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Check"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="6" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CancelCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Close" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type confirmations:AccountRecoveryConfirmation}">
|
||||||
|
<Grid >
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" DockPanel.Dock="Left" Kind="LockOutline" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="1" Text="{Tr MainWindow.ConfirmationTemplates.Recovery}"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Right" Text="{Binding Time, StringFormat=t}"/>
|
||||||
|
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.ConfirmCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Check"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CancelCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Close" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type confirmations:MarketConfirmation}">
|
||||||
|
<Grid >
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="ShoppingCart" Margin="0,0,10,0"></materialDesign:PackIcon>
|
||||||
|
<Image Grid.Column="1" VerticalAlignment="Center" DockPanel.Dock="Left" Width="32" Height="32" Source="{Binding ItemImageUri}" Margin="0,0,10,0"></Image>
|
||||||
|
<Grid Grid.Column="2">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock VerticalAlignment="Center" x:Name="ItemName" theme:FontScaleWindow.ResizeFont="True" TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" >
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Row="1" Foreground="LightGray" Text="{Binding PriceString}">
|
||||||
|
<TextBlock.FontSize>
|
||||||
|
<Binding ElementName="ItemName" Path="FontSize" ConverterParameter="0.7">
|
||||||
|
<Binding.Converter>
|
||||||
|
<converters:CoefficientConverter/>
|
||||||
|
</Binding.Converter>
|
||||||
|
</Binding>
|
||||||
|
</TextBlock.FontSize>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="4" HorizontalAlignment="Left" Margin="5,0,0,0" Text="{Binding Time, StringFormat=t}"/>
|
||||||
|
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.ConfirmCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Check"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="6" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CancelCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Close" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type confirmations:RegisterApiKeyConfirmation}">
|
||||||
|
<Grid >
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" DockPanel.Dock="Left" Kind="KeyLink" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="1" Text="{Tr MainWindow.ConfirmationTemplates.ApiKey}"></TextBlock>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Right" Text="{Binding Time, StringFormat=t}"/>
|
||||||
|
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.ConfirmCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Check"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CancelCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Close" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type entities:MarketMultiConfirmation}">
|
||||||
|
<Grid >
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="ShoppingCartPlus" Margin="0,0,10,0"></materialDesign:PackIcon>
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="1">
|
||||||
|
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market}"/>
|
||||||
|
<Run Text="{Binding Confirmations.Count, Mode=OneWay}"/>
|
||||||
|
<Run> </Run>
|
||||||
|
<Run Text="{Tr Common.Abbreviations.Count, IsDynamic=False}"/>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left" Margin="5,0,0,0" Text="{Binding Time, StringFormat=t}"/>
|
||||||
|
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.ConfirmCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Check"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||||
|
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CancelCommand}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<materialDesign:PackIcon Kind="Close" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<UserControl x:Class="NebulaAuth.View.Dialogs.ConfirmCancelDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="100" d:DesignWidth="400"
|
||||||
|
Height="Auto" Width="Auto" MaxWidth="500"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
<Grid Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock theme:FontScaleWindow.Scale="0.8"
|
||||||
|
theme:FontScaleWindow.ResizeFont="True"
|
||||||
|
HorizontalAlignment="Center" Margin="10,10,10,15"
|
||||||
|
Text="Подтвердите действие"
|
||||||
|
x:Name="ConfirmTextBlock"
|
||||||
|
TextWrapping="WrapWithOverflow"/>
|
||||||
|
<Grid Grid.Row="1" Margin="10,0,10,5">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button DockPanel.Dock="Left"
|
||||||
|
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||||
|
CommandParameter="{StaticResource True}"
|
||||||
|
FontSize="{Binding ElementName=ConfirmTextBlock, Path=FontSize,
|
||||||
|
Converter={StaticResource CoefficientConverter},
|
||||||
|
ConverterParameter=0.85}">
|
||||||
|
ОК
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="1" DockPanel.Dock="Right"
|
||||||
|
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||||
|
CommandParameter="{StaticResource False}"
|
||||||
|
FontSize="{Binding ElementName=ConfirmTextBlock, Path=FontSize,
|
||||||
|
Converter={StaticResource CoefficientConverter},
|
||||||
|
ConverterParameter=0.85}">
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace NebulaAuth.View.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для ConfirmCancelDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class ConfirmCancelDialog : UserControl
|
||||||
|
{
|
||||||
|
public ConfirmCancelDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfirmCancelDialog(string msg)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ConfirmTextBlock.Text = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<UserControl x:Class="NebulaAuth.View.Dialogs.LoginAgainDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||||
|
xmlns:model="clr-namespace:NebulaAuth.Model"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="250" d:DesignWidth="800"
|
||||||
|
theme:FontScaleWindow.Scale="0.9" theme:FontScaleWindow.ResizeFont="True"
|
||||||
|
Foreground="WhiteSmoke" Cursor="Hand"
|
||||||
|
d:DataContext="{d:DesignInstance other:LoginAgainVM}"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
|
||||||
|
<Grid MinHeight="100" MinWidth="300" Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock theme:FontScaleWindow.Scale="0.9" theme:FontScaleWindow.ResizeFont="True" Margin="10,0,0,0" HorizontalAlignment="Left">
|
||||||
|
<Run Text="{Tr LoginAgainDialog.LoginFor, IsDynamic=False}"/>
|
||||||
|
<Run FontWeight="Bold" Text="{Binding UserName}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBox Text="{Binding Password}" Margin="10" Grid.Row="1" Style="{StaticResource MaterialDesignFloatingHintTextBox}" materialDesign:HintAssist.Hint="{Tr LoginAgainDialog.PasswordBox}"></TextBox>
|
||||||
|
<CheckBox IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}" IsChecked="{Binding SavePassword}" Margin="10" Grid.Row="2" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}"/>
|
||||||
|
<Grid Grid.Row="3">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button IsDefault="True" Margin="10,5,5,5" Style="{StaticResource MaterialDesignOutlinedButton}" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" CommandParameter="{StaticResource True}" Content="{Tr LoginAgainDialog.LoginButton}"/>
|
||||||
|
<Button IsCancel="True" Grid.Column="1" Margin="5,5,10,5" Style="{StaticResource MaterialDesignOutlinedButton}" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" CommandParameter="{StaticResource False}" Content="{Tr LoginAgainDialog.CancelButton}"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace NebulaAuth.View.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для LoginAgainDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class LoginAgainDialog : UserControl
|
||||||
|
{
|
||||||
|
public LoginAgainDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<UserControl x:Class="NebulaAuth.View.Dialogs.SetCryptPasswordDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="clr-namespace:NebulaAuth.View.Dialogs"
|
||||||
|
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||||
|
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||||
|
xmlns:model="clr-namespace:NebulaAuth.Model"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Foreground="WhiteSmoke" Cursor="Hand"
|
||||||
|
d:DataContext="{d:DesignInstance other:LoginAgainVM}"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
|
||||||
|
<Grid MinHeight="100" MinWidth="300" MaxWidth="400" Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock IsHitTestVisible="False" TextWrapping="Wrap" FontWeight="Normal" Text="{Tr SetEncryptedPasswordDialog.DialogText}" theme:FontScaleWindow.Scale="0.8" theme:FontScaleWindow.ResizeFont="True" Margin="10,0,0,0" HorizontalAlignment="Left"/>
|
||||||
|
<PasswordBox FontSize="16" materialDesign:PasswordBoxAssist.Password="{Binding Password}" Margin="10" Grid.Row="1" Style="{StaticResource MaterialDesignFloatingHintRevealPasswordBox}" materialDesign:HintAssist.Hint="{Tr SetEncryptedPasswordDialog.Password}"></PasswordBox>
|
||||||
|
<Grid Grid.Row="3">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button Margin="10,5,5,5" Style="{StaticResource MaterialDesignOutlinedButton}" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" CommandParameter="{StaticResource True}" Content="{Tr SetEncryptedPasswordDialog.Ok}"/>
|
||||||
|
<Button IsCancel="True" Grid.Column="1" Margin="5,5,10,5" Style="{StaticResource MaterialDesignOutlinedButton}" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" CommandParameter="{StaticResource False}" Content="{Tr SetEncryptedPasswordDialog.Cancel}"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace NebulaAuth.View.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для SetCryptPasswordDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class SetCryptPasswordDialog : UserControl
|
||||||
|
{
|
||||||
|
public SetCryptPasswordDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<UserControl x:Class="NebulaAuth.View.Dialogs.WaitLoginDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="450" d:DesignWidth="800"
|
||||||
|
theme:FontScaleWindow.ResizeFont="True" theme:FontScaleWindow.Scale="1"
|
||||||
|
Foreground="WhiteSmoke"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}" IsIndeterminate="True"></ProgressBar>
|
||||||
|
<TextBlock Margin="10,0,0,0" Grid.Column="1" Text="{Tr WaitLoginDialog.Text}" VerticalAlignment="Center"></TextBlock>
|
||||||
|
</Grid>
|
||||||
|
<Grid x:Name="CaptchaGrid" Margin="0,25,0,0" Visibility="Collapsed" Grid.Row="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Image x:Name="CaptchaImage" Stretch="Uniform"></Image>
|
||||||
|
<TextBox Grid.Row="1" Style="{StaticResource MaterialDesignFloatingHintTextBox}" x:Name="CaptchaTB"/>
|
||||||
|
<Grid Grid.Row="2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button Grid.Column="0" Click="SendCaptchaBtn_Click">Отправить</Button>
|
||||||
|
<Button Grid.Column="1" Click="CancelButton_OnClick">Отмена</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
|
||||||
|
namespace NebulaAuth.View.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для WaitLoginDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class WaitLoginDialog : ICaptchaResolver
|
||||||
|
{
|
||||||
|
private TaskCompletionSource<string> _tcs = new();
|
||||||
|
public WaitLoginDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> Resolve(Uri imageUrl, HttpClient client)
|
||||||
|
{
|
||||||
|
|
||||||
|
CaptchaGrid.Visibility = Visibility.Visible;
|
||||||
|
var stream = await client.GetStreamAsync(imageUrl);
|
||||||
|
return await Application.Current.Dispatcher.Invoke(async () =>
|
||||||
|
{
|
||||||
|
var image = await LoadImage(stream);
|
||||||
|
CaptchaImage.Source = image;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _tcs.Task;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
throw new LoginException(LoginError.CaptchaRequired);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BitmapImage> LoadImage(Stream stream)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await stream.CopyToAsync(ms);
|
||||||
|
ms.Position = 0;
|
||||||
|
|
||||||
|
var image = new BitmapImage();
|
||||||
|
|
||||||
|
image.BeginInit();
|
||||||
|
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
|
image.StreamSource = ms;
|
||||||
|
image.EndInit();
|
||||||
|
await stream.DisposeAsync();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_tcs.SetCanceled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendCaptchaBtn_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CaptchaTB.Text)) return;
|
||||||
|
var oldTcs = _tcs;
|
||||||
|
_tcs = new TaskCompletionSource<string>();
|
||||||
|
oldTcs.SetResult(CaptchaTB.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<UserControl x:Class="NebulaAuth.View.LinkerView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="clr-namespace:NebulaAuth.View"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="450" d:DesignWidth="800"
|
||||||
|
Foreground="WhiteSmoke"
|
||||||
|
FontFamily="{materialDesign:MaterialDesignFont}"
|
||||||
|
MinHeight="640"
|
||||||
|
MinWidth="400"
|
||||||
|
Background="{DynamicResource WindowBackground}"
|
||||||
|
d:DataContext="{d:DesignInstance other:LinkAccountVM}"
|
||||||
|
FontSize="18">
|
||||||
|
<d:DesignerProperties.DesignStyle>
|
||||||
|
<Style TargetType="UserControl">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource WindowBackground}" />
|
||||||
|
</Style>
|
||||||
|
</d:DesignerProperties.DesignStyle>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
|
||||||
|
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.Resources>
|
||||||
|
<Style TargetType="materialDesign:PackIcon">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=StackPanel}, Path=Tag}" Value="False">
|
||||||
|
<Setter Property="Foreground" Value="IndianRed"/>
|
||||||
|
<Setter Property="Kind" Value="CheckBoxOutlineBlank"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=StackPanel}, Path=Tag}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="Green"/>
|
||||||
|
<Setter Property="Kind" Value="CheckBoxOutline"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=StackPanel}, Path=Tag}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="Gray"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Grid.Resources>
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock FontStyle="Normal" Foreground="DarkGray" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="18" Text="{Tr LinkerDialog.Title}"/>
|
||||||
|
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}" HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
|
||||||
|
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Separator Background="DarkGray" Grid.Row="1"></Separator>
|
||||||
|
<Grid Grid.Row="2" Margin="10,20,10,10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ComboBox Padding="10" FontSize="16" Style="{StaticResource MaterialDesignOutlinedComboBox}" SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}" materialDesign:HintAssist.Hint="{Tr LinkerDialog.Proxy}" IsEnabled="{Binding IsPasswordFieldVisible}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text="{Binding Key, Mode=OneWay}"/><Run Text=":"/>
|
||||||
|
<Run Text="{Binding Value.Address, Mode=OneWay}"/>
|
||||||
|
</TextBlock>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
<Button Grid.Column="1" IsEnabled="{Binding IsPasswordFieldVisible}" Margin="5,0,0,0" Command="{Binding ResetProxyCommand}" Content="{materialDesign:PackIcon Trash}"/>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Grid.Row="3" Orientation="Horizontal" Margin="10" Tag="{Binding IsLogin}">
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Tr LinkerDialog.Authorization}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" Margin="10" Tag="{Binding IsEmailCode}">
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Tr LinkerDialog.EmailCode}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="5" Orientation="Horizontal" Margin="10" Tag="{Binding IsPhoneNumber}">
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Tr LinkerDialog.PhoneNumber}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="6" Orientation="Horizontal" Margin="10" Tag="{Binding IsEmailConfirmation}">
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Tr LinkerDialog.EmailLink}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="7" Orientation="Horizontal" Margin="10" Tag="{Binding IsLinkCode}">
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Tr LinkerDialog.SmsOrCode}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="8" Orientation="Horizontal" Margin="10" Tag="{Binding IsCompleted}">
|
||||||
|
<materialDesign:PackIcon Width="20" Height="20" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Tr LinkerDialog.Completed}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid Grid.Row="9" Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBox Padding="10" Text="{Binding FieldText}" Visibility="{Binding IsFieldVisible, Converter={StaticResource BooleanToVisibilityConverter}}" Style="{StaticResource MaterialDesignOutlinedTextBox}" FontSize="16" Margin="0,0,0,10"></TextBox>
|
||||||
|
<TextBox Grid.Row="1" Padding="10" Text="{Binding PassFieldText}" Visibility="{Binding IsPasswordFieldVisible, Converter={StaticResource BooleanToVisibilityConverter}}" Style="{StaticResource MaterialDesignOutlinedTextBox}" FontSize="16"/>
|
||||||
|
<GroupBox Grid.Row="2" MaxWidth="400" BorderBrush="{StaticResource PrimaryHueMidBrush}" VerticalAlignment="Bottom" BorderThickness="1" Margin="0,10,0,0" Padding="5">
|
||||||
|
<GroupBox.HeaderTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Tr LinkerDialog.Message}" FontSize="16" Foreground="GhostWhite"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</GroupBox.HeaderTemplate>
|
||||||
|
<TextBlock Foreground="GhostWhite" FontSize="16" Text="{Binding HintText}" HorizontalAlignment="Stretch" TextWrapping="Wrap"/>
|
||||||
|
</GroupBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button Grid.Row="10" FontSize="18" Command="{Binding ProceedCommand}" Content="{Tr LinkerDialog.ProceedButton}"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace NebulaAuth.View
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для LinkerView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class LinkerView : UserControl
|
||||||
|
{
|
||||||
|
public LinkerView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<UserControl x:Class="NebulaAuth.View.ProxyManagerView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="clr-namespace:NebulaAuth.View"
|
||||||
|
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="450" d:DesignWidth="800"
|
||||||
|
Foreground="WhiteSmoke"
|
||||||
|
FontFamily="{md:MaterialDesignFont}"
|
||||||
|
MinHeight="500"
|
||||||
|
MinWidth="400"
|
||||||
|
MaxHeight="550"
|
||||||
|
d:DataContext="{d:DesignInstance other:ProxyManagerVM}"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
<d:DesignerProperties.DesignStyle>
|
||||||
|
<Style TargetType="UserControl">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource WindowBackground}" />
|
||||||
|
</Style>
|
||||||
|
</d:DesignerProperties.DesignStyle>
|
||||||
|
|
||||||
|
<Grid Margin="15,10,15,15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock FontStyle="Normal" Foreground="DarkGray" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="18" Text="{Tr ProxyManagerDialog.Title}"/>
|
||||||
|
<Button IsCancel="True" Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}" HorizontalAlignment="Right" Command="{x:Static md:DialogHost.CloseDialogCommand}">
|
||||||
|
<md:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed"></md:PackIcon>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
<Separator Grid.Row="1"></Separator>
|
||||||
|
<Grid Grid.Row="2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock HorizontalAlignment="Stretch"
|
||||||
|
Margin="15" FontSize="16">
|
||||||
|
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}"/>
|
||||||
|
<Run Text="{Binding DefaultProxy.Key, StringFormat='
0:', FallbackValue='X', Mode=OneWay}"/>
|
||||||
|
<Run Text="{Binding DefaultProxy.Value.Address, Mode=OneWay, FallbackValue=''}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<Button Grid.Column="1" Command="{Binding SetDefaultCommand}">
|
||||||
|
<md:PackIcon Kind="HeartBoxOutline" Width="20" Height="20"></md:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2" Command="{Binding RemoveDefaultCommand}" Cursor="Hand">
|
||||||
|
<md:PackIcon Kind="ClearBox" Width="20" Height="20"></md:PackIcon>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<md:Card Grid.Row="3" Margin="10">
|
||||||
|
<ListBox SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}">
|
||||||
|
<ListBox.ItemContainerStyle>
|
||||||
|
<Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignListBoxItem}">
|
||||||
|
<Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=DataContext}"/>
|
||||||
|
<Setter Property="ContextMenu">
|
||||||
|
<Setter.Value>
|
||||||
|
<ContextMenu>
|
||||||
|
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.Copy}"
|
||||||
|
Command="{Binding PlacementTarget.Tag.CopyProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
|
CommandParameter="{Binding Value}"/>
|
||||||
|
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.CopyAddress}"
|
||||||
|
Command="{Binding PlacementTarget.Tag.CopyProxyAddressCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
|
CommandParameter="{Binding Value}"/>
|
||||||
|
</ContextMenu>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
|
||||||
|
</Style>
|
||||||
|
</ListBox.ItemContainerStyle>
|
||||||
|
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text="{Binding Key, Mode=OneWay}"/><Run Text=": "/>
|
||||||
|
<Run Text="{Binding Value.Address, Mode=OneWay}"/>
|
||||||
|
|
||||||
|
</TextBlock>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</md:Card>
|
||||||
|
<Grid Grid.Row="4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Text="{Binding AddProxyField}" FontSize="12" md:TextFieldAssist.HasClearButton="True" AcceptsReturn="True" MaxHeight="100" Style="{StaticResource MaterialDesignOutlinedTextBox}" Margin="15" md:HintAssist.Hint="IP:PORT:USER:PASS{ID}"></TextBox>
|
||||||
|
<Button IsDefault="True" Grid.Column="1" Command="{Binding AddProxyCommand}">
|
||||||
|
<md:PackIcon Kind="Add"></md:PackIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2" Command="{Binding RemoveProxyCommand}">
|
||||||
|
<md:PackIcon Kind="Trash"></md:PackIcon>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace NebulaAuth.View
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для ProxyManagerView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class ProxyManagerView : UserControl
|
||||||
|
{
|
||||||
|
public ProxyManagerView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<UserControl x:Class="NebulaAuth.View.SettingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="700"
|
||||||
|
Foreground="WhiteSmoke"
|
||||||
|
FontFamily="{materialDesign:MaterialDesignFont}"
|
||||||
|
MinHeight="300"
|
||||||
|
MinWidth="400"
|
||||||
|
|
||||||
|
MaxWidth="200"
|
||||||
|
d:DataContext="{d:DesignInstance other:SettingsVM}"
|
||||||
|
Background="{DynamicResource WindowBackground}">
|
||||||
|
<d:DesignerProperties.DesignStyle>
|
||||||
|
<Style TargetType="UserControl">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource WindowBackground}" />
|
||||||
|
</Style>
|
||||||
|
</d:DesignerProperties.DesignStyle>
|
||||||
|
<Grid>
|
||||||
|
<Grid Margin="15,10,15,15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock FontStyle="Normal" Foreground="DarkGray" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="18" Text="{Tr SettingsDialog.Title}"/>
|
||||||
|
<Button IsCancel="True" Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}" HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
|
||||||
|
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed"></materialDesign:PackIcon>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Separator Grid.Row="1" Margin="0,10,0,0" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Margin="10,30,10,10" Orientation="Vertical" HorizontalAlignment="Center">
|
||||||
|
<ComboBox Style="{StaticResource MaterialDesignOutlinedComboBox}" FontSize="16" ItemsSource="{Binding BackgroundModes}" DisplayMemberPath="Value" SelectedValuePath="Key" SelectedValue="{Binding BackgroundMode}" materialDesign:HintAssist.Hint="{Tr SettingsDialog.BackgroundHint}" ></ComboBox>
|
||||||
|
<ComboBox Style="{StaticResource MaterialDesignFloatingHintComboBox}" Margin="0,20,0,0" FontSize="16" ItemsSource="{Binding Languages}" DisplayMemberPath="Value" SelectedValuePath="Key" SelectedValue="{Binding Language}" materialDesign:HintAssist.Hint="{Tr LanguageWord}" ></ComboBox>
|
||||||
|
<CheckBox Margin="0,20,0,0" FontSize="16" IsChecked="{Binding DisableTimersOnChange}" Content="{Tr SettingsDialog.DisableTimersOnSwitch}"/>
|
||||||
|
<CheckBox Margin="0,20,0,0" FontSize="16" IsChecked="{Binding HideToTray}" Content="{Tr SettingsDialog.MinimizeToTray}"/>
|
||||||
|
<CheckBox Margin="0,20,0,0" FontSize="16" IsChecked="{Binding UseIcon}" Content="{Tr SettingsDialog.UseIndicator}"/>
|
||||||
|
<materialDesign:ColorPicker IsEnabled="{Binding UseIcon}" Color="{Binding IconColor, Delay=50}" />
|
||||||
|
<CheckBox Margin="0,20,0,0" FontSize="16" IsChecked="{Binding UseBackground}" Content="{Tr SettingsDialog.UseCustomColor}"/>
|
||||||
|
|
||||||
|
<materialDesign:ColorPicker Height="100" IsEnabled="{Binding UseBackground}" Color="{Binding BackgroundColor, Delay=50}" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<PasswordBox MinWidth="250" materialDesign:PasswordBoxAssist.Password="{Binding Password}" Height="Auto" VerticalAlignment="Center" FontSize="16" Margin="0,10,0,0"
|
||||||
|
Style="{StaticResource MaterialDesignFloatingHintRevealPasswordBox}"
|
||||||
|
materialDesign:HintAssist.Hint="{Tr SettingsDialog.PasswordBox.CurrentCryptPassword}"
|
||||||
|
materialDesign:HintAssist.HelperText="{Tr SettingsDialog.PasswordBox.Hint}"></PasswordBox>
|
||||||
|
<Button Command="{Binding SetPasswordCommand}" Margin="5,0,0,0" VerticalAlignment="Bottom" Grid.Column="1"> <materialDesign:PackIcon Kind="ContentSave"/></Button>
|
||||||
|
</Grid>
|
||||||
|
<CheckBox Margin="0,25,0,0" FontSize="16" IsChecked="{Binding LegacyMode}" Content="{Tr SettingsDialog.LegacyMafileMode}" ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}"/>
|
||||||
|
<CheckBox Margin="0,20,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>
|
||||||
|
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace NebulaAuth.View
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логика взаимодействия для SettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class SettingsView : UserControl
|
||||||
|
{
|
||||||
|
public SettingsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ColorPicker_OnColorChanged(object sender, RoutedPropertyChangedEventArgs<Color> e)
|
||||||
|
{
|
||||||
|
Debug.WriteLine(e.NewValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using MaterialDesignThemes.Wpf;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using NebulaAuth.Utility;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.SteamMobile;
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NebulaAuth.View.Dialogs;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<Mafile> _maFiles = Storage.MaFiles;
|
||||||
|
public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
|
||||||
|
|
||||||
|
public Mafile? SelectedMafile
|
||||||
|
{
|
||||||
|
get => _selectedMafile;
|
||||||
|
set => SetMafile(value);
|
||||||
|
}
|
||||||
|
private Mafile? _selectedMafile;
|
||||||
|
|
||||||
|
|
||||||
|
public MainVM()
|
||||||
|
{
|
||||||
|
CreateCodeTimer();
|
||||||
|
_confirmTimer = new Timer(ConfirmByTimer, null, TimeSpan.FromSeconds(_timerCheckSeconds), TimeSpan.FromSeconds(_timerCheckSeconds));
|
||||||
|
Proxies = new ObservableCollection<MaProxy>(ProxyStorage.Proxies.Select(kvp =>
|
||||||
|
new MaProxy(kvp.Key, kvp.Value)));
|
||||||
|
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
|
||||||
|
QueryGroups();
|
||||||
|
SessionHandler.LoginStarted += SessionHandlerOnLoginStarted;
|
||||||
|
SessionHandler.LoginCompleted += SessionHandlerOnLoginCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void SetMafile(Mafile? mafile)
|
||||||
|
{
|
||||||
|
if (mafile != SelectedMafile)
|
||||||
|
{
|
||||||
|
_selectedMafile = mafile;
|
||||||
|
OnPropertyChanged(nameof(SelectedMafile));
|
||||||
|
MaClient.SetAccount(mafile);
|
||||||
|
OnPropertyChanged(nameof(ConfirmationsVisible));
|
||||||
|
if (Settings.DisableTimersOnChange) OffTimer(dispatcher: false);
|
||||||
|
SetCurrentProxy();
|
||||||
|
OnPropertyChanged(nameof(IsDefaultProxy));
|
||||||
|
if (mafile != null) Code = SteamGuardCodeGenerator.GenerateCode(mafile.SharedSecret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task LoginAgain()
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var loginAgainVm = await DialogsController.ShowLoginAgainDialog(SelectedMafile.AccountName);
|
||||||
|
if (loginAgainVm == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var password = loginAgainVm.Password;
|
||||||
|
var waitDialog = new WaitLoginDialog();
|
||||||
|
var wait = DialogHost.Show(waitDialog);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MaClient.LoginAgain(SelectedMafile, password, loginAgainVm.SavePassword, waitDialog);
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("SuccessfulLogin"));
|
||||||
|
}
|
||||||
|
catch (LoginException ex)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(ErrorTranslatorHelper.TranslateLoginError(ex.Error), TimeSpan.FromSeconds(1.5));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
when (ExceptionHandler.Handle(ex))
|
||||||
|
{
|
||||||
|
Shell.Logger.Error(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DialogsController.CloseDialog();
|
||||||
|
await wait;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
SearchText = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RefreshSession()
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MaClient.RefreshSession(SelectedMafile);
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("SessionRefreshed"));
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ExceptionHandler.Handle(ex))
|
||||||
|
{
|
||||||
|
Shell.Logger.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task LinkAccount()
|
||||||
|
{
|
||||||
|
OffTimer(false);
|
||||||
|
await DialogsController.ShowLinkerDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RemoveAuthenticator()
|
||||||
|
{
|
||||||
|
var selectedMafile = SelectedMafile;
|
||||||
|
if (selectedMafile == null) return;
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedMafile.RevocationCode))
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error") + ": " + GetLocalizationOrDefault("MissingRCode"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("ConfirmRemovingAuthenticator")))
|
||||||
|
{
|
||||||
|
var result = await SessionHandler.Handle(() => MaClient.RemoveAuthenticator(SelectedMafile), SelectedMafile);
|
||||||
|
SnackbarController.SendSnackbar(
|
||||||
|
result.Success ? GetLocalizationOrDefault("AuthenticatorRemoved") : GetLocalizationOrDefault("AuthenticatorNotRemoved"));
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Storage.MoveToRemoved(selectedMafile);
|
||||||
|
MaFiles.Remove(selectedMafile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Canceled", "Canceled"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
when (ExceptionHandler.Handle(ex, handleAllExceptions: true))
|
||||||
|
{
|
||||||
|
Shell.Logger.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ConfirmLogin()
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LoginConfirmationResult res = await SessionHandler.Handle(() => MaClient.ConfirmLoginRequest(SelectedMafile), SelectedMafile);
|
||||||
|
if (res.Success)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar($"{GetLocalizationOrDefault("ConfirmLoginSuccess")} {res.IP} ({res.Country})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string msg = res.Error switch
|
||||||
|
{
|
||||||
|
LoginConfirmationError.NoRequests => GetLocalizationOrDefault("ConfirmLoginFailedNoRequests"),
|
||||||
|
LoginConfirmationError.MoreThanOneRequest => GetLocalizationOrDefault("ConfirmLoginFailedMoreThanOneRequest"), //TODO
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
};
|
||||||
|
SnackbarController.SendSnackbar(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
when (ExceptionHandler.Handle(ex, handleAllExceptions: true))
|
||||||
|
{
|
||||||
|
Shell.Logger.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using SteamLib.SteamMobile;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM
|
||||||
|
{
|
||||||
|
private Timer _codeTimer;
|
||||||
|
[ObservableProperty] private double _codeProgress;
|
||||||
|
[ObservableProperty] private string _code;
|
||||||
|
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(_codeTimer))]
|
||||||
|
[MemberNotNull(nameof(_code))]
|
||||||
|
private void CreateCodeTimer()
|
||||||
|
{
|
||||||
|
var currentTime = TimeAligner.GetSteamTime();
|
||||||
|
_codeTimer = new Timer(UpdateCode, null, 0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCode(object? state = null)
|
||||||
|
{
|
||||||
|
var currentTime = TimeAligner.GetSteamTime();
|
||||||
|
var untilChange = currentTime - currentTime / 30L * 30L;
|
||||||
|
var codeProgress = untilChange / 30D * 100;
|
||||||
|
|
||||||
|
string? code = null;
|
||||||
|
if (untilChange == 0 && SelectedMafile != null)
|
||||||
|
{
|
||||||
|
code = SteamGuardCodeGenerator.GenerateCode(SelectedMafile!.SharedSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Application.Current == null) return;
|
||||||
|
Application.Current.Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
CodeProgress = codeProgress;
|
||||||
|
if (code != null) Code = code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using NebulaAuth.Utility;
|
||||||
|
using SteamLib.SteamMobile.Confirmations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM //Confirmations
|
||||||
|
{
|
||||||
|
public ObservableCollection<Confirmation> Confirmations { get; } = new();
|
||||||
|
private Mafile? _confirmationsLoadedForMafile;
|
||||||
|
public bool ConfirmationsVisible => SelectedMafile == _confirmationsLoadedForMafile;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task GetConfirmations()
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null) return;
|
||||||
|
var maf = SelectedMafile;
|
||||||
|
List<Confirmation> conf;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var confI = await SessionHandler.Handle(() => MaClient.GetConfirmations(SelectedMafile), SelectedMafile);
|
||||||
|
conf = confI.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
when (ExceptionHandler.Handle(ex))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_confirmationsLoadedForMafile = maf;
|
||||||
|
OnPropertyChanged(nameof(ConfirmationsVisible));
|
||||||
|
Confirmations.Clear();
|
||||||
|
var marketConfirmations = conf.Where(c => c.ConfType == ConfirmationType.MarketSellTransaction).ToList();
|
||||||
|
|
||||||
|
if (marketConfirmations.Count > 1)
|
||||||
|
{
|
||||||
|
var indexOfLast = conf.IndexOf(marketConfirmations.First());
|
||||||
|
foreach (var mCon in marketConfirmations)
|
||||||
|
{
|
||||||
|
conf.Remove(mCon);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mConf = new MarketMultiConfirmation(marketConfirmations.Cast<MarketConfirmation>());
|
||||||
|
conf.Insert(indexOfLast, mConf);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var con in conf)
|
||||||
|
{
|
||||||
|
Confirmations.Add(con);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
|
||||||
|
{
|
||||||
|
bool result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (confirmation is MarketMultiConfirmation multi)
|
||||||
|
{
|
||||||
|
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
when(ExceptionHandler.Handle(ex))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
Confirmations.Remove(confirmation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("ConfirmationError"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task Confirm(Confirmation? confirmation)
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null || confirmation == null) return Task.CompletedTask;
|
||||||
|
return SendConfirmation(SelectedMafile, confirmation, true);
|
||||||
|
}
|
||||||
|
[RelayCommand]
|
||||||
|
private Task Cancel(Confirmation? confirmation)
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null || confirmation == null) return Task.CompletedTask;
|
||||||
|
return SendConfirmation(SelectedMafile, confirmation, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using MaterialDesignThemes.Wpf;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.View;
|
||||||
|
using NebulaAuth.ViewModel.Other;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using AchiesUtilities.Extensions;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM //File //TODO: Refactor
|
||||||
|
{
|
||||||
|
|
||||||
|
public Settings Settings => Settings.Instance;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenMafileFolder()
|
||||||
|
{ var mafile = SelectedMafile;
|
||||||
|
|
||||||
|
var path = Storage.MafileFolder;
|
||||||
|
if (mafile?.SessionData != null)
|
||||||
|
{
|
||||||
|
var mafPath = Path.Combine(Storage.MafileFolder, mafile.SessionData.SteamId.Steam64 + ".maFile");
|
||||||
|
if (File.Exists(mafPath))
|
||||||
|
{
|
||||||
|
path = $"/e, /select, \"{mafPath}\""; ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start("explorer", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task AddMafile()
|
||||||
|
{
|
||||||
|
var openFileDialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Filter = "Mafile|*.mafile;*.maFile",
|
||||||
|
Multiselect = false,
|
||||||
|
|
||||||
|
};
|
||||||
|
var fs = openFileDialog.ShowDialog();
|
||||||
|
if (fs != true) return Task.CompletedTask;
|
||||||
|
var path = openFileDialog.FileName;
|
||||||
|
return AddMafile(new[] { path });
|
||||||
|
|
||||||
|
}
|
||||||
|
public async Task AddMafile(string[] path)
|
||||||
|
{
|
||||||
|
bool? confirmOverwrite = null;
|
||||||
|
var added = 0;
|
||||||
|
var notAdded = 0;
|
||||||
|
var errors = 0;
|
||||||
|
foreach (var str in path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Storage.AddNewMafile(str, confirmOverwrite ?? false);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
confirmOverwrite ??= await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("ConfirmMafileOverwrite"));
|
||||||
|
|
||||||
|
if (confirmOverwrite == true)
|
||||||
|
{
|
||||||
|
Storage.AddNewMafile(str, true);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
else if (confirmOverwrite == false)
|
||||||
|
{
|
||||||
|
notAdded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (SessionInvalidException ex)
|
||||||
|
{
|
||||||
|
if (path.Length == 1 && ex.Data.Contains("mafile"))
|
||||||
|
{
|
||||||
|
var mafile = (Mafile)ex.Data["mafile"]!;
|
||||||
|
if (await HandleAddMafileWithoutSession(mafile))
|
||||||
|
{
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar($"{GetLocalizationOrDefault("MafileImportError")} {Path.GetFileName(str)}{GetLocalizationOrDefault("MissingSessionInMafile")}", TimeSpan.FromSeconds(4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = GetLocalizationOrDefault("Import");
|
||||||
|
if (added > 0)
|
||||||
|
{
|
||||||
|
msg += $" {GetLocalizationOrDefault("ImportAdded")} {added}.";
|
||||||
|
}
|
||||||
|
if (notAdded > 0)
|
||||||
|
{
|
||||||
|
msg += $" {GetLocalizationOrDefault("ImportSkipped")} {notAdded}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors > 0)
|
||||||
|
{
|
||||||
|
msg += $" {GetLocalizationOrDefault("ImportErrors")} {errors}.";
|
||||||
|
}
|
||||||
|
SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> HandleAddMafileWithoutSession(Mafile data)
|
||||||
|
{
|
||||||
|
var oldMafile = SelectedMafile;
|
||||||
|
SelectedMafile = data;
|
||||||
|
await LoginAgain();
|
||||||
|
var result = data.SessionData != null;
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
var existed = MaFiles.FirstOrDefault(m => m.AccountName == data.AccountName); //TODO: more elegant way to handle overwrite
|
||||||
|
if (existed != null)
|
||||||
|
{
|
||||||
|
MaFiles.Remove(existed);
|
||||||
|
}
|
||||||
|
MaFiles.Add(data);
|
||||||
|
SelectedMafile = data;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedMafile = oldMafile;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RemoveMafile()
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null) return;
|
||||||
|
var confirm =
|
||||||
|
await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("RemoveMafileConfirmation"));
|
||||||
|
|
||||||
|
if (!confirm) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Storage.MoveToRemoved(SelectedMafile);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("CantRemoveAlreadyExist"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(
|
||||||
|
$"{GetLocalizationOrDefault("CantRemoveMafile")} {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OpenSettingsDialog()
|
||||||
|
{
|
||||||
|
var vm = new SettingsVM();
|
||||||
|
var view = new SettingsView()
|
||||||
|
{
|
||||||
|
DataContext = vm
|
||||||
|
};
|
||||||
|
await DialogHost.Show(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task CopyMafileFromBuffer()
|
||||||
|
{
|
||||||
|
StringCollection files;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
files = Clipboard.GetFileDropList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Error(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arr = files.Cast<string>().ToArray();
|
||||||
|
if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
await AddMafile(arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM //Groups
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<string> _groups = new();
|
||||||
|
|
||||||
|
|
||||||
|
public string? SelectedGroup
|
||||||
|
{
|
||||||
|
get => _selectedGroup;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedGroup, value))
|
||||||
|
PerformSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? _selectedGroup;
|
||||||
|
|
||||||
|
public string SearchText
|
||||||
|
{
|
||||||
|
get => _searchText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if(SetProperty(ref _searchText, value))
|
||||||
|
PerformSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
private string _searchText = string.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddGroup(string? value)
|
||||||
|
{
|
||||||
|
var mafile = SelectedMafile;
|
||||||
|
if (mafile == null) return;
|
||||||
|
if (string.IsNullOrEmpty(value)) return;
|
||||||
|
mafile.Group = value;
|
||||||
|
Storage.UpdateMafile(mafile);
|
||||||
|
QueryGroups();
|
||||||
|
SelectedGroup = value;
|
||||||
|
OnPropertyChanged(nameof(SelectedMafile)); //For bindings
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddToGroup(object[]? value)
|
||||||
|
{
|
||||||
|
if (value == null) return;
|
||||||
|
if (value.Length < 2) return;
|
||||||
|
var group = (string?)value[0];
|
||||||
|
var mafile = (Mafile?)value[1];
|
||||||
|
|
||||||
|
if (group == null || mafile == null) return;
|
||||||
|
mafile.Group = group;
|
||||||
|
Storage.UpdateMafile(mafile);
|
||||||
|
QueryGroups();
|
||||||
|
PerformSearch();
|
||||||
|
OnPropertyChanged(nameof(SelectedMafile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveGroup(Mafile? mafile)
|
||||||
|
{
|
||||||
|
if (mafile?.Group == null) return;
|
||||||
|
var mafGroup = mafile.Group;
|
||||||
|
mafile.Group = null;
|
||||||
|
Storage.UpdateMafile(mafile);
|
||||||
|
OnPropertyChanged(nameof(SelectedMafile)); //For bindings
|
||||||
|
QueryGroups();
|
||||||
|
if (Groups.All(g => g.Equals(mafGroup) == false))
|
||||||
|
{
|
||||||
|
SelectedGroup = null;
|
||||||
|
}
|
||||||
|
PerformSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void QueryGroups()
|
||||||
|
{
|
||||||
|
var groups = Storage.MaFiles
|
||||||
|
.Where(m => string.IsNullOrWhiteSpace(m.Group) == false)
|
||||||
|
.Select(m => m.Group)
|
||||||
|
.Distinct()
|
||||||
|
.Order()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Groups = new ObservableCollection<string>(groups!);
|
||||||
|
}
|
||||||
|
private void PerformSearch()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(SelectedGroup) && string.IsNullOrWhiteSpace(SearchText))
|
||||||
|
{
|
||||||
|
MaFiles = Storage.MaFiles;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long? searchSteamId = null;
|
||||||
|
if (long.TryParse(SearchText, out var steamId))
|
||||||
|
{
|
||||||
|
searchSteamId = steamId;
|
||||||
|
}
|
||||||
|
var query = Storage.MaFiles.AsEnumerable();
|
||||||
|
if (!string.IsNullOrWhiteSpace(SearchText))
|
||||||
|
{
|
||||||
|
query = query.Where(SearchPredicate);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(SelectedGroup))
|
||||||
|
{
|
||||||
|
query = query.Where(m => m.Group != null && m.Group.Equals(SelectedGroup));
|
||||||
|
}
|
||||||
|
var perform = query.ToList();
|
||||||
|
MaFiles = new ObservableCollection<Mafile>(perform);
|
||||||
|
if (SelectedMafile != null && !MaFiles.Contains(SelectedMafile))
|
||||||
|
{
|
||||||
|
SelectedMafile = MaFiles.FirstOrDefault();
|
||||||
|
}
|
||||||
|
bool SearchPredicate(Mafile mafile)
|
||||||
|
{
|
||||||
|
if (!mafile.AccountName.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return mafile.SessionData != null && mafile.SessionData.SteamId.Steam64.Id.Equals(searchSteamId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using NebulaAuth.Core;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM
|
||||||
|
{
|
||||||
|
private const string LOC_PATH = "MainVM";
|
||||||
|
private static string? GetLocalization(string key)
|
||||||
|
{
|
||||||
|
return LocManager.GetCodeBehind(LOC_PATH, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLocalizationOrDefault(string key)
|
||||||
|
{
|
||||||
|
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using MaterialDesignThemes.Wpf;
|
||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using NebulaAuth.View.Dialogs;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM //Other
|
||||||
|
{
|
||||||
|
private void SessionHandlerOnLoginCompleted(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var currentSession = DialogHost.GetDialogSession(null);
|
||||||
|
Application.Current.Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
if (currentSession is { Content: WaitLoginDialog, IsEnded: false })
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
currentSession.Close();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
//Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SessionHandlerOnLoginStarted(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DialogHost.IsDialogOpen(null)) return;
|
||||||
|
await Application.Current.Dispatcher.BeginInvoke(async () =>
|
||||||
|
{
|
||||||
|
await DialogHost.Show(new WaitLoginDialog());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NebulaAuth.Model.Comparers;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM
|
||||||
|
{
|
||||||
|
public ObservableCollection<MaProxy> Proxies { get; }
|
||||||
|
|
||||||
|
public MaProxy? SelectedProxy
|
||||||
|
{
|
||||||
|
get => _selectedProxy;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedProxy, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsDefaultProxy));
|
||||||
|
OnProxyChanged();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
private MaProxy? _selectedProxy;
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _proxyExist = true;
|
||||||
|
public bool IsDefaultProxy => SelectedProxy == null && MaClient.DefaultProxy != null;
|
||||||
|
private bool _handleProxyChange;
|
||||||
|
|
||||||
|
private void SetCurrentProxy()
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_selectedProxy, SelectedMafile?.Proxy) == false && _selectedProxy?.Equals(SelectedMafile?.Proxy) == false)
|
||||||
|
_handleProxyChange = true;
|
||||||
|
|
||||||
|
if (SelectedMafile == null)
|
||||||
|
{
|
||||||
|
SelectedProxy = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SelectedMafile.Proxy == null)
|
||||||
|
{
|
||||||
|
SelectedProxy = SelectedMafile.Proxy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existed = Proxies.FirstOrDefault(p => p.Id == SelectedMafile.Proxy.Id);
|
||||||
|
|
||||||
|
|
||||||
|
if (existed == null || ProxyDataComparer.Equal(existed.Data, SelectedMafile.Proxy.Data) == false)
|
||||||
|
{
|
||||||
|
SelectedProxy = SelectedMafile.Proxy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedProxy = existed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckProxyExist();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckProxyExist()
|
||||||
|
{
|
||||||
|
if (SelectedProxy == null)
|
||||||
|
{
|
||||||
|
ProxyExist = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedId = SelectedProxy.Id;
|
||||||
|
ProxyExist = ProxyStorage.Proxies.TryGetValue(selectedId, out var existedProxy)
|
||||||
|
&& ProxyStorage.CompareProxy(SelectedProxy.Data, existedProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OpenProxyManager()
|
||||||
|
{
|
||||||
|
await DialogsController.ShowProxyManager(SelectedProxy);
|
||||||
|
var oldSelection = SelectedProxy;
|
||||||
|
Proxies.Clear();
|
||||||
|
foreach (var kvp in ProxyStorage.Proxies)
|
||||||
|
{
|
||||||
|
Proxies.Add(new MaProxy(kvp.Key, kvp.Value));
|
||||||
|
}
|
||||||
|
SelectedProxy = oldSelection;
|
||||||
|
SetCurrentProxy();
|
||||||
|
OnPropertyChanged(nameof(IsDefaultProxy));
|
||||||
|
_handleProxyChange = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveProxy()
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null) return;
|
||||||
|
SelectedMafile.Proxy = null;
|
||||||
|
SelectedProxy = null;
|
||||||
|
Storage.UpdateMafile(SelectedMafile);
|
||||||
|
ProxyExist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnProxyChanged()
|
||||||
|
{
|
||||||
|
if (_handleProxyChange)
|
||||||
|
{
|
||||||
|
_handleProxyChange = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectedMafile == null) return;
|
||||||
|
ProxyExist = true;
|
||||||
|
SelectedMafile.Proxy = SelectedProxy;
|
||||||
|
Storage.UpdateMafile(SelectedMafile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using NebulaAuth.Utility;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.SteamMobile.Confirmations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
|
public partial class MainVM //Timer
|
||||||
|
{
|
||||||
|
private readonly Timer _confirmTimer;
|
||||||
|
[ObservableProperty] private bool _marketTimerEnabled;
|
||||||
|
[ObservableProperty] private bool _tradeTimerEnabled;
|
||||||
|
private int _timerCheckSeconds = Settings.Instance.TimerSeconds;
|
||||||
|
public int TimerCheckSeconds
|
||||||
|
{
|
||||||
|
get => _timerCheckSeconds;
|
||||||
|
set => SetTimer(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async void ConfirmByTimer(object? state = null)
|
||||||
|
{
|
||||||
|
if (SelectedMafile == null)
|
||||||
|
return;
|
||||||
|
var selected = SelectedMafile;
|
||||||
|
if (InterruptTimer(selected, null)) return;
|
||||||
|
List<Confirmation> conf;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
conf = (await HandleTimerRequest(() => MaClient.GetConfirmations(selected), SelectedMafile)).ToList();
|
||||||
|
}
|
||||||
|
catch (ApplicationException ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Warn(ex, "Error GetConf in timer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (InterruptTimer(selected, conf)) return;
|
||||||
|
var toConfirm = new List<Confirmation>();
|
||||||
|
if (MarketTimerEnabled)
|
||||||
|
{
|
||||||
|
var market = conf.Where(c => c.ConfType == ConfirmationType.MarketSellTransaction);
|
||||||
|
toConfirm.AddRange(market);
|
||||||
|
}
|
||||||
|
if (TradeTimerEnabled)
|
||||||
|
{
|
||||||
|
var trade = conf.Where(c => c.ConfType == ConfirmationType.Trade);
|
||||||
|
toConfirm.AddRange(trade);
|
||||||
|
}
|
||||||
|
if (InterruptTimer(selected, toConfirm)) return;
|
||||||
|
bool result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Shell.Logger.Debug("Sending confirmations. Count: {count}", toConfirm.Count);
|
||||||
|
result = await HandleTimerRequest(() => MaClient.SendMultipleConfirmation(SelectedMafile, toConfirm, confirm: true), SelectedMafile);
|
||||||
|
}
|
||||||
|
catch (ApplicationException ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Warn(ex, "MultiConf error in Timer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SnackbarController.SendSnackbar(result ? $"{GetLocalizationOrDefault("TimerConfirmed")} {toConfirm.Count}" : GetLocalizationOrDefault("TimerNotConfirmed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool InterruptTimer(Mafile cachedValue, List<Confirmation>? confirmations)
|
||||||
|
{
|
||||||
|
return SelectedMafile == null
|
||||||
|
|| (MarketTimerEnabled || TradeTimerEnabled) == false
|
||||||
|
|| SelectedMafile != cachedValue
|
||||||
|
|| confirmations is { Count: 0 };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OffTimer(bool dispatcher)
|
||||||
|
{
|
||||||
|
if (dispatcher)
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.BeginInvoke(new Action(OffTimerInline));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OffTimerInline();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
void OffTimerInline()
|
||||||
|
{
|
||||||
|
MarketTimerEnabled = false;
|
||||||
|
TradeTimerEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> HandleTimerRequest<T>(Func<Task<T>> func, Mafile mafile)
|
||||||
|
{
|
||||||
|
Exception innerException;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await SessionHandler.Handle(func, mafile);
|
||||||
|
}
|
||||||
|
catch (SessionInvalidException ex)
|
||||||
|
{
|
||||||
|
Shell.Logger.Warn("Session error while requesting in timer. Timer disabled");
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("TimerSessionError"));
|
||||||
|
OffTimer(dispatcher: true);
|
||||||
|
innerException = ex;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ExceptionHandler.Handle(ex, GetLocalizationOrDefault("TimerPrefix")))
|
||||||
|
{
|
||||||
|
innerException = ex;
|
||||||
|
}
|
||||||
|
throw new ApplicationException("Swallowed", innerException);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void SetTimer(int value)
|
||||||
|
{
|
||||||
|
if (_timerCheckSeconds == value) return;
|
||||||
|
if (value < 10)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("TimerTooFast"));
|
||||||
|
OnPropertyChanged(nameof(TimerCheckSeconds));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.TimerSeconds = value;
|
||||||
|
_timerCheckSeconds = value;
|
||||||
|
OnPropertyChanged(nameof(TimerCheckSeconds));
|
||||||
|
_confirmTimer.Change(value * 1000, value * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
using AchiesUtilities.Collections;
|
||||||
|
using AchiesUtilities.Web.Proxy;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using MaterialDesignThemes.Wpf;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using NebulaAuth.Model.Entities;
|
||||||
|
using NebulaAuth.Utility;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using NLog;
|
||||||
|
using SteamLib;
|
||||||
|
using SteamLib.Account;
|
||||||
|
using SteamLib.Authentication;
|
||||||
|
using SteamLib.Authentication.LoginV2;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.Exceptions.Mobile;
|
||||||
|
using SteamLib.ProtoCore.Exceptions;
|
||||||
|
using SteamLib.SteamMobile.AuthenticatorLinker;
|
||||||
|
using SteamLib.Web;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel.Other;
|
||||||
|
|
||||||
|
public partial class LinkAccountVM : ObservableObject, IEmailProvider, IPhoneNumberProvider, ISmsCodeProvider
|
||||||
|
{
|
||||||
|
private const string LOCALIZATION_KEY = "LinkVM";
|
||||||
|
private static Logger Logger => Shell.Logger;
|
||||||
|
private static Microsoft.Extensions.Logging.ILogger Logger2 => Shell.ExtensionsLogger;
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsPasswordFieldVisible))]
|
||||||
|
private bool _isLogin;
|
||||||
|
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isEmailCode;
|
||||||
|
[ObservableProperty] private bool _isPhoneNumber;
|
||||||
|
[ObservableProperty] private bool _isEmailConfirmation;
|
||||||
|
[ObservableProperty] private bool _isLinkCode;
|
||||||
|
[ObservableProperty] private bool _isCompleted;
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isFieldVisible = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[ObservableProperty] private string _fieldText;
|
||||||
|
[ObservableProperty] private string _passFieldText;
|
||||||
|
[ObservableProperty] private string _hintText = GetLocalizationOrDefault("EnterLoginAndPassword");
|
||||||
|
|
||||||
|
private TaskCompletionSource<string> _emailCodeTcs = new();
|
||||||
|
private TaskCompletionSource<long?> _phoneNumberTcs = new();
|
||||||
|
private TaskCompletionSource _emailConfTcs = new();
|
||||||
|
private TaskCompletionSource<string> _linkCodeTcs = new();
|
||||||
|
|
||||||
|
private bool isLinkStarted;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ProceedCommand))]
|
||||||
|
private bool _canProceed = true;
|
||||||
|
public bool IsPasswordFieldVisible => !IsLogin;
|
||||||
|
|
||||||
|
private LoginV2ExecutorOptions _loginV2ExecutorOptions;
|
||||||
|
private SteamAuthenticatorLinker _linker;
|
||||||
|
private MobileSessionData _sessionData;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region HttpClient
|
||||||
|
|
||||||
|
private static HttpClient Client { get; }
|
||||||
|
private static HttpClientHandler Handler { get; }
|
||||||
|
private static DynamicProxy Proxy { get; }
|
||||||
|
public ObservableDictionary<int, ProxyData> Proxies => ProxyStorage.Proxies;
|
||||||
|
|
||||||
|
public KeyValuePair<int, ProxyData>? SelectedProxy
|
||||||
|
{
|
||||||
|
get => _selectedProxy;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _selectedProxy, value);
|
||||||
|
SetProxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private KeyValuePair<int, ProxyData>? _selectedProxy;
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
public LinkAccountVM()
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
{
|
||||||
|
if (MaClient.DefaultProxy != null)
|
||||||
|
{
|
||||||
|
var def = Proxies.FirstOrDefault(p => p.Value.Equals(MaClient.DefaultProxy));
|
||||||
|
if (def.Value != null!)
|
||||||
|
{
|
||||||
|
SelectedProxy = def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(AllowConcurrentExecutions = true, CanExecute = nameof(CanProceed))]
|
||||||
|
public async Task Proceed()
|
||||||
|
{
|
||||||
|
if (IsCompleted)
|
||||||
|
DialogHost.Close(null);
|
||||||
|
|
||||||
|
CanProceed = false;
|
||||||
|
|
||||||
|
#region Login
|
||||||
|
|
||||||
|
if (IsLogin == false)
|
||||||
|
{
|
||||||
|
SetProxy();
|
||||||
|
ClearCookies();
|
||||||
|
_loginV2ExecutorOptions = new LoginV2ExecutorOptions(LoginV2Executor.NullConsumer, Client)
|
||||||
|
{
|
||||||
|
DeviceDetails = LoginV2ExecutorOptions.GetMobileDefaultDevice(),
|
||||||
|
WebsiteId = "Mobile",
|
||||||
|
EmailAuthProvider = this,
|
||||||
|
Logger = Logger2
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsLogin = true;
|
||||||
|
var userName = FieldText;
|
||||||
|
var pass = PassFieldText;
|
||||||
|
FieldText = string.Empty;
|
||||||
|
IsFieldVisible = false;
|
||||||
|
HintText = string.Empty;
|
||||||
|
_sessionData = (MobileSessionData)await LoginV2Executor.DoLogin(_loginV2ExecutorOptions, userName, pass);
|
||||||
|
Handler.CookieContainer.SetSteamMobileCookies(_sessionData);
|
||||||
|
IsEmailCode = true;
|
||||||
|
}
|
||||||
|
catch (EResultException ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Link exception on login");
|
||||||
|
HintText = GetLocalizationOrDefault("CantLogin") + ErrorTranslatorHelper.TranslateEResult(ex.Result);
|
||||||
|
InvokeOnDispatcher(ResetState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (LoginException ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Link exception on login");
|
||||||
|
HintText = GetLocalizationOrDefault("CantLogin") + ErrorTranslatorHelper.TranslateLoginError(ex.Error);
|
||||||
|
InvokeOnDispatcher(ResetState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Link exception on login");
|
||||||
|
HintText = GetLocalizationOrDefault("CantLogin") + ex.Message;
|
||||||
|
InvokeOnDispatcher(ResetState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEmailCode == false)
|
||||||
|
{
|
||||||
|
_emailCodeTcs.SetResult(FieldText);
|
||||||
|
HintText = string.Empty;
|
||||||
|
FieldText = string.Empty;
|
||||||
|
_emailCodeTcs = new TaskCompletionSource<string>();
|
||||||
|
IsFieldVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
if (isLinkStarted)
|
||||||
|
goto linkStarted;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isLinkStarted = true;
|
||||||
|
var linkOptions = new LinkOptions(Client, LoginV2Executor.NullConsumer, this,
|
||||||
|
null, this, this, backupHandler: Backup, Logger2);
|
||||||
|
_linker = new SteamAuthenticatorLinker(linkOptions);
|
||||||
|
var result = await _linker.LinkAccount(_sessionData);
|
||||||
|
IsLinkCode = true;
|
||||||
|
IsCompleted = true;
|
||||||
|
var mafile = Mafile.FromMobileDataExtended(result);
|
||||||
|
Storage.SaveMafile(mafile);
|
||||||
|
File.Delete(Path.Combine("mafiles_backup", mafile.AccountName + ".mafile"));
|
||||||
|
HintText =
|
||||||
|
string.Format(GetLocalizationOrDefault("MafileLinked"),
|
||||||
|
mafile.RevocationCode,
|
||||||
|
mafile.SessionData?.SteamId.Steam64);
|
||||||
|
|
||||||
|
CanProceed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (AuthenticatorLinkerException ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Link exception");
|
||||||
|
HintText = $"{GetLocalizationCommon("Error")}: {ErrorTranslatorHelper.TranslateLinkerError(ex.Error)}";
|
||||||
|
InvokeOnDispatcher(ResetState);
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
var msg = ex.StatusCode?.ToString() ?? ex.Message;
|
||||||
|
HintText = $"{GetLocalizationCommon("RequestError")}: {msg}";
|
||||||
|
InvokeOnDispatcher(ResetState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (EResultException ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Link exception");
|
||||||
|
HintText = GetLocalizationOrDefault("ErrorWithCode") + ErrorTranslatorHelper.TranslateEResult(ex.Result);
|
||||||
|
InvokeOnDispatcher(ResetState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Link exception");
|
||||||
|
HintText = GetLocalizationOrDefault("UnknownError") + ex.Message;
|
||||||
|
InvokeOnDispatcher(ResetState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
linkStarted:
|
||||||
|
if (IsPhoneNumber == false)
|
||||||
|
{
|
||||||
|
var phoneText = FieldText;
|
||||||
|
FieldText = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(phoneText))
|
||||||
|
{
|
||||||
|
HintText = string.Empty;
|
||||||
|
IsFieldVisible = false;
|
||||||
|
_phoneNumberTcs.SetResult(null);
|
||||||
|
_phoneNumberTcs = new();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(phoneText) && phoneText.Length >= 4 && long.TryParse(phoneText, out var phone))
|
||||||
|
{
|
||||||
|
HintText = string.Empty;
|
||||||
|
IsFieldVisible = false;
|
||||||
|
_phoneNumberTcs.SetResult(phone);
|
||||||
|
_phoneNumberTcs = new();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HintText = GetLocalizationOrDefault("PleaseEnterCorrectPhone");
|
||||||
|
CanProceed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEmailConfirmation == false)
|
||||||
|
{
|
||||||
|
HintText = string.Empty;
|
||||||
|
_emailConfTcs.SetResult();
|
||||||
|
_emailConfTcs = new();
|
||||||
|
CanProceed = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsLinkCode == false)
|
||||||
|
{
|
||||||
|
var linkCode = FieldText;
|
||||||
|
FieldText = string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(linkCode) && linkCode.Length >= 4)
|
||||||
|
{
|
||||||
|
HintText = string.Empty;
|
||||||
|
IsFieldVisible = false;
|
||||||
|
_linkCodeTcs.SetResult(linkCode);
|
||||||
|
_linkCodeTcs = new();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HintText = GetLocalizationOrDefault("PleaseEnterCorrectCode");
|
||||||
|
CanProceed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void ResetProxy()
|
||||||
|
{
|
||||||
|
if (IsPasswordFieldVisible == false) return;
|
||||||
|
SelectedProxy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvokeOnDispatcher(Action action)
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.BeginInvoke(action, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetState()
|
||||||
|
{
|
||||||
|
PassFieldText = string.Empty;
|
||||||
|
IsLogin = false;
|
||||||
|
IsFieldVisible = true;
|
||||||
|
IsEmailCode = false;
|
||||||
|
isLinkStarted = false;
|
||||||
|
IsPhoneNumber = false;
|
||||||
|
IsEmailConfirmation = false;
|
||||||
|
CanProceed = true;
|
||||||
|
_emailCodeTcs = new TaskCompletionSource<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Backup(MobileDataExtended data)
|
||||||
|
{
|
||||||
|
if (Directory.Exists("mafiles_backup") == false)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory("mafiles_backup");
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(data, Formatting.Indented);
|
||||||
|
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"), json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Providers
|
||||||
|
|
||||||
|
public int MaxRetryCount { get; }
|
||||||
|
public Task<string> GetEmailAuthCode(ILoginConsumer caller)
|
||||||
|
{
|
||||||
|
CanProceed = true;
|
||||||
|
HintText = GetLocalizationOrDefault("EnterEmailCode");
|
||||||
|
IsFieldVisible = true;
|
||||||
|
return _emailCodeTcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetAddAuthenticatorCode(ILoginConsumer caller)
|
||||||
|
{
|
||||||
|
IsPhoneNumber = true;
|
||||||
|
IsEmailConfirmation = true;
|
||||||
|
CanProceed = true;
|
||||||
|
HintText = GetLocalizationOrDefault("EnterEmailCode");
|
||||||
|
IsFieldVisible = true;
|
||||||
|
return _linkCodeTcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ConfirmEmailLink(ILoginConsumer caller, EmailConfirmationType confirmationType)
|
||||||
|
{
|
||||||
|
IsPhoneNumber = true;
|
||||||
|
CanProceed = true;
|
||||||
|
HintText = GetLocalizationOrDefault("ClickOnEmailLink");
|
||||||
|
return _emailConfTcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<long?> GetPhoneNumber(ILoginConsumer caller)
|
||||||
|
{
|
||||||
|
CanProceed = true;
|
||||||
|
HintText = GetLocalizationOrDefault("EnterPhoneNumber");
|
||||||
|
IsFieldVisible = true;
|
||||||
|
return _phoneNumberTcs.Task;
|
||||||
|
}
|
||||||
|
public async Task<int> GetSmsCode(ILoginConsumer caller, long? phoneNumber, string? hint)
|
||||||
|
{
|
||||||
|
IsPhoneNumber = true;
|
||||||
|
IsEmailConfirmation = true;
|
||||||
|
CanProceed = true;
|
||||||
|
HintText = string.Format(GetLocalizationOrDefault("PhoneHint"), hint);
|
||||||
|
IsFieldVisible = true;
|
||||||
|
var code = await _linkCodeTcs.Task;
|
||||||
|
return int.Parse(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Client
|
||||||
|
|
||||||
|
static LinkAccountVM()
|
||||||
|
{
|
||||||
|
Proxy = new DynamicProxy(null);
|
||||||
|
var clientPair = ClientBuilder.BuildMobileClient(Proxy, null);
|
||||||
|
Client = clientPair.Client;
|
||||||
|
Handler = clientPair.Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearCookies()
|
||||||
|
{
|
||||||
|
Handler.CookieContainer.ClearMobileSessionCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetProxy()
|
||||||
|
{
|
||||||
|
Proxy.SetData(SelectedProxy?.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
private static string GetLocalizationOrDefault(string key)
|
||||||
|
{
|
||||||
|
return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLocalizationCommon(string key)
|
||||||
|
{
|
||||||
|
return LocManager.GetCommonOrDefault(key, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel.Other;
|
||||||
|
|
||||||
|
public partial class LoginAgainVM : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _password = null!;
|
||||||
|
[ObservableProperty] private bool _savePassword;
|
||||||
|
[ObservableProperty] private string _userName = null!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
using AchiesUtilities.Collections;
|
||||||
|
using AchiesUtilities.Web.Proxy;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel.Other;
|
||||||
|
|
||||||
|
public partial class ProxyManagerVM : ObservableObject
|
||||||
|
{
|
||||||
|
private const string LOCALIZATION_KEY = "ProxyManagerVM";
|
||||||
|
|
||||||
|
[ObservableProperty] private KeyValuePair<int, ProxyData>? _selectedProxy;
|
||||||
|
[ObservableProperty] private string _addProxyField = string.Empty;
|
||||||
|
[ObservableProperty] private KeyValuePair<int, ProxyData>? _defaultProxy;
|
||||||
|
public ObservableDictionary<int, ProxyData> Proxies => ProxyStorage.Proxies;
|
||||||
|
|
||||||
|
private static readonly Regex IdRegex = new(@"(?:\{(\d+)\})");
|
||||||
|
|
||||||
|
|
||||||
|
public ProxyManagerVM()
|
||||||
|
{
|
||||||
|
if (MaClient.DefaultProxy != null)
|
||||||
|
DefaultProxy = Proxies.FirstOrDefault(kvp => kvp.Value.Equals(MaClient.DefaultProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddProxy()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(AddProxyField)) return;
|
||||||
|
if (AddProxyField.Contains(Environment.NewLine))
|
||||||
|
{
|
||||||
|
var split = AddProxyField.Split(Environment.NewLine);
|
||||||
|
var idPresent = (bool?)null;
|
||||||
|
var proxies = new List<KeyValuePair<int?, ProxyData>>();
|
||||||
|
var i = 0;
|
||||||
|
foreach (var str in split)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
int? id = null;
|
||||||
|
var match = IdRegex.Match(str);
|
||||||
|
if (match.Success) id = int.Parse(match.Groups[1].Value);
|
||||||
|
idPresent ??= match.Success;
|
||||||
|
|
||||||
|
if (idPresent.Value != match.Success)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("WrongFormatSomeIdsMissing"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var proxy = ProxyData.Parse(str, ProxyStorage.FORMAT);
|
||||||
|
if (id != null && proxies.Any(kvp => kvp.Key == id))
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(string.Format(GetLocalizationOrDefault("DuplicateId"), id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
proxies.Add(new KeyValuePair<int?, ProxyData>(id, proxy));
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(string.Format(GetLocalizationOrDefault("WrongFormatOnLine"), i));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in proxies)
|
||||||
|
{
|
||||||
|
ProxyStorage.SetProxy(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int? id = null;
|
||||||
|
var input = AddProxyField;
|
||||||
|
if (IdRegex.IsMatch(AddProxyField))
|
||||||
|
{
|
||||||
|
id = int.Parse(IdRegex.Match(AddProxyField).Groups[1].Value);
|
||||||
|
input = IdRegex.Replace(input, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyData data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = ProxyData.Parse(input, ProxyStorage.FORMAT);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
SnackbarController.SendSnackbar(GetLocalizationOrDefault("WrongFormat"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyStorage.SetProxy(id, data);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
AddProxyField = string.Empty;
|
||||||
|
CheckIfDefaultProxyStay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckIfDefaultProxyStay()
|
||||||
|
{
|
||||||
|
if (!DefaultProxy.HasValue || Proxies.Any(kvp => kvp.Equals(DefaultProxy.Value))) return;
|
||||||
|
DefaultProxy = null;
|
||||||
|
MaClient.DefaultProxy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveProxy()
|
||||||
|
{
|
||||||
|
if (SelectedProxy == null) return;
|
||||||
|
ProxyStorage.RemoveProxy(SelectedProxy.Value.Key);
|
||||||
|
CheckIfDefaultProxyStay();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void SetDefault()
|
||||||
|
{
|
||||||
|
if (SelectedProxy == null) return;
|
||||||
|
DefaultProxy = SelectedProxy;
|
||||||
|
MaClient.DefaultProxy = SelectedProxy.Value.Value;
|
||||||
|
ProxyStorage.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveDefault()
|
||||||
|
{
|
||||||
|
DefaultProxy = null;
|
||||||
|
MaClient.DefaultProxy = null;
|
||||||
|
ProxyStorage.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CopyProxy(ProxyData? obj)
|
||||||
|
{
|
||||||
|
if (obj == null) return;
|
||||||
|
Clipboard.SetText(obj.ToString(ProxyStorage.FORMAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CopyProxyAddress(ProxyData? obj)
|
||||||
|
{
|
||||||
|
if (obj == null) return;
|
||||||
|
Clipboard.SetText(obj.ToString(ProxyStorage.ADDRESS_FORMAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLocalizationOrDefault(string key)
|
||||||
|
{
|
||||||
|
return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel.Other;
|
||||||
|
|
||||||
|
public partial class SetEncryptPasswordVM : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string? _password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using NebulaAuth.Model;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using NebulaAuth.Core;
|
||||||
|
|
||||||
|
namespace NebulaAuth.ViewModel.Other;
|
||||||
|
|
||||||
|
public partial class SettingsVM : ObservableObject
|
||||||
|
{
|
||||||
|
public Settings Settings => Settings.Instance;
|
||||||
|
|
||||||
|
public bool DisableTimersOnChange
|
||||||
|
{
|
||||||
|
get => Settings.DisableTimersOnChange;
|
||||||
|
set => Settings.DisableTimersOnChange = value;
|
||||||
|
}
|
||||||
|
public BackgroundMode BackgroundMode
|
||||||
|
{
|
||||||
|
get => Settings.BackgroundMode;
|
||||||
|
set => Settings.BackgroundMode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HideToTray
|
||||||
|
{
|
||||||
|
get => Settings.HideToTray;
|
||||||
|
set => Settings.HideToTray = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<BackgroundMode, string> BackgroundModes => new()
|
||||||
|
{
|
||||||
|
{BackgroundMode.Default, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "Default")},
|
||||||
|
{BackgroundMode.Custom, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "Custom")},
|
||||||
|
{BackgroundMode.Color, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "NoBackground")},
|
||||||
|
};
|
||||||
|
|
||||||
|
public Dictionary<LocalizationLanguage, string> Languages { get; } = new()
|
||||||
|
{
|
||||||
|
{LocalizationLanguage.English, "English"},
|
||||||
|
{LocalizationLanguage.Russian, "Русский"},
|
||||||
|
{LocalizationLanguage.Ukrainian, "Українська"}
|
||||||
|
};
|
||||||
|
|
||||||
|
public Color? BackgroundColor
|
||||||
|
{
|
||||||
|
get => Settings.BackgroundColor;
|
||||||
|
set => Settings.BackgroundColor = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color? IconColor
|
||||||
|
{
|
||||||
|
get => Settings.IconColor;
|
||||||
|
set => Settings.IconColor = value;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseIcon
|
||||||
|
{
|
||||||
|
get => IconColor != null;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == false)
|
||||||
|
IconColor = null;
|
||||||
|
else
|
||||||
|
IconColor = Color.FromRgb(202, 39, 39);
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(IconColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseBackground
|
||||||
|
{
|
||||||
|
get => BackgroundColor != null;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == false)
|
||||||
|
BackgroundColor = null;
|
||||||
|
else
|
||||||
|
BackgroundColor = Color.FromRgb(202, 39, 39);
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(BackgroundColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public LocalizationLanguage Language
|
||||||
|
{
|
||||||
|
get => Settings.Language;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Settings.Language = value;
|
||||||
|
LocManager.SetApplicationLocalization(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool LegacyMode
|
||||||
|
{
|
||||||
|
get => Settings.LegacyMode;
|
||||||
|
set => Settings.LegacyMode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AllowAutoUpdate
|
||||||
|
{
|
||||||
|
get => Settings.AllowAutoUpdate;
|
||||||
|
set => Settings.AllowAutoUpdate = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty] private string _password;
|
||||||
|
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void SetPassword()
|
||||||
|
{
|
||||||
|
PHandler.SetPassword(Password);
|
||||||
|
Settings.IsPasswordSet = PHandler.IsPasswordSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,949 @@
|
|||||||
|
{
|
||||||
|
"LanguageName": {
|
||||||
|
"en": "English",
|
||||||
|
"ru": "Русский",
|
||||||
|
"ua": "Українська"
|
||||||
|
},
|
||||||
|
"LanguageWord": {
|
||||||
|
"en": "Language",
|
||||||
|
"ru": "Язык",
|
||||||
|
"ua": "Мова"
|
||||||
|
},
|
||||||
|
"Common": {
|
||||||
|
"No": {
|
||||||
|
"en": "No",
|
||||||
|
"ru": "Нет",
|
||||||
|
"ua": "Ні"
|
||||||
|
},
|
||||||
|
"Yes": {
|
||||||
|
"en": "Yes",
|
||||||
|
"ru": "Да",
|
||||||
|
"ua": "Так"
|
||||||
|
},
|
||||||
|
"Error": {
|
||||||
|
"en": "Error",
|
||||||
|
"ru": "Ошибка",
|
||||||
|
"ua": "Помилка"
|
||||||
|
},
|
||||||
|
"RequestError": {
|
||||||
|
"en": "Request error",
|
||||||
|
"ru": "Ошибка запроса",
|
||||||
|
"ua": "Помилка запиту"
|
||||||
|
},
|
||||||
|
"Canceled": {
|
||||||
|
"en": "Canceled",
|
||||||
|
"ru": "Отменено",
|
||||||
|
"ua": "Скасовано"
|
||||||
|
},
|
||||||
|
"Abbreviations": {
|
||||||
|
"Time": {
|
||||||
|
"Seconds": {
|
||||||
|
"en": "sec",
|
||||||
|
"ru": "сек",
|
||||||
|
"ua": "сек"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Count": {
|
||||||
|
"Items": {
|
||||||
|
"en": "items",
|
||||||
|
"ru": "шт",
|
||||||
|
"ua": "шт"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MainWindow": {
|
||||||
|
"Menu": {
|
||||||
|
"File": {
|
||||||
|
"Caption": {
|
||||||
|
"en": "File",
|
||||||
|
"ru": "Файл",
|
||||||
|
"ua": "Файл"
|
||||||
|
},
|
||||||
|
"Import": {
|
||||||
|
"en": "Import",
|
||||||
|
"ru": "Импорт",
|
||||||
|
"ua": "Імпорт"
|
||||||
|
},
|
||||||
|
"Remove": {
|
||||||
|
"en": "Remove",
|
||||||
|
"ru": "Удалить",
|
||||||
|
"ua": "Видалити"
|
||||||
|
},
|
||||||
|
"OpenFolder": {
|
||||||
|
"en": "Open folder",
|
||||||
|
"ru": "Открыть папку",
|
||||||
|
"ua": "Відкрити папку"
|
||||||
|
},
|
||||||
|
"Settings": {
|
||||||
|
"en": "Settings",
|
||||||
|
"ru": "Настройки",
|
||||||
|
"ua": "Налаштування"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Account": {
|
||||||
|
"Caption": {
|
||||||
|
"en": "Account",
|
||||||
|
"ru": "Аккаунт",
|
||||||
|
"ua": "Аккаунт"
|
||||||
|
},
|
||||||
|
"Link": {
|
||||||
|
"en": "Link",
|
||||||
|
"ru": "Привязать",
|
||||||
|
"ua": "Прив'язати"
|
||||||
|
},
|
||||||
|
"Unlink": {
|
||||||
|
"en": "Unlink",
|
||||||
|
"ru": "Отвязать",
|
||||||
|
"ua": "Відв'язати"
|
||||||
|
},
|
||||||
|
"RefreshSession": {
|
||||||
|
"en": "Refresh session",
|
||||||
|
"ru": "Обновить сессию",
|
||||||
|
"ua": "Оновити сесію"
|
||||||
|
},
|
||||||
|
"LoginAgain": {
|
||||||
|
"en": "Login",
|
||||||
|
"ru": "Войти",
|
||||||
|
"ua": "Увійти"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AppBar": {
|
||||||
|
"GroupsHint": {
|
||||||
|
"en": "Groups",
|
||||||
|
"ru": "Группы",
|
||||||
|
"ua": "Групи"
|
||||||
|
},
|
||||||
|
"Proxy": {
|
||||||
|
"ProxyHint": {
|
||||||
|
"en": "Proxy",
|
||||||
|
"ru": "Прокси",
|
||||||
|
"ua": "Проксі"
|
||||||
|
},
|
||||||
|
"ProxyOpenManagerHint": {
|
||||||
|
"en": "Open proxy manager",
|
||||||
|
"ru": "Открыть менеджер прокси",
|
||||||
|
"ua": "Відкрити менеджер проксі"
|
||||||
|
},
|
||||||
|
"ProxyAlert": {
|
||||||
|
"DefaultInUse": {
|
||||||
|
"en": "Default proxy is in use",
|
||||||
|
"ru": "Используется прокси по умолчанию",
|
||||||
|
"ua": "Використовується проксі за замовчуванням"
|
||||||
|
},
|
||||||
|
"MafileProxyInUse": {
|
||||||
|
"en": "Mafile proxy is in use",
|
||||||
|
"ru": "Используется прокси из mafile",
|
||||||
|
"ua": "Використовується проксі з mafile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"TradeTimerHint": {
|
||||||
|
"en": "Trade",
|
||||||
|
"ru": "Трейд",
|
||||||
|
"ua": "Трейд"
|
||||||
|
},
|
||||||
|
"MarketTimerHint": {
|
||||||
|
"en": "Market",
|
||||||
|
"ru": "Маркет",
|
||||||
|
"ua": "Маркет"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LeftPart": {
|
||||||
|
"SearchBoxHint": {
|
||||||
|
"en": "Search",
|
||||||
|
"ru": "Поиск",
|
||||||
|
"ua": "Пошук"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RightPart": {
|
||||||
|
"LoadConfirmations": {
|
||||||
|
"en": "Load confirmations",
|
||||||
|
"ru": "Загрузить подтверждения",
|
||||||
|
"ua": "Завантажити підтвердження"
|
||||||
|
},
|
||||||
|
"ConfirmLogin": {
|
||||||
|
"en": "Confirm login",
|
||||||
|
"ru": "Подтвердить вход",
|
||||||
|
"ua": "Підтвердити вхід"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Footer": {
|
||||||
|
"Account": {
|
||||||
|
"en": "Account: ",
|
||||||
|
"ru": "Аккаунт: ",
|
||||||
|
"ua": "Аккаунт: "
|
||||||
|
},
|
||||||
|
"Group": {
|
||||||
|
"en": "Group: ",
|
||||||
|
"ru": "Группа: ",
|
||||||
|
"ua": "Група: "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConfirmationTemplates": {
|
||||||
|
"TradeWith": {
|
||||||
|
"en": "Trade with",
|
||||||
|
"ru": "Трейд с",
|
||||||
|
"ua": "Трейд з"
|
||||||
|
},
|
||||||
|
"Recovery": {
|
||||||
|
"en": "Recovery",
|
||||||
|
"ru": "Восстановление",
|
||||||
|
"ua": "Відновлення"
|
||||||
|
},
|
||||||
|
"ApiKey": {
|
||||||
|
"en": "Api key",
|
||||||
|
"ru": "Api ключ",
|
||||||
|
"ua": "Api ключ"
|
||||||
|
},
|
||||||
|
"Market": {
|
||||||
|
"en": "Market",
|
||||||
|
"ru": "Маркет",
|
||||||
|
"ua": "Маркет"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CodeCopied": {
|
||||||
|
"ru": "Скопировано",
|
||||||
|
"en": "Copied",
|
||||||
|
"ua": "Скопійовано"
|
||||||
|
},
|
||||||
|
"ContextMenus": {
|
||||||
|
"Mafile": {
|
||||||
|
"AddToGroup": {
|
||||||
|
"en": "Add to group",
|
||||||
|
"ru": "Добавить в группу",
|
||||||
|
"ua": "Додати в групу"
|
||||||
|
},
|
||||||
|
"RemoveFromGroup": {
|
||||||
|
"en": "Remove from group",
|
||||||
|
"ru": "Удалить из группы",
|
||||||
|
"ua": "Видалити з групи"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Proxy": {
|
||||||
|
"Copy": {
|
||||||
|
"en": "Copy",
|
||||||
|
"ru": "Копировать",
|
||||||
|
"ua": "Копіювати"
|
||||||
|
},
|
||||||
|
"CopyAddress": {
|
||||||
|
"en": "Copy address",
|
||||||
|
"ru": "Копировать адрес",
|
||||||
|
"ua": "Копіювати адресу"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SettingsDialog": {
|
||||||
|
"Title": {
|
||||||
|
"en": "Settings",
|
||||||
|
"ru": "Настройки",
|
||||||
|
"ua": "Налаштування"
|
||||||
|
},
|
||||||
|
"BackgroundHint": {
|
||||||
|
"en": "Background",
|
||||||
|
"ru": "Фон",
|
||||||
|
"ua": "Фон"
|
||||||
|
},
|
||||||
|
"BackgroundMode": {
|
||||||
|
"Default": {
|
||||||
|
"en": "Default",
|
||||||
|
"ru": "По умолчанию",
|
||||||
|
"ua": "За замовчуванням"
|
||||||
|
},
|
||||||
|
"Custom": {
|
||||||
|
"en": "Custom",
|
||||||
|
"ru": "Свой",
|
||||||
|
"ua": "Свій"
|
||||||
|
},
|
||||||
|
"NoBackground": {
|
||||||
|
"en": "No background",
|
||||||
|
"ru": "Без фона",
|
||||||
|
"ua": "Без фону"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DisableTimersOnSwitch": {
|
||||||
|
"en": "Disable timers on account switch",
|
||||||
|
"ru": "Отключать таймеры при смене аккаунта",
|
||||||
|
"ua": "Вимикати таймери при зміні аккаунта"
|
||||||
|
},
|
||||||
|
"MinimizeToTray": {
|
||||||
|
"en": "Minimize to tray",
|
||||||
|
"ru": "Сворачивать в трей",
|
||||||
|
"ua": "Згортати в трей"
|
||||||
|
},
|
||||||
|
"UseIndicator": {
|
||||||
|
"en": "Use indicator",
|
||||||
|
"ru": "Использовать индикатор",
|
||||||
|
"ua": "Використовувати індикатор"
|
||||||
|
},
|
||||||
|
"UseCustomColor": {
|
||||||
|
"en": "Custom window color",
|
||||||
|
"ru": "Свой цвет окна",
|
||||||
|
"ua": "Свій колір вікна"
|
||||||
|
},
|
||||||
|
"PasswordBox": {
|
||||||
|
"CurrentCryptPassword": {
|
||||||
|
"en": "Current encryption password",
|
||||||
|
"ru": "Текущий пароль шифрования",
|
||||||
|
"ua": "Поточний пароль шифрування"
|
||||||
|
},
|
||||||
|
"Hint": {
|
||||||
|
"en": "Doesn't saved on disk",
|
||||||
|
"ru": "Не сохраняется в файл",
|
||||||
|
"ua": "Не зберігається у файлах"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LegacyMafileMode": {
|
||||||
|
"en": "Legacy mafile mode",
|
||||||
|
"ru": "Режим старых мафайлов",
|
||||||
|
"ua": "Режим старих мафайлів"
|
||||||
|
},
|
||||||
|
"LegacyMafileModeHint": {
|
||||||
|
"en": "Use old mafile format for compatibility with old versions of Steam Desktop Authenticator",
|
||||||
|
"ru": "Использовать старый формат мафайлов для совместимости со старыми версиями Steam Desktop Authenticator",
|
||||||
|
"ua": "Використовувати старий формат мафайлів для сумісності зі старими версіями Steam Desktop Authenticator"
|
||||||
|
},
|
||||||
|
"AllowAutoUpdate": {
|
||||||
|
"en": "Allow auto update",
|
||||||
|
"ru": "Разрешить автообновление",
|
||||||
|
"ua": "Дозволити автооновлення"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LoginAgainDialog": {
|
||||||
|
"Title": {
|
||||||
|
"en": "Login",
|
||||||
|
"ru": "Вход",
|
||||||
|
"ua": "Вхід"
|
||||||
|
},
|
||||||
|
"LoginFor": {
|
||||||
|
"en": "Login for",
|
||||||
|
"ru": "Войти в",
|
||||||
|
"ua": "Увійти в"
|
||||||
|
},
|
||||||
|
"LoginButton": {
|
||||||
|
"en": "Login",
|
||||||
|
"ru": "Войти",
|
||||||
|
"ua": "Увійти"
|
||||||
|
},
|
||||||
|
"CancelButton": {
|
||||||
|
"en": "Cancel",
|
||||||
|
"ru": "Отмена",
|
||||||
|
"ua": "Відміна"
|
||||||
|
},
|
||||||
|
"PasswordBox": {
|
||||||
|
"en": "Password",
|
||||||
|
"ru": "Пароль",
|
||||||
|
"ua": "Пароль"
|
||||||
|
},
|
||||||
|
"SaveEncryptedPassword": {
|
||||||
|
"en": "Save encrypted password to mafile",
|
||||||
|
"ru": "Сохранить зашифрованный пароль в мафайл",
|
||||||
|
"ua": "Зберегти зашифрований пароль в мафайлі"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ProxyManagerDialog": {
|
||||||
|
"Title": {
|
||||||
|
"en": "Proxy manager",
|
||||||
|
"ru": "Менеджер прокси",
|
||||||
|
"ua": "Менеджер проксі"
|
||||||
|
},
|
||||||
|
"DefaultProxy": {
|
||||||
|
"en": "Default proxy:",
|
||||||
|
"ru": "Прокси по умолчанию:",
|
||||||
|
"ua": "Проксі за замовчуванням:"
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
"WaitLoginDialog": {
|
||||||
|
"Text": {
|
||||||
|
"en": "Login in progress. Please wait",
|
||||||
|
"ru": "Производится логин. Ожидайте",
|
||||||
|
"ua": "Виконується вхід. Зачекайте"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LinkerDialog": {
|
||||||
|
"Title": {
|
||||||
|
"en": "Linker",
|
||||||
|
"ru": "Привязка",
|
||||||
|
"ua": "Прив'язка"
|
||||||
|
},
|
||||||
|
"Proxy": {
|
||||||
|
"en": "Proxy",
|
||||||
|
"ru": "Прокси",
|
||||||
|
"ua": "Проксі"
|
||||||
|
},
|
||||||
|
"Authorization": {
|
||||||
|
"en": "Authorization",
|
||||||
|
"ru": "Авторизация",
|
||||||
|
"ua": "Авторизація"
|
||||||
|
},
|
||||||
|
"EmailCode": {
|
||||||
|
"en": "Email code",
|
||||||
|
"ru": "Код из письма",
|
||||||
|
"ua": "Код з листа"
|
||||||
|
},
|
||||||
|
"PhoneNumber": {
|
||||||
|
"en": "Phone number",
|
||||||
|
"ru": "Номер телефона",
|
||||||
|
"ua": "Номер телефону"
|
||||||
|
},
|
||||||
|
"EmailLink": {
|
||||||
|
"en": "Email link",
|
||||||
|
"ru": "Ссылка из письма",
|
||||||
|
"ua": "Посилання з листа"
|
||||||
|
},
|
||||||
|
"SmsOrCode": {
|
||||||
|
"en": "Sms or code",
|
||||||
|
"ru": "Смс или код",
|
||||||
|
"ua": "Смс або код"
|
||||||
|
},
|
||||||
|
"Completed": {
|
||||||
|
"en": "Completed",
|
||||||
|
"ru": "Завершено",
|
||||||
|
"ua": "Завершено"
|
||||||
|
},
|
||||||
|
"Message": {
|
||||||
|
"en": "Message:",
|
||||||
|
"ru": "Сообщение:",
|
||||||
|
"ua": "Повідомлення:"
|
||||||
|
},
|
||||||
|
"ProceedButton": {
|
||||||
|
"en": "Proceed",
|
||||||
|
"ru": "Продолжить",
|
||||||
|
"ua": "Продовжити"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SetEncryptedPasswordDialog": {
|
||||||
|
"DialogText": {
|
||||||
|
"en": "You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session. (Optional)",
|
||||||
|
"ru": "Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией. (Не обязательно)",
|
||||||
|
"ua": "Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією. (Не обов'язково)"
|
||||||
|
},
|
||||||
|
"Ok": {
|
||||||
|
"en": "Ok",
|
||||||
|
"ru": "Ок",
|
||||||
|
"ua": "Ок"
|
||||||
|
},
|
||||||
|
"Cancel": {
|
||||||
|
"en": "Cancel",
|
||||||
|
"ru": "Отмена",
|
||||||
|
"ua": "Відміна"
|
||||||
|
},
|
||||||
|
"Password": {
|
||||||
|
"en": "Password",
|
||||||
|
"ru": "Пароль",
|
||||||
|
"ua": "Пароль"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CodeBehind": {
|
||||||
|
"MainVM": {
|
||||||
|
"SuccessfulLogin": {
|
||||||
|
"en": "Successful login",
|
||||||
|
"ru": "Успешный вход",
|
||||||
|
"ua": "Успішний вхід"
|
||||||
|
},
|
||||||
|
"SessionRefreshed": {
|
||||||
|
"en": "Session refreshed",
|
||||||
|
"ru": "Сессия обновлена",
|
||||||
|
"ua": "Сесія оновлена"
|
||||||
|
},
|
||||||
|
"MissingRCode": {
|
||||||
|
"en": "Missing RCode",
|
||||||
|
"ru": "Отсутствует RCode",
|
||||||
|
"ua": "Відсутній RCode"
|
||||||
|
},
|
||||||
|
"ConfirmRemovingAuthenticator": {
|
||||||
|
"ru": "Вы точно уверены что хотите удалить аутентификатор?",
|
||||||
|
"en": "Are you sure you want to remove authenticator?",
|
||||||
|
"ua": "Ви точно впевнені що хочете видалити аутентифікатор?"
|
||||||
|
},
|
||||||
|
"AuthenticatorRemoved": {
|
||||||
|
"ru": "Аутентификатор удален",
|
||||||
|
"en": "Authenticator removed",
|
||||||
|
"ua": "Аутентифікатор видалено"
|
||||||
|
},
|
||||||
|
"AuthenticatorNotRemoved": {
|
||||||
|
"ru": "Что-то пошло не так",
|
||||||
|
"en": "Something went wrong",
|
||||||
|
"ua": "Щось пішло не так"
|
||||||
|
},
|
||||||
|
"ConfirmLoginSuccess": {
|
||||||
|
"ru": "Вход осуществлен:",
|
||||||
|
"en": "Login success:",
|
||||||
|
"ua": "Вхід здійснено:"
|
||||||
|
},
|
||||||
|
"ConfirmLoginFailedNoRequests": {
|
||||||
|
"ru": "Запросов на вход не обнаружено",
|
||||||
|
"en": "No login requests found",
|
||||||
|
"ua": "Запитів на вхід не виявлено"
|
||||||
|
},
|
||||||
|
"ConfirmLoginFailedMoreThanOneRequest": {
|
||||||
|
"ru": "Обнаружено несколько запросов на вход. В целях безопасности операция отменена",
|
||||||
|
"en": "Multiple login requests found. For security reasons, the operation is canceled",
|
||||||
|
"ua": "Виявлено декілька запитів на вхід. З метою безпеки операція скасована"
|
||||||
|
},
|
||||||
|
"ConfirmationError": {
|
||||||
|
"ru": "Ошибка подтверждения",
|
||||||
|
"en": "Confirmation error",
|
||||||
|
"ua": "Помилка підтвердження"
|
||||||
|
},
|
||||||
|
"ConfirmMafileOverwrite": {
|
||||||
|
"ru": "Внимание, мафайл уже присутствует в папке mafiles.\r\nПерезаписать мафайлы, которые конфликтуют?",
|
||||||
|
"en": "Attention, mafile already exists in mafiles folder.\r\nOverwrite conflicting mafiles?",
|
||||||
|
"ua": "Увага, мафайл вже присутній у папці mafiles.\r\nПерезаписати мафайли, які конфліктують?"
|
||||||
|
},
|
||||||
|
"MafileImportError": {
|
||||||
|
"ru": "Ошибка импорта мафайла",
|
||||||
|
"en": "Mafile import error",
|
||||||
|
"ua": "Помилка імпорту мафайла"
|
||||||
|
},
|
||||||
|
"MissingSessionInMafile": {
|
||||||
|
"ru": ". У него отсутствует сессия. Добавьте этот файл отдельно от остальных",
|
||||||
|
"en": ". It has no session. Add this file separately from the others",
|
||||||
|
"ua": ". В нього відсутня сесія. Додайте цей файл окремо від інших"
|
||||||
|
},
|
||||||
|
"Import": {
|
||||||
|
"ru": "Импорт",
|
||||||
|
"en": "Import",
|
||||||
|
"ua": "Імпорт"
|
||||||
|
},
|
||||||
|
"ImportAdded": {
|
||||||
|
"ru": "добавлено:",
|
||||||
|
"en": "added:",
|
||||||
|
"ua": "додано:"
|
||||||
|
},
|
||||||
|
"ImportSkipped": {
|
||||||
|
"ru": "пропущено:",
|
||||||
|
"en": "skipped:",
|
||||||
|
"ua": "пропущено:"
|
||||||
|
},
|
||||||
|
"ImportErrors": {
|
||||||
|
"ru": "ошибки:",
|
||||||
|
"en": "errors:",
|
||||||
|
"ua": "помилки:"
|
||||||
|
},
|
||||||
|
"RemoveMafileConfirmation": {
|
||||||
|
"ru": "Удалить мафайл? Это действие переместит мафайл в mafiles_removed",
|
||||||
|
"en": "Remove mafile? This action will move mafile to mafiles_removed",
|
||||||
|
"ua": "Видалити мафайл? Ця дія перемістить мафайл в mafiles_removed"
|
||||||
|
},
|
||||||
|
"CantRemoveAlreadyExist": {
|
||||||
|
"ru": "Не удалось переместить мафайл, возможно в папке уже существует мафайл с таким именем.",
|
||||||
|
"en": "Can't move mafile, maybe mafile with same name already exists in mafiles_removed folder.",
|
||||||
|
"ua": "Не вдалося перемістити мафайл, можливо в папці вже існує мафайл з таким іменем."
|
||||||
|
},
|
||||||
|
"CantRemoveMafile": {
|
||||||
|
"ru": "Не удалось удалить (переместить) мафайл:",
|
||||||
|
"en": "Can't remove mafile:",
|
||||||
|
"ua": "Не вдалося видалити (перемістити) мафайл:"
|
||||||
|
},
|
||||||
|
"TimerConfirmed": {
|
||||||
|
"ru": "Авто: подтверждено ",
|
||||||
|
"en": "Auto: confirmed ",
|
||||||
|
"ua": "Авто: підтверджено "
|
||||||
|
},
|
||||||
|
"TimerNotConfirmed": {
|
||||||
|
"ru": "Авто: подтверждение не сработало",
|
||||||
|
"en": "Auto: confirmation was unsuccessful",
|
||||||
|
"ua": "Авто: підтвердження не спрацювало"
|
||||||
|
},
|
||||||
|
"TimerSessionError": {
|
||||||
|
"ru": "Авто: необходимо обновить сессию или перелогиниться",
|
||||||
|
"en": "Auto: need to refresh session or relogin",
|
||||||
|
"ua": "Авто: необхідно оновити сесію або перелогінитися"
|
||||||
|
},
|
||||||
|
"TimerPrefix": {
|
||||||
|
"ru": "Авто: ",
|
||||||
|
"en": "Auto: ",
|
||||||
|
"ua": "Авто: "
|
||||||
|
},
|
||||||
|
"TimerTooFast": {
|
||||||
|
"ru": "Слишком быстрый таймер.",
|
||||||
|
"en": "Too fast timer.",
|
||||||
|
"ua": "Занадто швидкий таймер."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorTranslator": {
|
||||||
|
"Login": {
|
||||||
|
"CaptchaRequired": {
|
||||||
|
"en": "Captcha",
|
||||||
|
"ru": "Капча",
|
||||||
|
"ua": "Капча"
|
||||||
|
},
|
||||||
|
"InvalidCredentials": {
|
||||||
|
"en": "Invalid password",
|
||||||
|
"ru": "Неверный пароль",
|
||||||
|
"ua": "Невірний пароль"
|
||||||
|
|
||||||
|
},
|
||||||
|
"EmailAuthRequired": {
|
||||||
|
"en": "Email code required",
|
||||||
|
"ru": "Требуется код с почты",
|
||||||
|
"ua": "Потрібен код з пошти"
|
||||||
|
},
|
||||||
|
"InvalidEmailAuthCode": {
|
||||||
|
"en": "Invalid email code",
|
||||||
|
"ru": "Неверный код с почты",
|
||||||
|
"ua": "Невірний код з пошти"
|
||||||
|
},
|
||||||
|
"InvalidTwoFactorCode": {
|
||||||
|
"en": "Invalid two factor code",
|
||||||
|
"ru": "Неверный двухфакторный код",
|
||||||
|
"ua": "Невірний двофакторний код"
|
||||||
|
},
|
||||||
|
"UndefinedError": {
|
||||||
|
"en": "Undefined error",
|
||||||
|
"ru": "Неизвестная ошибка",
|
||||||
|
"ua": "Невідома помилка"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EResult": {
|
||||||
|
"InvalidPassword": {
|
||||||
|
"ru": "Неверный пароль",
|
||||||
|
"en": "Invalid password",
|
||||||
|
"ua": "Невірний пароль"
|
||||||
|
},
|
||||||
|
"InvalidState": {
|
||||||
|
"ru": "Неверное состояние (11)",
|
||||||
|
"en": "Invalid state (11)",
|
||||||
|
"ua": "Невірний стан (11)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"Timeout": {
|
||||||
|
"ru": "Таймаут (16)",
|
||||||
|
"en": "Timeout (16)",
|
||||||
|
"ua": "Таймаут (16)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"Banned": {
|
||||||
|
"ru": "Забанен (17)",
|
||||||
|
"en": "Banned (17)",
|
||||||
|
"ua": "Забанений (17)"
|
||||||
|
},
|
||||||
|
"AccountNotFound": {
|
||||||
|
"ru": "Аккаунт не найден (18)",
|
||||||
|
"en": "Account not found (18)",
|
||||||
|
"ua": "Аккаунт не знайдено (18)"
|
||||||
|
},
|
||||||
|
"InvalidSteamID": {
|
||||||
|
"ru": "Неправильный SteamID (19)",
|
||||||
|
"en": "Invalid SteamID (19)",
|
||||||
|
"ua": "Неправильний SteamID (19)"
|
||||||
|
},
|
||||||
|
"ServiceUnavailable": {
|
||||||
|
"ru": "Сервер не отвечает (20)",
|
||||||
|
"en": "Service unavailable (20)",
|
||||||
|
"ua": "Сервер не відповідає (20)"
|
||||||
|
},
|
||||||
|
"Pending": {
|
||||||
|
"ru": "Ожидание (21)",
|
||||||
|
"en": "Pending (21)",
|
||||||
|
"ua": "Очікування (21)"
|
||||||
|
},
|
||||||
|
"LimitExceeded": {
|
||||||
|
"ru": "Лимит превышен (25)",
|
||||||
|
"en": "Limit exceeded (25)",
|
||||||
|
"ua": "Ліміт перевищено (25)"
|
||||||
|
},
|
||||||
|
"Expired": {
|
||||||
|
"ru": "Истекло (27)",
|
||||||
|
"en": "Expired (27)",
|
||||||
|
"ua": "Термін дії закінчився (27)"
|
||||||
|
},
|
||||||
|
"Blocked": {
|
||||||
|
"ru": "Заблокировано (40)",
|
||||||
|
"en": "Blocked (40)",
|
||||||
|
"ua": "Заблоковано (40)"
|
||||||
|
},
|
||||||
|
"AccountDisabled": {
|
||||||
|
"ru": "Аккаунт отключен (43)",
|
||||||
|
"en": "Account disabled (43)",
|
||||||
|
"ua": "Аккаунт відключено (43)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"Suspended": {
|
||||||
|
"ru": "Отключено (Suspended 51)",
|
||||||
|
"en": "Suspended (51)",
|
||||||
|
"ua": "Відключено (Suspended 51)"
|
||||||
|
},
|
||||||
|
"Cancelled": {
|
||||||
|
"ru": "Отмена (52)",
|
||||||
|
"en": "Cancelled (52)",
|
||||||
|
"ua": "Скасовано (52)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"ExpiredLoginAuthCode": {
|
||||||
|
"ru": "Истекло время кода (71)",
|
||||||
|
"en": "Expired login auth code (71)",
|
||||||
|
"ua": "Термін дії коду закінчився (71)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"AccountLockedDown": {
|
||||||
|
"ru": "Аккаунт заблокирован (КТ 73)",
|
||||||
|
"en": "Account locked down (73)",
|
||||||
|
"ua": "Аккаунт заблоковано (КТ 73)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"UnexpectedError": {
|
||||||
|
"ru": "Неожиданная ошибка (79)",
|
||||||
|
"en": "Unexpected error (79)",
|
||||||
|
"ua": "Непередбачувана помилка (79)"
|
||||||
|
},
|
||||||
|
"Disabled": {
|
||||||
|
"ru": "Отключено (80)",
|
||||||
|
"en": "Disabled (80)",
|
||||||
|
"ua": "Відключено (80)"
|
||||||
|
},
|
||||||
|
"RegionLocked": {
|
||||||
|
"ru": "Регион заблокирован (83)",
|
||||||
|
"en": "Region locked (83)",
|
||||||
|
"ua": "Регіон заблоковано (83)"
|
||||||
|
},
|
||||||
|
"RateLimitExceeded": {
|
||||||
|
"ru": "Слишком много запросов (84)",
|
||||||
|
"en": "Rate limit exceeded (84)",
|
||||||
|
"ua": "Забагато запитів (84)"
|
||||||
|
},
|
||||||
|
"TwoFactorCodeMismatch": {
|
||||||
|
"ru": "2FA код не подошел (88)",
|
||||||
|
"en": "2FA code mismatch (88)",
|
||||||
|
"ua": "2FA код не підійшов (88)"
|
||||||
|
},
|
||||||
|
"TwoFactorActivationCodeMismatch": {
|
||||||
|
"ru": "2FA код не прошел проверку (89)",
|
||||||
|
"en": "2FA activation code mismatch (89)",
|
||||||
|
"ua": "2FA код не пройшов перевірку (89)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"TimeNotSynced": {
|
||||||
|
"ru": "Время не синхронизировано (93)",
|
||||||
|
"en": "Time not synced (93)",
|
||||||
|
"ua": "Час не синхронізовано (93)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"SMSCodeFailed": {
|
||||||
|
"ru": "Ошибка SMS-кода (94)",
|
||||||
|
"en": "SMS code failed (94)",
|
||||||
|
"ua": "Помилка SMS-коду (94)"
|
||||||
|
},
|
||||||
|
"AccountLimitExceeded": {
|
||||||
|
"ru": "Лимит аккаунта превышен (95)",
|
||||||
|
"en": "Account limit exceeded (95)",
|
||||||
|
"ua": "Ліміт аккаунта перевищено (95)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"EmailSendFailure": {
|
||||||
|
"ru": "Не удалось отправить письмо (99)",
|
||||||
|
"en": "Email send failure (99)",
|
||||||
|
"ua": "Не вдалося відправити лист (99)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"NeedCaptcha": {
|
||||||
|
"ru": "Капча (101)",
|
||||||
|
"en": "Need captcha (101)",
|
||||||
|
"ua": "Капча (101)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"IPBanned": {
|
||||||
|
"ru": "IP заблокирован (105)",
|
||||||
|
"en": "IP banned (105)",
|
||||||
|
"ua": "IP заблоковано (105)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"LimitedUserAccount": {
|
||||||
|
"ru": "Аккаунт с лимитом (112)",
|
||||||
|
"en": "Limited user account (112)",
|
||||||
|
"ua": "Аккаунт з лімітом (112)"
|
||||||
|
},
|
||||||
|
"AccountDeleted": {
|
||||||
|
"ru": "Аккаунт удален (114)",
|
||||||
|
"en": "Account deleted (114)",
|
||||||
|
"ua": "Аккаунт видалено (114)"
|
||||||
|
|
||||||
|
},
|
||||||
|
"PhoneNumberIsVOIP": {
|
||||||
|
"ru": "Номера VOIP запрещены (127)",
|
||||||
|
"en": "Phone number is VOIP (127)",
|
||||||
|
"ua": "Номера VOIP заборонені (127)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AuthenticatorLinkerError": {
|
||||||
|
"PhoneAlreadyAttached": {
|
||||||
|
"ru": "Телефон привязан, попробуйте через 20 минут",
|
||||||
|
"en": "Phone already attached, try again in 20 minutes",
|
||||||
|
"ua": "Телефон прив'язано, спробуйте через 20 хвилин"
|
||||||
|
},
|
||||||
|
"InvalidPhoneNumber": {
|
||||||
|
"ru": "Неверный номер телефона",
|
||||||
|
"en": "Invalid phone number",
|
||||||
|
"ua": "Невірний номер телефону"
|
||||||
|
},
|
||||||
|
"CantAttachPhone": {
|
||||||
|
"ru": "Не удается привязать телефон",
|
||||||
|
"en": "Can't attach phone",
|
||||||
|
"ua": "Не вдається прив'язати телефон"
|
||||||
|
},
|
||||||
|
"CantConfirmAttachingEmail": {
|
||||||
|
"ru": "Не удалось подтвердить письмо",
|
||||||
|
"en": "Can't confirm attaching email",
|
||||||
|
"ua": "Не вдалося підтвердити лист"
|
||||||
|
},
|
||||||
|
"CantSendSms": {
|
||||||
|
"ru": "Не удалось отправить SMS",
|
||||||
|
"en": "Can't send SMS",
|
||||||
|
"ua": "Не вдалося відправити SMS"
|
||||||
|
},
|
||||||
|
"AuthenticatorPresent": {
|
||||||
|
"ru": "Аутентификатор привязан",
|
||||||
|
"en": "Authenticator present",
|
||||||
|
"ua": "Аутентифікатор прив'язано"
|
||||||
|
},
|
||||||
|
"BadConfirmationCode": {
|
||||||
|
"ru": "Код не подошел",
|
||||||
|
"en": "Bad confirmation code",
|
||||||
|
"ua": "Код не підійшов"
|
||||||
|
},
|
||||||
|
"UnableToGenerateCorrectCodes": {
|
||||||
|
"ru": "Не удается сгенерировать коды",
|
||||||
|
"en": "Unable to generate correct codes",
|
||||||
|
"ua": "Не вдається згенерувати коди"
|
||||||
|
},
|
||||||
|
"InvalidStateWithStatus2": {
|
||||||
|
"ru": "Неверное состояние (InvalidState) со статусом 2. Попробуйте привязку с помощью СМС. Если СМС придет, но ошибка повторится. Нужно попробовать еще раз.",
|
||||||
|
"en": "Invalid state (InvalidState) with status 2. Try to attach with SMS. If SMS will come, but error will repeat. You need to try again.",
|
||||||
|
"ua": "Невірний стан (InvalidState) зі статусом 2. Спробуйте прив'язати з допомогою СМС. Якщо СМС прийде, але помилка повториться. Потрібно спробувати ще раз."
|
||||||
|
},
|
||||||
|
"GeneralFailure": {
|
||||||
|
"ru": "General Failure",
|
||||||
|
"en": "General Failure",
|
||||||
|
"ua": "General Failure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ExceptionHandler": {
|
||||||
|
"SessionExpiredException": {
|
||||||
|
"ru": "Сессия истекла. Попробуйте обновить ее через меню",
|
||||||
|
"en": "Session expired. Try to refresh it through menu",
|
||||||
|
"ua": "Сесія закінчилася. Спробуйте оновити її через меню"
|
||||||
|
},
|
||||||
|
"SessionInvalidException": {
|
||||||
|
"ru": "Сессия невалидна. Нужно залогиниться заново",
|
||||||
|
"en": "Session invalid. Need to login again",
|
||||||
|
"ua": "Сесія невалідна. Потрібно залогінитися знову"
|
||||||
|
},
|
||||||
|
"TaskCanceledException": {
|
||||||
|
"ru": "Произошла отмена операции",
|
||||||
|
"en": "Operation canceled",
|
||||||
|
"ua": "Операція скасована"
|
||||||
|
},
|
||||||
|
"TimeoutException": {
|
||||||
|
"ru": "Таймаут подключения. Проверьте ваше соединение с Proxy или интернетом",
|
||||||
|
"en": "Connection timeout. Check your connection with Proxy or internet",
|
||||||
|
"ua": "Таймаут підключення. Перевірте ваше з'єднання з Proxy або інтернетом"
|
||||||
|
},
|
||||||
|
"UnsupportedResponseException": {
|
||||||
|
"ru": "Получен неожиданный ответ от Steam. Результат сохранен в log.log. Свяжитесь с разработчиком",
|
||||||
|
"en": "Unexpected response from Steam. Result saved in log.log. Contact developer",
|
||||||
|
"ua": "Отримано неочікувану відповідь від Steam. Результат збережено в log.log. Зв'яжіться з розробником"
|
||||||
|
},
|
||||||
|
"LoginException": {
|
||||||
|
"ru": "Ошибка входа: ",
|
||||||
|
"en": "Login error: ",
|
||||||
|
"ua": "Помилка входу: "
|
||||||
|
},
|
||||||
|
"UnknownException": {
|
||||||
|
"ru": "Неизвестная ошибка: ",
|
||||||
|
"en": "Unknown error: ",
|
||||||
|
"ua": "Невідома помилка: "
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
"ProxyManagerVM": {
|
||||||
|
"WrongFormatSomeIdsMissing": {
|
||||||
|
"ru": "Неверный формат. Некоторые прокси не имеют ID",
|
||||||
|
"en": "Wrong format. Some proxies have no ID",
|
||||||
|
"ua": "Невірний формат. Деякі проксі не мають ID"
|
||||||
|
},
|
||||||
|
"DuplicateId": {
|
||||||
|
"ru": "Прокси с ID {0} добавлен несколько раз",
|
||||||
|
"en": "Proxy with ID {0} added multiple times",
|
||||||
|
"ua": "Проксі з ID {0} додано декілька разів"
|
||||||
|
},
|
||||||
|
"WrongFormatOnLine": {
|
||||||
|
"ru": "Прокси на строке {0} имеет неверный формат.",
|
||||||
|
"en": "Proxy on line {0} has wrong format.",
|
||||||
|
"ua": "Проксі на строці {0} має невірний формат."
|
||||||
|
},
|
||||||
|
"WrongFormat": {
|
||||||
|
"ru": "Прокси имеет неверный формат.",
|
||||||
|
"en": "Proxy has wrong format.",
|
||||||
|
"ua": "Проксі має невірний формат."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LinkVM": {
|
||||||
|
"EnterLoginAndPassword": {
|
||||||
|
"ru": "Введите логин и пароль",
|
||||||
|
"en": "Enter login and password",
|
||||||
|
"ua": "Введіть логін і пароль"
|
||||||
|
},
|
||||||
|
"CantLogin": {
|
||||||
|
"ru": "Не удалось залогиниться: ",
|
||||||
|
"en": "Can't login: ",
|
||||||
|
"ua": "Не вдалося залогінитися: "
|
||||||
|
},
|
||||||
|
"MafileLinked": {
|
||||||
|
"ru": "Мафайл успешно привязан. RCode: {0}\r\nИмя файла: {1}.mafile",
|
||||||
|
"en": "Mafile successfully linked. RCode: {0}\r\nFile name: {1}.mafile",
|
||||||
|
"ua": "Мафайл успішно прив'язано. RCode: {0}\r\nІм'я файлу: {1}.mafile"
|
||||||
|
},
|
||||||
|
"ErrorWithCode": {
|
||||||
|
"ru": "Ошибка с кодом: ",
|
||||||
|
"en": "Error with code: ",
|
||||||
|
"ua": "Помилка з кодом: "
|
||||||
|
},
|
||||||
|
"UnknownError": {
|
||||||
|
"ru": "Неизвестная ошибка: ",
|
||||||
|
"en": "Unknown error: ",
|
||||||
|
"ua": "Невідома помилка: "
|
||||||
|
},
|
||||||
|
"PleaseEnterCorrectPhone": {
|
||||||
|
"ru": "Пожалуйста, введите корректный номер телефона",
|
||||||
|
"en": "Please enter correct phone number",
|
||||||
|
"ua": "Будь ласка, введіть коректний номер телефону"
|
||||||
|
},
|
||||||
|
"PleaseEnterCorrectCode": {
|
||||||
|
"ru": "Пожалуйста, введите корректный код",
|
||||||
|
"en": "Please enter correct code",
|
||||||
|
"ua": "Будь ласка, введіть коректний код"
|
||||||
|
},
|
||||||
|
"EnterEmailCode": {
|
||||||
|
"ru": "Введите код из письма",
|
||||||
|
"en": "Enter code from email",
|
||||||
|
"ua": "Введіть код з листа"
|
||||||
|
},
|
||||||
|
"ClickOnEmailLink": {
|
||||||
|
"ru": "На почту было отправлено письмо, нажмите по ссылке затем - 'Продолжить'",
|
||||||
|
"en": "Email was sent, click on link and then - 'Proceed'",
|
||||||
|
"ua": "На пошту було відправлено лист, натисніть по посиланню потім - 'Продовжити'"
|
||||||
|
},
|
||||||
|
"PhoneHint": {
|
||||||
|
"ru": "На телефон {0} была отправлена СМС",
|
||||||
|
"en": "SMS was sent to phone {0}",
|
||||||
|
"ua": "На телефон {0} було відправлено СМС"
|
||||||
|
},
|
||||||
|
"EnterPhoneNumber": {
|
||||||
|
"ru": "Введите номер телефона (без знака '+' и др. символов)",
|
||||||
|
"en": "Enter phone number (without '+' and other symbols)",
|
||||||
|
"ua": "Введіть номер телефону (без знака '+' та ін. символів)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CantAlignTimeError": {
|
||||||
|
"ru": "Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log",
|
||||||
|
"en": "Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log",
|
||||||
|
"ua": "Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace SteamLib.Account;
|
||||||
|
|
||||||
|
//WARNING: Any changes here should be reflected in MafileSerializer.cs
|
||||||
|
|
||||||
|
public sealed class MobileSessionData : SessionData, IMobileSessionData
|
||||||
|
{
|
||||||
|
public SteamAuthToken? MobileToken { get; private set; }
|
||||||
|
|
||||||
|
public SteamAuthToken? GetMobileToken() => MobileToken;
|
||||||
|
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(MobileToken))]
|
||||||
|
public void SetMobileToken(SteamAuthToken token)
|
||||||
|
{
|
||||||
|
if (token.Type != SteamAccessTokenType.Mobile)
|
||||||
|
throw new ArgumentException("Token must be of type MobileAccess", nameof(token))
|
||||||
|
{
|
||||||
|
Data = { { "ActualType", token.Type } }
|
||||||
|
};
|
||||||
|
|
||||||
|
MobileToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken,
|
||||||
|
SteamAuthToken? mobileToken, IDictionary<SteamDomain, SteamAuthToken>? tokens)
|
||||||
|
: base(sessionId, steamId, refreshToken, tokens)
|
||||||
|
{
|
||||||
|
MobileToken = mobileToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken,
|
||||||
|
SteamAuthToken? mobileToken, IEnumerable<SteamAuthToken>? tokens)
|
||||||
|
: base(sessionId, steamId, refreshToken, tokens)
|
||||||
|
{
|
||||||
|
MobileToken = mobileToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MobileSessionData Clone() => (MobileSessionData)((ISessionData)this).Clone();
|
||||||
|
object ICloneable.Clone()
|
||||||
|
{
|
||||||
|
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Web.Converters;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace SteamLib.Account;
|
||||||
|
|
||||||
|
public class SessionData : ISessionData
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool? IsValid { get; set; }
|
||||||
|
public string SessionId { get; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(SteamIdToSteam64Converter))]
|
||||||
|
public SteamId SteamId { get; }
|
||||||
|
public SteamAuthToken RefreshToken { get; }
|
||||||
|
public ConcurrentDictionary<SteamDomain, SteamAuthToken> Tokens { get; }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IDictionary<SteamDomain, SteamAuthToken>? tokens)
|
||||||
|
{
|
||||||
|
SessionId = sessionId;
|
||||||
|
SteamId = steamId;
|
||||||
|
RefreshToken = refreshToken;
|
||||||
|
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokens ?? new Dictionary<SteamDomain, SteamAuthToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IEnumerable<SteamAuthToken>? tokens)
|
||||||
|
{
|
||||||
|
SessionId = sessionId;
|
||||||
|
SteamId = steamId;
|
||||||
|
RefreshToken = refreshToken;
|
||||||
|
tokens ??= Array.Empty<SteamAuthToken>();
|
||||||
|
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokens.ToDictionary(t => t.Domain, t => t));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual SteamAuthToken? GetToken(SteamDomain domain)
|
||||||
|
{
|
||||||
|
if (domain == SteamDomain.Undefined)
|
||||||
|
throw new ArgumentException("SteamDomain.Undefined is not allowed");
|
||||||
|
if (Tokens.TryGetValue(domain, out var token))
|
||||||
|
return token;
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetToken(SteamDomain domain, SteamAuthToken token)
|
||||||
|
{
|
||||||
|
Tokens[domain] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public virtual SessionData Clone() => (SessionData)((ICloneable)this).Clone();
|
||||||
|
object ICloneable.Clone()
|
||||||
|
{
|
||||||
|
return new SessionData(SessionId, SteamId, RefreshToken, Tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using AchiesUtilities.Models;
|
||||||
|
using AchiesUtilities.Newtonsoft.JSON.Converters.Special;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using SteamLib.Authentication;
|
||||||
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Web.Converters;
|
||||||
|
|
||||||
|
namespace SteamLib.Account;
|
||||||
|
|
||||||
|
public readonly struct SteamAuthToken
|
||||||
|
{
|
||||||
|
public string Token { get; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(SteamIdToSteam64Converter))]
|
||||||
|
public SteamId SteamId { get; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(UnixTimeStampConverter))]
|
||||||
|
public UnixTimeStamp Expires { get; }
|
||||||
|
public SteamDomain Domain { get; init; }
|
||||||
|
public SteamAccessTokenType Type { get; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsExpired => Expires.Time < DateTime.UtcNow;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string SignedToken { get; }
|
||||||
|
|
||||||
|
public SteamAuthToken(string token, long steamId, UnixTimeStamp expires, SteamDomain domain, SteamAccessTokenType type)
|
||||||
|
{
|
||||||
|
Token = token;
|
||||||
|
Expires = expires;
|
||||||
|
Domain = domain;
|
||||||
|
Type = type;
|
||||||
|
SteamId = SteamId.FromSteam64(steamId);
|
||||||
|
SignedToken = SteamTokenHelper.CombineJwtWithSteamId(SteamId.Steam64.Id, Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public SteamAuthToken(string token, SteamId steamId, UnixTimeStamp expires, SteamDomain domain, SteamAccessTokenType type)
|
||||||
|
{
|
||||||
|
Token = token;
|
||||||
|
SteamId = steamId;
|
||||||
|
Expires = expires;
|
||||||
|
Domain = domain;
|
||||||
|
Type = type;
|
||||||
|
SignedToken = SteamTokenHelper.CombineJwtWithSteamId(SteamId.Steam64.Id, Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
using SteamLib.Utility;
|
||||||
|
|
||||||
|
namespace SteamLib.Account;
|
||||||
|
|
||||||
|
public struct SteamId //TODO: validation in parse methods
|
||||||
|
{
|
||||||
|
public SteamId64 Steam64 { get; }
|
||||||
|
public SteamId2 Steam2 { get; }
|
||||||
|
public SteamId3 Steam3 { get; }
|
||||||
|
public SteamId(SteamId64 steam64, char type = 'U', short universe = 0)
|
||||||
|
{
|
||||||
|
Steam64 = steam64;
|
||||||
|
Steam2 = steam64.ToSteam2(universe);
|
||||||
|
Steam3 = steam64.ToSteam3(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SteamId(SteamId2 steam2, char type = 'U')
|
||||||
|
{
|
||||||
|
Steam2 = steam2;
|
||||||
|
Steam64 = steam2.ToSteam64();
|
||||||
|
Steam3 = steam2.ToSteam3(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SteamId(SteamId3 steam3, byte universe = 0)
|
||||||
|
{
|
||||||
|
Steam3 = steam3;
|
||||||
|
Steam64 = steam3.ToSteam64();
|
||||||
|
Steam2 = steam3.ToSteam2(universe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SteamId FromSteam64(long steam64, char type = 'U', short universe = 0) => new(new SteamId64(steam64), type, universe);
|
||||||
|
public override string ToString() => Steam64.ToString();
|
||||||
|
public static bool TryParse(string input, out SteamId result) => SteamIdParser.TryParse(input, out result);
|
||||||
|
|
||||||
|
/// <exception cref="FormatException"></exception>
|
||||||
|
public static SteamId Parse(string input) => SteamIdParser.Parse(input);
|
||||||
|
public static bool operator ==(SteamId left, SteamId right) => left.Equals(right);
|
||||||
|
public static bool operator !=(SteamId left, SteamId right) => !left.Equals(right);
|
||||||
|
public bool Equals(SteamId other) => Steam64.Equals(other.Steam64);
|
||||||
|
public override bool Equals(object? obj) => obj is SteamId other && Equals(other);
|
||||||
|
public override int GetHashCode() => Steam64.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SteamId64
|
||||||
|
{
|
||||||
|
public const long SEED = 76561197960265728L;
|
||||||
|
public long Id { get; }
|
||||||
|
public SteamId64(long id)
|
||||||
|
{
|
||||||
|
if (id < SEED)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(id),$"Invalid SteamID provided {id}");
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SteamId2 ToSteam2(short universe = 0)
|
||||||
|
{
|
||||||
|
var accountIdLowBit = (byte)(Id & 1);
|
||||||
|
var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF;
|
||||||
|
return new SteamId2((byte)universe, accountIdLowBit, accountIdHighBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SteamId3 ToSteam3(char type = 'U')
|
||||||
|
{
|
||||||
|
var accountIdLowBit = (byte)(Id & 1);
|
||||||
|
var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF;
|
||||||
|
return new SteamId3(accountIdLowBit + accountIdHighBits * 2, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Id.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong ToUlong() => (ulong)Id;
|
||||||
|
public long ToLong() => Id;
|
||||||
|
|
||||||
|
public bool Equals(SteamId64 other) => Id == other.Id;
|
||||||
|
public override bool Equals(object? obj) => obj is SteamId64 other && Equals(other);
|
||||||
|
public override int GetHashCode() => Id.GetHashCode();
|
||||||
|
public static bool operator ==(SteamId64 left, SteamId64 right) => left.Equals(right);
|
||||||
|
public static bool operator !=(SteamId64 left, SteamId64 right) => !left.Equals(right);
|
||||||
|
public static bool TryParse(string input, out SteamId64 result) => SteamIdParser.TryParse64(input, out result);
|
||||||
|
/// <exception cref="FormatException"></exception>
|
||||||
|
public static SteamId64 Parse(string input) => SteamIdParser.Parse64(input);
|
||||||
|
|
||||||
|
public static implicit operator long(SteamId64 steamId64) => steamId64.ToLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SteamId2
|
||||||
|
{
|
||||||
|
|
||||||
|
public byte Universe { get; }
|
||||||
|
public byte LowestBit { get; }
|
||||||
|
public int HighestBit { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public SteamId2(byte lowestBit, int highestBit)
|
||||||
|
{
|
||||||
|
if(lowestBit > 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(lowestBit), $"Invalid SteamID2 lowestBit provided {lowestBit}. Max value is 1");
|
||||||
|
|
||||||
|
if(highestBit < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(highestBit), $"Invalid SteamID2 highestBit provided {highestBit}");
|
||||||
|
|
||||||
|
LowestBit = lowestBit;
|
||||||
|
HighestBit = highestBit;
|
||||||
|
Universe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SteamId2(byte universe, byte lowestBit, int highestBit)
|
||||||
|
{
|
||||||
|
Universe = universe;
|
||||||
|
LowestBit = lowestBit;
|
||||||
|
HighestBit = highestBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + LowestBit + HighestBit * 2);
|
||||||
|
|
||||||
|
public SteamId3 ToSteam3(char type = 'U') => new SteamId3(HighestBit * 2 + LowestBit);
|
||||||
|
|
||||||
|
public override string ToString() => $"STEAM_{Universe}:{LowestBit}:{HighestBit}";
|
||||||
|
|
||||||
|
public bool Equals(SteamId2 other) => Universe == other.Universe && LowestBit == other.LowestBit && HighestBit == other.HighestBit;
|
||||||
|
public override bool Equals(object? obj) => obj is SteamId2 other && Equals(other);
|
||||||
|
public override int GetHashCode() => HashCode.Combine(Universe, LowestBit, HighestBit);
|
||||||
|
public static bool operator ==(SteamId2 left, SteamId2 right) => left.Equals(right);
|
||||||
|
public static bool operator !=(SteamId2 left, SteamId2 right) => !left.Equals(right);
|
||||||
|
public static bool TryParse(string input, out SteamId2 result) => SteamIdParser.TryParse2(input, out result);
|
||||||
|
/// <exception cref="FormatException"></exception>
|
||||||
|
public static SteamId2 Parse(string input) => SteamIdParser.Parse2(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SteamId3
|
||||||
|
{
|
||||||
|
public char Type { get; }
|
||||||
|
public int Id { get; }
|
||||||
|
|
||||||
|
public SteamId3(int id, char type = 'U')
|
||||||
|
{
|
||||||
|
if (id < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(id), $"Invalid SteamID provided {id}");
|
||||||
|
Type = type;
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SteamId2 ToSteam2(byte universe = 0)
|
||||||
|
{
|
||||||
|
var bit = Id % 2;
|
||||||
|
var highestBits = Id / 2;
|
||||||
|
return new SteamId2(universe, (byte)bit, highestBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + Id);
|
||||||
|
public override string ToString() => $"[{Type}:1:{Id}]";
|
||||||
|
|
||||||
|
public string ToString(bool withBrackets)
|
||||||
|
{
|
||||||
|
if (withBrackets)
|
||||||
|
{
|
||||||
|
return ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return $"{Type}:1:{Id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ToInt() => Id;
|
||||||
|
public bool Equals(SteamId3 other) => Type == other.Type && Id == other.Id;
|
||||||
|
public override bool Equals(object? obj) => obj is SteamId3 other && Equals(other);
|
||||||
|
public readonly override int GetHashCode() => HashCode.Combine(Type, Id);
|
||||||
|
public static bool operator ==(SteamId3 left, SteamId3 right) => left.Equals(right);
|
||||||
|
public static bool operator !=(SteamId3 left, SteamId3 right) => !left.Equals(right);
|
||||||
|
public static bool TryParse(string input, out SteamId3 result) => SteamIdParser.TryParse3(input, out result);
|
||||||
|
|
||||||
|
/// <exception cref="FormatException"></exception>
|
||||||
|
public static SteamId3 Parse(string input) => SteamIdParser.Parse3(input);
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
using JetBrains.Annotations;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using SteamLib.Authentication;
|
||||||
|
using SteamLib.Core;
|
||||||
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Core.StatusCodes;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.Exceptions.Mobile;
|
||||||
|
using SteamLib.ProtoCore;
|
||||||
|
using SteamLib.ProtoCore.Enums;
|
||||||
|
using SteamLib.ProtoCore.Exceptions;
|
||||||
|
using SteamLib.ProtoCore.Services;
|
||||||
|
using SteamLib.SteamMobile;
|
||||||
|
using SteamLib.SteamMobile.AuthenticatorLinker;
|
||||||
|
|
||||||
|
namespace SteamLib.Api.Mobile;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public static class SteamAuthenticatorLinkerApi
|
||||||
|
{
|
||||||
|
private const string PHONE_REQ = SteamConstants.STEAM_COMMUNITY + "steamguard/phoneajax";
|
||||||
|
|
||||||
|
#region Global
|
||||||
|
|
||||||
|
public static Task<AccountPhoneStatus_Response> HasPhone(HttpClient client, string accessToken)
|
||||||
|
{
|
||||||
|
const string uri = SteamConstants.STEAM_API + "IPhoneService/AccountPhoneStatus/v1";
|
||||||
|
|
||||||
|
var reqUri = AddAccessToken(uri, accessToken);
|
||||||
|
return client.PostProto<AccountPhoneStatus_Response>(reqUri, request: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="deviceId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="AuthenticatorLinkerException"></exception>
|
||||||
|
/// <exception cref="EResultException"></exception>
|
||||||
|
public static async Task<AddAuthenticator_Response> LinkRequest(HttpClient client, ISessionData data, string deviceId)
|
||||||
|
{
|
||||||
|
const string uri = SteamConstants.STEAM_API + "ITwoFactorService/AddAuthenticator/v1";
|
||||||
|
data.EnsureValidated();
|
||||||
|
var reqUri = AddAccessToken(uri, data.GetAccessToken());
|
||||||
|
var req = new AddAuthenticator_Request
|
||||||
|
{
|
||||||
|
SteamId = (ulong)data.SteamId.Steam64.Id,
|
||||||
|
AuthenticatorType = 1,
|
||||||
|
DeviceIdentifier = deviceId,
|
||||||
|
Version = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
var resp = await client.PostProtoMsg<AddAuthenticator_Response>(reqUri, req);
|
||||||
|
if (resp is {Result: EResult.InvalidState, ResponseMsg.Status: 2})
|
||||||
|
{
|
||||||
|
throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidStateWithStatus2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.GetResponseEnsureSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<LinkResult> FinalizeLink(HttpClient client, string confirmationCode, ISessionData data,
|
||||||
|
byte[] sharedSecret, bool validateSmsCode)
|
||||||
|
{
|
||||||
|
const string uri = SteamConstants.STEAM_API + "ITwoFactorService/FinalizeAddAuthenticator/v1";
|
||||||
|
data.EnsureValidated();
|
||||||
|
var reqUri = AddAccessToken(uri, data.GetAccessToken());
|
||||||
|
var time = await TimeAligner.GetSteamTimeAsync();
|
||||||
|
if (validateSmsCode)
|
||||||
|
{
|
||||||
|
var validateSmsReq = new FinalizeAddAuthenticator_Request
|
||||||
|
{
|
||||||
|
SteamId = data.SteamId.Steam64.ToUlong(),
|
||||||
|
AuthenticatorTime = (ulong) time,
|
||||||
|
ConfirmationCode = confirmationCode,
|
||||||
|
ValidateConfirmationCode = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var validateResp = await client.PostProto<FinalizeAddAuthenticator_Response>(reqUri, validateSmsReq);
|
||||||
|
if (validateResp.Success == false || validateResp.Status != 2)
|
||||||
|
{
|
||||||
|
if (validateResp.Status == 89)
|
||||||
|
throw new AuthenticatorLinkerException(AuthenticatorLinkerError.BadConfirmationCode);
|
||||||
|
throw new AuthenticatorLinkerException("Can't accept sms code. Status: " + validateResp.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tries = 0;
|
||||||
|
|
||||||
|
|
||||||
|
while (tries < 30)
|
||||||
|
{
|
||||||
|
tries++;
|
||||||
|
time = await TimeAligner.GetSteamTimeAsync();
|
||||||
|
var code = SteamGuardCodeGenerator.GenerateCode(sharedSecret, time);
|
||||||
|
|
||||||
|
var req = new FinalizeAddAuthenticator_Request
|
||||||
|
{
|
||||||
|
SteamId = data.SteamId.Steam64.ToUlong(),
|
||||||
|
AuthenticatorCode = code,
|
||||||
|
AuthenticatorTime = (ulong)time,
|
||||||
|
ConfirmationCode = confirmationCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var resp = await client.PostProto<FinalizeAddAuthenticator_Response>(reqUri, req);
|
||||||
|
|
||||||
|
if (resp.Success && resp.WantMore == false)
|
||||||
|
{
|
||||||
|
return new LinkResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.Status == 89)
|
||||||
|
{
|
||||||
|
return new LinkResult(LinkError.BadConfirmationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (resp.Status == 88 && tries >= 30)
|
||||||
|
{
|
||||||
|
return new LinkResult(LinkError.UnableToGenerateCorrectCodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new LinkResult(LinkError.GeneralFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> IsValidPhoneNumber(HttpClient client, long phoneNumber, string sessionId)
|
||||||
|
{
|
||||||
|
return await CheckPhoneNumber(client, phoneNumber, sessionId) == CheckPhoneResult.Valid;
|
||||||
|
}
|
||||||
|
public static async Task<CheckPhoneResult> CheckPhoneNumber(HttpClient client, long phoneNumber, string sessionId)
|
||||||
|
{
|
||||||
|
var phone = '+' + phoneNumber.ToString();
|
||||||
|
var data = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{"phoneNumber", phone},
|
||||||
|
{"sessionID", sessionId}
|
||||||
|
};
|
||||||
|
|
||||||
|
var reqContent = new FormUrlEncodedContent(data);
|
||||||
|
var req = new HttpRequestMessage(HttpMethod.Post, SteamConstants.STEAM_STORE + "phone/validate");
|
||||||
|
req.Content = reqContent;
|
||||||
|
req.Headers.Referrer = new Uri("https://store.steampowered.com/phone/add");
|
||||||
|
var resp = await client.SendAsync(req);
|
||||||
|
var content = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||||
|
var j = JObject.Parse(content);
|
||||||
|
|
||||||
|
if (j["success"]!.Value<bool>() == false)
|
||||||
|
return CheckPhoneResult.GeneralFailure;
|
||||||
|
|
||||||
|
if (j["is_voip"]!.Value<bool>())
|
||||||
|
return CheckPhoneResult.VoIp;
|
||||||
|
|
||||||
|
return j["is_valid"]!.Value<bool>() ? CheckPhoneResult.Valid : CheckPhoneResult.NotValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> AttachPhone(HttpClient client, long phoneNumber, string accessToken)
|
||||||
|
{
|
||||||
|
const string uri = SteamConstants.STEAM_API + "IPhoneService/SetAccountPhoneNumber/v1";
|
||||||
|
var phone = '+' + phoneNumber.ToString();
|
||||||
|
var req = new SetAccountPhoneNumber_Request
|
||||||
|
{
|
||||||
|
PhoneNumber = phone
|
||||||
|
};
|
||||||
|
|
||||||
|
var reqUri = AddAccessToken(uri, accessToken);
|
||||||
|
var resp =
|
||||||
|
await client.PostProtoMsg<SetAccountPhoneNumber_Response>(reqUri, req);
|
||||||
|
|
||||||
|
return resp.Result == EResult.Pending;
|
||||||
|
}
|
||||||
|
public static async Task<bool> CheckEmailConfirmation(HttpClient client, string accessToken)
|
||||||
|
{
|
||||||
|
const string uri = SteamConstants.STEAM_API + "IPhoneService/IsAccountWaitingForEmailConfirmation/v1";
|
||||||
|
|
||||||
|
var reqUri = AddAccessToken(uri, accessToken);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (i < 5)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
var resp = await client.PostProto<IsAccountWaitingForEmailConfirmation_Response>(reqUri, new EmptyMessage()); ;
|
||||||
|
|
||||||
|
if (resp.IsWaiting == false) return true;
|
||||||
|
|
||||||
|
await Task.Delay(resp.SecondsToWait * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static Task<EResult> SendSmsCode(HttpClient client, string accessToken)
|
||||||
|
{
|
||||||
|
const string uri = SteamConstants.STEAM_API + "IPhoneService/SendPhoneVerificationCode/v1";
|
||||||
|
|
||||||
|
var reqUri = AddAccessToken(uri, accessToken);
|
||||||
|
return client.PostProto(reqUri, new SendPhoneVerificationCode_Request());
|
||||||
|
}
|
||||||
|
public static async Task<bool> CheckSmsCode(HttpClient client, int smsCode, string sessionId)
|
||||||
|
{
|
||||||
|
var data = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{"op", "check_sms_code"},
|
||||||
|
{"arg", smsCode.ToString()},
|
||||||
|
{"checkfortos", "0"},
|
||||||
|
{"skipvoip", "1"},
|
||||||
|
{"sessionid", sessionId}
|
||||||
|
};
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedContent(data);
|
||||||
|
var resp = await client.PostAsync(PHONE_REQ, content);
|
||||||
|
var respContent = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||||
|
var success = SteamStatusCode.Translate<SteamStatusCode>(respContent, out _).Equals(SteamStatusCode.Ok);
|
||||||
|
|
||||||
|
if (success) return true;
|
||||||
|
|
||||||
|
await Task.Delay(5000);
|
||||||
|
var hasPhone = await HasPhone(client, sessionId);
|
||||||
|
return hasPhone.HasPhone;
|
||||||
|
}
|
||||||
|
private static FormUrlEncodedContent CreateAjaxContent(string op, string arg, string sessionId)
|
||||||
|
{
|
||||||
|
var data = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{"op",op},
|
||||||
|
{"arg", arg},
|
||||||
|
{"sessionid", sessionId}
|
||||||
|
};
|
||||||
|
return new FormUrlEncodedContent(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GenerateDeviceId() => "android:" + Guid.NewGuid();
|
||||||
|
private static string AddAccessToken(string uri, string accessToken) => uri + "?access_token=" + accessToken;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ForLinker
|
||||||
|
|
||||||
|
internal static Task<AddAuthenticator_Response> LinkRequest(this SteamAuthenticatorLinker linker)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return LinkRequest(linker.Client, linker.SessionData, linker.DeviceId ??= GenerateDeviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Task<bool> IsValidPhoneNumber(this SteamAuthenticatorLinker linker, long phoneNumber)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return IsValidPhoneNumber(linker.Client, phoneNumber, linker.SessionData.SessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Task<AccountPhoneStatus_Response> HasPhone(this SteamAuthenticatorLinker linker)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return HasPhone(linker.Client, linker.SessionData.GetAccessToken());
|
||||||
|
}
|
||||||
|
internal static Task<bool> AttachPhone(this SteamAuthenticatorLinker linker, long phoneNumber)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return AttachPhone(linker.Client, phoneNumber, linker.SessionData.GetAccessToken());
|
||||||
|
}
|
||||||
|
internal static Task<bool> CheckEmailConfirmation(this SteamAuthenticatorLinker linker)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return CheckEmailConfirmation(linker.Client, linker.SessionData.GetAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Task<EResult> SendSmsCode(this SteamAuthenticatorLinker linker)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return SendSmsCode(linker.Client, linker.SessionData.GetAccessToken());
|
||||||
|
}
|
||||||
|
internal static Task<bool> CheckSmsCode(this SteamAuthenticatorLinker linker, int smsCode)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return CheckSmsCode(linker.Client, smsCode, linker.SessionData.SessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Task<LinkResult> FinalizeLink(this SteamAuthenticatorLinker linker, string confirmationCode, byte[] sharedSecret, bool validateSmsCode)
|
||||||
|
{
|
||||||
|
if (linker.SessionData == null)
|
||||||
|
throw new InvalidOperationException("SessionData is null");
|
||||||
|
|
||||||
|
return FinalizeLink(linker.Client, confirmationCode, linker.SessionData, sharedSecret, validateSmsCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
private static string GetAccessToken(this ISessionData s) => s.GetToken(SteamDomain.Community)?.Token ?? throw new SessionInvalidException("Access token was null. MobileEndpoints requires valid AccessToken");
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Net;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using SteamLib.Core;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.ProtoCore;
|
||||||
|
using SteamLib.ProtoCore.Enums;
|
||||||
|
using SteamLib.ProtoCore.Exceptions;
|
||||||
|
using SteamLib.ProtoCore.Services;
|
||||||
|
|
||||||
|
namespace SteamLib.Api.Mobile;
|
||||||
|
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public static class SteamMobileApi
|
||||||
|
//TODO: cancellation token
|
||||||
|
//TODO: HealthMonitor
|
||||||
|
{
|
||||||
|
|
||||||
|
private const string GENERATE_ACCESS_TOKEN =
|
||||||
|
SteamConstants.STEAM_API + "IAuthenticationService/GenerateAccessTokenForApp/v1";
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="refreshToken"></param>
|
||||||
|
/// <param name="steamId"></param>
|
||||||
|
/// <returns>Refreshed AccessToken</returns>
|
||||||
|
/// <exception cref="SessionInvalidException"></exception>
|
||||||
|
public static async Task<string> RefreshJwt(HttpClient client, string refreshToken, long steamId)
|
||||||
|
{
|
||||||
|
var req = new GenerateAccessTokenForApp_Request
|
||||||
|
{
|
||||||
|
RefreshToken = refreshToken,
|
||||||
|
SteamId = steamId,
|
||||||
|
TokenRenewalType = true
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resp = await client.PostProto<GenerateAccessTokenForApp_Response>(GENERATE_ACCESS_TOKEN, req);
|
||||||
|
return resp.AccessToken;
|
||||||
|
}
|
||||||
|
catch (EResultException ex)
|
||||||
|
when (ex.Result == EResult.AccessDenied)
|
||||||
|
{
|
||||||
|
throw new SessionInvalidException("RefreshToken is not accepted by Steam. You must login again and use new token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> HasPhoneAttached(HttpClient client, string sessionId)
|
||||||
|
{
|
||||||
|
var data = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{"op", "has_phone"},
|
||||||
|
{"arg", "null"},
|
||||||
|
{"sessionid", sessionId}
|
||||||
|
|
||||||
|
};
|
||||||
|
var content = new FormUrlEncodedContent(data);
|
||||||
|
var resp = await client.PostAsync(SteamConstants.STEAM_COMMUNITY + "steamguard/phoneajax", content);
|
||||||
|
var respContent = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||||
|
var json = JObject.Parse(respContent);
|
||||||
|
return json["has_phone"]!.Value<bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task<RemoveAuthenticator_Response> RemoveAuthenticator(HttpClient client, string accessToken, string rCode)
|
||||||
|
{
|
||||||
|
var req = SteamConstants.STEAM_API + "ITwoFactorService/RemoveAuthenticator/v1?access_token=" + accessToken;
|
||||||
|
var reqData = new RemoveAuthenticator_Request
|
||||||
|
{
|
||||||
|
RevocationCode = rCode,
|
||||||
|
RevocationReason = 1,
|
||||||
|
SteamGuardScheme = 1
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await client.PostProto<RemoveAuthenticator_Response>(req, reqData);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new SessionExpiredException("Session expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using SteamLib.Core;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
using SteamLib.SteamMobile.Confirmations;
|
||||||
|
using SteamLib.SteamMobile;
|
||||||
|
using System.Net;
|
||||||
|
using AchiesUtilities.Web.Extensions;
|
||||||
|
using SteamLib.Web.Scrappers.JSON;
|
||||||
|
using SteamLib.Core.StatusCodes;
|
||||||
|
using SteamLib.Utility;
|
||||||
|
|
||||||
|
namespace SteamLib.Api.Mobile;
|
||||||
|
|
||||||
|
public static class SteamMobileConfirmationsApi
|
||||||
|
{
|
||||||
|
private const string CONF = SteamConstants.STEAM_COMMUNITY + "mobileconf";
|
||||||
|
private const string CONF_OP = SteamConstants.STEAM_COMMUNITY + "mobileconf/ajaxop";
|
||||||
|
private const string MULTI_CONF_OP = SteamConstants.STEAM_COMMUNITY + "mobileconf/multiajaxop";
|
||||||
|
|
||||||
|
public static async Task<IEnumerable<Confirmation>> GetConfirmations(HttpClient client, MobileData data, long steamId)
|
||||||
|
{
|
||||||
|
var nvc = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, "list");
|
||||||
|
|
||||||
|
var req = new Uri(CONF + "/getlist" + nvc.ToQueryString());
|
||||||
|
var reqMsg = new HttpRequestMessage(HttpMethod.Get, req);
|
||||||
|
var resp = await client.SendAsync(reqMsg);
|
||||||
|
|
||||||
|
var respStr = await resp.Content.ReadAsStringAsync();
|
||||||
|
if (resp.StatusCode == HttpStatusCode.Redirect)
|
||||||
|
{
|
||||||
|
throw new SessionExpiredException("Mobile session expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
|
||||||
|
return HealthMonitor.LogOnException(respStr, MobileConfirmationScrapper.Scrap, typeof(SessionExpiredException));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> SendConfirmation(HttpClient client, Confirmation confirmation, long steamId, MobileData data, bool confirm)
|
||||||
|
{
|
||||||
|
var op = confirm ? "allow" : "cancel";
|
||||||
|
|
||||||
|
var query = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, op).ToList();
|
||||||
|
|
||||||
|
var id = confirmation.Id.ToString();
|
||||||
|
var key = confirmation.Nonce.ToString();
|
||||||
|
|
||||||
|
query.Insert(0, new KeyValuePair<string, string>("op", op));
|
||||||
|
query.Add(new KeyValuePair<string, string>("cid", id));
|
||||||
|
query.Add(new KeyValuePair<string, string>("ck", key));
|
||||||
|
|
||||||
|
var req = CONF_OP + query.ToQueryString();
|
||||||
|
var resp = await client.GetStringAsync(req);
|
||||||
|
var successCode = HealthMonitor.LogOnException(resp, () => SteamStatusCode.Translate<SteamStatusCode>(resp, out _));
|
||||||
|
|
||||||
|
return successCode.Equals(SteamStatusCode.Ok);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task<bool> SendMultipleConfirmations(HttpClient client, IEnumerable<Confirmation> confirmations,
|
||||||
|
long steamId, MobileData data, bool confirm)
|
||||||
|
{
|
||||||
|
var op = confirm ? "allow" : "cancel";
|
||||||
|
var query = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, op).ToList();
|
||||||
|
query.Insert(0, new KeyValuePair<string, string>("op", op));
|
||||||
|
|
||||||
|
foreach (var confirmation in confirmations)
|
||||||
|
{
|
||||||
|
query.Add(new KeyValuePair<string, string>("cid[]", confirmation.Id.ToString()));
|
||||||
|
query.Add(new KeyValuePair<string, string>("ck[]", confirmation.Nonce.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedContent(query);
|
||||||
|
var resp = await client.PostAsync(MULTI_CONF_OP, content);
|
||||||
|
var respStr = await resp.Content.ReadAsStringAsync();
|
||||||
|
var successCode = HealthMonitor.LogOnException(respStr, () => SteamStatusCode.Translate<SteamStatusCode>(respStr, out _));
|
||||||
|
return successCode.Equals(SteamStatusCode.Ok);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IEnumerable<KeyValuePair<string, string>> GetConfirmationKvp(long steamId, string deviceId, string identitySecret, string tag = "conf")
|
||||||
|
{
|
||||||
|
var time = TimeAligner.GetSteamTime();
|
||||||
|
var hash = EncryptionHelper.GenerateConfirmationHash(time, identitySecret, tag);
|
||||||
|
return new KeyValuePair<string, string>[]
|
||||||
|
{
|
||||||
|
new("p", deviceId),
|
||||||
|
new("a", steamId.ToString()),
|
||||||
|
new("k", hash),
|
||||||
|
new("t", time.ToString()),
|
||||||
|
new("m", "react"),
|
||||||
|
new("tag", tag)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using SteamLib.Authentication;
|
||||||
|
using SteamLib.Core;
|
||||||
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Core.StatusCodes;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
|
||||||
|
namespace SteamLib.Api;
|
||||||
|
|
||||||
|
public static class SteamGlobalApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Raw AccessToken value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="domain"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="SessionInvalidException"></exception>
|
||||||
|
public static async Task<string> RefreshJwt(HttpClient client, SteamDomain domain, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var domainUri = SteamDomains.GetDomain(domain);
|
||||||
|
var data = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{"redir", domainUri}
|
||||||
|
};
|
||||||
|
var cont = new FormUrlEncodedContent(data);
|
||||||
|
var resp = await client.PostAsync("https://login.steampowered.com/jwt/ajaxrefresh", cont, cancellationToken);
|
||||||
|
var respStr = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var jwtRefresh = JsonConvert.DeserializeObject<JwtRefreshJson>(respStr);
|
||||||
|
if (jwtRefresh?.Success != true)
|
||||||
|
{
|
||||||
|
Exception? inner = null;
|
||||||
|
if (jwtRefresh?.Error != null)
|
||||||
|
{
|
||||||
|
var errorCode = jwtRefresh.Error.Value;
|
||||||
|
var translated = SteamStatusCode.Translate<SteamStatusCode>(errorCode, out _);
|
||||||
|
inner = new SteamStatusCodeException(translated, respStr);
|
||||||
|
}
|
||||||
|
throw new SessionInvalidException("AjaxRefresh was not successful. 'steamRefresh_steam' cookie is missing or refresh token is invalid", inner);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"steamID", jwtRefresh.SteamId.ToString()},
|
||||||
|
{"nonce", jwtRefresh.Nonce},
|
||||||
|
{"redir", jwtRefresh.Redir},
|
||||||
|
{"auth", jwtRefresh.Auth},
|
||||||
|
};
|
||||||
|
|
||||||
|
cont = new FormUrlEncodedContent(data);
|
||||||
|
var update = await client.PostAsync(jwtRefresh.LoginUrl, cont, cancellationToken);
|
||||||
|
var updateResp = await update.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
if (updateResp != "{\"result\":1}")
|
||||||
|
{
|
||||||
|
throw new SessionInvalidException(
|
||||||
|
"AjaxRefresh (set-token) response was not successful. Response string stored in Exception.Data")
|
||||||
|
{
|
||||||
|
Data = {{"Response", updateResp}}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return SteamTokenHelper.ExtractJwtFromSetCookiesHeader(update.Headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class JwtRefreshJson
|
||||||
|
{
|
||||||
|
[JsonProperty("success")] public bool Success { get; set; }
|
||||||
|
[JsonProperty("login_url")] public string LoginUrl { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("steamID")] public long SteamId { get; set; }
|
||||||
|
[JsonProperty("nonce")] public string Nonce { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("redir")] public string Redir { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("auth")] public string Auth { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("error")] public int? Error { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
using AchiesUtilities.Extensions;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using SteamLib.Account;
|
||||||
|
using SteamLib.Core;
|
||||||
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace SteamLib.Authentication;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public static class AdmissionHelper
|
||||||
|
{
|
||||||
|
public const string ACCESS_COOKIE_NAME = "steamLoginSecure";
|
||||||
|
public const string REFRESH_COOKIE_NAME = "steamRefresh_steam";
|
||||||
|
public const string LANGUAGE_COOKIE_NAME = "Steam_Language";
|
||||||
|
|
||||||
|
#region Main
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear and set new session
|
||||||
|
/// </summary>
|
||||||
|
public static void SetSteamCookies(this CookieContainer container, ISessionData sessionData, string setLanguage = "english")
|
||||||
|
{
|
||||||
|
container.ClearSteamCookies(setLanguage);
|
||||||
|
|
||||||
|
|
||||||
|
AddRefreshToken(container, sessionData.RefreshToken);
|
||||||
|
|
||||||
|
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||||
|
container.Add(community, new Cookie("sessionid", sessionData.SessionId, "/"));
|
||||||
|
container.Add(community, new Cookie("Steam_Language", setLanguage, "/"));
|
||||||
|
TransferCommunityCookies(container);
|
||||||
|
foreach (var domain in SteamDomains.AllDomains)
|
||||||
|
{
|
||||||
|
var token = sessionData.GetToken(domain);
|
||||||
|
if (token == null) continue;
|
||||||
|
AddTokenCookie(container, token.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetDomainCookie(this CookieContainer container, SteamDomain domain, SteamAuthToken token)
|
||||||
|
{
|
||||||
|
var uri = SteamDomains.GetDomainUri(domain);
|
||||||
|
foreach (var cookie in container.GetCookies(uri).Where(c => c.Expired == false && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
|
||||||
|
{
|
||||||
|
cookie.Expired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTokenCookie(container, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear and set new session
|
||||||
|
/// </summary>
|
||||||
|
public static void SetSteamMobileCookies(this CookieContainer container, IMobileSessionData mobileSession,
|
||||||
|
string setLanguage = "english")
|
||||||
|
{
|
||||||
|
|
||||||
|
container.ClearSteamCookies(setLanguage);
|
||||||
|
container.AddMinimalMobileCookies();
|
||||||
|
|
||||||
|
AddRefreshToken(container, mobileSession.RefreshToken);
|
||||||
|
|
||||||
|
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||||
|
container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString()));
|
||||||
|
container.Add(community, new Cookie("sessionid", mobileSession.SessionId));
|
||||||
|
container.Add(community, new Cookie("Steam_Language", setLanguage));
|
||||||
|
TransferCommunityCookies(container);
|
||||||
|
|
||||||
|
foreach (var domain in SteamDomains.AllDomains)
|
||||||
|
{
|
||||||
|
var token = mobileSession.GetToken(domain);
|
||||||
|
if (token == null) continue;
|
||||||
|
AddTokenCookie(container, token.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void AddMinimalMobileCookies(this CookieContainer container)
|
||||||
|
{
|
||||||
|
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||||
|
container.Add(community, new Cookie("mobileClientVersion", "777777 3.6.1"));
|
||||||
|
container.Add(community, new Cookie("mobileClient", "android"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Clear
|
||||||
|
public static void ClearSteamCookies(this CookieContainer container, string setLanguage = "english")
|
||||||
|
{
|
||||||
|
var cookies = container.GetAllCookies().Where(IsSteamCookie).ToList();
|
||||||
|
foreach (var cookie in cookies)
|
||||||
|
{
|
||||||
|
cookie.Expired = true;
|
||||||
|
}
|
||||||
|
container.Add(SteamDomains.GetDomainUri(SteamDomain.Community), new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
|
||||||
|
TransferCommunityCookies(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearMobileSessionCookies(this CookieContainer container, string setLanguage = "english")
|
||||||
|
{
|
||||||
|
container.ClearSteamCookies(setLanguage);
|
||||||
|
container.AddMinimalMobileCookies();
|
||||||
|
TransferCommunityCookies(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
public static void TransferCommunityCookies(CookieContainer container)
|
||||||
|
{
|
||||||
|
var cookies = container.GetAllCookies();
|
||||||
|
|
||||||
|
|
||||||
|
foreach (Cookie cookie in cookies)
|
||||||
|
{
|
||||||
|
if (cookie.Domain.Contains("steamcommunity.com") == false || cookie.Expired || cookie.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)) continue;
|
||||||
|
|
||||||
|
|
||||||
|
container.Add(SteamDomains.GetDomainUri(SteamDomain.Store), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly });
|
||||||
|
container.Add(SteamDomains.GetDomainUri(SteamDomain.Help), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly });
|
||||||
|
container.Add(SteamDomains.GetDomainUri(SteamDomain.TV), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddRefreshToken(CookieContainer container, SteamAuthToken token)
|
||||||
|
{
|
||||||
|
if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh))
|
||||||
|
throw new ArgumentException($"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}", nameof(token));
|
||||||
|
var refreshToken = token.SignedToken;
|
||||||
|
container.Add(SteamDomains.LoginSteamDomain, new Cookie(REFRESH_COOKIE_NAME, refreshToken)
|
||||||
|
{
|
||||||
|
Expires = token.Expires.ToLocalDateTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddTokenCookie(CookieContainer container, SteamAuthToken token)
|
||||||
|
{
|
||||||
|
if (token.Type is not SteamAccessTokenType.AccessToken)
|
||||||
|
throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}", nameof(token));
|
||||||
|
|
||||||
|
AddTokenCookie(container, token.Domain, token);
|
||||||
|
}
|
||||||
|
private static void AddTokenCookie(CookieContainer container, SteamDomain domain, SteamAuthToken token)
|
||||||
|
{
|
||||||
|
var domainUri = SteamDomains.GetDomainUri(token.Domain);
|
||||||
|
container.Add(domainUri, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken)
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
Secure = true,
|
||||||
|
Expires = token.Expires.ToLocalDateTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static bool IsSteamCookie(Cookie cookie) =>
|
||||||
|
cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") || cookie.Domain.Contains("steam.tv");
|
||||||
|
|
||||||
|
|
||||||
|
public static string? GetSessionId(this CookieContainer container, string domain = "steamcommunity.com")
|
||||||
|
{
|
||||||
|
var cookies = container.GetAllCookies();
|
||||||
|
return cookies
|
||||||
|
.FirstOrDefault(c => c.Name.Equals("sessionid", StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
&& c.Expired == false
|
||||||
|
&& c.Domain.Contains(domain, StringComparison.InvariantCultureIgnoreCase))?
|
||||||
|
.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SteamLib.Authentication;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Exceptions;
|
||||||
|
|
||||||
|
namespace SteamLib.Login.Default;
|
||||||
|
|
||||||
|
[Obsolete("Method removed from Steam")]
|
||||||
|
public class LoginExecutor
|
||||||
|
{
|
||||||
|
public static ILoginConsumer NullConsumer { get; } = new NullLoginConsumer();
|
||||||
|
public ILoginConsumer Caller { get; }
|
||||||
|
public HttpClient HttpClient { get; }
|
||||||
|
public ILogger? Logger { get; init; }
|
||||||
|
public IEmailProvider? EmailAuthProvider { get; init; }
|
||||||
|
public ICaptchaResolver? CaptchaResolver { get; init; }
|
||||||
|
public ISteamGuardProvider? SteamGuardProvider { get; init; }
|
||||||
|
|
||||||
|
private LoginExecutor(LoginExecutorOptions options)
|
||||||
|
{
|
||||||
|
Caller = options.Consumer;
|
||||||
|
HttpClient = options.HttpClient;
|
||||||
|
Logger = options.Logger;
|
||||||
|
EmailAuthProvider = options.EmailAuthProvider;
|
||||||
|
SteamGuardProvider = options.SteamGuardProvider;
|
||||||
|
CaptchaResolver = options.CaptchaResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task<TransferParameters> DoLogin(LoginExecutorOptions options, string username, string password, CancellationToken cancellationToken = default) //TODO: logs
|
||||||
|
{
|
||||||
|
var executor = new LoginExecutor(options);
|
||||||
|
var client = executor.HttpClient;
|
||||||
|
|
||||||
|
LoginStage loginStage = new GetRsaStage(username);
|
||||||
|
loginStage = await ((GetRsaStage)loginStage).Proceed(client, cancellationToken);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (loginStage is ReadyToLoginStage rdyToLoginStage)
|
||||||
|
{
|
||||||
|
loginStage = await rdyToLoginStage.Proceed(client, password, string.Empty, cancellationToken);
|
||||||
|
}
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var twoFactorTry = 0;
|
||||||
|
var emailTry = 0;
|
||||||
|
while (loginStage.GetType() != typeof(LoginErrorStage) && loginStage is not LoginSuccessStage)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
switch (loginStage)
|
||||||
|
{
|
||||||
|
case CaptchaNeededStage captchaNeededStage:
|
||||||
|
{
|
||||||
|
if (executor.CaptchaResolver == null) throw new LoginException(LoginError.CaptchaRequired);
|
||||||
|
var captchaText = await executor.CaptchaResolver.Resolve(captchaNeededStage.CaptchaImage, client);
|
||||||
|
loginStage = await captchaNeededStage.Proceed(client, captchaText, cancellationToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EmailAuthStage emailAuthStage:
|
||||||
|
{
|
||||||
|
if (executor.EmailAuthProvider == null) throw new LoginException(LoginError.EmailAuthRequired);
|
||||||
|
if (emailTry > executor.EmailAuthProvider.MaxRetryCount) throw new LoginException(LoginError.InvalidEmailAuthCode);
|
||||||
|
var code = await executor.EmailAuthProvider.GetEmailAuthCode(executor.Caller);
|
||||||
|
loginStage = await emailAuthStage.Proceed(client, code, cancellationToken);
|
||||||
|
emailTry++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TwoFactorStage twoFactorStage:
|
||||||
|
{
|
||||||
|
if (executor.SteamGuardProvider == null) throw new LoginException(LoginError.SteamGuardRequired);
|
||||||
|
if (twoFactorTry > executor.SteamGuardProvider.MaxRetryCount) throw new LoginException(LoginError.InvalidTwoFactorCode);
|
||||||
|
var twoFactor = await executor.SteamGuardProvider.GetSteamGuardCode(executor.Caller);
|
||||||
|
loginStage = await twoFactorStage.Proceed(client, twoFactor, cancellationToken);
|
||||||
|
twoFactorTry++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ReadyToLoginStage readyToLoginStage: //When captcha proceeded, stage goes there
|
||||||
|
{
|
||||||
|
loginStage = await readyToLoginStage.Proceed(client, password, string.Empty, cancellationToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(loginStage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loginStage is LoginErrorStage error)
|
||||||
|
{
|
||||||
|
var content = await error.Response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
if (content.Contains("The account name or password that you have entered is incorrect"))
|
||||||
|
{
|
||||||
|
throw new LoginException(LoginError.InvalidCredentials);
|
||||||
|
}
|
||||||
|
throw new LoginException(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (loginStage is not LoginSuccessStage successStage)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unexpected login stage at this point. " + loginStage.GetType())
|
||||||
|
{
|
||||||
|
Data = { { "stage", loginStage } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return successStage.TransferParameters;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SteamLib.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace SteamLib.Login.Default;
|
||||||
|
|
||||||
|
|
||||||
|
public class LoginExecutorOptions
|
||||||
|
{
|
||||||
|
public ILoginConsumer Consumer { get; }
|
||||||
|
public HttpClient HttpClient { get; }
|
||||||
|
public ILogger? Logger { get; init; }
|
||||||
|
public IEmailProvider? EmailAuthProvider { get; init; }
|
||||||
|
public ICaptchaResolver? CaptchaResolver { get; init; }
|
||||||
|
public ISteamGuardProvider? SteamGuardProvider { get; init; }
|
||||||
|
public LoginExecutorOptions(ILoginConsumer consumer, HttpClient httpClient)
|
||||||
|
{
|
||||||
|
Consumer = consumer;
|
||||||
|
HttpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace SteamLib.Login.Default;
|
||||||
|
|
||||||
|
#pragma warning disable CS8618
|
||||||
|
/// <summary>
|
||||||
|
/// Class to Deserialize the json response strings after the login/>
|
||||||
|
/// </summary>
|
||||||
|
internal class LoginResultJson
|
||||||
|
{
|
||||||
|
[JsonProperty("success")] public bool Success { get; set; }
|
||||||
|
[JsonProperty("message")] public string Message { get; set; }
|
||||||
|
[JsonProperty("captcha_needed")] public bool CaptchaNeeded { get; set; }
|
||||||
|
[JsonProperty("captcha_gid")] public string CaptchaGid { get; set; }
|
||||||
|
[JsonProperty("emailauth_needed")] public bool EmailAuthNeeded { get; set; }
|
||||||
|
[JsonProperty("emailsteamid")] public string EmailSteamId { get; set; }
|
||||||
|
[JsonProperty("requires_twofactor")] public bool RequiresTwoFactor { get; set; }
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user