This commit is contained in:
SongSongso
2023-07-19 19:55:36 +08:00
parent 1fae68e920
commit db048f7463
24 changed files with 1175 additions and 1113 deletions
@@ -1,8 +1,8 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:spp="https://steampp.net/ui"
xmlns:ui="using:FluentAvalonia.UI.Controls">
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="10">
@@ -11,16 +11,16 @@
</spp:StepperItem>
<spp:Stepper>
<spp:StepperItem Title="Title1" Index="1">
<spp:StepperItem Index="1" Title="Title1">
<TextBlock Text="Test1" />
</spp:StepperItem>
<spp:StepperItem
Title="Title2"
Index="2"
IsSkip="True">
IsSkip="True"
Title="Title2">
<TextBlock Text="Test2" />
</spp:StepperItem>
<spp:StepperItem Title="Title3" Index="3">
<spp:StepperItem Index="3" Title="Title3">
<TextBlock Text="Test3" />
</spp:StepperItem>
</spp:Stepper>
@@ -43,62 +43,64 @@
</ItemsPresenter>
</ScrollViewer>
<Border
DockPanel.Dock="Bottom"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
DockPanel.Dock="Bottom">
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,Auto">
<Panel>
<Border
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsFinish, Mode=OneWay}">
IsVisible="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsFinish, Mode=OneWay}"
VerticalAlignment="Center">
<WrapPanel>
<Border
Width="50"
Height="50"
Margin="6"
Background="{DynamicResource InfoBadgeBackground}"
ClipToBounds="True"
CornerRadius="25">
CornerRadius="25"
Height="50"
Margin="6"
Width="50">
<ui:SymbolIcon Symbol="Checkmark" />
</Border>
<TextBlock
VerticalAlignment="Center"
FontSize="16"
Text="你已经完成了所有步骤~" />
Text="你已经完成了所有步骤~"
VerticalAlignment="Center" />
</WrapPanel>
</Border>
<ScrollViewer
Grid.Row="0"
Grid.ColumnSpan="3"
Margin="0,0,0,5"
IsVisible="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=!IsFinish, Mode=OneWay}">
Grid.Row="0"
IsVisible="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=!IsFinish, Mode=OneWay}"
Margin="0,0,0,5">
<ContentPresenter
Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}" />
ContentTemplate="{TemplateBinding SelectedContentTemplate}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
Name="PART_SelectedContentHost"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</ScrollViewer>
</Panel>
<Button
Name="PreviousButton"
Content="{TemplateBinding BackButtonName}"
Grid.Row="1"
Content="{TemplateBinding BackButtonName}" />
IsVisible="False"
Name="PreviousButton" />
<Button
Name="SkipButton"
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Right"
Content="{TemplateBinding SkipButtonName}"
IsVisible="False" />
<Button
Name="NextButton"
Grid.Column="1"
Grid.Row="1"
HorizontalAlignment="Right"
IsVisible="False"
Name="SkipButton" />
<Button
Content="{TemplateBinding NextButtonName}"
Grid.Column="2"
Content="{TemplateBinding NextButtonName}" />
Grid.Row="1"
IsVisible="False"
Name="NextButton" />
</Grid>
</Border>
</DockPanel>
@@ -108,34 +110,34 @@
<Style Selector="spp|StepperItem">
<Setter Property="Cursor" Value="Hand" />
<!-- <Setter Property="Cursor" Value="Hand" /> -->
<!-- Set Defaults -->
<Setter Property="Template">
<ControlTemplate>
<DockPanel>
<Border
Name="PART_Index"
Width="30"
Height="30"
Margin="6"
Background="{DynamicResource ControlFillColorDefaultBrush}"
ClipToBounds="True"
CornerRadius="15"
DockPanel.Dock="Left">
DockPanel.Dock="Left"
Height="30"
Margin="6"
Name="PART_Index"
Width="30">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Index, Mode=OneWay}" />
HorizontalAlignment="Center"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Index, Mode=OneWay}"
VerticalAlignment="Center" />
</Border>
<Border DockPanel.Dock="Right">
<TextBlock
Name="PART_Title"
VerticalAlignment="Center"
Foreground="{DynamicResource AccentTextFillColorDisabled}"
Name="PART_Title"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Title, Mode=OneWay}"
TextWrapping="Wrap" />
TextWrapping="Wrap"
VerticalAlignment="Center" />
</Border>
</DockPanel>
</ControlTemplate>
@@ -161,7 +161,7 @@ public class Stepper : SelectingItemsControl
_previousButton = e.NameScope.Find<Button>("PreviousButton");
_skipButton = e.NameScope.Find<Button>("SkipButton");
_nextButton = e.NameScope.Find<Button>("NextButton");
if (_previousButton != null)
{
_previousButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, _previousButton));
@@ -246,29 +246,29 @@ public class Stepper : SelectingItemsControl
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
// protected override void OnPointerPressed(PointerPressedEventArgs e)
// {
// base.OnPointerPressed(e);
//
// if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse)
// {
// e.Handled = UpdateSelectionFromEventSource(e.Source);
// }
// }
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse)
{
var container = GetContainerFromEventSource(e.Source);
if (container != null
&& container.GetVisualsAt(e.GetPosition(container))
.Any(c => container == c || container.IsVisualAncestorOf(c)))
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
}
// protected override void OnPointerReleased(PointerReleasedEventArgs e)
// {
// if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse)
// {
// var container = GetContainerFromEventSource(e.Source);
// if (container != null
// && container.GetVisualsAt(e.GetPosition(container))
// .Any(c => container == c || container.IsVisualAncestorOf(c)))
// {
// e.Handled = UpdateSelectionFromEventSource(e.Source);
// }
// }
// }
protected virtual void OnNexting(CancelEventArgs args)
{
@@ -0,0 +1,10 @@
namespace BD.WTTS.Models;
public interface IAuthenticatorImport
{
public string Name { get; }
public string Description { get; }
public ICommand AuthenticatorImportCommand { get; set; }
}
@@ -0,0 +1,146 @@
using WinAuth;
namespace BD.WTTS.Models;
public abstract class AuthenticatorFileImportBase : AuthenticatorImportBase
{
public abstract override string Name { get; }
public abstract override string Description { get; }
protected abstract string? FileExtension { get; }
public abstract override ICommand AuthenticatorImportCommand { get; set; }
protected async Task<string> SelectFolderPath()
{
var filePath = (await SelectFile())?.FullPath.TrimStart("file:///");
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) throw new FileNotFoundException("文件路径不存在");
return filePath;
}
async Task<IFileResult?> SelectFile()
{
var options = new PickOptions();
if (string.IsNullOrEmpty(FileExtension)) return await FilePicker2.PickAsync(options);
FilePickerFileType fileTypes = new ValueTuple<string, string[]>[]
{
("File", new[] { $"*.{FileExtension}" }),
};
options.FileTypes = fileTypes;
return await FilePicker2.PickAsync(options);
}
protected bool ReadXml(ref AuthenticatorDTO authenticatorDto, XmlReader reader, string? password)
{
bool changed = false;
var authenticatorType = reader.GetAttribute("type");
switch (authenticatorType)
{
case "WinAuth.SteamAuthenticator":
authenticatorDto.Value = new SteamAuthenticator();
break;
case "WinAuth.BattleNetAuthenticator":
authenticatorDto.Value = new BattleNetAuthenticator();
break;
case "WinAuth.GoogleAuthenticator":
authenticatorDto.Value = new GoogleAuthenticator();
break;
case "WinAuth.HOTPAuthenticator":
authenticatorDto.Value = new HOTPAuthenticator();
break;
case "WinAuth.MicrosoftAuthenticator":
authenticatorDto.Value = new MicrosoftAuthenticator();
break;
default:
return false;
}
reader.MoveToContent();
if (reader.IsEmptyElement)
{
reader.Read();
return changed;
}
reader.Read();
while (reader.EOF == false)
{
if (reader.IsStartElement())
{
switch (reader.Name)
{
case "name":
authenticatorDto.Name = reader.ReadElementContentAsString();
break;
case "created":
long t = reader.ReadElementContentAsLong();
t += Convert.ToInt64(new TimeSpan(new DateTime(1970, 1, 1).Ticks).TotalMilliseconds);
t *= TimeSpan.TicksPerMillisecond;
authenticatorDto.Created = new DateTimeOffset(new DateTime(t).ToLocalTime());
break;
case "authenticatordata":
try
{
// we don't pass the password as they are locked till clicked
changed = authenticatorDto.Value!.ReadXml(reader) || changed;
}
catch (WinAuthEncryptedSecretDataException)
{
// no action needed
}
catch (WinAuthBadPasswordException)
{
// no action needed
}
break;
// v2
case "authenticator":
authenticatorDto.Value = AuthenticatorValueDTO.ReadXmlv2(reader, password);
break;
// v2
case "servertimediff":
authenticatorDto.Value!.ServerTimeDiff = reader.ReadElementContentAsLong();
break;
default:
reader.Skip();
break;
}
}
else
{
reader.Read();
break;
}
}
return changed;
}
protected IEnumerable<string> ReadUrlsByFilePath(string filePath)
{
StringBuilder lines = new();
bool retry;
do
{
retry = false;
lines.Length = 0;
// read a plain text file
lines.Append(File.ReadAllText(filePath));
} while (retry);
using var sr = new StringReader(lines.ToString());
while (sr.ReadLine() is { } line)
{
yield return line;
}
}
}
@@ -0,0 +1,18 @@
namespace BD.WTTS.Models;
public abstract class AuthenticatorImportBase : IAuthenticatorImport
{
public abstract string Name { get; }
public abstract string Description { get; }
public abstract ICommand AuthenticatorImportCommand { get; set; }
public async Task<bool> VerifyMaxValue()
{
var auths = await AuthenticatorService.GetAllSourceAuthenticatorAsync();
if (auths.Length < IAccountPlatformAuthenticatorRepository.MaxValue) return true;
Toast.Show(ToastIcon.Info, "已达到本地令牌数量上限");
return false;
}
}
@@ -0,0 +1,68 @@
using WinAuth;
namespace BD.WTTS.Models;
public class AuthenticatorSdaFileImport : AuthenticatorFileImportBase
{
public override string Name => "Mafile 文件导入";
public override string Description => "从 Mafile 文件导入令牌";
protected override string FileExtension => FileEx.maFile;
public sealed override ICommand AuthenticatorImportCommand { get; set; }
public AuthenticatorSdaFileImport(string? password = null)
{
AuthenticatorImportCommand = ReactiveCommand.Create(async () =>
{
await ImportFromSdaFile(password);
});
}
async Task ImportFromSdaFile(string? password = null)
{
try
{
var filePath = await SelectFolderPath();
var text = await File.ReadAllTextAsync(filePath);
var index = text.IndexOf("\"server_time\":", StringComparison.Ordinal) + 14;
if (index != -1)
{
var temp = text.Substring(index, text[index..].IndexOf(",", StringComparison.Ordinal));
if (!temp.Contains('"'))
{
var temp2 = "\"" + temp + "\"";
text = text.Replace(temp, temp2);
}
}
var sdaFileModel = JsonSerializer.Deserialize(text, ImportFileModelJsonContext.Default.SdaFileModel);
sdaFileModel.ThrowIsNull();
var steamDataModel = new SdaFileConvertToSteamDataModel(sdaFileModel);
SteamAuthenticator steamAuthenticator = new()
{
DeviceId = sdaFileModel.DeviceId,
Serial = sdaFileModel.SerialNumber,
SecretKey = Convert.FromBase64String(sdaFileModel.SharedSecret),
SteamData = JsonSerializer.Serialize(steamDataModel, ImportFileModelJsonContext.Default.SdaFileConvertToSteamDataModel),
};
var authDto =
new AuthenticatorDTO()
{
Name = $"(Steam){steamAuthenticator.AccountName}", Value = steamAuthenticator, Created = DateTimeOffset.Now,
};
await AuthenticatorService.AddOrUpdateSaveAuthenticatorsAsync(authDto, password);
Toast.Show(ToastIcon.Success, $"{authDto.Name} 导入成功");
}
catch (Exception e)
{
Log.Error(nameof(AuthenticatorSdaFileImport), e, nameof(ImportFromSdaFile));
Toast.Show(ToastIcon.Error, $"导入失败,错误信息:{e.Message}");
}
}
}
@@ -0,0 +1,18 @@
namespace BD.WTTS.Models;
public class AuthenticatorSteamGuardImport : AuthenticatorImportBase
{
public override string Name => "Steam 「移动验证器」导入";
public override string Description => "通过使用 Steam 「移动验证器」内的可移植数据导入令牌";
public sealed override ICommand AuthenticatorImportCommand { get; set; }
public AuthenticatorSteamGuardImport(string? password = null)
{
AuthenticatorImportCommand = ReactiveCommand.Create(() =>
{
//TODO 移动验证器导入Page
});
}
}
@@ -0,0 +1,23 @@
using BD.WTTS.UI.Views.Pages;
namespace BD.WTTS.Models;
public class AuthenticatorSteamLoginImport : AuthenticatorImportBase
{
public override string Name => "Steam 登陆导入";
public override string Description => Strings.Steam_UserLoginTip;
public sealed override ICommand AuthenticatorImportCommand { get; set; }
public AuthenticatorSteamLoginImport(string? password = null)
{
AuthenticatorImportCommand = ReactiveCommand.Create(async () =>
{
if (await VerifyMaxValue())
await IWindowManager.Instance.ShowTaskDialogAsync(new SteamLoginImportViewModel(password),
Name,
pageContent: new SteamLoginImportPage(), isOkButton: false);
});
}
}
@@ -0,0 +1,69 @@
namespace BD.WTTS.Models;
public class AuthenticatorWattToolKitV1Import : AuthenticatorFileImportBase
{
public override string Name => "WattToolKitV1 导入";
public override string Description => "通过 WattToolKitV1 工具箱导出的文件,导入令牌";
public sealed override ICommand AuthenticatorImportCommand { get; set; }
protected override string FileExtension => FileEx.Dat;
public AuthenticatorWattToolKitV1Import(string? password = null)
{
AuthenticatorImportCommand = ReactiveCommand.Create(async () =>
{
await ImportFromWattToolKitV1(password);
});
}
async Task ImportFromWattToolKitV1(string? password = null)
{
var filePath = await SelectFolderPath();
if (IOPath.TryReadAllText(filePath, out var content, out var _))
{
string authString;
try
{
authString = content.DecompressString();
}
catch
{
return;
}
if (!string.IsNullOrEmpty(authString))
{
var reader = XmlReader.Create(new StringReader(authString));
await reader.ReadAsync();
while (reader is { EOF: false, IsEmptyElement: true })
{
await reader.ReadAsync();
}
await reader.MoveToContentAsync();
while (reader.EOF == false)
{
if (reader.IsStartElement())
{
if (reader.Name == "Auth")
{
await reader.ReadAsync();
}
if (reader.Name != "WinAuthAuthenticator") continue;
var authDto = new AuthenticatorDTO();
ReadXml(ref authDto, reader, null);
await AuthenticatorService.AddOrUpdateSaveAuthenticatorsAsync(authDto, password);
}
else
{
await reader.ReadAsync();
break;
}
}
}
}
}
}
@@ -0,0 +1,57 @@
namespace BD.WTTS.Models;
public class AuthenticatorWattToolKitV2Import : AuthenticatorFileImportBase
{
public override string Name => "WattToolKitV2 导入";
public override string Description => "通过 WattToolKitV2 工具箱导出的文件,导入令牌";
public sealed override ICommand AuthenticatorImportCommand { get; set; }
protected override string FileExtension => FileEx.MPO;
public AuthenticatorWattToolKitV2Import(string? password = null)
{
AuthenticatorImportCommand = ReactiveCommand.Create(async () =>
{
await ImportFromWattToolKitV2(password: password);
});
}
async Task ImportFromWattToolKitV2(string? exportPassword = null, string? password = null)
{
var filePath = await SelectFolderPath();
var metadata = await IOPath.TryReadAllBytesAsync(filePath);
if (!metadata.success || metadata.byteArray == null) return;
var result = await AuthenticatorService.ImportAsync(exportPassword, metadata.byteArray);
switch (result.resultCode)
{
case IAccountPlatformAuthenticatorRepository.ImportResultCode.Success
or IAccountPlatformAuthenticatorRepository.ImportResultCode.PartSuccess:
foreach (var item in result.result)
{
await AuthenticatorService.AddOrUpdateSaveAuthenticatorsAsync(item, password);
}
Toast.Show(ToastIcon.Success, result.resultCode == IAccountPlatformAuthenticatorRepository.ImportResultCode.Success
? Strings.LocalAuth_AddAuthSuccess
: Strings.LocalAuth_AddAuth_PartSuccess);
break;
case IAccountPlatformAuthenticatorRepository.ImportResultCode.SecondaryPasswordFail:
Toast.Show(ToastIcon.Warning, Strings.LocalAuth_ProtectionAuth_PasswordErrorTip);
var textViewModel =
new TextBoxWindowViewModel { InputType = TextBoxWindowViewModel.TextBoxInputType.Password };
if (await IWindowManager.Instance.ShowTaskDialogAsync(textViewModel, "请输入此令牌导出时设置的密码", isDialog: false,
isCancelButton: true))
{
exportPassword = textViewModel.Value;
}
await ImportFromWattToolKitV2(exportPassword, password);
break;
default:
Toast.Show(ToastIcon.Error, Strings.LocalAuth_ExportAuth_Error.Format(result.resultCode));
break;
}
}
}
@@ -0,0 +1,208 @@
using WinAuth;
namespace BD.WTTS.Models;
public class AuthenticatorWinAuthFileImport : AuthenticatorFileImportBase
{
public override string Name => "WinAuth 导入";
public override string Description => "通过 WinAuth 导出的文件,导入令牌";
public sealed override ICommand AuthenticatorImportCommand { get; set; }
protected override string FileExtension => FileEx.Txt;
public AuthenticatorWinAuthFileImport(string? password = null)
{
AuthenticatorImportCommand = ReactiveCommand.Create(async () =>
{
await ImportFromWinAuthFile(password);
});
}
async Task ImportFromWinAuthFile(string? password = null)
{
var filePath = await SelectFolderPath();
var urls = ReadUrlsByFilePath(filePath);
int linenumber = 0;
try
{
string? line;
foreach (var url in urls)
{
linenumber++;
line = url;
// ignore blank lines or comments
line = line.Trim();
if (line.Length == 0 || line.IndexOf("#", StringComparison.Ordinal) == 0)
{
continue;
}
// bug if there is a hash before ?
var hash = line.IndexOf("#", StringComparison.Ordinal);
var qm = line.IndexOf("?", StringComparison.Ordinal);
if (hash != -1 && hash < qm)
{
line = $"{line.Substring(0, hash)}%23{line[(hash + 1)..]}";
}
// parse and validate URI
var uri = new Uri(line);
// we only support "otpauth"
if (uri.Scheme != "otpauth")
{
throw new ApplicationException("Import only supports otpauth://");
}
// we only support totp (not hotp)
if (uri.Host != "totp" && uri.Host != "hotp")
{
throw new ApplicationException("Import only supports otpauth://totp/ or otpauth://hotp/");
}
// get the label and optional issuer
string issuer = string.Empty;
string label = string.IsNullOrEmpty(uri.LocalPath) == false
? uri.LocalPath[1..]
: string.Empty; // skip past initial /
int p = label.IndexOf(":", StringComparison.Ordinal);
if (p != -1)
{
issuer = label.Substring(0, p);
label = label[(p + 1)..];
}
// + aren't decoded
label = label.Replace("+", " ");
var query = HttpUtility.ParseQueryString(uri.Query);
string? secret = query["secret"];
if (string.IsNullOrEmpty(secret))
{
throw new ApplicationException("Authenticator does not contain secret");
}
string? counter = query["counter"];
if (uri.Host == "hotp" && string.IsNullOrEmpty(counter))
{
throw new ApplicationException("HOTP authenticator should have a counter");
}
AuthenticatorDTO authenticatorDto = new();
AuthenticatorValueDTO auth;
if (string.Compare(issuer, "BattleNet", StringComparison.OrdinalIgnoreCase) == 0)
{
string? serial = query["serial"];
if (string.IsNullOrEmpty(serial))
{
throw new ApplicationException("Battle.net Authenticator does not have a serial");
}
serial = serial.ToUpper();
if (Regex.IsMatch(serial, @"^[A-Z]{2}-?[\d]{4}-?[\d]{4}-?[\d]{4}$") == false)
{
throw new ApplicationException("Invalid serial for Battle.net Authenticator");
}
auth = new BattleNetAuthenticator();
//char[] decoded = Base32.getInstance().Decode(secret).Select(c => Convert.ToChar(c)).ToArray(); // this is hex string values
//string hex = new string(decoded);
//((BattleNetAuthenticator)auth).SecretKey = Authenticator.StringToByteArray(hex);
((BattleNetAuthenticator)auth).SecretKey = Base32.GetInstance().Decode(secret);
((BattleNetAuthenticator)auth).Serial = serial;
issuer = string.Empty;
}
else if (string.Compare(issuer, "Steam", StringComparison.OrdinalIgnoreCase) == 0)
{
auth = new SteamAuthenticator();
((SteamAuthenticator)auth).SecretKey = Base32.GetInstance().Decode(secret);
((SteamAuthenticator)auth).Serial = string.Empty;
((SteamAuthenticator)auth).DeviceId = query["deviceid"] ?? string.Empty;
((SteamAuthenticator)auth).SteamData = query["data"] ?? string.Empty;
issuer = string.Empty;
}
else if (uri.Host == "hotp")
{
auth = new HOTPAuthenticator();
((HOTPAuthenticator)auth).SecretKey = Base32.GetInstance().Decode(secret);
((HOTPAuthenticator)auth).Counter = int.Parse(counter!);
if (!string.IsNullOrEmpty(issuer))
{
auth.Issuer = issuer;
}
}
else // if (string.Compare(issuer, "Google", true) == 0)
{
auth = new GoogleAuthenticator();
((GoogleAuthenticator)auth).Enroll(secret);
if (string.Compare(issuer, "Google", StringComparison.OrdinalIgnoreCase) == 0)
{
issuer = string.Empty;
}
else if (!string.IsNullOrEmpty(issuer))
{
auth.Issuer = issuer;
}
}
int.TryParse(query["period"], out int period);
if (period != 0)
{
auth.Period = period;
}
int.TryParse(query["digits"], out int digits);
if (digits != 0)
{
auth.CodeDigits = digits;
}
if (Enum.TryParse(query["algorithm"], true, out IAuthenticatorValueDTO.HMACTypes hmactype))
{
auth.HMACType = hmactype;
}
if (label.Length != 0)
{
authenticatorDto.Name = issuer.Length != 0 ? issuer + " (" + label + ")" : label;
}
else if (issuer.Length != 0)
{
authenticatorDto.Name = issuer;
}
else
{
authenticatorDto.Name = "Imported";
}
authenticatorDto.Value = auth;
// sync
Toast.Show(ToastIcon.Info, Strings.LocalAuth_AddAuthSyncTip, ToastLength.Short);
authenticatorDto.Value.Sync();
await AuthenticatorService.AddOrUpdateSaveAuthenticatorsAsync(authenticatorDto, password);
}
Toast.Show(ToastIcon.Success, Strings.LocalAuth_AddAuthSuccess);
}
catch (UriFormatException)
{
Toast.Show(ToastIcon.Error, $"UriFormatException Invalid authenticator at line {linenumber}");
}
catch (Exception ex)
{
Toast.Show(ToastIcon.Error, $"Error importing at line {ex.Message}");
}
}
}
@@ -26,7 +26,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
yield return new MenuTabItemViewModel()
{
ResourceKeyOrName = nameof(Strings.LocalAuth),
PageType = typeof(AuthenticatorHomePage),
PageType = typeof(MainFramePage),
IsResourceGet = true,
IconKey = Icon,
};
@@ -192,11 +192,17 @@ public sealed partial class AuthenticatorPageViewModel : ViewModelBase
public async Task EncryptHelp()
{
var messageViewmodel = new MessageBoxWindowViewModel();
messageViewmodel.Content += Strings.LocalAuth_ProtectionAuth_Info + "\r\n\r\n";
messageViewmodel.Content += Strings.LocalAuth_ProtectionAuth_EnablePassword + ":\r\n\r\n";
messageViewmodel.Content += Strings.LocalAuth_ProtectionAuth_EnablePasswordTip + "\r\n\r\n";
messageViewmodel.Content += Strings.LocalAuth_ProtectionAuth_IsOnlyCurrentComputerEncrypt + ":\r\n\r\n";
messageViewmodel.Content += Strings.LocalAuth_ProtectionAuth_IsOnlyCurrentComputerEncryptTip + "\r\n\r\n";
messageViewmodel.Content = $"""
{Strings.LocalAuth_ProtectionAuth_Info}
{Strings.LocalAuth_ProtectionAuth_EnablePassword}:
{Strings.LocalAuth_ProtectionAuth_EnablePasswordTip}
{Strings.LocalAuth_ProtectionAuth_IsOnlyCurrentComputerEncrypt}:
{Strings.LocalAuth_ProtectionAuth_IsOnlyCurrentComputerEncryptTip}
""";
await IWindowManager.Instance.ShowTaskDialogAsync(messageViewmodel, "令牌加密帮助");
}
@@ -510,14 +516,6 @@ public sealed partial class AuthenticatorPageViewModel : ViewModelBase
Initialize();
}
public async Task OpenSteamOtherImportWindow()
{
if (VerifyMaxValue())
await IWindowManager.Instance.ShowTaskDialogAsync(new SteamOtherImportViewModel(_currentPassword), "令牌导入",
pageContent: new SteamOtherImportPage(), isOkButton: false);
Initialize();
}
public async Task OpenGeneralAuthenticatorImportWindow()
{
if (VerifyMaxValue())
@@ -0,0 +1,141 @@
using WinAuth;
namespace BD.WTTS.UI.ViewModels;
public class AuthenticatorSteamGuardViewModel : ViewModelBase
{
string _phoneImportUuid = string.Empty;
string _phoneImportSteamGuard = string.Empty;
string? _importAuthNewName;
public string PhoneImportUuid
{
get => _phoneImportUuid;
set
{
if (value == _phoneImportUuid) return;
if (!value.StartsWith("android:", StringComparison.Ordinal))
{
value = $"android:{value}";
}
_phoneImportUuid = value;
this.RaisePropertyChanged();
}
}
public string PhoneImportSteamGuard
{
get => _phoneImportSteamGuard;
set
{
if (value == _phoneImportSteamGuard) return;
_phoneImportSteamGuard = value;
this.RaisePropertyChanged();
}
}
public string? ImportAuthNewName
{
get => _importAuthNewName;
set
{
if (value == _importAuthNewName) return;
if (value != null && value.Length > IAuthenticatorDTO.MaxLength_Name)
{
value = value.Substring(0, IAuthenticatorDTO.MaxLength_Name);
}
_importAuthNewName = value;
this.RaisePropertyChanged();
}
}
public async Task<bool> ImportFromPhoneSteamGuard(string? password = null)
{
/* AuthService.ImportSteamGuard (System.String name, System.String uuid, System.String steamGuard, System.Boolean isLocal, System.String password)
* System.NullReferenceException: Object reference not set to an instance of an object
* Crash Version 2.6.5(20220206) 12 users 14 reports
* Android 9 ~ 12
*/
// check the deviceid
string deviceId;
if (PhoneImportUuid.IndexOf("?xml", StringComparison.Ordinal) != -1)
{
try
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(PhoneImportUuid);
var node = doc.SelectSingleNode("//string[@name='uuidKey']");
if (node == null)
{
//WinAuthForm.ErrorDialog(this, "Cannot find uuidKey in xml");
return false;
}
deviceId = node.InnerText;
}
catch (Exception ex)
{
//WinAuthForm.ErrorDialog(this, "Invalid uuid xml: " + ex.Message);
//ToastService.Current.Notify("Invalid uuid xml");
Log.Error(nameof(AuthenticatorSteamGuardViewModel), ex, nameof(ImportFromPhoneSteamGuard));
return false;
}
}
else
{
deviceId = PhoneImportUuid;
}
if (string.IsNullOrEmpty(deviceId) || Regex.IsMatch(deviceId, @"android:[0-9abcdef-]+",
RegexOptions.Singleline | RegexOptions.IgnoreCase) == false)
{
//WinAuthForm.ErrorDialog(this, "Invalid deviceid, expecting \"android:NNNN...\"");
return false;
}
// check the steamguard
byte[] secret;
string serial;
try
{
var steamGuardModel = JsonSerializer.Deserialize(PhoneImportSteamGuard,
ImportFileModelJsonContext.Default.SteamGuardModel);
if (steamGuardModel == null) return false;
if (string.IsNullOrEmpty(steamGuardModel.SharedSecret))
{
throw new ApplicationException("no shared_secret");
}
secret = Convert.FromBase64String(steamGuardModel.SharedSecret);
if (string.IsNullOrEmpty(steamGuardModel.SerialNumber))
{
throw new ApplicationException("no serial_number");
}
serial = steamGuardModel.SerialNumber;
}
catch (Exception ex)
{
//WinAuthForm.ErrorDialog(this, "Invalid SteamGuard JSON contents: " + ex.Message);
//ToastService.Current.Notify("Invalid SteamGuard JSON");
Log.Error(nameof(AuthenticatorSteamGuardViewModel), ex, nameof(ImportFromPhoneSteamGuard));
return false;
}
var auth = new SteamAuthenticator
{
SecretKey = secret, Serial = serial, SteamData = PhoneImportSteamGuard, DeviceId = deviceId
};
await AuthenticatorService.AddOrUpdateSaveAuthenticatorsAsync(
new AuthenticatorDTO()
{
Name = $"(Steam){ImportAuthNewName}", Value = auth, Created = DateTimeOffset.Now,
}, password);
return true;
}
}
@@ -239,7 +239,6 @@ public sealed partial class SteamLoginImportViewModel
Created = DateTimeOffset.Now,
};
await AuthenticatorService.AddOrUpdateSaveAuthenticatorsAsync(iADTO, _currentPassword);
await IWindowManager.Instance.ShowTaskDialogAsync(new MessageBoxWindowViewModel { Content = Strings.LocalAuth_SteamUserImportSuccess, IsCancelcBtn = true });
SelectIndex = 4;
return;
}
@@ -1,748 +0,0 @@
using BD.WTTS.Client.Resources;
using WinAuth;
namespace BD.WTTS.UI.ViewModels;
public class SteamOtherImportViewModel : DialogWindowViewModel
{
readonly string? _currentPassword;
string? _importAuthNewName;
string _importAuthFilePath = string.Empty;
bool _importFromFile;
string _phoneImportUuid = string.Empty;
string _phoneImportSteamGuard = string.Empty;
AuthImportType _authImportType;
public string? ImportAuthNewName
{
get => _importAuthNewName;
set
{
if (value == _importAuthNewName) return;
if (value != null && value.Length > IAuthenticatorDTO.MaxLength_Name)
{
value = value.Substring(0, IAuthenticatorDTO.MaxLength_Name);
}
_importAuthNewName = value;
this.RaisePropertyChanged();
}
}
public string ImportAuthFilePath
{
get => _importAuthFilePath;
set
{
if (value == _importAuthFilePath) return;
_importAuthFilePath = value;
this.RaisePropertyChanged();
}
}
public bool ImportFromFile
{
get => _importFromFile;
set
{
if (value == _importFromFile) return;
_importFromFile = value;
this.RaisePropertyChanged();
}
}
public string PhoneImportUuid
{
get => _phoneImportUuid;
set
{
if (value == _phoneImportUuid) return;
if (value != null && !value.StartsWith("android:", StringComparison.Ordinal))
{
value = $"android:{value}";
}
_phoneImportUuid = value;
this.RaisePropertyChanged();
}
}
public string PhoneImportSteamGuard
{
get => _phoneImportSteamGuard;
set
{
if (value == _phoneImportSteamGuard) return;
_phoneImportSteamGuard = value;
this.RaisePropertyChanged();
}
}
public AuthImportType[] AuthImportTypes => Enum.GetValues<AuthImportType>();
public AuthImportType CurrentAuthImportType
{
get => _authImportType;
set
{
if (value == _authImportType) return;
switch (value)
{
case AuthImportType.None:
break;
case AuthImportType.SteamPhoneImport:
ImportFromFile = false;
break;
default:
ImportFromFile = true;
break;
}
_authImportType = value;
this.RaisePropertyChanged();
}
}
public SteamOtherImportViewModel()
{
ImportFromFile = true;
}
public SteamOtherImportViewModel(string? password)
{
ImportFromFile = true;
_currentPassword = password;
}
public async void OpenFolder()
{
// switch (CurrentAuthImportType)
// {
// case AuthImportType.WattToolkitV1:
// break;
// case AuthImportType.WattToolkitV2:
// break;
// case AuthImportType.WinAuth:
// break;
// case AuthImportType.SteamDesktopAuth:
// PickOptions pickOptions = new();
// if (IApplication.IsDesktop())
// {
// FilePickerFileType filetypes = new[] { ("maFile 文件", new[] { FileEx.maFile }), ("Json 文件", new[] { FileEx.JSON }) };
// pickOptions.FileTypes = filetypes;
// }
// var tempfilepath = await FilePicker2.PickAsync(pickOptions);
// if (tempfilepath == null) return;
// ImportAuthFilePath = tempfilepath.FullPath;
// break;
// }
var tempfilepath = await FilePicker2.PickAsync(new PickOptions());
if (tempfilepath == null) return;
var extension = Path.GetExtension(tempfilepath.FullPath);
if (string.Equals(extension, FileEx.Txt, StringComparison.OrdinalIgnoreCase))
{
CurrentAuthImportType = AuthImportType.WinAuth;
}
else if (string.Equals(extension, FileEx.MPO, StringComparison.OrdinalIgnoreCase))
{
CurrentAuthImportType = AuthImportType.WattToolkitV2;
}
else if (string.Equals(extension, FileEx.Dat, StringComparison.OrdinalIgnoreCase))
{
CurrentAuthImportType = AuthImportType.WattToolkitV1;
}
else if (string.Equals(extension, FileEx.maFile, StringComparison.OrdinalIgnoreCase))
{
CurrentAuthImportType = AuthImportType.SteamDesktopAuth;
}
else
{
Toast.Show(ToastIcon.Error, "不支持该文件类型的导入方式。");
return;
}
ImportAuthFilePath = tempfilepath.FullPath.TrimStart("file:///");
}
public async void Import()
{
if (CurrentAuthImportType == AuthImportType.SteamPhoneImport)
{
if (string.IsNullOrEmpty(PhoneImportUuid) || string.IsNullOrEmpty(PhoneImportSteamGuard))
{
Toast.Show(ToastIcon.Warning, "UUID及SteamGuard不可为空");
return;
}
ImportFromPhoneSteamGuard();
return;
}
if (string.IsNullOrEmpty(ImportAuthFilePath) || !File.Exists(ImportAuthFilePath))
{
Toast.Show(ToastIcon.Warning, "文件路径不存在");
return;
}
switch (CurrentAuthImportType)
{
case AuthImportType.None:
Toast.Show(ToastIcon.Warning, "请选择导入方式");
break;
case AuthImportType.WattToolkitV1:
if (ImportFromWattToolKitV1())
Toast.Show(ToastIcon.Success, "WattToolKitV1令牌导入成功");
else
Toast.Show(ToastIcon.Error, "WattToolKitV1令牌导入失败");
break;
case AuthImportType.WattToolkitV2:
if (await ImportFromWattToolKitV2())
Toast.Show(ToastIcon.Success, "WattToolKitV2令牌导入成功");
else
Toast.Show(ToastIcon.Error, "WattToolKitV2令牌导入失败");
break;
case AuthImportType.WinAuth:
if (ImportFromWinAuthFile())
Toast.Show(ToastIcon.Success, "WinAuth令牌导入成功");
else
Toast.Show(ToastIcon.Error, "WinAuth令牌导入失败");
break;
case AuthImportType.SteamDesktopAuth:
if (ImportFromSDAFile())
Toast.Show(ToastIcon.Success, "SteamDesktopAuth令牌导入成功");
else
Toast.Show(ToastIcon.Error, "SteamDesktopAuth令牌导入失败");
break;
}
}
public bool ImportFromSDAFile()
{
try
{
string text = File.ReadAllText(ImportAuthFilePath);
var index = text.IndexOf("\"server_time\":", StringComparison.Ordinal) + 14;
if (index != -1)
{
var temp = text.Substring(index, text.Substring(index).IndexOf(",", StringComparison.Ordinal));
if (!temp.Contains("\""))
{
var temp2 = "\"" + temp + "\"";
text = text.Replace(temp, temp2);
}
}
var sdaFileModel = JsonSerializer.Deserialize(text, ImportFileModelJsonContext.Default.SdaFileModel);
if (sdaFileModel == null) return false;
var steamDataModel = new SdaFileConvertToSteamDataModel(sdaFileModel);
SteamAuthenticator steamAuthenticator = new()
{
DeviceId = sdaFileModel.DeviceId,
Serial = sdaFileModel.SerialNumber,
SecretKey = Convert.FromBase64String(sdaFileModel.SharedSecret),
SteamData = JsonSerializer.Serialize(steamDataModel, ImportFileModelJsonContext.Default.SdaFileConvertToSteamDataModel),
};
var authDto =
new AuthenticatorDTO()
{
Name = $"(Steam){ImportAuthNewName}", Value = steamAuthenticator, Created = DateTimeOffset.Now,
};
SaveImport(authDto);
}
catch (Exception e)
{
Log.Error(nameof(SteamOtherImportViewModel), e, nameof(ImportFromSDAFile));
return false;
}
return true;
}
public async Task<bool> ImportFromWattToolKitV2(string? exportPassword = null)
{
var metadata = await IOPath.TryReadAllBytesAsync(ImportAuthFilePath);
if (!metadata.success || metadata.byteArray == null) return false;
var result = await AuthenticatorService.ImportAsync(exportPassword, metadata.byteArray);
switch (result.resultCode)
{
case IAccountPlatformAuthenticatorRepository.ImportResultCode.Success
or IAccountPlatformAuthenticatorRepository.ImportResultCode.PartSuccess:
foreach (var item in result.result)
{
SaveImport(item);
}
Toast.Show(ToastIcon.Success, result.resultCode == IAccountPlatformAuthenticatorRepository.ImportResultCode.Success
? Strings.LocalAuth_AddAuthSuccess
: Strings.LocalAuth_AddAuth_PartSuccess);
break;
case IAccountPlatformAuthenticatorRepository.ImportResultCode.SecondaryPasswordFail:
Toast.Show(ToastIcon.Warning, Strings.LocalAuth_ProtectionAuth_PasswordErrorTip);
var textviewmodel =
new TextBoxWindowViewModel { InputType = TextBoxWindowViewModel.TextBoxInputType.Password };
if (await IWindowManager.Instance.ShowTaskDialogAsync(textviewmodel, "请输入此令牌导出时设置的密码", isDialog: false,
isCancelButton: true))
{
exportPassword = textviewmodel.Value;
}
ImportFromWattToolKitV2(exportPassword);
break;
default:
Toast.Show(ToastIcon.Error, Strings.LocalAuth_ExportAuth_Error.Format(result.resultCode));
break;
}
return true;
}
public bool ImportFromWattToolKitV1()
{
if (IOPath.TryReadAllText(ImportAuthFilePath, out var content, out var _))
{
string authString;
try
{
authString = content.DecompressString();
}
catch
{
return false;
}
if (!string.IsNullOrEmpty(authString))
{
XmlReader reader = XmlReader.Create(new StringReader(authString));
reader.Read();
while (reader.EOF == false && reader.IsEmptyElement == true)
{
reader.Read();
}
reader.MoveToContent();
while (reader.EOF == false)
{
if (reader.IsStartElement())
{
if (reader.Name == "Auth")
{
reader.Read();
}
if (reader.Name == "WinAuthAuthenticator")
{
var authDto = new AuthenticatorDTO();
ReadXml(ref authDto, reader, null);
SaveImport(authDto);
}
}
else
{
reader.Read();
break;
}
}
}
}
return true;
}
public bool ImportFromPhoneSteamGuard()
{
/* AuthService.ImportSteamGuard (System.String name, System.String uuid, System.String steamGuard, System.Boolean isLocal, System.String password)
* System.NullReferenceException: Object reference not set to an instance of an object
* Crash Version 2.6.5(20220206) 12 users 14 reports
* Android 9 ~ 12
*/
// check the deviceid
string deviceId;
if (PhoneImportUuid.IndexOf("?xml", StringComparison.Ordinal) != -1)
{
try
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(PhoneImportUuid);
var node = doc.SelectSingleNode("//string[@name='uuidKey']");
if (node == null)
{
//WinAuthForm.ErrorDialog(this, "Cannot find uuidKey in xml");
return false;
}
deviceId = node.InnerText;
}
catch (Exception ex)
{
//WinAuthForm.ErrorDialog(this, "Invalid uuid xml: " + ex.Message);
//ToastService.Current.Notify("Invalid uuid xml");
Log.Error(nameof(SteamOtherImportViewModel), ex, nameof(ImportFromPhoneSteamGuard));
return false;
}
}
else
{
deviceId = PhoneImportUuid;
}
if (string.IsNullOrEmpty(deviceId) || Regex.IsMatch(deviceId, @"android:[0-9abcdef-]+", RegexOptions.Singleline | RegexOptions.IgnoreCase) == false)
{
//WinAuthForm.ErrorDialog(this, "Invalid deviceid, expecting \"android:NNNN...\"");
return false;
}
// check the steamguard
byte[] secret;
string serial;
try
{
var steamGuardModel = JsonSerializer.Deserialize(PhoneImportSteamGuard,
ImportFileModelJsonContext.Default.SteamGuardModel);
if (steamGuardModel == null) return false;
if (string.IsNullOrEmpty(steamGuardModel.SharedSecret))
{
throw new ApplicationException("no shared_secret");
}
secret = Convert.FromBase64String(steamGuardModel.SharedSecret);
if (string.IsNullOrEmpty(steamGuardModel.SerialNumber))
{
throw new ApplicationException("no serial_number");
}
serial = steamGuardModel.SerialNumber;
}
catch (Exception ex)
{
//WinAuthForm.ErrorDialog(this, "Invalid SteamGuard JSON contents: " + ex.Message);
//ToastService.Current.Notify("Invalid SteamGuard JSON");
Log.Error(nameof(SteamOtherImportViewModel), ex, nameof(ImportFromPhoneSteamGuard));
return false;
}
var auth = new SteamAuthenticator
{
SecretKey = secret,
Serial = serial,
SteamData = PhoneImportSteamGuard,
DeviceId = deviceId
};
SaveImport(new AuthenticatorDTO()
{
Name = $"(Steam){ImportAuthNewName}", Value = auth, Created = DateTimeOffset.Now,
});
return true;
}
public bool ImportFromWinAuthFile()
{
var urls = ReadUrlsByFilePath(ImportAuthFilePath);
var isOK = false;
int linenumber = 0;
try
{
string? line;
foreach (var url in urls)
{
linenumber++;
line = url;
// ignore blank lines or comments
line = line.Trim();
if (line.Length == 0 || line.IndexOf("#", StringComparison.Ordinal) == 0)
{
continue;
}
// bug if there is a hash before ?
var hash = line.IndexOf("#", StringComparison.Ordinal);
var qm = line.IndexOf("?", StringComparison.Ordinal);
if (hash != -1 && hash < qm)
{
line = $"{line.Substring(0, hash)}%23{line[(hash + 1)..]}";
}
// parse and validate URI
var uri = new Uri(line);
// we only support "otpauth"
if (uri.Scheme != "otpauth")
{
throw new ApplicationException("Import only supports otpauth://");
}
// we only support totp (not hotp)
if (uri.Host != "totp" && uri.Host != "hotp")
{
throw new ApplicationException("Import only supports otpauth://totp/ or otpauth://hotp/");
}
// get the label and optional issuer
string issuer = string.Empty;
string label = string.IsNullOrEmpty(uri.LocalPath) == false
? uri.LocalPath[1..]
: string.Empty; // skip past initial /
int p = label.IndexOf(":");
if (p != -1)
{
issuer = label.Substring(0, p);
label = label[(p + 1)..];
}
// + aren't decoded
label = label.Replace("+", " ");
var query = HttpUtility.ParseQueryString(uri.Query);
string secret = query["secret"];
if (string.IsNullOrEmpty(secret))
{
throw new ApplicationException("Authenticator does not contain secret");
}
string counter = query["counter"];
if (uri.Host == "hotp" && string.IsNullOrEmpty(counter))
{
throw new ApplicationException("HOTP authenticator should have a counter");
}
AuthenticatorDTO authenticatorDto = new();
//
AuthenticatorValueDTO auth;
if (String.Compare(issuer, "BattleNet", StringComparison.OrdinalIgnoreCase) == 0)
{
string serial = query["serial"];
if (string.IsNullOrEmpty(serial))
{
throw new ApplicationException("Battle.net Authenticator does not have a serial");
}
serial = serial.ToUpper();
if (Regex.IsMatch(serial, @"^[A-Z]{2}-?[\d]{4}-?[\d]{4}-?[\d]{4}$") == false)
{
throw new ApplicationException("Invalid serial for Battle.net Authenticator");
}
auth = new BattleNetAuthenticator();
//char[] decoded = Base32.getInstance().Decode(secret).Select(c => Convert.ToChar(c)).ToArray(); // this is hex string values
//string hex = new string(decoded);
//((BattleNetAuthenticator)auth).SecretKey = Authenticator.StringToByteArray(hex);
((BattleNetAuthenticator)auth).SecretKey = Base32.GetInstance().Decode(secret);
((BattleNetAuthenticator)auth).Serial = serial;
issuer = string.Empty;
}
else if (string.Compare(issuer, "Steam", true) == 0)
{
auth = new SteamAuthenticator();
((SteamAuthenticator)auth).SecretKey = Base32.GetInstance().Decode(secret);
((SteamAuthenticator)auth).Serial = string.Empty;
((SteamAuthenticator)auth).DeviceId = query["deviceid"] ?? string.Empty;
((SteamAuthenticator)auth).SteamData = query["data"] ?? string.Empty;
issuer = string.Empty;
}
else if (uri.Host == "hotp")
{
auth = new HOTPAuthenticator();
((HOTPAuthenticator)auth).SecretKey = Base32.GetInstance().Decode(secret);
((HOTPAuthenticator)auth).Counter = int.Parse(counter!);
if (!string.IsNullOrEmpty(issuer))
{
auth.Issuer = issuer;
}
}
else // if (string.Compare(issuer, "Google", true) == 0)
{
auth = new GoogleAuthenticator();
((GoogleAuthenticator)auth).Enroll(secret);
if (string.Compare(issuer, "Google", true) == 0)
{
issuer = string.Empty;
}
else if (!string.IsNullOrEmpty(issuer))
{
auth.Issuer = issuer;
}
}
int.TryParse(query["period"], out int period);
if (period != 0)
{
auth.Period = period;
}
int.TryParse(query["digits"], out int digits);
if (digits != 0)
{
auth.CodeDigits = digits;
}
if (Enum.TryParse(query["algorithm"], true, out IAuthenticatorValueDTO.HMACTypes hmactype) == true)
{
auth.HMACType = hmactype;
}
//
if (label.Length != 0)
{
authenticatorDto.Name = (issuer.Length != 0 ? issuer + " (" + label + ")" : label);
}
else if (issuer.Length != 0)
{
authenticatorDto.Name = issuer;
}
else
{
authenticatorDto.Name = "Imported";
}
//
authenticatorDto.Value = auth;
// sync
Toast.Show(ToastIcon.Info, Strings.LocalAuth_AddAuthSyncTip, ToastLength.Short);
authenticatorDto.Value.Sync();
SaveImport(authenticatorDto);
}
Toast.Show(ToastIcon.Success, Strings.LocalAuth_AddAuthSuccess);
isOK = true;
}
catch (UriFormatException)
{
Toast.Show(ToastIcon.Error, string.Format("UriFormatException Invalid authenticator at line {0}", linenumber));
}
catch (Exception ex)
{
Toast.Show(ToastIcon.Error, string.Format("Error importing at line {0}", ex.Message));
}
return isOK;
}
static IEnumerable<string> ReadUrlsByFilePath(string filePath)
{
StringBuilder lines = new();
bool retry;
do
{
retry = false;
lines.Length = 0;
// read a plain text file
lines.Append(File.ReadAllText(filePath));
} while (retry);
using var sr = new StringReader(lines.ToString());
string? line;
while ((line = sr.ReadLine()) != null)
{
yield return line;
}
}
private async void SaveImport(IAuthenticatorDTO authenticatorDto)
{
await AuthenticatorService.AddOrUpdateSaveAuthenticatorsAsync(authenticatorDto, _currentPassword);
}
public bool ReadXml(ref AuthenticatorDTO authenticatorDto, XmlReader reader, string? password)
{
bool changed = false;
var authenticatorType = reader.GetAttribute("type");
switch (authenticatorType)
{
case "WinAuth.SteamAuthenticator":
authenticatorDto.Value = new SteamAuthenticator();
break;
case "WinAuth.BattleNetAuthenticator":
authenticatorDto.Value = new BattleNetAuthenticator();
break;
case "WinAuth.GoogleAuthenticator":
authenticatorDto.Value = new GoogleAuthenticator();
break;
case "WinAuth.HOTPAuthenticator":
authenticatorDto.Value = new HOTPAuthenticator();
break;
case "WinAuth.MicrosoftAuthenticator":
authenticatorDto.Value = new MicrosoftAuthenticator();
break;
default:
return false;
}
reader.MoveToContent();
if (reader.IsEmptyElement)
{
reader.Read();
return changed;
}
reader.Read();
while (reader.EOF == false)
{
if (reader.IsStartElement())
{
switch (reader.Name)
{
case "name":
authenticatorDto.Name = reader.ReadElementContentAsString();
break;
case "created":
long t = reader.ReadElementContentAsLong();
t += Convert.ToInt64(new TimeSpan(new DateTime(1970, 1, 1).Ticks).TotalMilliseconds);
t *= TimeSpan.TicksPerMillisecond;
authenticatorDto.Created = new DateTimeOffset(new DateTime(t).ToLocalTime());
break;
case "authenticatordata":
try
{
// we don't pass the password as they are locked till clicked
changed = authenticatorDto.Value.ReadXml(reader) || changed;
}
catch (WinAuthEncryptedSecretDataException)
{
// no action needed
}
catch (WinAuthBadPasswordException)
{
// no action needed
}
break;
// v2
case "authenticator":
authenticatorDto.Value = AuthenticatorValueDTO.ReadXmlv2(reader, password);
break;
// v2
case "servertimediff":
authenticatorDto.Value.ServerTimeDiff = reader.ReadElementContentAsLong();
break;
default:
reader.Skip();
break;
}
}
else
{
reader.Read();
break;
}
}
return changed;
}
}
@@ -75,7 +75,7 @@
<StackPanel Spacing="5">
<Button Command="{Binding OpenSteamLoginImportWindow}" Content="Steam登陆导入" />
<Button Command="{Binding OpenGeneralAuthenticatorImportWindow}" Content="2FA通用导入" />
<Button Command="{Binding OpenSteamOtherImportWindow}" Content="导入自文件" />
<!-- <Button Command="{Binding OpenSteamOtherImportWindow}" Content="导入自文件" /> -->
</StackPanel>
</Flyout>
</Button.Flyout>
@@ -0,0 +1,11 @@
<UserControl
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="BD.WTTS.UI.Views.Pages.AuthenticatorImportPage"
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Welcome to Avalonia!
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace BD.WTTS.UI.Views.Pages;
public partial class AuthenticatorImportPage : UserControl
{
public AuthenticatorImportPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,46 @@
<UserControl
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="BD.WTTS.UI.Views.Pages.MainFramePage"
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:s="https://steampp.net/services"
xmlns:spp="https://steampp.net/ui"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Panel>
<TabStrip
Background="{DynamicResource ControlOnImageFillColorDefaultBrush}"
CornerRadius="{StaticResource ControlCornerRadius}"
HorizontalAlignment="Right"
ItemContainerTheme="{StaticResource TabItemStyle}"
Margin="9"
Name="Tabs"
Theme="{StaticResource TabStripStyle}"
VerticalAlignment="Top"
ZIndex="100">
<TabStrip.ItemsPanel>
<OnFormFactor Default="{StaticResource DefaultPanel}">
<On Options="Mobile">
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</On>
</OnFormFactor>
</TabStrip.ItemsPanel>
<TabStripItem Content="令牌">
<TabStripItem.Tag>
<x:Type TypeName="spp:AuthenticatorHomePage" />
</TabStripItem.Tag>
</TabStripItem>
<TabStripItem Content="新增">
<TabStripItem.Tag>
<x:Type TypeName="spp:AuthenticatorImportPage" />
</TabStripItem.Tag>
</TabStripItem>
</TabStrip>
<ui:Frame IsNavigationStackEnabled="False" Name="InnerNavFrame" />
</Panel>
</UserControl>
@@ -0,0 +1,41 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using BD.WTTS.UI.Styling;
using FluentAvalonia.UI.Media.Animation;
namespace BD.WTTS.UI.Views.Pages;
public partial class MainFramePage : UserControl
{
int lastIndex = -1;
public MainFramePage()
{
InitializeComponent();
Tabs.SelectionChanged += Tabs_SelectionChanged;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
Tabs_SelectionChanged(null, null);
}
private void Tabs_SelectionChanged(object? sender, SelectionChangedEventArgs? e)
{
if (Tabs.SelectedItem is TabStripItem tab && tab.Tag is Type t)
{
InnerNavFrame.Navigate(t, null, new SlideNavigationTransitionInfo
{
Effect = FrameEffect.GetEffect(lastIndex, Tabs.SelectedIndex),
FromHorizontalOffset = 70,
});
lastIndex = Tabs.SelectedIndex;
}
}
}
@@ -1,6 +1,6 @@
<UserControl
d:DesignHeight="450"
d:DesignWidth="800"
d:DesignHeight="300"
d:DesignWidth="600"
mc:Ignorable="d"
x:Class="BD.WTTS.UI.Views.Pages.SteamLoginImportPage"
x:CompileBindings="True"
@@ -11,201 +11,188 @@
xmlns:s="https://steampp.net/services"
xmlns:spp="https://steampp.net/ui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Border Padding="10">
<spp:Stepper
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
SelectedIndex="{Binding SelectIndex}"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch">
<spp:StepperItem Index="0" Title="Steam登录">
<Grid>
<StackPanel
HorizontalAlignment="Center"
Orientation="Vertical"
Spacing="25"
VerticalAlignment="Center">
<TextBox
MaxWidth="300"
Text="{Binding UserNameText}"
Watermark="{ReflectionBinding Path=Res.Steam_User,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
<TextBox
Classes="revealPasswordButton"
MaxWidth="300"
PasswordChar="{StaticResource PasswordChar}"
Text="{Binding PasswordText}"
Watermark="{ReflectionBinding Path=Res.Steam_Password,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Login,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
MinWidth="280" />
</StackPanel>
<TextBlock
Margin="500,0,0,0"
<UserControl.Styles>
<Style Selector="StackPanel">
<Setter Property="Spacing" Value="20" />
</Style>
</UserControl.Styles>
<spp:Stepper SelectedIndex="{Binding SelectIndex}">
<spp:StepperItem Index="0" Title="Steam登录">
<Grid
Margin="10"
MinHeight="300"
MinWidth="600">
<StackPanel
HorizontalAlignment="Center"
Margin="0,50,0,0"
MinWidth="600"
VerticalAlignment="Top">
<TextBox
HorizontalContentAlignment="Center"
MaxWidth="300"
Text="{Binding UserNameText}"
Watermark="{ReflectionBinding Path=Res.Steam_User,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
<TextBox
Classes="revealPasswordButton"
HorizontalContentAlignment="Center"
MaxWidth="300"
PasswordChar="{StaticResource PasswordChar}"
Text="{Binding PasswordText}"
Watermark="{ReflectionBinding Path=Res.Steam_Password,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
</StackPanel>
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Login,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
MinWidth="280"
VerticalAlignment="Bottom" />
</Grid>
</spp:StepperItem>
<!-- <spp:StepperItem Index="1" Title="图片验证码"> -->
<!-- <StackPanel -->
<!-- HorizontalAlignment="Center" -->
<!-- Orientation="Vertical" -->
<!-- Spacing="10" -->
<!-- VerticalAlignment="Center"> -->
<!-- <spp:Image2 -->
<!-- Height="37" -->
<!-- Source="{Binding CaptchaImageText}" -->
<!-- Stretch="Uniform" -->
<!-- Width="206" /> -->
<!-- <StackPanel Orientation="Horizontal" Spacing="5"> -->
<!-- <TextBox -->
<!-- MaxWidth="200" -->
<!-- Text="{Binding CaptchaCodeText}" -->
<!-- Watermark="{ReflectionBinding Path=Res.Steam_ImageCodeTip, -->
<!-- Mode=OneWay, -->
<!-- Source={x:Static s:ResourceService.Current}}" /> -->
<!-- <Button -->
<!-- Command="{ReflectionBinding ShowCaptchaUrl}" -->
<!-- Content="{ReflectionBinding Path=Res.Steam_ImageCodeShowError, -->
<!-- Mode=OneWay, -->
<!-- Source={x:Static s:ResourceService.Current}}" -->
<!-- Margin="5,0,0,0" /> -->
<!-- </StackPanel> -->
<!-- <Button -->
<!-- Command="{ReflectionBinding LoginSteamImport}" -->
<!-- Content="{ReflectionBinding Path=Res.Continue, -->
<!-- Mode=OneWay, -->
<!-- Source={x:Static s:ResourceService.Current}}" -->
<!-- HorizontalAlignment="Center" -->
<!-- HorizontalContentAlignment="Center" -->
<!-- MinWidth="280" /> -->
<!-- </StackPanel> -->
<!-- </spp:StepperItem> -->
<spp:StepperItem Index="1" Title="邮箱验证码">
<Grid
Margin="10"
MinHeight="300"
MinWidth="600">
<StackPanel Margin="0,50,0,0" VerticalAlignment="Top">
<TextBlock HorizontalAlignment="Center" Text="{ReflectionBinding Path=Res.EmailCode, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
<TextBox
HorizontalContentAlignment="Center"
MaxWidth="200"
Text="{ReflectionBinding Path=Res.Steam_UserLoginTip,
Text="{Binding EmailAuthText}"
Watermark="{ReflectionBinding Path=Res.EmailCode,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
</StackPanel>
<StackPanel VerticalAlignment="Bottom">
<TextBlock
HorizontalAlignment="Center"
Margin="10,0"
Text="{Binding EmailDomainText}" />
<TextBlock
HorizontalAlignment="Center"
Margin="10,0"
Text="{ReflectionBinding Path=Res.LocalAuth_EmailCodeTip,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
TextAlignment="Center"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</Grid>
</spp:StepperItem>
<!-- <spp:StepperItem Index="1" Title="图片验证码"> -->
<!-- <StackPanel -->
<!-- HorizontalAlignment="Center" -->
<!-- Orientation="Vertical" -->
<!-- Spacing="10" -->
<!-- VerticalAlignment="Center"> -->
<!-- <spp:Image2 -->
<!-- Height="37" -->
<!-- Source="{Binding CaptchaImageText}" -->
<!-- Stretch="Uniform" -->
<!-- Width="206" /> -->
<!-- <StackPanel Orientation="Horizontal" Spacing="5"> -->
<!-- <TextBox -->
<!-- MaxWidth="200" -->
<!-- Text="{Binding CaptchaCodeText}" -->
<!-- Watermark="{ReflectionBinding Path=Res.Steam_ImageCodeTip, -->
<!-- Mode=OneWay, -->
<!-- Source={x:Static s:ResourceService.Current}}" /> -->
<!-- <Button -->
<!-- Command="{ReflectionBinding ShowCaptchaUrl}" -->
<!-- Content="{ReflectionBinding Path=Res.Steam_ImageCodeShowError, -->
<!-- Mode=OneWay, -->
<!-- Source={x:Static s:ResourceService.Current}}" -->
<!-- Margin="5,0,0,0" /> -->
<!-- </StackPanel> -->
<!-- <Button -->
<!-- Command="{ReflectionBinding LoginSteamImport}" -->
<!-- Content="{ReflectionBinding Path=Res.Continue, -->
<!-- Mode=OneWay, -->
<!-- Source={x:Static s:ResourceService.Current}}" -->
<!-- HorizontalAlignment="Center" -->
<!-- HorizontalContentAlignment="Center" -->
<!-- MinWidth="280" /> -->
<!-- </StackPanel> -->
<!-- </spp:StepperItem> -->
<spp:StepperItem Index="1" Title="邮箱验证码">
<Grid Margin="10" RowDefinitions="Auto,*,Auto">
<StackPanel
Grid.Row="1"
Spacing="10"
VerticalAlignment="Center">
<TextBox
MaxWidth="200"
Text="{Binding EmailAuthText}"
Watermark="{ReflectionBinding Path=Res.EmailCode,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
MinWidth="280" />
</StackPanel>
<StackPanel Grid.Row="2">
<TextBlock
HorizontalAlignment="Center"
Margin="10,0"
Text="{Binding EmailDomainText}" />
<TextBlock
HorizontalAlignment="Center"
Margin="10,0"
Text="{ReflectionBinding Path=Res.LocalAuth_EmailCodeTip,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
TextAlignment="Center"
TextWrapping="WrapWithOverflow" />
</StackPanel>
</Grid>
</spp:StepperItem>
<spp:StepperItem Index="2" Title="绑定手机号">
<StackPanel Margin="10">
<TextBlock
TextWrapping="WrapWithOverflow" />
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
Margin="0,10,0,0"
Text="请输入您需要绑定的手机号"
VerticalAlignment="Top" />
<StackPanel
HorizontalAlignment="Center"
Spacing="10"
VerticalAlignment="Center">
<TextBox
HorizontalAlignment="Center"
MaxWidth="200"
Text="{Binding PhoneNumberText}"
Watermark="18888888888" />
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
MinWidth="280" />
</StackPanel>
HorizontalContentAlignment="Center"
MinWidth="280" />
</StackPanel>
</Grid>
</spp:StepperItem>
<spp:StepperItem Index="2" Title="绑定手机号">
<Grid
Margin="10"
MinHeight="300"
MinWidth="600">
<!-- <TextBlock -->
<!-- HorizontalAlignment="Center" -->
<!-- Margin="0,10,0,0" -->
<!-- Text="请输入您需要绑定的手机号" -->
<!-- VerticalAlignment="Top" /> -->
<StackPanel Margin="0,50,0,0" VerticalAlignment="Top">
<TextBlock HorizontalAlignment="Center" Text="{ReflectionBinding Path=Res.User_Phone, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
<TextBox
HorizontalContentAlignment="Center"
MaxWidth="200"
Text="{Binding PhoneNumberText}"
Watermark="123-4567-8910" />
</StackPanel>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Bottom">
<TextBlock
HorizontalAlignment="Center"
Margin="0,0,0,10"
Text="请在收到邮件后,点击邮件内的「添加手机号码」进行确认后,再点击继续按钮。"
VerticalAlignment="Bottom" />
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
MinWidth="280" />
</StackPanel>
</Grid>
</spp:StepperItem>
<spp:StepperItem Index="3" Title="手机验证码">
<Grid
Margin="10"
MinHeight="300"
MinWidth="600">
<StackPanel Margin="0,50,0,0" VerticalAlignment="Top">
<TextBlock HorizontalAlignment="Center" Text="{ReflectionBinding Path=Res.RecoveryCode, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
<!-- 恢复代码 -->
<TextBox
IsReadOnly="True"
MaxWidth="200"
Text="{Binding RevocationCodeText, Mode=OneWay}" />
<TextBox
HorizontalContentAlignment="Center"
MaxWidth="200"
Text="{Binding PhoneCodeText}"
Watermark="{ReflectionBinding Path=Res.SMSCode,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
<CheckBox
Content="{ReflectionBinding Path=Res.RecoveryCode_Remember,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
MinWidth="200" />
</StackPanel>
</spp:StepperItem>
<spp:StepperItem Index="3" Title="手机验证码">
<Grid Margin="10" RowDefinitions="Auto,*,Auto">
<StackPanel
Grid.Row="1"
Spacing="10"
VerticalAlignment="Center">
<StackPanel
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="10">
<TextBlock Text="{ReflectionBinding Path=Res.RecoveryCode, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" VerticalAlignment="Center" />
<!-- 恢复代码 -->
<TextBox
IsReadOnly="True"
MinWidth="200"
Text="{Binding RevocationCodeText, Mode=OneWay}" />
</StackPanel>
<TextBox
MaxWidth="200"
Text="{Binding PhoneCodeText}"
Watermark="{ReflectionBinding Path=Res.SMSCode,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}" />
<CheckBox
Content="{ReflectionBinding Path=Res.RecoveryCode_Remember,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
MinWidth="280" />
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
MinWidth="280" />
</StackPanel>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Bottom">
<TextBlock
Grid.Row="2"
HorizontalAlignment="Center"
Margin="10,0"
Text="{ReflectionBinding Path=Res.LocalAuth_SMSCodeTip,
@@ -213,38 +200,51 @@
Source={x:Static s:ResourceService.Current}}"
TextAlignment="Center"
TextWrapping="WrapWithOverflow" />
</Grid>
</spp:StepperItem>
<spp:StepperItem Index="4" Title="完成">
<Grid Margin="10">
<StackPanel
<Button
Command="{ReflectionBinding LoginSteamImport}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
Orientation="Vertical"
Spacing="10">
<TextBlock HorizontalAlignment="Center" Text="{ReflectionBinding Path=Res.RecoveryCode, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
<TextBox
IsReadOnly="True"
MinWidth="200"
Text="{Binding RevocationCodeText, Mode=OneWay}" />
<Button
Command="{ReflectionBinding Reset}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
MinWidth="280" />
</StackPanel>
<TextBlock
HorizontalAlignment="Center"
Margin="10,0"
MaxWidth="250"
Text="{ReflectionBinding Path=Res.RecoveryCode_RememberTip,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
TextAlignment="Center"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</Grid>
</spp:StepperItem>
</spp:Stepper>
</Border>
HorizontalContentAlignment="Center"
MinWidth="280" />
</StackPanel>
</Grid>
</spp:StepperItem>
<spp:StepperItem Index="4" Title="完成">
<Grid
Margin="10"
MinHeight="300"
MinWidth="600">
<TextBlock
HorizontalAlignment="Center"
Margin="0,50,0,0"
MaxWidth="300"
Text="{ReflectionBinding Path=Res.LocalAuth_SteamUserImportSuccess,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
TextAlignment="Center"
TextWrapping="Wrap"
VerticalAlignment="Top" />
<!-- <StackPanel -->
<!-- HorizontalAlignment="Center" -->
<!-- Spacing="10" -->
<!-- VerticalAlignment="Bottom"> -->
<!-- ~1~ <TextBlock HorizontalAlignment="Center" Text="{ReflectionBinding Path=Res.RecoveryCode, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" /> @1@ -->
<!-- ~1~ <TextBox @1@ -->
<!-- ~1~ IsReadOnly="True" @1@ -->
<!-- ~1~ MaxWidth="200" @1@ -->
<!-- ~1~ Text="{Binding RevocationCodeText, Mode=OneWay}" /> @1@ -->
<!-- </StackPanel> -->
<Button
Command="{ReflectionBinding Reset}"
Content="{ReflectionBinding Path=Res.Continue,
Mode=OneWay,
Source={x:Static s:ResourceService.Current}}"
HorizontalAlignment="Center"
MinWidth="280"
VerticalAlignment="Bottom" />
</Grid>
</spp:StepperItem>
</spp:Stepper>
</UserControl>
@@ -1,44 +0,0 @@
<UserControl
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="BD.WTTS.UI.Views.Pages.SteamOtherImportPage"
x:CompileBindings="True"
x:DataType="spp:SteamOtherImportViewModel"
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:spp="https://steampp.net/ui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel
HorizontalAlignment="Center"
Spacing="10"
VerticalAlignment="Center">
<ComboBox
HorizontalContentAlignment="Center"
ItemsSource="{Binding AuthImportTypes}"
SelectedItem="{Binding CurrentAuthImportType}"
Width="200" />
<TextBox
Text="{Binding ImportAuthNewName}"
Watermark="自定义令牌名称"
Width="200" />
<TextBox IsVisible="{Binding !ImportFromFile}" Text="{Binding PhoneImportUuid}" />
<TextBox IsVisible="{Binding !ImportFromFile}" Text="{Binding PhoneImportSteamGuard}" />
<TextBox
IsReadOnly="True"
IsVisible="{Binding ImportFromFile}"
Text="{Binding ImportAuthFilePath}"
Watermark="令牌文件导入路径"
Width="200" />
<Button
Command="{Binding OpenFolder}"
Content="选择文件路径"
HorizontalAlignment="Center"
IsVisible="{Binding ImportFromFile}" />
<Button
Command="{Binding Import}"
Content="确认导入"
HorizontalAlignment="Center" />
</StackPanel>
</UserControl>
@@ -1,19 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace BD.WTTS.UI.Views.Pages;
public partial class SteamOtherImportPage : UserControl
{
public SteamOtherImportPage()
{
InitializeComponent();
}
// public SteamOtherImportPage(string? password)
// {
// InitializeComponent();
// DataContext = new SteamOtherImportViewModel(password);
// }
}