mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-25 06:14:31 +00:00
Added MAAC persistence support and password management features
- Introduced `MAACStorage` for managing multi-account auto-confirmers persistence - Added `SetAccountPasswordsView` dialog for managing account passwords setup - Enhanced `Storage` with async methods for file operations - Added `BoolToValueConverter` for flexible XAML data bindings - Enhanced user experience in dialogs with `IsDefault` and `IsCancel` properties
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using NebulaAuth.Model.MAAC;
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace NebulaAuth;
|
||||
|
||||
@@ -20,6 +21,7 @@ public partial class App
|
||||
Shell.Initialize();
|
||||
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
|
||||
await Storage.Initialize(threads);
|
||||
MAACStorage.Initialize();
|
||||
var mainWindow = new MainWindow();
|
||||
Current.MainWindow = mainWindow;
|
||||
mainWindow.Show();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace NebulaAuth.Converters;
|
||||
|
||||
public class BoolToValueConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not bool b) return Binding.DoNothing;
|
||||
if (parameter is Array)
|
||||
{
|
||||
var arr = (Array)parameter;
|
||||
if (arr.Length == 0) return Binding.DoNothing;
|
||||
var first = arr.GetValue(0);
|
||||
var second = arr.Length > 1 ? arr.GetValue(1) : null;
|
||||
return b ? first : second;
|
||||
}
|
||||
|
||||
return b ? parameter : null;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
|
||||
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
|
||||
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter"/>
|
||||
|
||||
<converters:BoolToValueConverter x:Key="BoolToValueConverter"/>
|
||||
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
|
||||
<!-- Background converters-->
|
||||
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
|
||||
|
||||
@@ -121,4 +121,14 @@ public static class DialogsController
|
||||
vm?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ShowSetAccountsPasswordDialog()
|
||||
{
|
||||
var vm = new SetAccountPasswordsVM();
|
||||
var dialog = new SetAccountPasswordsView
|
||||
{
|
||||
DataContext = vm
|
||||
};
|
||||
await DialogHost.Show(dialog);
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,17 @@
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.OpenFolder}"
|
||||
Command="{Binding OpenMafileFolderCommand}" />
|
||||
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.ProxyManager}"
|
||||
Command="{Binding OpenProxyManagerCommand}">
|
||||
</MenuItem>
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Settings}"
|
||||
Command="{Binding OpenSettingsDialogCommand}" />
|
||||
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.Title}">
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.SetPasswords}"
|
||||
Command="{Binding DebugCommand}"
|
||||
/>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Menu>
|
||||
@@ -314,6 +323,9 @@
|
||||
<KeyBinding Key="X" Modifiers="Control"
|
||||
Command="{Binding DataContext.CopyMafileCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}" />
|
||||
<KeyBinding Modifiers="Control+Shift" Key="C"
|
||||
Command="{Binding DataContext.CopyPasswordCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}" />
|
||||
</ListBox.InputBindings>
|
||||
<FrameworkElement.ContextMenu>
|
||||
<ContextMenu Tag="{Binding Groups}">
|
||||
@@ -321,7 +333,11 @@
|
||||
Header="{Tr MainWindow.ContextMenus.Mafile.CopyLogin}"
|
||||
Command="{Binding CopyLoginCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopySteamId}"
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopyPassword}"
|
||||
Command="{Binding Path=CopyPasswordCommand}"
|
||||
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
|
||||
InputGestureText="CTRL+⇧+C"/>
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopySteamId}"
|
||||
Command="{Binding CopySteamIdCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
<MenuItem InputGestureText="Ctrl+X"
|
||||
@@ -374,9 +390,6 @@
|
||||
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>
|
||||
|
||||
@@ -64,7 +64,7 @@ public partial class MainWindow
|
||||
if (ItemsControl.ContainerFromElement((ListBox) sender, (DependencyObject) e.OriginalSource) is ListBoxItem
|
||||
{
|
||||
IsSelected: true
|
||||
} item)
|
||||
})
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,16 @@ public partial class Mafile : MobileDataExtended
|
||||
set => SetProperty(ref _linkedClient, value);
|
||||
}
|
||||
|
||||
[JsonIgnore] public string? Filename { get; set; }
|
||||
[JsonIgnore] public string? Filename
|
||||
{
|
||||
get => _filename;
|
||||
set => SetProperty(ref _filename, value);
|
||||
}
|
||||
|
||||
[JsonIgnore] private PortableMaClient? _linkedClient;
|
||||
|
||||
private string? _filename;
|
||||
|
||||
public void SetSessionData(MobileSessionData? sessionData)
|
||||
{
|
||||
SessionData = sessionData;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NebulaAuth.Core;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public static class MAACStorage
|
||||
{
|
||||
private static Dictionary<string, StoredClient> Clients { get; set; } = [];
|
||||
|
||||
static MAACStorage(){}
|
||||
|
||||
private static void ClientsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
Clients.Clear();
|
||||
}
|
||||
else if (e.NewItems != null)
|
||||
{
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
if (item is Mafile { Filename: not null } mafile)
|
||||
{
|
||||
if (mafile.LinkedClient != null)
|
||||
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
|
||||
|
||||
Clients.Add(mafile.Filename, new StoredClient
|
||||
{
|
||||
AutoConfirmMarket = mafile.LinkedClient?.AutoConfirmMarket ?? false,
|
||||
AutoConfirmTrades = mafile.LinkedClient?.AutoConfirmTrades ?? false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
foreach (var item in e.OldItems)
|
||||
{
|
||||
if (item is Mafile { Filename: not null } mafile)
|
||||
{
|
||||
if (mafile.LinkedClient != null)
|
||||
mafile.LinkedClient.PropertyChanged -= LinkedClientOnPropertyChanged;
|
||||
|
||||
Clients.Remove(mafile.Filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private static void LinkedClientOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is not PortableMaClient client) return;
|
||||
if (client.Mafile.Filename == null) return;
|
||||
var anyChanges = false;
|
||||
if (!Clients.TryGetValue(client.Mafile.Filename, out var storedClient))
|
||||
{
|
||||
client.PropertyChanged -= LinkedClientOnPropertyChanged;
|
||||
return;
|
||||
}
|
||||
if (e.PropertyName == nameof(PortableMaClient.AutoConfirmMarket))
|
||||
{
|
||||
anyChanges = storedClient.AutoConfirmMarket != client.AutoConfirmMarket;
|
||||
storedClient.AutoConfirmMarket = client.AutoConfirmMarket;
|
||||
}
|
||||
else if (e.PropertyName == nameof(PortableMaClient.AutoConfirmTrades))
|
||||
{
|
||||
anyChanges = storedClient.AutoConfirmTrades != client.AutoConfirmTrades;
|
||||
storedClient.AutoConfirmTrades = client.AutoConfirmTrades;
|
||||
}
|
||||
if (anyChanges)
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(Clients, Formatting.Indented);
|
||||
File.WriteAllText("maac.json", json);
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (!File.Exists("maac.json")) return;
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText("maac.json");
|
||||
var clients = JsonConvert.DeserializeObject<Dictionary<string, StoredClient>>(json) ?? [];
|
||||
foreach (var (fileName, storedClient) in clients)
|
||||
{
|
||||
var mafile = Storage.MaFiles.FirstOrDefault(x => x.Filename == fileName);
|
||||
if (mafile == null) continue;
|
||||
if(storedClient is {AutoConfirmMarket: false, AutoConfirmTrades: false}) continue;
|
||||
if (MultiAccountAutoConfirmer.TryAddToConfirm(mafile) && mafile.LinkedClient != null)
|
||||
{
|
||||
mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket;
|
||||
mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades;
|
||||
Clients[fileName] = storedClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, "Failed to load MAAC storage");
|
||||
SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage"));
|
||||
}
|
||||
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
|
||||
|
||||
}
|
||||
|
||||
public static void NotifyMafilesRenamed(IDictionary<string, string> oldNewNames)
|
||||
{
|
||||
var updatedClients = new Dictionary<string, StoredClient>();
|
||||
foreach (var (oldName, newName) in oldNewNames)
|
||||
{
|
||||
if(Clients.Remove(oldName, out var storedClient))
|
||||
{
|
||||
updatedClients[newName] = storedClient;
|
||||
}
|
||||
}
|
||||
Clients = updatedClients;
|
||||
Save();
|
||||
}
|
||||
|
||||
private class StoredClient
|
||||
{
|
||||
public bool AutoConfirmMarket { get; set; }
|
||||
public bool AutoConfirmTrades { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public static class MultiAccountAutoConfirmer
|
||||
Clients = [];
|
||||
Timer = new Timer(TimerConfirm);
|
||||
Settings.Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||
UpdateTimer();
|
||||
UpdateTimer(true);
|
||||
}
|
||||
|
||||
// ReSharper disable once AsyncVoidMethod //Already safe
|
||||
@@ -120,8 +120,8 @@ public static class MultiAccountAutoConfirmer
|
||||
return Lock.WriteLock(() =>
|
||||
{
|
||||
if (Clients.Contains(mafile)) return false;
|
||||
Clients.Add(mafile);
|
||||
mafile.LinkedClient = new PortableMaClient(mafile);
|
||||
Clients.Add(mafile);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -131,9 +131,10 @@ public static class MultiAccountAutoConfirmer
|
||||
{
|
||||
Lock.WriteLock(() =>
|
||||
{
|
||||
Clients.Remove(mafile);
|
||||
mafile.LinkedClient?.Dispose();
|
||||
mafile.LinkedClient = null;
|
||||
Clients.Remove(mafile);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,11 +145,16 @@ public static class MultiAccountAutoConfirmer
|
||||
UpdateTimer();
|
||||
}
|
||||
|
||||
private static void UpdateTimer()
|
||||
private static void UpdateTimer(bool firstStart = false)
|
||||
{
|
||||
var timerInterval = Settings.Instance.TimerSeconds;
|
||||
var intervalTimeSpan = TimeSpan.FromSeconds(timerInterval);
|
||||
Timer.Change(intervalTimeSpan, intervalTimeSpan);
|
||||
var dueTime = intervalTimeSpan;
|
||||
if (firstStart)
|
||||
{
|
||||
dueTime = timerInterval >= 30 ? intervalTimeSpan : TimeSpan.FromSeconds(30);
|
||||
}
|
||||
Timer.Change(dueTime, intervalTimeSpan);
|
||||
}
|
||||
|
||||
private static string GetLocalization(string key)
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using AchiesUtilities.Web.Models;
|
||||
using AchiesUtilities.Web.Models;
|
||||
using AchiesUtilities.Web.Proxy;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using NebulaAuth.Core;
|
||||
@@ -19,6 +11,14 @@ using SteamLib.Exceptions.Authorization;
|
||||
using SteamLib.SteamMobile.Confirmations;
|
||||
using SteamLib.Web;
|
||||
using SteamLibForked.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
|
||||
@@ -20,11 +20,8 @@ namespace NebulaAuth.Model;
|
||||
public static class MaClient
|
||||
{
|
||||
private static HttpClientHandler ClientHandler { get; }
|
||||
|
||||
private static HttpClient Client { get; }
|
||||
|
||||
private static DynamicProxy Proxy { get; }
|
||||
|
||||
public static ProxyData? DefaultProxy { get; set; }
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public partial class SessionHandler //API
|
||||
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
|
||||
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
|
||||
mafile.SetSessionData(mafile.SessionData);
|
||||
Storage.UpdateMafile(mafile);
|
||||
await Storage.UpdateMafileAsync(mafile);
|
||||
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,6 @@ public partial class SessionHandler //API
|
||||
mafile.SetSessionData((MobileSessionData) result);
|
||||
if (PHandler.IsPasswordSet)
|
||||
mafile.Password = savePassword ? PHandler.Encrypt(password) : null;
|
||||
Storage.UpdateMafile(mafile);
|
||||
await Storage.UpdateMafileAsync(mafile);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using NLog;
|
||||
using NLog.Extensions.Logging;
|
||||
using SteamLib.Core;
|
||||
using SteamLib.SteamMobile;
|
||||
using System;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
@@ -39,7 +39,7 @@ public static class Shell
|
||||
|
||||
private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.Fatal((Exception) e.ExceptionObject);
|
||||
Logger.Fatal((Exception)e.ExceptionObject);
|
||||
LogManager.Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using SteamLib.Exceptions.Authorization;
|
||||
using SteamLib.SteamMobile;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
@@ -14,6 +15,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
using NebulaAuth.Model.MAAC;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
|
||||
@@ -131,12 +133,37 @@ public static class Storage
|
||||
MaFiles.Add(data);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SaveMafileAsync(Mafile data)
|
||||
{
|
||||
var path = GetOrCreateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
|
||||
|
||||
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
|
||||
if (existed != null)
|
||||
{
|
||||
var index = MaFiles.IndexOf(existed);
|
||||
MaFiles[index] = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaFiles.Add(data);
|
||||
}
|
||||
}
|
||||
public static void UpdateMafile(Mafile data)
|
||||
{
|
||||
var path = GetOrCreateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
File.WriteAllText(Path.GetFullPath(path), str);
|
||||
}
|
||||
|
||||
public static async Task UpdateMafileAsync(Mafile data)
|
||||
{
|
||||
var path = GetOrCreateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
|
||||
}
|
||||
public static void MoveToRemoved(Mafile data)
|
||||
{
|
||||
var sourcePath = GetOrCreateMafilePath(data);
|
||||
@@ -229,19 +256,22 @@ public static class Storage
|
||||
BackupFileName = backupFileName
|
||||
};
|
||||
|
||||
var dic = new Dictionary<string, string>();
|
||||
foreach (var mafile in mafiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var targetFileName = CreateMafileFileName(mafile, loginAsFileName);
|
||||
if (mafile.Filename == targetFileName || mafile.Filename == null)
|
||||
{
|
||||
res.Renamed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceFileName = mafile.Filename;
|
||||
var fullSourcePath = Path.Combine(MafilesDirectory, sourceFileName);
|
||||
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",
|
||||
@@ -254,11 +284,11 @@ public static class Storage
|
||||
{
|
||||
Shell.Logger.Warn("Can't rename mafile {old} to {new} because target file already exist",
|
||||
mafile.Filename, targetFileName);
|
||||
res.AlreadyExist += 1;
|
||||
res.Conflict += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
File.Move(fullSourcePath, fullTargetPath);
|
||||
dic[sourceFileName] = targetFileName;
|
||||
res.Renamed += 1;
|
||||
mafile.Filename = targetFileName;
|
||||
|
||||
@@ -269,8 +299,9 @@ public static class Storage
|
||||
mafile.AccountName);
|
||||
IncErrors();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MAACStorage.NotifyMafilesRenamed(dic);
|
||||
return res;
|
||||
|
||||
void IncErrors() => res.Errors += 1;
|
||||
@@ -280,9 +311,9 @@ public static class Storage
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int Renamed { get; set; }
|
||||
public int NotRenamed => Errors + AlreadyExist;
|
||||
public int NotRenamed => Errors + Conflict;
|
||||
public int Errors { get; set; }
|
||||
public int AlreadyExist { get; set; }
|
||||
public int Conflict { get; set; }
|
||||
public string BackupFileName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,8 @@
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource True}"
|
||||
Content="{Tr Common.OK}"/>
|
||||
Content="{Tr Common.OK}"
|
||||
IsDefault="True"/>
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Width="130"
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{Binding Text, ElementName=TextField}"
|
||||
Content="{Tr Common.OK}"
|
||||
IsDefault="True"
|
||||
/>
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<UserControl x:Class="NebulaAuth.View.SetAccountPasswordsView"
|
||||
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:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="500" d:DesignWidth="400"
|
||||
d:DataContext="{d:DesignInstance other:SetAccountPasswordsVM}"
|
||||
d:Background="#141119"
|
||||
d:Foreground="White"
|
||||
FontFamily="{md:MaterialDesignFont}"
|
||||
MinHeight="500"
|
||||
MaxHeight="500"
|
||||
MinWidth="400"
|
||||
MaxWidth="400"
|
||||
Background="Transparent"
|
||||
FontSize="16">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ===== HEADER ===== -->
|
||||
<Grid Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="ShieldKey" Width="20" Height="20"
|
||||
Margin="0,0,5,0" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />
|
||||
<TextBlock Text="{Tr SetAccountPasswordsView.Title}"
|
||||
FontSize="18"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource BaseContentBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1" Width="30" Height="30"
|
||||
Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Static md:DialogHost.CloseDialogCommand}"
|
||||
IsCancel="True">
|
||||
<md:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Separator Grid.Row="1" />
|
||||
|
||||
<!-- ===== ENCRYPTION PASSWORD SECTION ===== -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<md:Card Margin="15,15,15,15" BorderThickness="1" BorderBrush="Gray" Padding="12" >
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Tr SetAccountPasswordsView.EncryptionPassword}" FontSize="16"
|
||||
Margin="0,0,0,5"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
md:HintAssist.Hint="{Tr SetAccountPasswordsView.EnterPassword}"
|
||||
md:HintAssist.IsFloating="False"
|
||||
Margin="0,0,10,0"
|
||||
Text="{Binding EncryptionPassword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<Button Grid.Column="1" Command="{Binding SetEncryptionPasswordCommand}">
|
||||
<md:PackIcon Kind="ContentSave"/>
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<StackPanel.Resources>
|
||||
<x:Array x:Key="BoolToIcon" Type="{x:Type md:PackIconKind}">
|
||||
<md:PackIconKind>CheckBox</md:PackIconKind>
|
||||
<md:PackIconKind>CloseBox</md:PackIconKind>
|
||||
</x:Array>
|
||||
<x:Array x:Key="BoolToColor" Type="{x:Type SolidColorBrush}">
|
||||
<SolidColorBrush Color="{DynamicResource SuccessColor}"/>
|
||||
<SolidColorBrush Color="{DynamicResource ErrorColor}"/>
|
||||
</x:Array>
|
||||
|
||||
</StackPanel.Resources>
|
||||
<TextBlock Text="{Tr SetAccountPasswordsView.Status}" VerticalAlignment="Center"/>
|
||||
<md:PackIcon Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{Binding IsEncryptionPasswordSet, Converter={StaticResource BoolToValueConverter}, ConverterParameter={StaticResource BoolToColor}}"
|
||||
Kind="{Binding IsEncryptionPasswordSet, Converter={StaticResource BoolToValueConverter}, ConverterParameter={StaticResource BoolToIcon}}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</md:Card>
|
||||
</Grid>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- ===== ACCOUNTS INPUT ===== -->
|
||||
<TextBox Grid.Row="3"
|
||||
Margin="15,0,15,10"
|
||||
Text="{Binding AccountsPasswords, UpdateSourceTrigger=PropertyChanged}"
|
||||
FontSize="13"
|
||||
AcceptsReturn="True"
|
||||
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Style="{StaticResource MaterialDesignOutlinedTextBox}"
|
||||
md:TextFieldAssist.HasClearButton="True"
|
||||
md:HintAssist.IsFloating="False"
|
||||
md:HintAssist.Hint="{Tr SetAccountPasswordsView.InputFormat}"
|
||||
md:TextFieldAssist.TextBoxIsMultiLine="True"
|
||||
md:TextFieldAssist.TextBoxViewVerticalAlignment="Top"
|
||||
VerticalContentAlignment="Top"
|
||||
>
|
||||
|
||||
</TextBox>
|
||||
<!-- ===== INFO TEXT ===== -->
|
||||
<TextBlock Grid.Row="4"
|
||||
Margin="20,0,20,15"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource BaseContentBrush}"
|
||||
Text="{Tr SetAccountPasswordsView.EncryptionHint}" />
|
||||
|
||||
<Button Grid.Row="5"
|
||||
Margin="10,0,0,10"
|
||||
Width="120"
|
||||
Content="{Tr Common.Save}"
|
||||
Command="{Binding SetPasswordsCommand}"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}" Cursor="Hand" />
|
||||
|
||||
<!-- ===== HINT / RESULT BOX ===== -->
|
||||
<nebulaAuth:HintBox Grid.Row="6"
|
||||
Margin="15,0,15,10"
|
||||
Visibility="{Binding Tip, Converter={StaticResource NullableToVisibilityConverter}}"
|
||||
CloseCommand="{Binding ClearTipCommand}"
|
||||
Text="{Binding Tip}"
|
||||
FontSize="13"
|
||||
Severity="Info"
|
||||
ShowCloseButton="True" />
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace NebulaAuth.View;
|
||||
|
||||
|
||||
public partial class SetAccountPasswordsView
|
||||
{
|
||||
public SetAccountPasswordsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@ public partial class MafileMoverVM : ObservableObject, IAuthProvider, IDisposabl
|
||||
Logger.Error(ex, "Error during saving Nebula data to mafile");
|
||||
}
|
||||
|
||||
Storage.SaveMafile(mafile);
|
||||
await Storage.SaveMafileAsync(mafile);
|
||||
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString());
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public partial class MainVM : ObservableObject
|
||||
[RelayCommand]
|
||||
public async Task Debug()
|
||||
{
|
||||
|
||||
await DialogsController.ShowSetAccountsPasswordDialog();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ public partial class MainVM //File //TODO: Refactor
|
||||
|
||||
var result = data.SessionData != null;
|
||||
if (!result) return result;
|
||||
Storage.SaveMafile(data);
|
||||
await Storage.SaveMafileAsync(data);
|
||||
{
|
||||
ResetQuery();
|
||||
SearchText = data.AccountName ?? string.Empty;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Model;
|
||||
using SteamLibForked.Models.SteamIds;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NebulaAuth.ViewModel.Other;
|
||||
|
||||
public partial class SetAccountPasswordsVM : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SetPasswordsCommand))]
|
||||
private bool _isEncryptionPasswordSet;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SetEncryptionPasswordCommand))]
|
||||
private string? _encryptionPassword;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SetPasswordsCommand))]
|
||||
private string? _accountsPasswords;
|
||||
[ObservableProperty] private string? _tip;
|
||||
|
||||
public SetAccountPasswordsVM()
|
||||
{
|
||||
_isEncryptionPasswordSet = PHandler.IsPasswordSet;
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand(CanExecute = nameof(SetEncryptionPasswordCanExecute))]
|
||||
private void SetEncryptionPassword()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EncryptionPassword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Settings.Instance.IsPasswordSet = PHandler.SetPassword(EncryptionPassword);
|
||||
IsEncryptionPasswordSet = PHandler.IsPasswordSet;
|
||||
EncryptionPassword = null;
|
||||
}
|
||||
|
||||
private bool SetEncryptionPasswordCanExecute() => !string.IsNullOrWhiteSpace(EncryptionPassword);
|
||||
|
||||
|
||||
[RelayCommand(CanExecute = nameof(SetPasswordsCanExecute))]
|
||||
private async Task SetPasswords()
|
||||
{
|
||||
Tip = null;
|
||||
var input = AccountsPasswords;
|
||||
if (string.IsNullOrWhiteSpace(input)) return;
|
||||
var lines = input.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var success = 0;
|
||||
var errors = 0;
|
||||
var notFound = 0;
|
||||
|
||||
var mafs = Storage.MaFiles.ToList();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
try
|
||||
{
|
||||
var split = line.Split(":", 2);
|
||||
if (split.Length != 2)
|
||||
{
|
||||
errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
string login = split[0];
|
||||
string password = split[1];
|
||||
SteamId64? steamId = null;
|
||||
|
||||
if (SteamId64.TryParse(login, out var id64))
|
||||
{
|
||||
steamId = id64;
|
||||
}
|
||||
|
||||
var maf = steamId != null
|
||||
? mafs.FirstOrDefault(m => m.SteamId == steamId || m.AccountName == login)
|
||||
: mafs.FirstOrDefault(m => string.Equals(m.AccountName, login, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (maf == null)
|
||||
{
|
||||
notFound++;
|
||||
continue;
|
||||
}
|
||||
|
||||
maf.Password = PHandler.Encrypt(password);
|
||||
await Storage.UpdateMafileAsync(maf);
|
||||
success++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
if (success > 0 && errors == 0 && notFound == 0)
|
||||
{
|
||||
Tip = "Успех";
|
||||
}
|
||||
else
|
||||
{
|
||||
Tip = $"Успешно: {success}, Не найдено: {notFound}, Ошибки: {errors}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private bool SetPasswordsCanExecute()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(AccountsPasswords) && IsEncryptionPasswordSet;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearTip()
|
||||
{
|
||||
Tip = null;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
|
||||
@@ -241,7 +240,7 @@ public partial class SettingsVM : ObservableObject
|
||||
{
|
||||
var l = GetLoc("PartialSuccess");
|
||||
RenameResultText =
|
||||
string.Format(l, result.Total, result.Renamed, result.Errors, result.AlreadyExist, result.BackupFileName);
|
||||
string.Format(l, result.Total, result.Renamed, result.Errors, result.Conflict, result.BackupFileName);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
"ru": "ОК",
|
||||
"ua": "ОК"
|
||||
},
|
||||
"Save": {
|
||||
"en": "Save",
|
||||
"ru": "Сохранить",
|
||||
"ua": "Зберегти"
|
||||
},
|
||||
"Abbreviations": {
|
||||
"Time": {
|
||||
"Seconds": {
|
||||
@@ -88,9 +93,9 @@
|
||||
"Menu": {
|
||||
"File": {
|
||||
"Caption": {
|
||||
"en": "File",
|
||||
"ru": "Файл",
|
||||
"ua": "Файл"
|
||||
"en": "Menu",
|
||||
"ru": "Меню",
|
||||
"ua": "Меню"
|
||||
},
|
||||
"Import": {
|
||||
"en": "Import",
|
||||
@@ -111,6 +116,23 @@
|
||||
"en": "Settings",
|
||||
"ru": "Настройки",
|
||||
"ua": "Налаштування"
|
||||
},
|
||||
"ProxyManager": {
|
||||
"en": "Proxy manager",
|
||||
"ru": "Менеджер прокси",
|
||||
"ua": "Менеджер проксі"
|
||||
},
|
||||
"Other": {
|
||||
"Title": {
|
||||
"en": "Other",
|
||||
"ru": "Прочее",
|
||||
"ua": "Інше"
|
||||
},
|
||||
"SetPasswords": {
|
||||
"en": "Set passwords",
|
||||
"ru": "Назначить пароли",
|
||||
"ua": "Призначити паролі"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Account": {
|
||||
@@ -459,8 +481,7 @@
|
||||
},
|
||||
"LegacyMafileModeHint": {
|
||||
"en": "Save mafiles in compatibility format with Steam Desktop Authenticator and other applications",
|
||||
"ru":
|
||||
"Сохранять мафайлы в старом формате для совместимости со Steam Desktop Authenticator и другими приложениями",
|
||||
"ru": "Сохранять мафайлы в старом формате для совместимости со Steam Desktop Authenticator и другими приложениями",
|
||||
"ua": "Зберігати мафайли у старому форматі для сумісності зі Steam Desktop Authenticator та іншими додатками"
|
||||
},
|
||||
"AllowAutoUpdate": {
|
||||
@@ -484,12 +505,9 @@
|
||||
"ua": "Ігнорувати помилки Patch Tuesday у таймері (експ.)"
|
||||
},
|
||||
"IgnorePatchTuesdayErrorsHint": {
|
||||
"en":
|
||||
"Ignore errors that occur every Tuesday (Wednesday night in GMT) when Steam doing technical works and auto-confirm timers turns off unnecessarily",
|
||||
"ru":
|
||||
"Игнорировать ошибки, которые возникают каждый вторник (ночь среды по GMT) когда Steam проводит технические работы и автоподтверждения отключаются без надобности",
|
||||
"ua":
|
||||
"Ігнорувати помилки, які виникають щосереди (ніч середи за GMT) коли Steam проводить технічні роботи і автопідтвердження вимикаються без потреби"
|
||||
"en": "Ignore errors that occur every Tuesday (Wednesday night in GMT) when Steam doing technical works and auto-confirm timers turns off unnecessarily",
|
||||
"ru": "Игнорировать ошибки, которые возникают каждый вторник (ночь среды по GMT) когда Steam проводит технические работы и автоподтверждения отключаются без надобности",
|
||||
"ua": "Ігнорувати помилки, які виникають щосереди (ніч середи за GMT) коли Steam проводить технічні роботи і автопідтвердження вимикаються без потреби"
|
||||
}
|
||||
|
||||
|
||||
@@ -674,12 +692,9 @@
|
||||
"ua": "Пароль шифрування"
|
||||
},
|
||||
"DialogText": {
|
||||
"en":
|
||||
"You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session.\r\n(Optional)",
|
||||
"ru":
|
||||
"Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией.\r\n(Не обязательно)",
|
||||
"ua":
|
||||
"Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією.\r\n(Не обов'язково)"
|
||||
"en": "You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session.\r\n(Optional)",
|
||||
"ru": "Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией.\r\n(Не обязательно)",
|
||||
"ua": "Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією.\r\n(Не обов'язково)"
|
||||
},
|
||||
"Ok": {
|
||||
"en": "Ok",
|
||||
@@ -735,6 +750,39 @@
|
||||
"ua": "Введіть значення"
|
||||
}
|
||||
},
|
||||
"SetAccountPasswordsView": {
|
||||
"Title": {
|
||||
"en": "Set passwords",
|
||||
"ru": "Назначить пароли",
|
||||
"ua": "Призначити паролі"
|
||||
},
|
||||
"EncryptionPassword": {
|
||||
"en": "Encryption password:",
|
||||
"ru": "Пароль шифрования:",
|
||||
"ua": "Пароль шифрування:"
|
||||
},
|
||||
"EnterPassword": {
|
||||
"en": "Enter password",
|
||||
"ru": "Введите пароль",
|
||||
"ua": "Введіть пароль"
|
||||
},
|
||||
"Status": {
|
||||
"en": "Status:",
|
||||
"ru": "Статус:",
|
||||
"ua": "Статус:"
|
||||
},
|
||||
"EncryptionHint": {
|
||||
"en": "All passwords in mafiles will be encrypted using the encryption password.",
|
||||
"ru": "Все пароли в мафайлах будут зашифрованы с помощью пароля шифрования.",
|
||||
"ua": "Всі паролі в мафайлах будуть зашифровані за допомогою пароля шифрування."
|
||||
},
|
||||
"InputFormat": {
|
||||
"en": "Supported formats:\r\n\r\nlogin:password\r\nsteamid:password",
|
||||
"ru": "Поддерживаемые форматы:\r\n\r\nlogin:password\r\nsteamid:password",
|
||||
"ua": "Підтримувані формати:\r\n\r\nlogin:password\r\nsteamid:password"
|
||||
|
||||
}
|
||||
},
|
||||
"CodeBehind": {
|
||||
"MainVM": {
|
||||
"SuccessfulLogin": {
|
||||
@@ -1381,7 +1429,12 @@
|
||||
"ru": "Авто: таймер подтверждений пытался начать новый цикл, когда предыдущий еще не завершился. Советуем увеличить задержку",
|
||||
"en": "Auto: confirmation timer tried to start new cycle when previous one was not finished yet. We recommend to increase delay",
|
||||
"ua": "Авто: таймер підтверджень намагався запустити новий цикл, коли попередній ще не закінчився. Рекомендуємо збільшити затримку"
|
||||
}
|
||||
},
|
||||
"FailedToLoadStorage": {
|
||||
"en": "Failed to load auto-confirm timers. File seems to be corrupted",
|
||||
"ru": "Не удалось загрузить таймеры авто-подтверждений. Файл, похоже, поврежден",
|
||||
"ua": "Не вдалося завантажити таймери авто-підтверджень. Файл, схоже, пошкоджено"
|
||||
}
|
||||
},
|
||||
"SettingsVM": {
|
||||
"MafileRenaming": {
|
||||
@@ -1414,15 +1467,12 @@
|
||||
"en": "Error while loading settings. Settings were reset",
|
||||
"ru": "Ошибка при загрузке настроек. Настройки были сброшены",
|
||||
"ua": "Помилка при завантаженні налаштувань. Налаштування були скинуті"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CantAlignTimeError": {
|
||||
"ru":
|
||||
"Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log",
|
||||
"en":
|
||||
"Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log",
|
||||
"ua":
|
||||
"Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log"
|
||||
"ru": "Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log",
|
||||
"en": "Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log",
|
||||
"ua": "Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user