Refactor and enhance UI, localization, and file name handling

- Refactored file handling: added backup directories, improved mafile naming strategy, and introduced renaming functionality.
- Enhanced UI: updated `MainWindow`, `SettingsView`, and dialogs with new controls, commands, and improved layouts.
- Introduced `TextFieldDialog` for user input with localization support.
- Improved localization: added new strings for buttons, errors, and tooltips. Removed all user-observed not localized strings
- Fixed clipboard handling logic in `ClipboardHelper`.
- Added 'Create Group' button in mafile's context menu and corresponding dialog view
- Implemented `RequiresAdminAccess` check before updating program and requesting previlegies
This commit is contained in:
achiez
2025-11-06 18:44:43 +02:00
parent ffcc7405a7
commit 882d39b8f3
41 changed files with 792 additions and 387 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ public class CodeProgressBar : ProgressBar
public static readonly DependencyProperty MaxTimeProperty =
DependencyProperty.Register(nameof(MaxTime), typeof(double), typeof(CodeProgressBar),
new PropertyMetadata(30.0)); // По умолчанию 30 сек
new PropertyMetadata(30.0));
public double TimeRemaining
{
-5
View File
@@ -26,15 +26,12 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Иконка типа (info/error) -->
<materialDesign:PackIcon Height="22" Width="22"
VerticalAlignment="Center"
Margin="5,0,0,0"
Foreground="{Binding IconBrush, RelativeSource={RelativeSource AncestorType=UserControl}}"
Kind="{Binding IconKind, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<!-- Текст подсказки -->
<TextBlock Grid.Column="1"
FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="WrapWithOverflow"
@@ -42,12 +39,10 @@
Margin="10,10,10,10"
Text="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<!-- Крестик (кнопка закрытия) -->
<Button Grid.Column="2"
Visibility="{Binding ShowCloseButton, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource BooleanToVisibilityConverter}}"
Command="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
Style="{StaticResource MaterialDesignToolButton}"
ToolTip="Закрыть"
Margin="0,0,5,0"
Padding="2">
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
@@ -0,0 +1,20 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace NebulaAuth.Converters;
public class BoolToMafileNamingConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not bool boolValue)
return null;
return boolValue ? "Login" : "Steam ID";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -13,6 +13,7 @@
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter"/>
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
<!-- Background converters-->
+6 -6
View File
@@ -23,12 +23,12 @@ public static class DialogsController
return result != null && (bool) result;
}
//public static async Task<string?> ShowTextFieldDialog(string? msg = null)
//{
// var content = msg == null ? new TextFieldDialog() : new TextFieldDialog(msg);
// var result = await DialogHost.Show(content);
// return result as string;
//}
public static async Task<string?> ShowTextFieldDialog(string? title = null, string? msg = null)
{
var content = new TextFieldDialog(title, msg);
var result = await DialogHost.Show(content);
return result as string;
}
#endregion
+1 -1
View File
@@ -29,7 +29,7 @@ public static class TrayManager
var contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("Выйти", null!, OnExitClick);
contextMenu.Items.Add("Exit", null!, OnExitClick);
_notifyIcon.ContextMenuStrip = contextMenu;
+16 -2
View File
@@ -15,11 +15,25 @@ public static class UpdateManager
var jsonPath = Path.Combine(Environment.CurrentDirectory, "update-settings.json");
AutoUpdater.PersistenceProvider = new JsonFilePersistenceProvider(jsonPath);
AutoUpdater.ShowSkipButton = false;
if (Settings.Instance.AllowAutoUpdate)
AutoUpdater.UpdateMode = Mode.ForcedDownload;
AutoUpdater.RunUpdateAsAdmin = RequiresAdminAccess();
AutoUpdater.Start(UPDATE_URL);
}
private static bool RequiresAdminAccess()
{
try
{
var testFile = Path.Combine(Environment.CurrentDirectory, "test.tmp");
using (File.Create(testFile)) { }
File.Delete(testFile);
return false;
}
catch (UnauthorizedAccessException)
{
return true;
}
}
//static UpdateManager()
//{
+84 -44
View File
@@ -28,7 +28,14 @@
<KeyBinding Command="{Binding Path=PasteMafilesFromClipboardCommand}"
Key="V"
Modifiers="Control" />
<KeyBinding Command="{x:Static ApplicationCommands.Find}"
Key="F"
Modifiers="Control" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Find"
Executed="FocusSearchBox"/>
</Window.CommandBindings>
<Border Name="DragNDropBorder" Panel.ZIndex="3" Opacity="1" AllowDrop="True"
DragDrop.DragEnter="Rectangle_DragEnter" DragLeave="Rectangle_DragLeave" Drop="Rectangle_Drop">
<md:DialogHost DialogOpened="DialogHost_DialogOpened"
@@ -101,7 +108,9 @@
<Separator />
<ComboBox ToolTip="{Tr MainWindow.AppBar.GroupToolTip}"
md:HintAssist.Hint="{Tr MainWindow.AppBar.GroupsHint}"
MinWidth="100" Margin="8,0,8,0" VerticalAlignment="Center" IsEditable="True"
MinWidth="100"
MaxWidth="200"
Margin="8,0,8,0" VerticalAlignment="Center" IsEditable="True"
ItemsSource="{Binding Groups}"
SelectedValue="{Binding SelectedGroup}"
md:TextFieldAssist.HasClearButton="True">
@@ -122,6 +131,7 @@
<ComboBox ToolTip="{Tr MainWindow.AppBar.Proxy.ProxyManipulateToolTip}"
MinWidth="80"
MaxWidth="200"
Margin="8,0,8,0"
VerticalAlignment="Center"
md:HintAssist.Hint="{Tr MainWindow.AppBar.Proxy.ProxyHint}"
@@ -256,6 +266,10 @@
ItemsSource="{Binding MaFiles}"
SelectedValue="{Binding SelectedMafile}"
PreviewMouseDown="MafileListBox_OnPreviewMouseDown">
<ListBox.Resources>
<DiscreteObjectKeyFrame x:Key="groupsProxy" Value="{Binding Groups}"/>
<DiscreteObjectKeyFrame x:Key="listBoxProxy" Value="{Binding ElementName=MafileListBox}"/>
</ListBox.Resources>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem"
BasedOn="{StaticResource MaterialDesignListBoxItem}">
@@ -301,48 +315,71 @@
Command="{Binding DataContext.CopyMafileCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}" />
</ListBox.InputBindings>
<FrameworkElement.ContextMenu>
<ContextMenu>
<MenuItem InputGestureText="Ctrl+C"
Header="{Tr MainWindow.ContextMenus.Mafile.CopyLogin}"
Command="{Binding CopyLoginCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopySteamId}"
Command="{Binding CopySteamIdCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem InputGestureText="Ctrl+X"
Header="{Tr MainWindow.ContextMenus.Mafile.CopyMafile}"
Command="{Binding CopyMafileCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AddToGroup}"
ItemsSource="{Binding Groups}">
<ItemsControl.ItemContainerStyle>
<Style BasedOn="{StaticResource MaterialDesignMenuItem}"
TargetType="{x:Type MenuItem}">
<Setter Property="Command"
Value="{Binding DataContext.AddToGroupCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<Setter Property="CommandParameter">
<Setter.Value>
<MultiBinding
Converter="{StaticResource MultiCommandParameterConverter}">
<Binding />
<Binding Path="PlacementTarget.SelectedValue"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
</MenuItem>
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}"
Command="{Binding Path=RemoveGroupCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopyPassword}"
Command="{Binding Path=CopyPasswordCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
<FrameworkElement.ContextMenu>
<ContextMenu Tag="{Binding Groups}">
<MenuItem InputGestureText="Ctrl+C"
Header="{Tr MainWindow.ContextMenus.Mafile.CopyLogin}"
Command="{Binding CopyLoginCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopySteamId}"
Command="{Binding CopySteamIdCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem InputGestureText="Ctrl+X"
Header="{Tr MainWindow.ContextMenus.Mafile.CopyMafile}"
Command="{Binding CopyMafileCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AddToGroup}"
>
<MenuItem.ItemsSource>
<CompositeCollection>
<MenuItem
Command="{Binding CreateGroupCommand}"
CommandParameter="{Binding Value.SelectedValue, Source={StaticResource listBoxProxy}}">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<md:PackIcon Kind="PlusCircle" VerticalAlignment="Center"/>
<TextBlock VerticalAlignment="Center" Margin="5,0,0,0" Grid.Column="1" Text="{Tr MainWindow.ContextMenus.Mafile.CreateGroup}"></TextBlock>
</Grid>
</MenuItem.Header>
</MenuItem>
<Separator />
<CollectionContainer Collection="{Binding Value, Source={StaticResource groupsProxy}}" />
</ContextMenu>
</FrameworkElement.ContextMenu>
</CompositeCollection>
</MenuItem.ItemsSource>
<ItemsControl.ItemContainerStyle>
<Style BasedOn="{StaticResource MaterialDesignMenuItem}"
TargetType="{x:Type MenuItem}">
<Setter Property="Command"
Value="{Binding DataContext.AddToGroupCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<Setter Property="CommandParameter">
<Setter.Value>
<MultiBinding
Converter="{StaticResource MultiCommandParameterConverter}">
<Binding />
<Binding Path="PlacementTarget.SelectedValue"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
</MenuItem>
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}"
Command="{Binding Path=RemoveGroupCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
/>
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopyPassword}"
Command="{Binding Path=CopyPasswordCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
</ContextMenu>
</FrameworkElement.ContextMenu>
</ListBox>
@@ -368,7 +405,7 @@
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.3" />
</Border.BorderBrush>
</Border>
<md:Card Margin="10,10,15,10" Padding="2" UniformCornerRadius="12"
<md:Card Margin="10,10,15,10" Padding="2" UniformCornerRadius="12"
Background="Transparent" >
<Grid>
<Grid.RowDefinitions>
@@ -404,10 +441,13 @@
<Button IsEnabled="{Binding IsMafileSelected}"
FontSize="14"
Grid.Row="1"
Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,10,0,10"
Style="{StaticResource MaterialDesignOutlinedButton}"
Margin="0,10,0,10"
HorizontalAlignment="Center"
Content="{Tr MainWindow.RightPart.LoadConfirmations}"
Command="{Binding GetConfirmationsCommand}"
md:ButtonProgressAssist.IsIndeterminate="True"
md:ButtonProgressAssist.IsIndicatorVisible="{Binding GetConfirmationsCommand.IsRunning}"
Width="{Binding Path=ActualWidth, Converter={StaticResource CoefficientConverter}, ConverterParameter=0.6, RelativeSource={RelativeSource AncestorType=md:Card}}" />
<ItemsControl md:RippleAssist.IsDisabled="True" Focusable="False" Grid.Row="2"
FontSize="16"
+5
View File
@@ -121,4 +121,9 @@ public partial class MainWindow
}
#endregion
private void FocusSearchBox(object sender, ExecutedRoutedEventArgs e)
{
SearchField.Focus();
}
}
-1
View File
@@ -21,7 +21,6 @@ public partial class Mafile : MobileDataExtended
}
[JsonIgnore] public string? Filename { get; set; }
[JsonIgnore] private PortableMaClient? _linkedClient;
public void SetSessionData(MobileSessionData? sessionData)
@@ -0,0 +1,72 @@
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace NebulaAuth.Model.Mafiles;
public interface IMafileNamingStrategy
{
bool CanNameMafile(Mafile mafile);
string GetMafileName(Mafile mafile);
}
public class MafileNamingStrategy : IMafileNamingStrategy
{
public static MafileNamingStrategy SteamId { get; } = new("{steamid}");
public static MafileNamingStrategy Login { get; } = new("{login}");
public const string DEF_EXTENSION = ".mafile";
public static readonly IDictionary<string, Func<Mafile, string?>> Placeholders =
new Dictionary<string, Func<Mafile, string?>>
{
{"{steamid}", GetSteamId},
{"{login}", GetLogin},
{"{group}", GetGroup},
{"{servertime}", GetServerTime}
}.ToFrozenDictionary();
public string Pattern { get; }
public bool IncludeExtension { get; }
public MafileNamingStrategy(string pattern, bool includeExtension = true)
{
Pattern = pattern;
IncludeExtension = includeExtension;
}
public bool CanNameMafile(Mafile mafile)
{
if (string.IsNullOrWhiteSpace(Pattern)) return false;
var fileName = ApplyPatternSubstitution(mafile, Pattern, IncludeExtension);
return FileNameValidator.IsValidFileName(fileName);
}
public string GetMafileName(Mafile mafile)
{
if (!CanNameMafile(mafile)) throw new InvalidOperationException($"Cannot name mafile with the current pattern {Pattern}");
return ApplyPatternSubstitution(mafile, Pattern, IncludeExtension);
}
private static string ApplyPatternSubstitution(Mafile mafile, string pattern, bool includeExtension)
{
var result = pattern;
foreach (var placeholder in Placeholders)
{
if (result.Contains(placeholder.Key, StringComparison.OrdinalIgnoreCase))
{
var value = placeholder.Value(mafile);
result = result.Replace(placeholder.Key, value, StringComparison.OrdinalIgnoreCase);
}
}
return includeExtension ? result + DEF_EXTENSION : result;
}
private static string GetSteamId(Mafile mafile) => mafile.SteamId.Steam64.ToString();
private static string GetLogin(Mafile mafile) => mafile.AccountName;
private static string? GetGroup(Mafile mafile) => mafile.Group;
private static string GetServerTime(Mafile mafile) => mafile.ServerTime.ToString();
}
+2 -1
View File
@@ -46,7 +46,8 @@ public static class ProxyStorage
}
catch (Exception ex)
{
SnackbarController.SendSnackbar("Ошибка при загрузке прокси");
Shell.Logger.Error(ex, "Error while loading proxies");
SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("Error when loading proxies", "ProxyStorage", "ErrorWhileLoadingProxies"));
SnackbarController.SendSnackbar(ex.Message);
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ public partial class Settings : ObservableObject
}
catch (Exception ex)
{
SnackbarController.SendSnackbar("Ошибка при загрузке настроек. Настройки были сброшены");
SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("Error when loading settings", "Settings", "ErrorWhileLoadingSettings"));
SnackbarController.SendSnackbar(ex.Message);
Instance = new Settings();
}
+129 -98
View File
@@ -1,45 +1,42 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using AchiesUtilities.Extensions;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.Mafiles;
using SteamLib;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile;
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;
namespace NebulaAuth.Model;
public static class Storage
{
public const string MAFILE_F = "maFiles";
public const string REMOVED_F = "maFiles_removed";
public static string MafileFolder { get; } = Path.GetFullPath(MAFILE_F);
public static string RemovedMafileFolder { get; } = Path.GetFullPath(REMOVED_F);
public const string DIR_MAFILES = "maFiles";
public const string DIR_REMOVED_MAFILES = "maFiles_removed";
public const string DIR_BACKUP_MAFILES = "maFiles_backup";
public static string MafilesDirectory { get; } = Path.GetFullPath(DIR_MAFILES);
public static string RemovedMafilesDirectory { get; } = Path.GetFullPath(DIR_REMOVED_MAFILES);
public static string BackupMafilesDirectory { get; } = Path.GetFullPath(DIR_BACKUP_MAFILES);
public static ObservableCollection<Mafile> MaFiles { get; private set; } = new();
static Storage()
{
}
public static ObservableCollection<Mafile> MaFiles { get; private set; } = [];
public static async Task Initialize(int threadCount, CancellationToken token = default)
{
if (!Directory.Exists(MafileFolder))
Directory.CreateDirectory(MafileFolder);
Directory.CreateDirectory(MafilesDirectory);
Directory.CreateDirectory(RemovedMafilesDirectory);
Directory.CreateDirectory(BackupMafilesDirectory);
if (!Directory.Exists(RemovedMafileFolder))
Directory.CreateDirectory(RemovedMafileFolder);
var comparer = new MafileNameComparer(Settings.Instance.UseAccountNameAsMafileName);
var files = Directory
.GetFiles(MafileFolder)
.GetFiles(MafilesDirectory)
.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile"))
.Order(comparer)
.ToList();
@@ -47,7 +44,7 @@ public static class Storage
await Task.Run(() =>
{
return Parallel.ForEachAsync(files,
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = threadCount },
async (file, ct) =>
{
try
@@ -98,7 +95,7 @@ public static class Storage
throw new FormatException("Can't generate code on this mafile", ex);
}
if (overwrite == false && File.Exists(GetMafilePath(data)))
if (overwrite == false && File.Exists(GetOrCreateMafilePath(data)))
{
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
}
@@ -106,13 +103,11 @@ public static class Storage
SaveMafile(data);
}
public static Mafile ReadMafile(string path)
{
var str = File.ReadAllText(path);
return NebulaSerializer.Deserialize(str, path);
}
public static async Task<Mafile> ReadMafileAsync(string path)
{
var str = await File.ReadAllTextAsync(path);
@@ -121,7 +116,7 @@ public static class Storage
public static void SaveMafile(Mafile data)
{
var path = GetMafilePath(data);
var path = GetOrCreateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
@@ -136,18 +131,16 @@ public static class Storage
MaFiles.Add(data);
}
}
public static void UpdateMafile(Mafile data)
{
var path = GetMafilePath(data);
var path = GetOrCreateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
}
public static void MoveToRemoved(Mafile data)
{
var sourcePath = GetMafilePath(data);
var destinationPath = Path.Combine(REMOVED_F, data.Filename + ".mafile");
var sourcePath = GetOrCreateMafilePath(data);
var destinationPath = Path.Combine(DIR_REMOVED_MAFILES, data.Filename + ".mafile");
var destinationPathFinal = destinationPath;
var i = 0;
while (File.Exists(destinationPathFinal))
@@ -160,98 +153,136 @@ public static class Storage
File.Delete(sourcePath);
MaFiles.Remove(data);
}
private static string GetMafilePath(Mafile data)
private static string GetOrCreateMafilePath(Mafile data)
{
if (data.Filename != null)
{
return Path.Combine(MafileFolder, data.Filename);
return Path.Combine(MafilesDirectory, data.Filename);
}
var fileName = Settings.Instance.UseAccountNameAsMafileName
? CreateFileNameWithAccountName(data.AccountName)
: CreateFileNameWithSteamId(data.SteamId);
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
data.Filename = fileName;
return Path.Combine(MafileFolder, fileName);
return Path.Combine(MafilesDirectory, fileName);
}
private static string CreateFileNameWithAccountName(string accountName)
private static string CreateMafileFileName(Mafile data, bool useAccountName)
{
return accountName + ".mafile";
}
private static string CreateFileNameWithSteamId(SteamId steamId)
{
return steamId.Steam64.Id + ".mafile";
return useAccountName
? MafileNamingStrategy.Login.GetMafileName(data)
: MafileNamingStrategy.SteamId.GetMafileName(data);
}
public static string? TryGetMafilePath(Mafile data)
{
if (data.Filename == null) return null;
return Path.Combine(MafileFolder, data.Filename);
return Path.Combine(MafilesDirectory, data.Filename);
}
public static void BackupHandler(MobileDataExtended data)
public static void WriteBackup(MobileDataExtended data)
{
if (Directory.Exists("mafiles_backup") == false)
{
Directory.CreateDirectory("mafiles_backup");
}
var json = NebulaSerializer.SerializeMafile(data, null);
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"),
json);
WriteBackup(data.AccountName, json);
}
public static void WriteBackup(string accountName, string data)
{
Directory.CreateDirectory(DIR_BACKUP_MAFILES);
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
}
public static void BackupHandlerStr(string accountName, string data)
public static async Task<MafileRenameResult> RenameMafiles(bool loginAsFileName, IProgress<double>? progress = null)
{
if (Directory.Exists("mafiles_backup") == false)
if (MaFiles.Count == 0) return new MafileRenameResult();
var now = DateTime.Now;
var backupFileName = $"rename_backup_{now:yyyy-MM-dd_HH-mm-ss}.zip";
var zipPath = Path.Combine(BackupMafilesDirectory, backupFileName);
await using (var zipStream = new FileStream(zipPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
Directory.CreateDirectory("mafiles_backup");
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: false);
var files = Directory
.EnumerateFiles(MafilesDirectory, "*.*", SearchOption.TopDirectoryOnly)
.Where(f => Path.GetExtension(f).Equals(".mafile", StringComparison.OrdinalIgnoreCase))
.ToList();
var counter = 0;
foreach (var file in files)
{
counter++;
var entry = archive.CreateEntry(Path.GetFileName(file), CompressionLevel.Optimal);
await using var entryStream = entry.Open();
await using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096,
useAsync: true);
await fileStream.CopyToAsync(entryStream);
if (counter % 5 == 0)
{
progress?.Report(counter / (double)files.Count);
await Task.Delay(10);
}
}
}
File.WriteAllText(Path.Combine("mafiles_backup", accountName + ".mafile"),
data);
}
}
//TODO: Refactor
//TODO: use numeric orderer when .net 10 released
internal class MafileNameComparer : IComparer<string>
{
private const string MAF_64_START = "765";
private static readonly IComparer<string> DefaultComparer = Comparer<string>.Default;
public bool MafileNameMode { get; }
public MafileNameComparer(bool mafileNameMode)
{
MafileNameMode = mafileNameMode;
}
public int Compare(string? x, string? y)
{
if (x == null && y == null) return 0;
if (x == null) return -1;
if (y == null) return 1;
var xisSteamId = Path.GetFileName(x).StartsWith(MAF_64_START);
var yisSteamId = Path.GetFileName(y).StartsWith(MAF_64_START);
if (xisSteamId ^ yisSteamId)
var mafiles = MaFiles.ToList();
var res = new MafileRenameResult
{
if (MafileNameMode)
Total = mafiles.Count,
BackupFileName = backupFileName
};
foreach (var mafile in mafiles)
{
try
{
return xisSteamId ? 1 : -1;
var targetFileName = CreateMafileFileName(mafile, loginAsFileName);
if (mafile.Filename == targetFileName || mafile.Filename == null)
{
res.Renamed += 1;
continue;
}
var fullTargetPath = Path.Combine(MafilesDirectory, targetFileName);
var fullSourcePath = Path.Combine(MafilesDirectory, mafile.Filename);
if (!File.Exists(fullSourcePath))
{
Shell.Logger.Warn("Can't rename mafile {old} to {new} because source file not found",
mafile.Filename, targetFileName);
IncErrors();
continue;
}
if (File.Exists(fullTargetPath))
{
Shell.Logger.Warn("Can't rename mafile {old} to {new} because target file already exist",
mafile.Filename, targetFileName);
res.AlreadyExist += 1;
continue;
}
File.Move(fullSourcePath, fullTargetPath);
res.Renamed += 1;
mafile.Filename = targetFileName;
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Error renaming mafile {file} {accountName}", mafile.Filename,
mafile.AccountName);
IncErrors();
}
return yisSteamId ? 1 : -1;
}
return res;
return DefaultComparer.Compare(x, y);
void IncErrors() => res.Errors += 1;
}
public class MafileRenameResult
{
public int Total { get; set; }
public int Renamed { get; set; }
public int NotRenamed => Errors + AlreadyExist;
public int Errors { get; set; }
public int AlreadyExist { get; set; }
public string BackupFileName { get; set; }
}
}
@@ -2,9 +2,6 @@
namespace NebulaAuth.Theme.WindowStyle;
/// <summary>
/// Логика взаимодействия для WindowStyle.xaml
/// </summary>
partial class WindowStyle
{
public WindowStyle()
+19 -39
View File
@@ -1,8 +1,7 @@
using System;
using System.Collections.Specialized;
using System.Windows;
using NebulaAuth.Core;
using NebulaAuth.Core;
using NebulaAuth.Model;
using System;
using System.Collections.Specialized;
namespace NebulaAuth.Utility;
@@ -10,24 +9,15 @@ public class ClipboardHelper
{
public static bool Set(string text)
{
var i = 0;
while (i < 20)
try
{
try
{
Clipboard.SetText(text);
return true;
}
catch (Exception ex)
{
if (i == 19)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
}
i++;
System.Windows.Forms.Clipboard.SetText(text);
return true;
}
catch (Exception ex)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
return false;
@@ -35,26 +25,16 @@ public class ClipboardHelper
public static bool SetFiles(StringCollection files)
{
var i = 0;
while (i < 20)
try
{
try
{
Clipboard.SetFileDropList(files);
return true;
}
catch (Exception ex)
{
if (i == 19)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
}
i++;
System.Windows.Forms.Clipboard.SetFileDropList(files);
return true;
}
catch (Exception ex)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
return false;
}
}
@@ -0,0 +1,23 @@
using System;
using System.IO;
using System.Linq;
namespace NebulaAuth.Utility;
public static class FileNameValidator
{
public static bool IsValidFileName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return false;
var invalidChars = Path.GetInvalidFileNameChars();
return !name.Any(c => invalidChars.Contains(c));
}
public static string GetInvalidChars(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
return new string(name.Where(c => invalidChars.Contains(c)).Distinct().ToArray());
}
}
@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
Height="Auto" Width="Auto" MaxWidth="500"
Background="{DynamicResource WindowBackground}">
@@ -40,27 +41,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Visibility="{Binding Tip, Converter={StaticResource NullableToVisibilityConverter}}"
Margin="10"
Padding="5"
BorderThickness="1"
CornerRadius="12">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseShadowColor}" Opacity="0.5" />
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.2" />
</Border.Background>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="QuestionMarkCircleOutline" Foreground="{DynamicResource InfoBrush}"
Height="22"
Width="22" VerticalAlignment="Center" Margin="5,0,0,0" />
<TextBlock x:Name="ConfirmTextBlock" VerticalAlignment="Center"
MaxWidth="420" FontSize="14"
TextWrapping="WrapWithOverflow"
Margin="10,10,10,10" Text="Confirm?" />
</StackPanel>
</Border>
<nebulaAuth:HintBox FontSize="14" x:Name="ConfirmHint" Text="Confirm?" Margin="10"/>
<Grid Grid.Row="1" Margin="10,0,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
@@ -73,18 +54,18 @@
Margin="2,0,4,0"
Style="{StaticResource MaterialDesignRaisedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource True}">
ОК
</Button>
CommandParameter="{StaticResource True}"
Content="{Tr Common.OK}"/>
<Button Grid.Column="1"
HorizontalAlignment="Right"
Width="130"
Margin="0,0,2,0"
IsCancel="True"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource False}">
Отмена
</Button>
CommandParameter="{StaticResource False}"
Content="{Tr Common.Cancel}"
/>
</Grid>
</Grid>
</Grid>
@@ -1,8 +1,5 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для ConfirmCancelDialog.xaml
/// </summary>
public partial class ConfirmCancelDialog
{
public ConfirmCancelDialog()
@@ -13,6 +10,6 @@ public partial class ConfirmCancelDialog
public ConfirmCancelDialog(string msg)
{
InitializeComponent();
ConfirmTextBlock.Text = msg;
ConfirmHint.Text = msg;
}
}
@@ -1,8 +1,5 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для LoginAgainDialog.xaml
/// </summary>
public partial class LoginAgainDialog
{
public LoginAgainDialog()
@@ -1,8 +1,5 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для LoginAgainDialog.xaml
/// </summary>
public partial class LoginAgainOnImportDialog
{
public LoginAgainOnImportDialog()
@@ -1,8 +1,6 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для SetCryptPasswordDialog.xaml
/// </summary>
public partial class SetCryptPasswordDialog
{
public SetCryptPasswordDialog()
@@ -0,0 +1,71 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.TextFieldDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Height="Auto" Width="Auto" MaxWidth="500"
Background="{DynamicResource WindowBackground}">
<Grid MinHeight="140" MinWidth="350">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<!--<materialDesign:PackIcon Kind="WarningCircleOutline" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />-->
<TextBlock x:Name="TitleTextBlock" FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr TextFieldDialog.Title}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" CommandParameter="{x:Null}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<Grid Grid.Row="2" Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox x:Name="TextField" Margin="10" materialDesign:HintAssist.Hint=""></TextBox>
<Grid Grid.Row="1" Margin="10,0,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button
Width="130"
HorizontalAlignment="Left"
Margin="2,0,4,0"
Style="{StaticResource MaterialDesignRaisedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{Binding Text, ElementName=TextField}"
Content="{Tr Common.OK}"
/>
<Button Grid.Column="1"
HorizontalAlignment="Right"
Width="130"
Margin="0,0,2,0"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsCancel="True"
CommandParameter="{x:Null}"
Content="{Tr Common.Cancel}"
/>
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,23 @@
using MaterialDesignThemes.Wpf;
namespace NebulaAuth.View.Dialogs;
public partial class TextFieldDialog
{
public TextFieldDialog()
{
InitializeComponent();
}
public TextFieldDialog(string? title, string? msg)
{
InitializeComponent();
if(!string.IsNullOrWhiteSpace(title))
TitleTextBlock.Text = title;
if (!string.IsNullOrEmpty(msg))
HintAssist.SetHint(TextField, msg);
}
}
@@ -3,9 +3,7 @@ using System.Windows;
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для WaitLoginDialog.xaml
/// </summary>
public partial class WaitLoginDialog
{
private TaskCompletionSource<string> _tcs = new();
-3
View File
@@ -1,8 +1,5 @@
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для LinkerView.xaml
/// </summary>
public partial class LinkerView
{
public LinkerView()
+1 -3
View File
@@ -4,9 +4,7 @@ using System.Windows.Controls;
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для LinksView.xaml
/// </summary>
public partial class LinksView : UserControl
{
public LinksView()
+1 -3
View File
@@ -2,9 +2,7 @@
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для MafileMoverView.xaml
/// </summary>
public partial class MafileMoverView : UserControl
{
public MafileMoverView()
+12 -2
View File
@@ -58,8 +58,18 @@
<TextBlock HorizontalAlignment="Stretch">
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" />
<Run Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" />
<Run
Text="{Binding DefaultProxy.Value, Converter='{StaticResource ProxyDataTextConverter}', Mode=OneWay, FallbackValue=''}" />
<Run>
<Run.Text>
<MultiBinding Mode="OneWay"
Converter="{StaticResource ProxyDataTextMultiConverter}">
<Binding Path="DefaultProxy.Value" />
<Binding Path="DataContext.DisplayProtocol"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext.DisplayCredentials"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
</MultiBinding>
</Run.Text>
</Run>
</TextBlock>
<Button Grid.Column="1" Command="{Binding RemoveDefaultCommand}">
+1 -3
View File
@@ -3,9 +3,7 @@ using System.Windows.Input;
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для ProxyManagerView.xaml
/// </summary>
public partial class ProxyManagerView
{
public ProxyManagerView()
+53 -26
View File
@@ -5,12 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
d:DesignHeight="650"
FontFamily="{materialDesign:MaterialDesignFont}"
MinHeight="400"
MinWidth="400"
MaxWidth="400"
MinWidth="480"
MaxWidth="480"
d:DataContext="{d:DesignInstance other:SettingsVM}"
Background="Transparent"
FontSize="15">
@@ -38,6 +39,7 @@
<Button Grid.Column="1" Width="30" Height="30"
Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsEnabled="{Binding ApplyRenameSettingCommand.IsRunning, Converter={StaticResource ReverseBooleanConverter}, Mode=OneWay}"
IsCancel="True">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
@@ -47,15 +49,33 @@
<TabControl materialDesign:ColorZoneAssist.Background="{DynamicResource Base100Brush}"
Style="{StaticResource MaterialDesignUniformTabControl}" Grid.Row="2">
<TabItem Header="{Tr SettingsDialog.MainSettings}">
<StackPanel Width="340" Margin="10,20,10,40" Orientation="Vertical" HorizontalAlignment="Center">
<StackPanel Width="400" Margin="10,18,10,40" Orientation="Vertical" HorizontalAlignment="Center">
<ComboBox Style="{StaticResource MaterialDesignFloatingHintComboBox}" Margin="0,15,0,0"
ItemsSource="{Binding Languages}" DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding Language}"
materialDesign:HintAssist.Hint="{Tr LanguageWord}" />
<CheckBox Margin="0,15,0,0" IsChecked="{Binding HideToTray}"
<CheckBox Margin="0,18,0,0" IsChecked="{Binding HideToTray}"
Content="{Tr SettingsDialog.MinimizeToTray}" />
<CheckBox Margin="0,15,0,0" IsChecked="{Binding UseIcon}"
<CheckBox Margin="0,18,0,0" IsChecked="{Binding LegacyMode}"
Content="{Tr SettingsDialog.LegacyMafileMode}"
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" BorderBrush="AliceBlue"
Margin="0,18,0,0"
BorderThickness="2"
IsChecked="{Binding IgnorePatchTuesdayErrors}"
ToolTip="{Tr SettingsDialog.IgnorePatchTuesdayErrorsHint}">
<TextBlock TextWrapping="WrapWithOverflow"
Text="{Tr SettingsDialog.IgnorePatchTuesdayErrors}" />
</CheckBox>
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
</CheckBox>-->
<CheckBox Margin="0,20,0,0" IsChecked="{Binding UseIcon}"
Content="{Tr SettingsDialog.UseIndicator}"
ToolTip="{Tr SettingsDialog.UseIndicatorHint}" />
<materialDesign:ColorPicker Margin="-5,0,-5,0" IsEnabled="{Binding UseIcon}"
@@ -75,29 +95,36 @@
<materialDesign:PackIcon Kind="ContentSave" />
</Button>
</Grid>
<CheckBox Margin="0,16,0,0" IsChecked="{Binding LegacyMode}"
Content="{Tr SettingsDialog.LegacyMafileMode}"
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" BorderBrush="AliceBlue"
Margin="0,12,0,0"
BorderThickness="2"
IsChecked="{Binding IgnorePatchTuesdayErrors}"
ToolTip="{Tr SettingsDialog.IgnorePatchTuesdayErrorsHint}">
<TextBlock TextWrapping="WrapWithOverflow"
Text="{Tr SettingsDialog.IgnorePatchTuesdayErrors}" />
</CheckBox>
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
</CheckBox>-->
<ComboBox materialDesign:HintAssist.Hint="Именование мафайлов"
Margin="0,12,0,0"
Style="{StaticResource MaterialDesignFloatingHintComboBox}">
<ComboBoxItem Content="steamId.mafile" />
<ComboBoxItem Content="login.mafile" />
<ComboBox materialDesign:HintAssist.Hint="{Tr SettingsDialog.MafileNamingMode}"
Margin="0,25,0,0"
Style="{StaticResource MaterialDesignFloatingHintComboBox}"
SelectedItem="{Binding UseAccountNameAsMafileNamePreview}">
<ComboBox.ItemsSource>
<x:Array Type="{x:Type sys:Boolean}"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<sys:Boolean>True</sys:Boolean>
<sys:Boolean>False</sys:Boolean>
</x:Array>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource BoolToMafileNamingConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button>Применить и переименовать</Button>
<Button Margin="0,10,0,0"
materialDesign:ButtonProgressAssist.Maximum="1"
materialDesign:ButtonProgressAssist.Value="{Binding RenameMafilesProgress}"
materialDesign:ButtonProgressAssist.IsIndicatorVisible="{Binding ApplyRenameSettingCommand.IsRunning}"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{Binding ApplyRenameSettingCommand}"
Content="{Tr SettingsDialog.ApplyNamingMode}"
/>
<nebulaAuth:HintBox Margin="0,10,0,0" Text="{Binding RenameResultText}"
FontSize="14"
Visibility="{Binding RenameResultText, Converter={StaticResource NullableToVisibilityConverter}, Mode=OneWay}"/>
</StackPanel>
</TabItem>
+1 -3
View File
@@ -5,9 +5,7 @@ using MaterialDesignThemes.Wpf;
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для SettingsView.xaml
/// </summary>
public partial class SettingsView
{
private readonly DialogHost? _dialogHost;
-3
View File
@@ -1,8 +1,5 @@
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для UpdaterView.xaml
/// </summary>
public partial class UpdaterView
{
public UpdaterView()
@@ -177,7 +177,7 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
var linkOptions = new LinkOptions(_client, StaticLoginConsumer.Instance, this,
this, this, Storage.BackupHandler, Logger2);
this, this, Storage.WriteBackup, Logger2);
var linker = new SteamAuthenticatorLinker(linkOptions);
MobileDataExtended result;
try
@@ -226,7 +226,6 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
}
Storage.SaveMafile(mafile);
File.Delete(Path.Combine("mafiles_backup", mafile.AccountName + ".mafile"));
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString(), login);
}
@@ -1,13 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using AchiesUtilities.Web.Proxy;
using AchiesUtilities.Web.Proxy;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
@@ -33,6 +24,14 @@ using SteamLibForked.Abstractions;
using SteamLibForked.Exceptions.Authorization;
using SteamLibForked.Models.Core;
using SteamLibForked.Models.Session;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace NebulaAuth.ViewModel.MafileMover;
@@ -236,7 +235,7 @@ public partial class MafileMoverVM : ObservableObject, IAuthProvider, IDisposabl
var t = res.ReplacementToken;
var j = JsonConvert.SerializeObject(t);
Storage.BackupHandlerStr(login, j);
Storage.WriteBackup(login, j);
var mobileData = t.ToMobileDataExtended(SteamAuthenticatorLinkerApi.GenerateDeviceId(), msd);
@@ -254,7 +253,6 @@ public partial class MafileMoverVM : ObservableObject, IAuthProvider, IDisposabl
}
Storage.SaveMafile(mafile);
File.Delete(Path.Combine("mafiles_backup", mafile.AccountName + ".mafile"));
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString());
}
+1
View File
@@ -49,6 +49,7 @@ public partial class MainVM : ObservableObject
[RelayCommand]
public async Task Debug()
{
}
@@ -1,20 +1,20 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
using SteamLib.SteamMobile.Confirmations;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace NebulaAuth.ViewModel;
public partial class MainVM //Confirmations
{
public ObservableCollection<Confirmation> Confirmations { get; } = new();
public ObservableCollection<Confirmation> Confirmations { get; } = [];
public bool ConfirmationsVisible => SelectedMafile == _confirmationsLoadedForMafile;
private Mafile? _confirmationsLoadedForMafile;
+1 -1
View File
@@ -30,7 +30,7 @@ public partial class MainVM //File //TODO: Refactor
{
var mafile = SelectedMafile;
var path = Storage.MafileFolder;
var path = Storage.MafilesDirectory;
string? mafilePath = null;
if (mafile != null)
{
+17 -2
View File
@@ -1,8 +1,11 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
@@ -33,9 +36,17 @@ public partial class MainVM //Groups
[ObservableProperty] private ObservableCollection<string> _groups = [];
private string _searchText = string.Empty;
private string? _selectedGroup;
[RelayCommand]
private async Task CreateGroup(Mafile? mafile)
{
if(mafile == null) return;
var res = await DialogsController.ShowTextFieldDialog(GetLocalization("CreateGroupTitle"), GetLocalization("CreateGroupInput"));
if(string.IsNullOrWhiteSpace(res)) return;
AddToGroup([res, mafile]);
SnackbarController.SendSnackbar(GetLocalization("CreateGroupSuccess"));
}
[RelayCommand]
private void AddGroup(string? value)
@@ -68,7 +79,7 @@ public partial class MainVM //Groups
OnPropertyChanged(nameof(SelectedMafile));
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(RemoveGroupCanExecute))]
private void RemoveGroup(Mafile? mafile)
{
if (mafile?.Group == null) return;
@@ -85,6 +96,10 @@ public partial class MainVM //Groups
PerformQuery();
}
private bool RemoveGroupCanExecute(Mafile? mafile)
{
return mafile is {Group: not null};
}
private void QueryGroups()
{
+99 -24
View File
@@ -1,9 +1,12 @@
using System.Collections.Generic;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace NebulaAuth.ViewModel.Other;
@@ -11,6 +14,7 @@ public partial class SettingsVM : ObservableObject
{
public Settings Settings => Settings.Instance;
#region SettingsProps
public bool HideToTray
{
@@ -88,7 +92,11 @@ public partial class SettingsVM : ObservableObject
public bool UseAccountNameAsMafileName
{
get => Settings.UseAccountNameAsMafileName;
set => Settings.UseAccountNameAsMafileName = value;
set
{
Settings.UseAccountNameAsMafileName = value;
ApplyRenameSettingCommand.NotifyCanExecuteChanged();
}
}
public bool IgnorePatchTuesdayErrors
@@ -97,28 +105,9 @@ public partial class SettingsVM : ObservableObject
set => Settings.IgnorePatchTuesdayErrors = value;
}
[ObservableProperty] private string? _password;
[RelayCommand]
private void SetPassword()
{
Settings.IsPasswordSet = PHandler.SetPassword(Password);
}
[RelayCommand]
private void ResetThemeDefaults()
{
Settings.ResetThemeDefaults();
OnPropertyChanged(nameof(BackgroundBlur));
OnPropertyChanged(nameof(BackgroundOpacity));
OnPropertyChanged(nameof(BackgroundGamma));
OnPropertyChanged(nameof(LeftOpacity));
OnPropertyChanged(nameof(RightOpacity));
OnPropertyChanged(nameof(ApplyBlurBackground));
OnPropertyChanged(nameof(RippleDisabled));
}
#endregion
#region Theme
@@ -177,4 +166,90 @@ public partial class SettingsVM : ObservableObject
}
#endregion
[ObservableProperty] private string? _password;
[ObservableProperty] private string? _renameResultText;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ApplyRenameSettingCommand))]
private bool _useAccountNameAsMafileNamePreview;
[ObservableProperty]
private double _renameMafilesProgress;
public SettingsVM()
{
_useAccountNameAsMafileNamePreview = Settings.UseAccountNameAsMafileName;
}
[RelayCommand]
private void SetPassword()
{
Settings.IsPasswordSet = PHandler.SetPassword(Password);
}
[RelayCommand]
private void ResetThemeDefaults()
{
Settings.ResetThemeDefaults();
OnPropertyChanged(nameof(BackgroundBlur));
OnPropertyChanged(nameof(BackgroundOpacity));
OnPropertyChanged(nameof(BackgroundGamma));
OnPropertyChanged(nameof(LeftOpacity));
OnPropertyChanged(nameof(RightOpacity));
OnPropertyChanged(nameof(ApplyBlurBackground));
OnPropertyChanged(nameof(RippleDisabled));
}
[RelayCommand(CanExecute = nameof(ApplyRenameSettingCanExecute))]
private async Task ApplyRenameSetting()
{
RenameResultText = null;
var targetValue = UseAccountNameAsMafileNamePreview;
if (UseAccountNameAsMafileName == targetValue) return;
RenameMafilesProgress = 0;
Storage.MafileRenameResult? result = null;
try
{
result = await Storage.RenameMafiles(targetValue, new Progress<double>(p => RenameMafilesProgress = p));
UseAccountNameAsMafileName = targetValue;
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Error while renaming mafiles");
}
finally
{
RenameMafilesProgress = 0;
}
if (result == null)
{
RenameResultText = GetLoc("Error");
return;
}
if (result.Total == 0) return;
if (result.NotRenamed == 0)
{
var l = GetLoc("AllRenamed");
RenameResultText = string.Format(l, result.Total, result.BackupFileName);
return;
}
else
{
var l = GetLoc("PartialSuccess");
RenameResultText =
string.Format(l, result.Total, result.Renamed, result.Errors, result.AlreadyExist, result.BackupFileName);
}
string GetLoc(string key)
{
return LocManager.GetCodeBehindOrDefault(key, "SettingsVM", "MafileRenaming", key);
}
}
private bool ApplyRenameSettingCanExecute() => UseAccountNameAsMafileNamePreview != UseAccountNameAsMafileName;
}
+102 -48
View File
@@ -45,6 +45,16 @@
"ru": "Удалить",
"ua": "Видалити"
},
"Cancel": {
"en": "Cancel",
"ru": "Отмена",
"ua": "Скасувати"
},
"OK": {
"en": "OK",
"ru": "ОК",
"ua": "ОК"
},
"Abbreviations": {
"Time": {
"Seconds": {
@@ -290,6 +300,11 @@
"ru": "Скопировать SteamID",
"ua": "Скопіювати SteamID"
},
"CreateGroup": {
"en": "Create group",
"ru": "Создать группу",
"ua": "Створити групу"
},
"AddToGroup": {
"en": "Add to group",
"ru": "Добавить в группу",
@@ -432,7 +447,7 @@
"ua": "Поточний пароль шифрування"
},
"Hint": {
"en": "Doesn't saved on disk",
"en": "Not saved to disk",
"ru": "Не сохраняется в системе",
"ua": "Не зберігається у системі"
}
@@ -453,10 +468,15 @@
"ru": "Разрешить автообновление",
"ua": "Дозволити автооновлення"
},
"UseAccountName": {
"en": "Use account login on mafiles",
"ru": "Использовать логин аккаунта на мафайлах",
"ua": "Використовувати логін акаунта на мафайлах"
"MafileNamingMode": {
"en": "Mafile naming mode",
"ru": "Режим именования мафайлов",
"ua": "Режим іменування мафайлів"
},
"ApplyNamingMode": {
"en": "Apply and rename",
"ru": "Применить и переименовать",
"ua": "Застосувати та перейменувати"
},
"IgnorePatchTuesdayErrors": {
"en": "Ignore Patch Tuesday errors in timer (exp.)",
@@ -708,6 +728,13 @@
"ua": "Підтвердіть дію"
}
},
"TextFieldDialog": {
"Title": {
"ru": "Введите значение",
"en": "Enter value",
"ua": "Введіть значення"
}
},
"CodeBehind": {
"MainVM": {
"SuccessfulLogin": {
@@ -726,12 +753,9 @@
"ua": "Відсутній RCode"
},
"ConfirmRemovingAuthenticator": {
"ru":
"Вы точно уверены, что хотите удалить аутентификатор?\r\nПосле этого вы не сможете обмениваться, использовать торговую площадку 15 дней. Мафайл будет удалён навсегда",
"en":
"Are you sure you want to remove the authenticator?\r\nAfter that, you will not be able to trade or use the marketplace for 15 days. Mafile will be deleted permanently",
"ua":
"Ви точно впевнені, що хочете видалити автентифікатор?\r\nПісля цього ви не зможете обмінюватися, використовувати торгову площадку 15 днів. Мафайл буде видалено назавжди"
"ru": "Вы точно уверены, что хотите удалить аутентификатор?\r\nПосле этого вы не сможете обмениваться, использовать торговую площадку 15 дней. Мафайл будет удалён навсегда",
"en": "Are you sure you want to remove the authenticator?\r\nAfter that, you will not be able to trade or use the marketplace for 15 days. Mafile will be deleted permanently",
"ua": "Ви точно впевнені, що хочете видалити автентифікатор?\r\nПісля цього ви не зможете обмінюватися, використовувати торгову площадку 15 днів. Мафайл буде видалено назавжди"
},
"AuthenticatorRemoved": {
@@ -800,12 +824,9 @@
"ua": "помилки:"
},
"RemoveMafileConfirmation": {
"ru":
"Удалить mafile?\r\nМафайл будет перемещен в папку 'mafiles_removed'\r\nЭто действие НЕ удаляет Steam Guard с аккаунта",
"en":
"Remove mafile?\r\nMafile will be moved to 'mafiles_removed' directory\r\nThis action will NOT remove the Steam Guard from the account",
"ua":
"Видалити mafile?\r\nМафайл буде переміщено у каталог 'mafiles_removed'\r\nЦя дія НЕ видаляє автентифікатор з облікового запису"
"ru": "Удалить mafile?\r\nМафайл будет перемещен в папку 'mafiles_removed'\r\nЭто действие НЕ удаляет Steam Guard с аккаунта",
"en": "Remove mafile?\r\nMafile will be moved to 'mafiles_removed' directory\r\nThis action will NOT remove the Steam Guard from the account",
"ua": "Видалити mafile?\r\nМафайл буде переміщено у каталог 'mafiles_removed'\r\nЦя дія НЕ видаляє автентифікатор з облікового запису"
},
"CantRemoveAlreadyExist": {
"ru": "Не удалось переместить mafile, возможно в папке уже существует мафайл с таким именем.",
@@ -859,9 +880,23 @@
},
"CantDecryptPassword": {
"en": "Can't decrypt password. Maybe password from this mafile was encrypted with another encryption password",
"ru":
"Не удалось расшифровать пароль. Возможно пароль из этого мафайла был зашифрован другим паролем шифрования",
"ru": "Не удалось расшифровать пароль. Возможно пароль из этого мафайла был зашифрован другим паролем шифрования",
"ua": "Не вдалося розшифрувати пароль. Можливо пароль з цього мафайла був зашифрований іншим паролем шифрування"
},
"CreateGroupTitle": {
"en": "New group",
"ru": "Новая группа",
"ua": "Нова група"
},
"CreateGroupInput": {
"en": "Enter group name",
"ru": "Введите имя группы",
"ua": "Введіть назву групи"
},
"CreateGroupSuccess": {
"en": "Group created",
"ru": "Группа создана",
"ua": "Групу створено"
}
},
"ErrorTranslator": {
@@ -1116,10 +1151,8 @@
},
"InvalidStateWithStatus2": {
"ru": "Неверное состояние (InvalidState) со статусом 2. Откройте ссылку на руководство сверху этого окна.",
"en":
"Invalid state (InvalidState) with status 2. Open link to the troubleshooting guide at the top of this window.",
"ua":
"Невірний стан (InvalidState) зі статусом 2. Відкрийте посилання на посібник з усунення несправностей у верхній частині цього вікна."
"en": "Invalid state (InvalidState) with status 2. Open link to the troubleshooting guide at the top of this window.",
"ua": "Невірний стан (InvalidState) зі статусом 2. Відкрийте посилання на посібник з усунення несправностей у верхній частині цього вікна."
},
"GeneralFailure": {
"ru": "General Failure",
@@ -1168,8 +1201,7 @@
"Guard": {
"ru": "Ошибка: Был запрошен вход с помощью Steam Guard. На аккаунте уже активирован мобильный аутентификатор",
"en": "Error: Login with Steam Guard was requested. Mobile authenticator is already activated on the account",
"ua":
"Помилка: Був запрошений вхід за допомогою Steam Guard. На акаунті вже активовано мобільний автентифікатор"
"ua": "Помилка: Був запрошений вхід за допомогою Steam Guard. На акаунті вже активовано мобільний автентифікатор"
},
"Unknown": {
"ru": "Ошибка: Неизвестный тип аутентификации",
@@ -1272,12 +1304,9 @@
"ua": "На пошту надіслано лист. Відкрийте посилання з листа, а потім натисніть — 'Продовжити'"
},
"ClickOnEmailLinkRetry": {
"ru":
"Не удалось подтвердить переход по ссылке. Убедитесь, что вы открыли ссылку из письма, затем нажмите — 'Продолжить'",
"en":
"We couldnt verify that you clicked the link. Make sure you opened the link from the email, then click 'Proceed'",
"ua":
"Не вдалося підтвердити перехід за посиланням. Переконайтесь, що ви відкрили посилання з листа, а потім натисніть — 'Продовжити'"
"ru": "Не удалось подтвердить переход по ссылке. Убедитесь, что вы открыли ссылку из письма, затем нажмите — 'Продолжить'",
"en": "We couldnt verify that you clicked the link. Make sure you opened the link from the email, then click 'Proceed'",
"ua": "Не вдалося підтвердити перехід за посиланням. Переконайтесь, що ви відкрили посилання з листа, а потім натисніть — 'Продовжити'"
},
"PhoneHint": {
"ru": "На телефон {0} была отправлена СМС",
@@ -1285,12 +1314,9 @@
"ua": "На телефон {0} було відправлено СМС"
},
"EnterPhoneNumber": {
"ru":
"Введите номер в международном формате, только цифры (без '+', пробелов и скобок), или оставьте поле пустым, чтобы привязать без номера",
"en":
"Enter number in international format, digits only (no '+', spaces or brackets), or leave the field empty to link without a number",
"ua":
"Введіть номер у міжнародному форматі, лише цифри (без '+', пробілів і дужок), або залиште поле порожнім, щоб прив’язати без номера"
"ru": "Введите номер в международном формате, только цифры (без '+', пробелов и скобок), или оставьте поле пустым, чтобы привязать без номера",
"en": "Enter number in international format, digits only (no '+', spaces or brackets), or leave the field empty to link without a number",
"ua": "Введіть номер у міжнародному форматі, лише цифри (без '+', пробілів і дужок), або залиште поле порожнім, щоб прив’язати без номера"
}
},
"MafileMoverVM": {
@@ -1315,11 +1341,9 @@
"ua": "RCode: {0}\r\nІм'я файлу: {1}.mafile"
},
"GuardIsNotActive": {
"ru":
"Steam Guard установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"",
"ru": "Steam Guard установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"",
"en": "Steam Guard is set on this account. To link mafile to the account, use the 'Link' window",
"ua":
"Steam Guard встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'"
"ua": "Steam Guard встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'"
},
"SeemsNoPhoneNumber": {
"en": "The account has no phone number linked, returned Fail code (2)",
@@ -1354,14 +1378,44 @@
"ua": "Авто "
},
"TimerPreventedOverlap": {
"ru":
"Авто: таймер подтверждений пытался начать новый цикл, когда предыдущий еще не завершился. Советуем увеличить задержку",
"en":
"Auto: confirmation timer tried to start new cycle when previous one was not finished yet. We recommend to increase delay",
"ua":
"Авто: таймер підтверджень намагався запустити новий цикл, коли попередній ще не закінчився. Рекомендуємо збільшити затримку"
"ru": "Авто: таймер подтверждений пытался начать новый цикл, когда предыдущий еще не завершился. Советуем увеличить задержку",
"en": "Auto: confirmation timer tried to start new cycle when previous one was not finished yet. We recommend to increase delay",
"ua": "Авто: таймер підтверджень намагався запустити новий цикл, коли попередній ще не закінчився. Рекомендуємо збільшити затримку"
}
}
},
"SettingsVM": {
"MafileRenaming": {
"Error": {
"en": "Error while renaming mafiles. It was saved in log",
"ru": "Ошибка при переименовании мафайлов. Она была сохранена в log",
"ua": "Помилка при перейменуванні мафайлів. Вона була збережена в log"
},
"AllRenamed": {
"ru": "Все мафайлы успешно переименованы ({0}).\r\nБыла создана резервная копия: {1}",
"en": "All mafiles successfully renamed ({0}).\r\nBackup was created: {1}",
"ua": "Всі мафайли успішно перейменовані ({0}).\r\nБуло створено резервну копію: {1}"
},
"PartialSuccess": {
"en": "Total mafiles: {0}\r\nRenamed: {1}\r\nConflicts: {2}\r\nErrors: {3}\r\n\r\nBackup of mafiles saved in file: {4}",
"ru": "Всего мафайлов: {0}\r\nПереименовано: {1}\r\nКонфликты: {2}\r\nОшибок: {3}\r\n\r\nРезервная копия мафайлов сохранена в файле: {4}",
"ua": "Всього мафайлів: {0}\r\nПерейменовано: {1}\r\nКонфлікти: {2}\r\nПомилок: {3}\r\n\r\nРезервна копія мафайлів збережена у файлі: {4}"
}
}
},
"ProxyStorage": {
"ErrorWhileLoadingProxies": {
"en": "Error while loading proxies",
"ru": "Ошибка при загрузке прокси",
"ua": "Помилка при завантаженні проксі"
}
},
"Settings": {
"ErrorWhileLoadingSettings": {
"en": "Error while loading settings. Settings were reset",
"ru": "Ошибка при загрузке настроек. Настройки были сброшены",
"ua": "Помилка при завантаженні налаштувань. Налаштування були скинуті"
}
}
},
"CantAlignTimeError": {
"ru":