diff --git a/NebulaAuth.sln.DotSettings b/NebulaAuth.sln.DotSettings index e2bba46..140e175 100644 --- a/NebulaAuth.sln.DotSettings +++ b/NebulaAuth.sln.DotSettings @@ -1,3 +1,314 @@  DO_NOT_SHOW + <Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="Non-reorderable types" Priority="99999999"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + <HasAttribute Name="System.Runtime.InteropServices.StructLayoutAttribute" /> + <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Inherited="True" Name="Xunit.FactAttribute" /> + <HasAttribute Inherited="True" Name="Xunit.TheoryAttribute" /> + </And> + </HasMember> + </And> + </TypePattern.Match> + <Entry DisplayName="Fields"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Constructors"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <ImplementsInterface Name="System.IDisposable" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" /> + <HasAttribute Name="Xunit.TheoryAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <Or> + <HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureAttribute" /> + <HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureSourceAttribute" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" /> + </And> + </HasMember> + </Or> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Inherited="True" Name="NUnit.Framework.SetUpAttribute" /> + <HasAttribute Inherited="True" Name="NUnit.Framework.TearDownAttribute" /> + <HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureSetUpAttribute" /> + <HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureTearDownAttribute" /> + <HasAttribute Inherited="True" Name="NUnit.Framework.OneTimeSetUpAttribute" /> + <HasAttribute Inherited="True" Name="NUnit.Framework.OneTimeTearDownAttribute" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Grains"> + <TypePattern.Match> + <And> + <Or> + <ImplementsInterface Name="^Orleans\.IGrain$" /> + <ImplementsInterface Name="Orleans.IGrainBase" /> + <ImplementsInterface Name="Achiefy.Orleans.Tenancy.Abstractions.ITenantGrain" /> + <InheritsFromType Name="Orleans.Grain" /> + <InheritsFromType Name="Achiefy.Tenant.Application.Stateful.Core.TenantGrain" /> + </Or> + <Not> + <Abstract /> + </Not> + <Kind Is="Class" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Public Delegates" Priority="100"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Delegate" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Public Enums" Priority="100"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Enum" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Nested Records"> + <Entry.Match> + <Kind Is="Record" /> + </Entry.Match> + </Entry> + <Group DisplayName="Group"> + <Entry DisplayName="Static Fields and Constants"> + <Entry.Match> + <Or> + <Kind Is="Constant" /> + <And> + <Kind Is="Field" /> + <Static /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Is="0" Order="Constant Field" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Properties, Indexers"> + <Entry.Match> + <Or> + <Kind Is="Property" /> + <Kind Is="Indexer" /> + </Or> + </Entry.Match> + </Entry> + <Entry DisplayName="Fields"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Entry DisplayName="Constructors"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Lifecycle Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Name Is="^On(Activate|Deactivate)Async.*$" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Nested Types"> + <Entry.Match> + <Kind Is="Type" /> + </Entry.Match> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern"> + <Entry DisplayName="Public Enums" Priority="100"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Enum" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Public Delegates" Priority="100"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Delegate" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Nested Records"> + <Entry.Match> + <Kind Is="Record" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + </Entry.SortBy> + </Entry> + <Group DisplayName="Group"> + <Entry DisplayName="Static Fields and Constants"> + <Entry.Match> + <Or> + <Kind Is="Constant" /> + <And> + <Kind Is="Field" /> + <Static /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Is="0" Order="Constant Field" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Properties, Indexers"> + <Entry.Match> + <Or> + <Kind Is="Property" /> + <Kind Is="Indexer" /> + </Or> + </Entry.Match> + </Entry> + <Entry DisplayName="Fields"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Entry DisplayName="Constructors"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Nested Types"> + <Entry.Match> + <Kind Is="Type" /> + </Entry.Match> + </Entry> + </TypePattern> +</Patterns> True \ No newline at end of file diff --git a/NebulaAuth/links.json b/NebulaAuth/links.json new file mode 100644 index 0000000..e337513 --- /dev/null +++ b/NebulaAuth/links.json @@ -0,0 +1,4 @@ +{ + "Website": "https://achiefy.pro/?nebula=true", + "Documentation": "https://achiefy-project.gitbook.io/nebulaauth" +} diff --git a/changelog/1.8.1.html b/changelog/1.8.1.html index 03a1b77..7c159c4 100644 --- a/changelog/1.8.1.html +++ b/changelog/1.8.1.html @@ -63,25 +63,25 @@ -
-
-
Version 1.8.1
-
25.01.2026
-
-
    -
  • NEW: Added Mafile Export tool with templates, batch export and flexible field filtering.
  • -
  • IMPROVEMENT: Reworked auto-confirmation error handling logic (MAAC) — fewer false disables, smarter retries.
  • -
  • UPDATE: Added “Unattach Proxy” option to the account context menu.
  • -
  • LOCALIZATION: Added Chinese (Simplified) and French languages.
  • -
  • FIX: Fixed crashes related to duplicate mafiles, timers, search filtering and import logic.
  • -
  • - DETAILS: Full patch notes: - ENG | - RUS -
  • -
-
+
+
+
Version 1.8.1
+
25.01.2026
+
+
    +
  • NEW: Added Mafile Export tool with templates, batch export and flexible field filtering.
  • +
  • IMPROVEMENT: Reworked auto-confirmation error handling logic (MAAC) — fewer false disables, smarter retries.
  • +
  • UPDATE: Added “Unattach Proxy” option to the account context menu.
  • +
  • LOCALIZATION: Added Chinese (Simplified) and French languages.
  • +
  • FIX: Fixed crashes related to duplicate mafiles, timers, search filtering and import logic.
  • +
  • + DETAILS: Full patch notes: + ENG | + RUS +
  • +
+
\ No newline at end of file diff --git a/changelog/1.8.2.html b/changelog/1.8.2.html index 7a44a8a..9a4308b 100644 --- a/changelog/1.8.2.html +++ b/changelog/1.8.2.html @@ -63,16 +63,16 @@ -
-
-
Version 1.8.2
-
10.02.2026
-
-
    -
  • FIX: AutoConfirm timer now correctly resets the status after a successful request (Warning → Ok).
  • -
-
+
+
+
Version 1.8.2
+
10.02.2026
+
+
    +
  • FIX: AutoConfirm timer now correctly resets the status after a successful request (Warning → Ok).
  • +
+
\ No newline at end of file diff --git a/changelog/1.8.5.json b/changelog/1.8.5.json new file mode 100644 index 0000000..f90e91c --- /dev/null +++ b/changelog/1.8.5.json @@ -0,0 +1,60 @@ +{ + "version": "1.8.5", + "date": "2026-06-19", + "changes": [ + { + "type": "NEW", + "text": "Bulk proxy assignment: assign proxies to multiple accounts at once via flexible login:proxy, login:ID or login-only input", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22" + }, + { + "type": "NEW", + "text": "Import dialog now allows assigning imported mafiles to a group directly; added option to skip confirmation when no conflicts are detected", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/24" + }, + { + "type": "NEW", + "text": "Assign random free proxy to the account from context menu", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22" + }, + { + "type": "NEW", + "text": "Export: added ZIP archive output, export-all mode when the input field is empty, and support for login:password input format", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/28" + }, + { + "type": "IMPROVEMENT", + "text": "When a session permanently expires during a user-initiated action, a login dialog is now shown instead of a generic error notification", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/29" + }, + { + "type": "IMPROVEMENT", + "text": "Proxy manager: account count badges now show how many accounts are assigned to each proxy entry", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22" + }, + { + "type": "IMPROVEMENT", + "text": "Added login:pass@ip:port proxy format support", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/25" + }, + { + "type": "IMPROVEMENT", + "text": "Improved dialog header design", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/27" + }, + { + "type": "IMPROVEMENT", + "text": "Website and Documentation links are now available from the links window", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/30" + }, + { + "type": "FIX", + "text": "Application no longer fails to start when Steam time synchronization is unavailable (network issues, firewall)" + }, + { + "type": "FIX", + "text": "Fixed incorrect Steam Guard error message shown in the Mafile Mover dialog", + "link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/23" + } + ] +} diff --git a/src/NebulaAuth/Components/DialogHeader.xaml b/src/NebulaAuth/Components/DialogHeader.xaml new file mode 100644 index 0000000..39f64a7 --- /dev/null +++ b/src/NebulaAuth/Components/DialogHeader.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NebulaAuth/Components/DialogHeader.xaml.cs b/src/NebulaAuth/Components/DialogHeader.xaml.cs new file mode 100644 index 0000000..5adf9dd --- /dev/null +++ b/src/NebulaAuth/Components/DialogHeader.xaml.cs @@ -0,0 +1,63 @@ +using System.Windows; +using System.Windows.Controls; +using MaterialDesignThemes.Wpf; + +namespace NebulaAuth; + +public partial class DialogHeader : UserControl +{ + public static readonly DependencyProperty IconKindProperty = + DependencyProperty.Register(nameof(IconKind), typeof(PackIconKind), typeof(DialogHeader), + new PropertyMetadata(PackIconKind.None)); + + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register(nameof(Title), typeof(string), typeof(DialogHeader), + new PropertyMetadata(null)); + + public static readonly DependencyProperty TitleAccentProperty = + DependencyProperty.Register(nameof(TitleAccent), typeof(string), typeof(DialogHeader), + new PropertyMetadata(null)); + + public static readonly DependencyProperty CloseParameterProperty = + DependencyProperty.Register(nameof(CloseParameter), typeof(object), typeof(DialogHeader), + new PropertyMetadata(null)); + + public static readonly DependencyProperty IsCloseEnabledProperty = + DependencyProperty.Register(nameof(IsCloseEnabled), typeof(bool), typeof(DialogHeader), + new PropertyMetadata(true)); + + public PackIconKind IconKind + { + get => (PackIconKind) GetValue(IconKindProperty); + set => SetValue(IconKindProperty, value); + } + + public string? Title + { + get => (string?) GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public string? TitleAccent + { + get => (string?) GetValue(TitleAccentProperty); + set => SetValue(TitleAccentProperty, value); + } + + public object? CloseParameter + { + get => GetValue(CloseParameterProperty); + set => SetValue(CloseParameterProperty, value); + } + + public bool IsCloseEnabled + { + get => (bool) GetValue(IsCloseEnabledProperty); + set => SetValue(IsCloseEnabledProperty, value); + } + + public DialogHeader() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Components/HintBox.xaml.cs b/src/NebulaAuth/Components/HintBox.xaml.cs index 38d84d1..8943147 100644 --- a/src/NebulaAuth/Components/HintBox.xaml.cs +++ b/src/NebulaAuth/Components/HintBox.xaml.cs @@ -77,6 +77,10 @@ public partial class HintBox : UserControl IconKind = PackIconKind.ErrorOutline; IconBrush = (Brush) Application.Current.FindResource("ErrorBrush")!; break; + case HintBoxSeverity.Warning: + IconKind = PackIconKind.WarningOutline; + IconBrush = (Brush) Application.Current.FindResource("WarningBrush")!; + break; default: IconKind = PackIconKind.InfoCircleOutline; IconBrush = (Brush) Application.Current.FindResource("InfoBrush")!; @@ -94,5 +98,6 @@ public partial class HintBox : UserControl public enum HintBoxSeverity { Info, + Warning, Error } \ No newline at end of file diff --git a/src/NebulaAuth/Converters/Converters.xaml b/src/NebulaAuth/Converters/Converters.xaml index 97888f1..8ef5bd7 100644 --- a/src/NebulaAuth/Converters/Converters.xaml +++ b/src/NebulaAuth/Converters/Converters.xaml @@ -15,6 +15,7 @@ + diff --git a/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs b/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs new file mode 100644 index 0000000..f39d5b4 --- /dev/null +++ b/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using NebulaAuth.Model; + +namespace NebulaAuth.Converters; + +/// +/// Converts a proxy ID (int) to the number of accounts assigned to it. +/// Returns empty string when count is zero so the badge is invisible. +/// +public class ProxyAccountCountConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not int proxyId) return string.Empty; + var count = ProxyAssignmentCache.GetAccountCount(proxyId); + return count > 0 ? count.ToString() : string.Empty; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Core/DialogsController.cs b/src/NebulaAuth/Core/DialogsController.cs index 8ee1339..2fe5ae8 100644 --- a/src/NebulaAuth/Core/DialogsController.cs +++ b/src/NebulaAuth/Core/DialogsController.cs @@ -15,13 +15,15 @@ namespace NebulaAuth.Core; public static class DialogsController { - public static async Task ShowLoginAgainDialog(string username, string? currentPassword = null) + public static async Task ShowLoginAgainDialog(string username, string? currentPassword = null, + bool showNoProxyWarning = false) { var vm = new LoginAgainVM { UserName = username, Password = currentPassword ?? string.Empty, - SavePassword = PHandler.IsPasswordSet + SavePassword = PHandler.IsPasswordSet, + ShowNoProxyWarning = showNoProxyWarning }; var content = new LoginAgainDialog { @@ -73,6 +75,22 @@ public static class DialogsController return null; } + public static async Task ShowMafileImportDialog(IEnumerable groups, + int totalCount, int conflictCount) + { + var vm = new MafileImportDialogVM(groups, totalCount, conflictCount); + var content = new MafileImportDialog + { + DataContext = vm + }; + var result = await DialogHost.Show(content); + return result is MafileImportDialogVM + ? new MafileImportDialogResult( + !vm.AddToGroup || string.IsNullOrWhiteSpace(vm.Group) ? null : vm.Group.Trim(), + vm.OverwriteConflicts) + : null; + } + public static async Task ShowProxyManager() { var vm = new ProxyManagerVM(); diff --git a/src/NebulaAuth/Core/LinksManager.cs b/src/NebulaAuth/Core/LinksManager.cs new file mode 100644 index 0000000..b37b857 --- /dev/null +++ b/src/NebulaAuth/Core/LinksManager.cs @@ -0,0 +1,33 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using NebulaAuth.Model; +using Newtonsoft.Json; + +namespace NebulaAuth.Core; + +public static class LinksManager +{ + private const string LINKS_URL = + "https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/NebulaAuth/links.json"; + + private static readonly HttpClient HttpClient = new(); + + public static string? WebsiteUrl { get; private set; } + public static string? DocumentationUrl { get; private set; } + + public static async Task FetchAsync() + { + try + { + var json = await HttpClient.GetStringAsync(LINKS_URL).ConfigureAwait(false); + var links = JsonConvert.DeserializeObject(json); + WebsiteUrl = links?.Website; + DocumentationUrl = links?.Documentation; + } + catch (Exception ex) + { + Shell.Logger.Debug(ex, "Failed to fetch remote links"); + } + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Localization/dialogs.export.loc.json b/src/NebulaAuth/Localization/dialogs.export.loc.json index fe35dc2..787b67c 100644 --- a/src/NebulaAuth/Localization/dialogs.export.loc.json +++ b/src/NebulaAuth/Localization/dialogs.export.loc.json @@ -196,6 +196,16 @@ "es": "Grupo", "tr": "Grup", "kk": "Топ" + }, + "ExportToZip": { + "ru": "Экспортировать в ZIP архив", + "en": "Export to ZIP archive", + "uk": "Експортувати в ZIP архів", + "fr": "Exporter en archive ZIP", + "zh": "导出为 ZIP 压缩包", + "es": "Exportar a archivo ZIP", + "tr": "ZIP arşivine aktar", + "kk": "ZIP мұрағатқа экспорттау" } }, "Tooltips": { @@ -288,17 +298,27 @@ "es": "Incluir datos de grupo compatibles con Nebula.", "tr": "Nebula uyumlu grup verilerini dahil et.", "kk": "Nebula үйлесімді топ деректерін қосу." + }, + "ExportToZip": { + "ru": "Экспортировать в ZIP архив вместо папки. Архив будет сохранён в выбранной директории с именем вида N_timestamp.zip.", + "en": "Export to a ZIP archive instead of a folder. The archive will be saved in the selected directory as N_timestamp.zip.", + "uk": "Експортувати в ZIP архів замість папки. Архів буде збережено у вибраній директорії з назвою N_timestamp.zip.", + "fr": "Exporter dans une archive ZIP plutôt que dans un dossier. L'archive sera enregistrée dans le répertoire sélectionné sous le nom N_timestamp.zip.", + "zh": "导出为 ZIP 压缩包而非文件夹。压缩包将保存在所选目录中,名称格式为 N_timestamp.zip。", + "es": "Exportar a un archivo ZIP en lugar de una carpeta. El archivo se guardará en el directorio seleccionado como N_timestamp.zip.", + "tr": "Klasör yerine ZIP arşivine aktar. Arşiv, seçilen dizine N_timestamp.zip adıyla kaydedilecektir.", + "kk": "Қалта орнына ZIP мұрағатына экспорттау. Мұрағат таңдалған каталогта N_timestamp.zip атауымен сақталады." } }, "ExportAccountsPlaceholder": { - "ru": "Введите логины аккаунтов или SteamID (по одному на строку) для экспорта в выбранную директорию.", - "en": "Enter account logins or SteamIDs (one per line) to export to the selected directory.", - "uk": "Введіть логіни акаунтів або SteamID (по одному на рядок) для експорту у вибрану директорію.", - "fr": "Entrez les identifiants de compte ou les SteamID (un par ligne) pour exporter vers le répertoire sélectionné.", - "zh": "输入要导出到所选目录的账户登录名或 SteamID(每行一个)。", - "es": "Introduce los logins de las cuentas o SteamID (uno por línea) para exportarlos al directorio seleccionado.", - "tr": "Seçilen dizine dışa aktarmak için hesap girişlerini veya SteamID’leri (her satıra bir tane) girin.", - "kk": "Таңдалған каталогқа экспорттау үшін аккаунт логиндерін немесе SteamID (әр жолға біреуден) енгізіңіз." + "ru": "Введите логины или SteamID (по одному на строку). Поддерживается формат логин:пароль — пароль будет проигнорирован. Оставьте пустым для экспорта всех аккаунтов.", + "en": "Enter logins or SteamIDs (one per line). Supports login:password format — the password will be ignored. Leave empty to export all accounts.", + "uk": "Введіть логіни або SteamID (по одному на рядок). Підтримується формат логін:пароль — пароль буде проігноровано. Залиште порожнім для експорту всіх акаунтів.", + "fr": "Entrez les identifiants ou SteamID (un par ligne). Supporte le format login:mot_de_passe — le mot de passe sera ignoré. Laissez vide pour exporter tous les comptes.", + "zh": "输入登录名或 SteamID(每行一个)。支持 login:password 格式——密码将被忽略。留空则导出所有账户。", + "es": "Introduce logins o SteamID (uno por línea). Soporta el formato login:contraseña — la contraseña será ignorada. Déjalo vacío para exportar todas las cuentas.", + "tr": "Girişleri veya SteamID’leri girin (her satıra bir tane). login:şifre formatı desteklenir — şifre yok sayılır. Tüm hesapları dışa aktarmak için boş bırakın.", + "kk": "Логиндерді немесе SteamID (әр жолға біреуден) енгізіңіз. логин:құпиясөз форматы қолданылады — құпиясөз еленбейді. Барлық аккаунттарды экспорттау үшін бос қалдырыңыз." } } } \ No newline at end of file diff --git a/src/NebulaAuth/Localization/dialogs.loc.json b/src/NebulaAuth/Localization/dialogs.loc.json index 9749916..df5fecf 100644 --- a/src/NebulaAuth/Localization/dialogs.loc.json +++ b/src/NebulaAuth/Localization/dialogs.loc.json @@ -35,5 +35,67 @@ "tr": "Değer girin", "kk": "Мән енгізіңіз" } + }, + "MafileImportDialog": { + "Title": { + "en": "Mafile import", + "ru": "Импорт мафайлов", + "zh": "导入 mafile", + "uk": "Імпорт mafile", + "fr": "Import de mafiles", + "es": "Importación de mafiles", + "tr": "Mafile içe aktarma", + "kk": "Mafile импорттау" + }, + "TotalCount": { + "en": "Mafiles to import", + "ru": "Мафайлов к импорту", + "zh": "要导入的 mafile:", + "uk": "Mafile до імпорту:", + "fr": "Mafiles à importer :", + "es": "Mafiles para importar:", + "tr": "İçe aktarılacak mafile:", + "kk": "Импортталатын mafile:" + }, + "ConflictCount": { + "en": "Already exist", + "ru": "Уже существуют", + "zh": "已存在:", + "uk": "Уже існують:", + "fr": "Déjà existants :", + "es": "Ya existen:", + "tr": "Zaten mevcut:", + "kk": "Бұрыннан бар:" + }, + "OverwriteConflicts": { + "en": "Overwrite conflicting mafiles", + "ru": "Перезаписать конфликтующие мафайлы", + "zh": "覆盖冲突的 mafile", + "uk": "Перезаписати конфліктні mafile", + "fr": "Remplacer les mafiles en conflit", + "es": "Sobrescribir mafiles en conflicto", + "tr": "Çakışan mafile'ların üzerine yaz", + "kk": "Қақтығысатын mafile файлдарын қайта жазу" + }, + "AddToGroup": { + "en": "Add imported accounts to a group", + "ru": "Добавить импортированные аккаунты в группу", + "zh": "将导入的账号添加到组", + "uk": "Додати імпортовані акаунти до групи", + "fr": "Ajouter les comptes importés à un groupe", + "es": "Añadir las cuentas importadas a un grupo", + "tr": "İçe aktarılan hesapları bir gruba ekle", + "kk": "Импортталған аккаунттарды топқа қосу" + }, + "GroupFieldHint": { + "en": "Existing or new group", + "ru": "Существующая или новая группа", + "zh": "现有或新组", + "uk": "Наявна або нова група", + "fr": "Groupe existant ou nouveau", + "es": "Grupo existente o nuevo", + "tr": "Mevcut veya yeni grup", + "kk": "Бар немесе жаңа топ" + } } -} \ No newline at end of file +} diff --git a/src/NebulaAuth/Localization/dialogs.loginagain.loc.json b/src/NebulaAuth/Localization/dialogs.loginagain.loc.json index c8cade7..20ace34 100644 --- a/src/NebulaAuth/Localization/dialogs.loginagain.loc.json +++ b/src/NebulaAuth/Localization/dialogs.loginagain.loc.json @@ -79,6 +79,16 @@ "es": "Pulsa 'DEL' para eliminar", "tr": "Silmek için 'DEL' tuşuna bas", "kk": "Жою үшін 'DEL' пернесін басыңыз" + }, + "NoProxyWarning": { + "en": "No proxy is assigned", + "ru": "Прокси не установлен", + "zh": "未分配代理", + "uk": "Проксі не призначено", + "fr": "Aucun proxy assigné", + "es": "No hay ningún proxy asignado", + "tr": "Proxy atanmamış", + "kk": "Прокси тағайындалмаған" } } } \ No newline at end of file diff --git a/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json b/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json index 9c61462..2464707 100644 --- a/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json +++ b/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json @@ -42,14 +42,14 @@ "kk": "RCode: {0}\r\nФайл атауы: {1}.mafile" }, "GuardIsNotActive": { - "ru": "Steam Guard установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"", - "en": "Steam Guard is set on this account. To link mafile to the account, use the 'Link' window", - "zh": "此账户已启用令牌。要将 mafile 绑定到此账户,请使用“绑定”窗口。", - "uk": "Steam Guard встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'", - "fr": "Steam Guard est configuré sur ce compte. Pour lier mafile au compte, utilisez la fenêtre 'Lien'", - "es": "Steam Guard está activado en esta cuenta. Para vincular el mafile a la cuenta, usa la ventana 'Vincular'", - "tr": "Steam Guard bu hesapta zaten etkin. Mafile'ı hesaba bağlamak için 'Bağla' penceresini kullanın", - "kk": "Бұл аккаунтта Steam Guard орнатылған. Mafile-ды аккаунтқа байланыстыру үшін 'Байланыстыру' терезесін пайдаланыңыз" + "ru": "Steam Guard НЕ установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"", + "en": "Steam Guard is NOT set on this account. To link mafile to the account, use the 'Link' window", + "zh": "此账户尚未启用令牌。要将 mafile 绑定到此账户,请使用“绑定”窗口。", + "uk": "Steam Guard НЕ встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'", + "fr": "Steam Guard n'est PAS configuré sur ce compte. Pour lier le mafile au compte, utilisez la fenêtre 'Lier'", + "es": "Steam Guard NO está activado en esta cuenta. Para vincular el mafile a la cuenta, usa la ventana 'Vincular'", + "tr": "Steam Guard bu hesapta etkin DEĞİL. Mafile'ı hesaba bağlamak için 'Bağla' penceresini kullanın", + "kk": "Бұл аккаунтта Steam Guard орнатылмаған. Mafile-ды аккаунтқа байланыстыру үшін 'Байланыстыру' терезесін пайдаланыңыз" }, "SeemsNoPhoneNumber": { "en": "The account has no phone number linked, returned Fail code (2)", diff --git a/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json b/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json index 07193f8..04a7baf 100644 --- a/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json +++ b/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json @@ -40,6 +40,26 @@ "es": "El proxy tiene un formato incorrecto", "tr": "Proxy yanlış formatta", "kk": "Прокси қате форматта" + }, + "AssignmentSuccess": { + "ru": "Прокси назначены: {0}", + "en": "Assigned: {0}", + "zh": "已分配:{0}", + "uk": "Проксі призначено: {0}", + "fr": "Attribués: {0}", + "es": "Asignados: {0}", + "tr": "Atandı: {0}", + "kk": "Тағайындалды: {0}" + }, + "AssignmentPartialSuccess": { + "ru": "Назначено: {0}, не найдено: {1}, ошибки: {2}", + "en": "Assigned: {0}, not found: {1}, errors: {2}", + "zh": "已分配:{0},未找到:{1},错误:{2}", + "uk": "Призначено: {0}, не знайдено: {1}, помилки: {2}", + "fr": "Attribués: {0}, non trouvés: {1}, erreurs: {2}", + "es": "Asignados: {0}, no encontrados: {1}, errores: {2}", + "tr": "Atandı: {0}, bulunamadı: {1}, hatalar: {2}", + "kk": "Тағайындалды: {0}, табылмады: {1}, қателер: {2}" } } }, @@ -93,6 +113,96 @@ "es": "Mostrar credenciales", "tr": "Kimlik bilgilerini göster", "kk": "Тіркелгі деректерін көрсету" + }, + "TabManager": { + "en": "Manager", + "ru": "Менеджер", + "zh": "管理", + "uk": "Менеджер", + "fr": "Gestionnaire", + "es": "Administrador", + "tr": "Yönetici", + "kk": "Менеджер" + }, + "TabAssignment": { + "en": "Assignment", + "ru": "Назначение", + "zh": "分配", + "uk": "Призначення", + "fr": "Attribution", + "es": "Asignación", + "tr": "Atama", + "kk": "Тағайындау" + }, + "TotalAccounts": { + "en": "Accounts", + "ru": "Аккаунтов", + "zh": "账户", + "uk": "Акаунтів", + "fr": "Comptes", + "es": "Cuentas", + "tr": "Hesaplar", + "kk": "Аккаунттар" + }, + "TotalProxies": { + "en": "Proxies", + "ru": "Прокси", + "zh": "代理", + "uk": "Проксі", + "fr": "Proxies", + "es": "Proxies", + "tr": "Proxy'ler", + "kk": "Проксилер" + }, + "AssignedProxies": { + "en": "Assigned", + "ru": "Назначено", + "zh": "已分配", + "uk": "Призначено", + "fr": "Attribués", + "es": "Asignados", + "tr": "Atandı", + "kk": "Тағайындалды" + }, + "UnspecifiedBehavior": { + "en": "Unspecified proxy:", + "ru": "Не указанный прокси:", + "zh": "未指定代理:", + "uk": "Не вказаний проксі:", + "fr": "Proxy non spécifié:", + "es": "Proxy no especificado:", + "tr": "Belirtilmemiş proxy:", + "kk": "Көрсетілмеген прокси:" + }, + "UnspecifiedRandom": { + "en": "Assign random", + "ru": "Назначить случайный", + "zh": "随机分配", + "uk": "Призначити випадковий", + "fr": "Attribuer aléatoirement", + "es": "Asignar aleatorio", + "tr": "Rastgele ata", + "kk": "Кездейсоқ тағайындау" + }, + "UnspecifiedRemove": { + "en": "Remove proxy", + "ru": "Удалить прокси", + "zh": "删除代理", + "uk": "Видалити проксі", + "fr": "Supprimer le proxy", + "es": "Eliminar proxy", + "tr": "Proxy'yi kaldır", + "kk": "Проксиді жою" + }, + "ApplyAssignment": { + "en": "Apply", + "ru": "Применить", + "zh": "应用", + "uk": "Застосувати", + "fr": "Appliquer", + "es": "Aplicar", + "tr": "Uygula", + "kk": "Қолдану" } } } \ No newline at end of file diff --git a/src/NebulaAuth/Localization/dialogs.settings.loc.json b/src/NebulaAuth/Localization/dialogs.settings.loc.json index b6bd9a5..3ef9807 100644 --- a/src/NebulaAuth/Localization/dialogs.settings.loc.json +++ b/src/NebulaAuth/Localization/dialogs.settings.loc.json @@ -262,25 +262,25 @@ "kk": "Дискке сақталмайды" } }, - "LegacyMafileMode": { - "en": "Common mafiles format", - "ru": "Общепринятый формат мафайлов", - "zh": "常见的邮件文件格式", - "uk": "Загальноприйнятий формат мафайлів", - "fr": "Format de mafiles commun", - "es": "Formato común de mafiles", - "tr": "Ortak mafile formatı", - "kk": "Жалпы mafile форматы" + "ConfirmMafileImport": { + "en": "Ask confirmation when importing mafiles", + "ru": "Спрашивать подтверждение при импорте мафайлов", + "zh": "导入 mafile 时请求确认", + "uk": "Запитувати підтвердження під час імпорту mafile", + "fr": "Demander confirmation lors de l'import des mafiles", + "es": "Pedir confirmación al importar mafiles", + "tr": "Mafile içe aktarırken onay sor", + "kk": "Mafile импорттау кезінде растауды сұрау" }, - "LegacyMafileModeHint": { - "en": "Save mafiles in compatibility format with Steam Desktop Authenticator and other applications", - "ru": "Сохранять мафайлы в старом формате для совместимости со Steam Desktop Authenticator и другими приложениями", - "zh": "以与 Steam 桌面认证器及其他应用程序兼容的格式保存 mafiles", - "uk": "Зберігати мафайли у старому форматі для сумісності зі Steam Desktop Authenticator та іншими додатками", - "fr": "Enregistrer les mafiles dans un format compatible avec Steam Desktop Authenticator et d'autres applications", - "es": "Guardar mafiles en formato compatible con Steam Desktop Authenticator y otras aplicaciones", - "tr": "Mafile'ları Steam Desktop Authenticator ve diğer uygulamalarla uyumlu formatta kaydet", - "kk": "Mafile файлдарын Steam Desktop Authenticator және басқа қолданбалармен үйлесімді форматта сақтау" + "ConfirmMafileImportHint": { + "en": "Allows selecting a group when importing maFiles. Does not affect showing the dialog when file conflicts occur", + "ru": "Позволяет выбрать группу при импорте maFiles. Не влияет на показ окна при конфликте файлов", + "zh": "允许在导入 maFiles 时选择分组。不影响发生文件冲突时窗口的显示", + "uk": "Дозволяє вибрати групу під час імпорту maFiles. Не впливає на показ вікна при конфлікті файлів", + "fr": "Permet de choisir un groupe lors de l'import des maFiles. N'affecte pas l'affichage de la fenêtre en cas de conflit de fichiers", + "es": "Permite seleccionar un grupo al importar maFiles. No afecta a la aparición de la ventana cuando hay conflictos de archivos", + "tr": "maFiles içe aktarılırken grup seçilmesine izin verir. Dosya çakışmalarında pencerenin gösterilmesini etkilemez", + "kk": "maFiles импорттау кезінде топ таңдауға мүмкіндік береді. Файл қайшылықтары болғанда терезенің көрсетілуіне әсер етпейді" }, "AllowAutoUpdate": { "en": "Allow auto update", @@ -333,4 +333,4 @@ "kk": "Steam техникалық жұмыстар жүргізген кезде әр сейсенбіде (GMT бойынша сәрсенбі түні) пайда болатын және авторастау таймерлерін қажетсіз өшіретін қателерді елемеу" } } -} \ No newline at end of file +} diff --git a/src/NebulaAuth/Localization/mainwindow.loc.json b/src/NebulaAuth/Localization/mainwindow.loc.json index c8b57e8..8a54a68 100644 --- a/src/NebulaAuth/Localization/mainwindow.loc.json +++ b/src/NebulaAuth/Localization/mainwindow.loc.json @@ -101,16 +101,6 @@ "tr": "Onay hatası", "kk": "Растау қатесі" }, - "ConfirmMafileOverwrite": { - "ru": "Внимание, мафайл(-ы) уже присутствует в папке mafiles.\r\nПерезаписать мафайлы, которые конфликтуют?", - "en": "Attention, mafile(-s) already exists in mafiles folder.\r\nOverwrite conflicting mafiles?", - "zh": "注意,mafile 已经存在于 mafiles 文件夹中。\r\n覆盖冲突的 mafiles 吗?", - "uk": "Увага, мафайл(-и) вже присутній у папці mafiles.\r\nПерезаписати мафайли, які конфліктують?", - "fr": "Attention, mafiles déjà présents dans le dossier mafiles.\r\nRemplacer les mafiles en conflit?", - "es": "Atención, los mafiles ya existen en la carpeta mafiles.\r\n¿Sobrescribir los mafiles en conflicto?", - "tr": "Dikkat, mafile(ler) zaten mafiles klasöründe mevcut.\r\nÇakışan mafile'lar üzerine yazılsın mı?", - "kk": "Назар аударыңыз, mafile(дер) mafiles қалтасында бар.\r\nҚайшылық тудырған mafile файлдарының үстінен жазу керек пе?" - }, "MafileImportError": { "ru": "Ошибка при импорте мафайла", "en": "Mafile import error", @@ -904,6 +894,16 @@ "es": "Desvincular proxy", "tr": "Proxy bağlantısını kaldır", "kk": "Проксиді ажырату" + }, + "AssignFreeProxy": { + "en": "Assign free proxy", + "ru": "Назначить свободный прокси", + "zh": "分配空闲代理", + "uk": "Призначити вільний проксі", + "fr": "Attribuer un proxy libre", + "es": "Asignar proxy libre", + "tr": "Serbest proxy ata", + "kk": "Бос проксиді тағайындау" } }, "Proxy": { diff --git a/src/NebulaAuth/MainWindow.xaml b/src/NebulaAuth/MainWindow.xaml index 6eb2983..28ee118 100644 --- a/src/NebulaAuth/MainWindow.xaml +++ b/src/NebulaAuth/MainWindow.xaml @@ -42,6 +42,7 @@ DialogClosed="DialogHost_DialogClosed" ApplyBlurBackground="{Binding Settings.ApplyBlurBackground}" BlurRadius="12" + DialogContentUniformCornerRadius="12" x:Name="DialogHostInstance"> @@ -420,6 +421,9 @@ + diff --git a/src/NebulaAuth/Model/MAAC/MAACRequestHandler.cs b/src/NebulaAuth/Model/MAAC/MAACRequestHandler.cs index bf3f5e6..68e71a3 100644 --- a/src/NebulaAuth/Model/MAAC/MAACRequestHandler.cs +++ b/src/NebulaAuth/Model/MAAC/MAACRequestHandler.cs @@ -82,7 +82,7 @@ public static class MAACRequestHandler { return withSessionHandler ? Result.Success(await SessionHandler.Handle(req, client.Mafile, client.Chp(), - GetTimerPrefix(client.Mafile))) + GetTimerPrefix(client.Mafile), false)) : Result.Success(await req()); } catch (SessionInvalidException ex) diff --git a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs index 6395256..23555e6 100644 --- a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs +++ b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs @@ -62,7 +62,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable SetStatus(newStatus); ClientHandler.CookieContainer.ClearAllCookies(); - ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData); + ClientHandler.CookieContainer.SetSteamMobileCookies(sessionData); } diff --git a/src/NebulaAuth/Model/MaClient.cs b/src/NebulaAuth/Model/MaClient.cs index 115f5e3..74598a2 100644 --- a/src/NebulaAuth/Model/MaClient.cs +++ b/src/NebulaAuth/Model/MaClient.cs @@ -38,7 +38,7 @@ public static class MaClient { ClientHandler.CookieContainer.ClearAllCookies(); if (account == null) return; - ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData); + ClientHandler.CookieContainer.SetSteamMobileCookies(account.SessionData); Proxy.SetData(account.Proxy?.Data); } diff --git a/src/NebulaAuth/Model/MafileExport/MafileExportTemplate.cs b/src/NebulaAuth/Model/MafileExport/MafileExportTemplate.cs index 3a5311f..01459c5 100644 --- a/src/NebulaAuth/Model/MafileExport/MafileExportTemplate.cs +++ b/src/NebulaAuth/Model/MafileExport/MafileExportTemplate.cs @@ -13,4 +13,5 @@ public class MafileExportTemplate public bool IncludeNebulaPassword { get; set; } public bool IncludeNebulaGroup { get; set; } public string? Path { get; set; } + public bool ExportToZip { get; set; } } \ No newline at end of file diff --git a/src/NebulaAuth/Model/MafileExport/MafileExporter.cs b/src/NebulaAuth/Model/MafileExport/MafileExporter.cs index 9774193..b808d6b 100644 --- a/src/NebulaAuth/Model/MafileExport/MafileExporter.cs +++ b/src/NebulaAuth/Model/MafileExport/MafileExporter.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Text; using System.Threading.Tasks; using AchiesUtilities.Extensions; using NebulaAuth.Core; @@ -36,48 +39,79 @@ public static class MafileExporter return new ExportResult(LocManager.GetCodeBehindOrDefault("AccessDenied", "MafileExporter.AccessDenied")); } - var exported = new Dictionary(); var notFound = new List(); var conflict = new List(); + var toExport = new List<(string key, Mafile mafile)>(); foreach (var key in keys) { SteamId? steamId = null; if (SteamId64.TryParse(key, out var id64)) - { steamId = id64; - } var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId); if (maf != null) + toExport.Add((key, maf)); + else + notFound.Add(key); + } + + if (template.ExportToZip) + { + if (toExport.Count > 0) { - var fileName = await ExportMafile(template.Path, template, maf); - if (fileName == null) + var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + var zipName = $"{toExport.Count}_{timestamp}.zip"; + var zipPath = Path.Combine(template.Path, zipName); + + await using var zipStream = File.Create(zipPath); + using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create); + var addedEntries = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var (key, maf) in toExport) + { + var (content, fileName) = BuildMafileContent(template, maf); + if (!addedEntries.Add(fileName)) + { + conflict.Add(key); + continue; + } + + var entry = archive.CreateEntry(fileName); + await using var entryStream = entry.Open(); + await using var writer = new StreamWriter(entryStream, Encoding.UTF8); + await writer.WriteAsync(content); + exported[maf] = fileName; + } + } + } + else + { + foreach (var (key, maf) in toExport) + { + var (content, fileName) = BuildMafileContent(template, maf); + var fullPath = Path.Combine(template.Path, fileName); + if (File.Exists(fullPath)) { conflict.Add(key); continue; } - exported[maf] = fileName; - } - else - { - notFound.Add(key); + await File.WriteAllTextAsync(fullPath, content); + exported[maf] = fullPath; } } return new ExportResult(exported, notFound, conflict); } - private static async Task ExportMafile(string path, MafileExportTemplate template, Mafile mafile) + private static (string content, string fileName) BuildMafileContent(MafileExportTemplate template, Mafile mafile) { - // We preserve SteamId even if other info is not included var session = template.IncludeSessionData ? mafile.SessionData : new MobileSessionData(null!, mafile.SteamId, default, null, tokens: null); - var serializeMaf = new Mafile { SharedSecret = template.IncludeSharedSecret ? mafile.SharedSecret : null!, @@ -102,14 +136,7 @@ public static class MafileExporter var serialized = NebulaSerializer.SerializeMafile(serializeMaf); var strategy = template.UseLoginAsMafileName ? MafileNamingStrategy.Login : MafileNamingStrategy.SteamId; var fileName = strategy.GetMafileName(serializeMaf); - var fullPath = Path.Combine(path, fileName); - if (File.Exists(fullPath)) - { - return null; - } - - await File.WriteAllTextAsync(fullPath, serialized); - return fullPath; + return (serialized, fileName); } private static bool PathIsValid(string path) diff --git a/src/NebulaAuth/Model/NebulaSerializer.cs b/src/NebulaAuth/Model/NebulaSerializer.cs index 7908887..642d772 100644 --- a/src/NebulaAuth/Model/NebulaSerializer.cs +++ b/src/NebulaAuth/Model/NebulaSerializer.cs @@ -85,11 +85,6 @@ public static class NebulaSerializer public static string SerializeMafile(MobileDataExtended data, Dictionary? properties) { - if (Settings.Instance.LegacyMode) - { - return MafileSerializer.SerializeLegacy(data, Serializer.Settings.SerializationOptions, properties); - } - - return Serializer.Serialize(data); + return MafileSerializer.SerializeLegacy(data, Serializer.Settings.SerializationOptions, properties); } } \ No newline at end of file diff --git a/src/NebulaAuth/Model/ProxyAssignmentCache.cs b/src/NebulaAuth/Model/ProxyAssignmentCache.cs new file mode 100644 index 0000000..0a02f32 --- /dev/null +++ b/src/NebulaAuth/Model/ProxyAssignmentCache.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AchiesUtilities.Collections; +using AchiesUtilities.Web.Proxy; +using NebulaAuth.Model.Entities; + +namespace NebulaAuth.Model; + +/// +/// In-memory cache tracking which proxy IDs are currently assigned to accounts. +/// Provides fast lookup for free proxy selection without iterating all mafiles. +/// Initialized at startup and kept in sync whenever proxy assignments change. +/// +public static class ProxyAssignmentCache +{ + private static readonly Dictionary> _proxyToAccounts = new(); + + public static void Initialize(IEnumerable mafiles) + { + _proxyToAccounts.Clear(); + foreach (var maf in mafiles) + { + if (maf.Proxy != null) + AddInternal(maf.Proxy.Id, maf.AccountName); + } + } + + /// + /// Call whenever a proxy assignment on a mafile changes and is persisted to disk. + /// + public static void UpdateAssignment(string? accountName, int? oldProxyId, int? newProxyId) + { + if (accountName == null) return; + if (oldProxyId.HasValue) RemoveInternal(oldProxyId.Value, accountName); + if (newProxyId.HasValue) AddInternal(newProxyId.Value, accountName); + } + + public static bool IsProxyFree(int proxyId) + { + return !_proxyToAccounts.TryGetValue(proxyId, out var set) || set.Count == 0; + } + + public static int GetAccountCount(int proxyId) + { + return _proxyToAccounts.TryGetValue(proxyId, out var set) ? set.Count : 0; + } + + /// + /// Returns a random free proxy (not the default proxy), or null if none available. + /// + public static KeyValuePair? GetRandomFreeProxy( + ObservableDictionary proxies, + int? excludeDefaultProxyId) + { + var free = proxies + .Where(kvp => kvp.Key != excludeDefaultProxyId && IsProxyFree(kvp.Key)) + .ToList(); + if (free.Count == 0) return null; + return free[Random.Shared.Next(free.Count)]; + } + + private static void AddInternal(int proxyId, string accountName) + { + if (!_proxyToAccounts.TryGetValue(proxyId, out var set)) + { + set = new HashSet(StringComparer.OrdinalIgnoreCase); + _proxyToAccounts[proxyId] = set; + } + + set.Add(accountName); + } + + private static void RemoveInternal(int proxyId, string accountName) + { + if (_proxyToAccounts.TryGetValue(proxyId, out var set)) + set.Remove(accountName); + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Model/ProxyStorage.cs b/src/NebulaAuth/Model/ProxyStorage.cs index 7f94419..1f96e1b 100644 --- a/src/NebulaAuth/Model/ProxyStorage.cs +++ b/src/NebulaAuth/Model/ProxyStorage.cs @@ -22,6 +22,13 @@ public static class ProxyStorage PatternRequirement.Optional, PatternRequirement.Optional); + public static readonly ProxyParser SignAtScheme = new( + ProxyDefaultFormats.UniversalSignAt, false, ProxyProtocol.HTTP, + ProxyPatternProtocol.All, + ProxyPatternHostFormat.Domain | ProxyPatternHostFormat.IPv4, + PatternRequirement.Required, + PatternRequirement.Required); + public static ObservableDictionary Proxies { get; } = new(); diff --git a/src/NebulaAuth/Model/RemoteLinks.cs b/src/NebulaAuth/Model/RemoteLinks.cs new file mode 100644 index 0000000..0f50b92 --- /dev/null +++ b/src/NebulaAuth/Model/RemoteLinks.cs @@ -0,0 +1,7 @@ +namespace NebulaAuth.Model; + +public class RemoteLinks +{ + public string? Website { get; set; } + public string? Documentation { get; set; } +} \ No newline at end of file diff --git a/src/NebulaAuth/Model/SessionHandler/SessionHandler.cs b/src/NebulaAuth/Model/SessionHandler/SessionHandler.cs index b1c9979..46fa778 100644 --- a/src/NebulaAuth/Model/SessionHandler/SessionHandler.cs +++ b/src/NebulaAuth/Model/SessionHandler/SessionHandler.cs @@ -6,7 +6,9 @@ using AchiesUtilities.Web.Models; using MaterialDesignThemes.Wpf; using NebulaAuth.Core; using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; using NebulaAuth.View.Dialogs; +using NebulaAuth.ViewModel.Other; using SteamLib.Exceptions.Authorization; namespace NebulaAuth.Model; @@ -16,13 +18,13 @@ public static partial class SessionHandler private static readonly SemaphoreSlim Semaphore = new(1, 1); public static async Task Handle(Func> func, Mafile mafile, - HttpClientHandlerPair? chp = null, string? snackbarPrefix = null) + HttpClientHandlerPair? chp = null, string? snackbarPrefix = null, bool allowInteractiveLogin = true) { chp ??= MaClient.GetHttpClientHandlerPair(mafile); await Semaphore.WaitAsync(); try { - return await HandleInternal(func, chp.Value, mafile, snackbarPrefix); + return await HandleInternal(func, chp.Value, mafile, snackbarPrefix, allowInteractiveLogin); } finally { @@ -31,7 +33,7 @@ public static partial class SessionHandler } private static async Task HandleInternal(Func> func, HttpClientHandlerPair chp, Mafile mafile, - string? snackbarPrefix = null) + string? snackbarPrefix = null, bool allowInteractiveLogin = true) { using var scope = Shell.Logger.PushScopeProperty("Scope", "SessionHandler"); Exception? currentException = null; @@ -86,6 +88,32 @@ public static partial class SessionHandler } } + if (allowInteractiveLogin && !DialogHost.IsDialogOpen(null)) + { + var noProxy = mafile.Proxy == null && MaClient.DefaultProxy == null; + var currentPassword = GetPassword(mafile); + + Task dialogTask = null!; + Application.Current.Dispatcher.Invoke(() => + { + dialogTask = DialogsController.ShowLoginAgainDialog(mafile.AccountName, currentPassword, noProxy); + }); + + var vm = await dialogTask; + if (vm != null) + { + Shell.Logger.Info("User provided credentials interactively for {name}, attempting login", + mafile.AccountName); + var logged = await LoginAgainInternal(chp, mafile, vm.Password, vm.SavePassword); + if (logged) + { + Shell.Logger.Debug("Interactive login for {name} succeeded, retrying operation", + mafile.AccountName); + return await func(); + } + } + } + throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG, currentException); } @@ -118,6 +146,7 @@ public static partial class SessionHandler { Shell.Logger.Debug(ex, "Failed to relogin mafile {name} {steamid}", mafile.AccountName, mafile.SessionData?.SteamId); + ExceptionHandler.Handle(ex); return false; } finally diff --git a/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs b/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs index b644462..fe40011 100644 --- a/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs +++ b/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs @@ -30,7 +30,7 @@ public partial class SessionHandler //API //Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient mafile.SetSessionData(mafile.SessionData); await Storage.UpdateMafileAsync(mafile); - chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData); + chp.Handler.CookieContainer.SetSteamMobileCookies(mafile.SessionData); } public static async Task LoginAgain(HttpClientHandlerPair chp, Mafile mafile, string password, bool savePassword) diff --git a/src/NebulaAuth/Model/Settings.cs b/src/NebulaAuth/Model/Settings.cs index afa052b..a422e32 100644 --- a/src/NebulaAuth/Model/Settings.cs +++ b/src/NebulaAuth/Model/Settings.cs @@ -91,7 +91,7 @@ public partial class Settings : ObservableObject [ObservableProperty] private Color? _iconColor; [ObservableProperty] private bool _isPasswordSet; [ObservableProperty] private LocalizationLanguage _language = LocalizationLanguage.English; - [ObservableProperty] private bool _legacyMode = true; + [ObservableProperty] private bool _confirmMafileImport = true; [ObservableProperty] private bool _allowAutoUpdate; [ObservableProperty] private bool _useAccountNameAsMafileName; diff --git a/src/NebulaAuth/Model/Shell.cs b/src/NebulaAuth/Model/Shell.cs index 7a74a5d..f4e61dd 100644 --- a/src/NebulaAuth/Model/Shell.cs +++ b/src/NebulaAuth/Model/Shell.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NebulaAuth.Core; -using NebulaAuth.Model.Exceptions; using NebulaAuth.Model.MAAC; using NebulaAuth.Model.MafileExport; using NebulaAuth.Model.Mafiles; @@ -40,13 +39,16 @@ public static class Shell } catch (Exception ex) { - throw new CantAlignTimeException("", ex); + Logger.Error(ex, "Failed to align time with Steam"); + TimeAligner.SetTimeDifference(0); } var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1; await Storage.Initialize(threads); + ProxyAssignmentCache.Initialize(Storage.MaFiles); MAACStorage.Initialize(); MafileExporterStorage.Initialize(); + _ = LinksManager.FetchAsync(); ExtensionsLogger.LogDebug("Application started"); } diff --git a/src/NebulaAuth/Model/Update/ChangelogEntry.cs b/src/NebulaAuth/Model/Update/ChangelogEntry.cs index bda8ba9..17b7225 100644 --- a/src/NebulaAuth/Model/Update/ChangelogEntry.cs +++ b/src/NebulaAuth/Model/Update/ChangelogEntry.cs @@ -3,20 +3,20 @@ using Newtonsoft.Json; namespace NebulaAuth.Model.Update; - public class ChangelogEntry - { - [JsonProperty("version")] public string Version { get; set; } = string.Empty; +public class ChangelogEntry +{ + [JsonProperty("version")] public string Version { get; set; } = string.Empty; - [JsonProperty("date")] public string Date { get; set; } = string.Empty; + [JsonProperty("date")] public string Date { get; set; } = string.Empty; - [JsonProperty("changes")] public List Changes { get; set; } = new(); - } + [JsonProperty("changes")] public List Changes { get; set; } = new(); +} - public class ChangeItem - { - [JsonProperty("type")] public string Type { get; set; } = string.Empty; +public class ChangeItem +{ + [JsonProperty("type")] public string Type { get; set; } = string.Empty; - [JsonProperty("text")] public string Text { get; set; } = string.Empty; + [JsonProperty("text")] public string Text { get; set; } = string.Empty; - [JsonProperty("link")] public string? Link { get; set; } - } \ No newline at end of file + [JsonProperty("link")] public string? Link { get; set; } +} \ No newline at end of file diff --git a/src/NebulaAuth/NebulaAuth.csproj b/src/NebulaAuth/NebulaAuth.csproj index 7e9bd4c..c5a31df 100644 --- a/src/NebulaAuth/NebulaAuth.csproj +++ b/src/NebulaAuth/NebulaAuth.csproj @@ -10,7 +10,7 @@ en;ru;ua Theme\lock.ico 7.0 - 1.8.4 + 1.8.5 true diff --git a/src/NebulaAuth/Theme/Brushes.xaml b/src/NebulaAuth/Theme/Brushes.xaml index a75f635..5c13e96 100644 --- a/src/NebulaAuth/Theme/Brushes.xaml +++ b/src/NebulaAuth/Theme/Brushes.xaml @@ -89,4 +89,7 @@ Color="{StaticResource SecondaryColor}" /> + + \ No newline at end of file diff --git a/src/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml b/src/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml index c17a41b..0a74beb 100644 --- a/src/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml +++ b/src/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml @@ -36,7 +36,8 @@ + Orientation="Horizontal" + TextElement.Foreground="{DynamicResource PrimaryContentBrush}"> - - - - - - - - - - - - - - - - - + + RenderOptions.BitmapScalingMode="HighQuality" + Loaded="LinksView_Loaded"> @@ -49,16 +50,20 @@ - - - - - + + @@ -133,6 +113,12 @@ IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"> + + @@ -233,8 +219,8 @@ - - - - + + Grid.Row="2" Margin="20" Content="{Binding CurrentStep}" /> \ No newline at end of file diff --git a/src/NebulaAuth/View/ProxyManagerView.xaml b/src/NebulaAuth/View/ProxyManagerView.xaml index 6eea83f..8266353 100644 --- a/src/NebulaAuth/View/ProxyManagerView.xaml +++ b/src/NebulaAuth/View/ProxyManagerView.xaml @@ -20,115 +20,46 @@ FontSize="16"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - + + + + + + + - - + + + - + - - - + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + - + + + + + + + + + + + + - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - + @@ -109,7 +82,7 @@ - - - - - + + Style="{StaticResource MaterialDesignUniformTabControl}" Grid.Row="1"> - +