mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-29 07:27:36 +08:00
🚧Misc
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
+11
-13
@@ -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())
|
||||
|
||||
+141
@@ -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>
|
||||
+18
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+217
-217
@@ -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);
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user