Compare commits

...

6 Commits

Author SHA1 Message Date
Achies 7348bc2673 Merge branch 'dev' into feat/group-on-import 2026-06-19 19:03:16 +03:00
Achies 3a9d929c9a Merge pull request #23 from achiez/fix/mafile-mover-loc
fix(localization): update mafile mover Steam Guard error message
2026-06-19 19:01:40 +03:00
Achies 25818997af Merge pull request #22 from achiez/feat/proxy-assignment
feat(proxy): add bulk assignment and proxy usage stats
2026-06-19 19:00:47 +03:00
achiez 537d5d907f feat(import): add group assignment and skip-confirmation setting; remove obsolete format setting
- Add ability to assign imported mafiles to a group directly from the import dialog
- Add setting to skip the import confirmation dialog when no conflicts are detected
- Remove the deprecated "legacy mafile format" setting as it is no longer relevant
2026-06-19 18:54:37 +03:00
achiez 0e90f1826b fix(localization): update mafile mover Steam Guard error message 2026-05-19 22:42:58 +03:00
achiez b5d3b4a8af feat(proxy): add bulk assignment and proxy usage stats
- Implement bulk proxy assignment for accounts with flexible input (login:proxy, login:ID, or login)
- Add UI for mass assignment with counters and behavior selection for unspecified proxies
- Show assigned account count badges in proxy list for better visibility
- Enable quick assignment of a free proxy to an account
- Ensure fast and consistent proxy assignment state via in-memory cache
- Perform ReSharper cleanup
2026-05-14 16:44:14 +03:00
26 changed files with 1046 additions and 217 deletions
@@ -15,6 +15,7 @@
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
<converters:BoolToValueConverter x:Key="BoolToValueConverter" />
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
<converters:ProxyAccountCountConverter x:Key="ProxyAccountCountConverter" />
<!-- Background converters-->
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
<background:BackgroundSourceConverter x:Key="BackgroundSourceConverter" />
@@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Windows.Data;
using NebulaAuth.Model;
namespace NebulaAuth.Converters;
/// <summary>
/// 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.
/// </summary>
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();
}
}
+17 -1
View File
@@ -73,6 +73,22 @@ public static class DialogsController
return null;
}
public static async Task<MafileImportDialogResult?> ShowMafileImportDialog(IEnumerable<string> 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<bool> ShowProxyManager()
{
var vm = new ProxyManagerVM();
@@ -174,4 +190,4 @@ public static class DialogsController
}
#endregion
}
}
+63 -1
View File
@@ -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": "Бар немесе жаңа топ"
}
}
}
}
@@ -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)",
@@ -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": "Қолдану"
}
}
}
@@ -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 бойынша сәрсенбі түні) пайда болатын және авторастау таймерлерін қажетсіз өшіретін қателерді елемеу"
}
}
}
}
+10 -10
View File
@@ -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": {
+3
View File
@@ -420,6 +420,9 @@
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
Command="{Binding RemoveProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AssignFreeProxy}"
Command="{Binding AssignFreeProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</ContextMenu>
</FrameworkElement.ContextMenu>
+2 -7
View File
@@ -85,11 +85,6 @@ public static class NebulaSerializer
public static string SerializeMafile(MobileDataExtended data, Dictionary<string, object?>? 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);
}
}
}
@@ -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;
/// <summary>
/// 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.
/// </summary>
public static class ProxyAssignmentCache
{
private static readonly Dictionary<int, HashSet<string>> _proxyToAccounts = new();
public static void Initialize(IEnumerable<Mafile> mafiles)
{
_proxyToAccounts.Clear();
foreach (var maf in mafiles)
{
if (maf.Proxy != null)
AddInternal(maf.Proxy.Id, maf.AccountName);
}
}
/// <summary>
/// Call whenever a proxy assignment on a mafile changes and is persisted to disk.
/// </summary>
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;
}
/// <summary>
/// Returns a random free proxy (not the default proxy), or null if none available.
/// </summary>
public static KeyValuePair<int, ProxyData>? GetRandomFreeProxy(
ObservableDictionary<int, ProxyData> 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<string>(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);
}
}
+2 -2
View File
@@ -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;
@@ -127,4 +127,4 @@ public enum ThemeType
Light = 2,
Luxury = 3,
Shadcn = 4
}
}
+1
View File
@@ -45,6 +45,7 @@ public static class Shell
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
ProxyAssignmentCache.Initialize(Storage.MaFiles);
MAACStorage.Initialize();
MafileExporterStorage.Initialize();
+3
View File
@@ -89,4 +89,7 @@
Color="{StaticResource SecondaryColor}" />
<SolidColorBrush x:Key="MaterialDesign.Brush.Secondary.Dark.Foreground"
Color="{StaticResource BaseContentColor}" />
<SolidColorBrush x:Key="MaterialDesign.Brush.RadioButton.Outline"
Color="{StaticResource BaseContentColor}" />
</ResourceDictionary>
@@ -0,0 +1,129 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.MafileImportDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Height="Auto" Width="Auto" MaxWidth="520"
Background="{DynamicResource WindowBackground}">
<Grid MinHeight="240" MinWidth="440">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr MafileImportDialog.Title}" />
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" CommandParameter="{x:Null}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<Grid Grid.Row="2" Margin="14,14,14,16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Margin="6,0,6,12" Padding="12,10"
CornerRadius="8"
BorderThickness="1">
<Border.Background>
<SolidColorBrush Color="{DynamicResource BaseContentColor}" Opacity="0.07"/>
</Border.Background>
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseContentColor}" Opacity="0.12"/>
</Border.BorderBrush>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Tr MafileImportDialog.TotalCount}" Opacity="0.82" />
<TextBlock Grid.Column="1" Text="{Binding TotalCount, Mode=OneWay}" FontWeight="SemiBold" />
<TextBlock Grid.Row="1" Text="{Tr MafileImportDialog.ConflictCount}" Opacity="0.82"
Visibility="{Binding HasConflicts, Converter={StaticResource BooleanToVisibilityConverter}}" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ConflictCount, Mode=OneWay}"
FontWeight="SemiBold"
Foreground="{DynamicResource WarningBrush}"
Visibility="{Binding HasConflicts, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
</Border>
<CheckBox Grid.Row="1"
Margin="10,4,10,8"
IsChecked="{Binding OverwriteConflicts}"
Visibility="{Binding HasConflicts, Converter={StaticResource BooleanToVisibilityConverter}}"
Content="{Tr MafileImportDialog.OverwriteConflicts}" />
<Separator Grid.Row="2"
Margin="10,2,10,2" Opacity="0.35"
Visibility="{Binding HasConflicts, Converter={StaticResource BooleanToVisibilityConverter}}" />
<CheckBox Grid.Row="3"
Margin="10,4,10,0"
IsChecked="{Binding AddToGroup}"
Content="{Tr MafileImportDialog.AddToGroup}" />
<ComboBox x:Name="GroupBox"
Grid.Row="4"
Margin="42,0,10,0"
IsEditable="True"
ItemsSource="{Binding Groups}"
Text="{Binding Group, UpdateSourceTrigger=PropertyChanged}"
materialDesign:HintAssist.Hint="{Tr MafileImportDialog.GroupFieldHint}"
materialDesign:HintAssist.IsFloating="False"
Visibility="{Binding AddToGroup, Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource MaterialDesignFloatingHintComboBox}"
/>
<Grid Grid.Row="5" Margin="10,16,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Width="130"
HorizontalAlignment="Left"
Margin="2,0,4,0"
Style="{StaticResource MaterialDesignFlatDarkBgButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{Binding}"
Content="{Tr Common.OK}"
IsDefault="True" />
<Button Grid.Column="1"
HorizontalAlignment="Right"
Width="130"
Margin="0,0,2,0"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{x:Null}"
Content="{Tr Common.Cancel}"
IsCancel="True" />
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View.Dialogs;
public partial class MafileImportDialog
{
public MafileImportDialog()
{
InitializeComponent();
}
}
+238 -126
View File
@@ -20,12 +20,12 @@
FontSize="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Title -->
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -45,90 +45,41 @@
</Button>
</Grid>
<Separator Grid.Row="1" />
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Stretch">
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" />
<Run Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" />
<Run>
<Run.Text>
<MultiBinding Mode="OneWay"
Converter="{StaticResource ProxyDataTextMultiConverter}">
<Binding Path="DefaultProxy.Value" />
<Binding Path="DataContext.DisplayProtocol"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext.DisplayCredentials"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
</MultiBinding>
</Run.Text>
</Run>
</TextBlock>
<Button Grid.Column="1" Command="{Binding RemoveDefaultCommand}">
<md:PackIcon Kind="ClearBox" Width="20" Height="20" />
</Button>
<!-- Tabs -->
<TabControl Grid.Row="2"
md:ColorZoneAssist.Background="{DynamicResource Base100Brush}"
Style="{StaticResource MaterialDesignUniformTabControl}">
</Grid>
<StackPanel Grid.Row="1" Margin="15,0,15,15">
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyProtocol}" IsChecked="{Binding DisplayProtocol}" />
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyCredentials}"
IsChecked="{Binding DisplayCredentials}" />
</StackPanel>
</Grid>
<md:Card Grid.Row="3" Margin="10">
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" FontSize="14"
SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}">
<ListBox.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DataContext.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</ListBox.InputBindings>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignListBoxItem}">
<Setter Property="Tag"
Value="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=DataContext}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.Copy}"
Command="{Binding PlacementTarget.Tag.CopyProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.CopyAddress}"
Command="{Binding PlacementTarget.Tag.CopyProxyAddressCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr Common.Delete}"
Command="{Binding PlacementTarget.Tag.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
</ContextMenu>
</Setter.Value>
</Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!-- Tab 1: Proxy Manager -->
<TabItem Header="{Tr ProxyManagerDialog.TabManager}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<!-- Default proxy + display options -->
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">
<Run Text="{Binding Key, Mode=OneWay}" /><Run Text=": " />
<TextBlock HorizontalAlignment="Stretch">
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" />
<Run
Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" />
<Run>
<Run.Text>
<MultiBinding Mode="OneWay"
Converter="{StaticResource ProxyDataTextMultiConverter}">
<Binding Path="Value" />
<Binding Path="DefaultProxy.Value" />
<Binding Path="DataContext.DisplayProtocol"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext.DisplayCredentials"
@@ -136,66 +87,227 @@
</MultiBinding>
</Run.Text>
</Run>
</TextBlock>
<Button Style="{StaticResource MaterialDesignIconButton}" Padding="0" Width="24"
Height="24" md:RippleAssist.IsDisabled="True" Grid.Column="1"
HorizontalAlignment="Right"
Command="{Binding DataContext.SetDefaultCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<md:PackIcon Height="16" Width="16" Kind="Heart" />
<Button Grid.Column="1" Command="{Binding RemoveDefaultCommand}">
<md:PackIcon Kind="ClearBox" Width="20" Height="20" />
</Button>
</Grid>
<StackPanel Grid.Row="1" Margin="15,0,15,15">
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyProtocol}"
IsChecked="{Binding DisplayProtocol}" />
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyCredentials}"
IsChecked="{Binding DisplayCredentials}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</md:Card>
<Grid Grid.Row="4" Margin="5,0,15,0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<nebulaAuth:HintBox
Visibility="{Binding ErrorText, Converter={StaticResource NullableToVisibilityConverter}}"
Margin="15,0,15,0"
Grid.Row="0"
Severity="Error"
FontSize="14"
Text="{Binding ErrorText}"
CloseCommand="{Binding ClearErrorCommand}"
ShowCloseButton="True" />
<!-- Proxy list -->
<md:Card Grid.Row="1" Margin="10">
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" FontSize="14"
SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}">
<ListBox.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DataContext.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</ListBox.InputBindings>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignListBoxItem}">
<Setter Property="Tag"
Value="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=DataContext}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.Copy}"
Command="{Binding PlacementTarget.Tag.CopyProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.CopyAddress}"
Command="{Binding PlacementTarget.Tag.CopyProxyAddressCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr Common.Delete}"
Command="{Binding PlacementTarget.Tag.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
</ContextMenu>
</Setter.Value>
</Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">
<Run Text="{Binding Key, Mode=OneWay}" /><Run Text=": " />
<Run>
<Run.Text>
<MultiBinding Mode="OneWay"
Converter="{StaticResource ProxyDataTextMultiConverter}">
<Binding Path="Value" />
<Binding Path="DataContext.DisplayProtocol"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext.DisplayCredentials"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
</MultiBinding>
</Run.Text>
</Run>
</TextBlock>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" VerticalAlignment="Center" FontSize="10" Margin="0,0,2,0"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}"
Text="{Binding Key, Converter={StaticResource ProxyAccountCountConverter}}" />
<Button Style="{StaticResource MaterialDesignIconButton}" Padding="0"
Width="24"
Height="24" md:RippleAssist.IsDisabled="True" Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DataContext.SetDefaultCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<md:PackIcon Height="16" Width="16" Kind="Heart" />
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</md:Card>
<TextBox Text="{Binding AddProxyField}"
KeyDown="ProxyInput_KeyDown"
FontSize="12"
md:TextFieldAssist.HasClearButton="True"
AcceptsReturn="True"
MaxHeight="100" Style="{StaticResource MaterialDesignOutlinedTextBox}"
Margin="15"
Height="90"
md:HintAssist.IsFloating="False"
md:HintAssist.Hint="IP:PORT&#x0a;IP:PORT:USER:PASS&#x0a;PROTOCOL://IP:PORT:USER:PASS&#x0a;IP:PORT:USER:PASS{ID}
<!-- Add / Remove area -->
<Grid Grid.Row="2" Margin="5,0,15,0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<nebulaAuth:HintBox
Visibility="{Binding ErrorText, Converter={StaticResource NullableToVisibilityConverter}}"
Margin="15,0,15,0"
Grid.Row="0"
Severity="Error"
FontSize="14"
Text="{Binding ErrorText}"
CloseCommand="{Binding ClearErrorCommand}"
ShowCloseButton="True" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding AddProxyField}"
KeyDown="ProxyInput_KeyDown"
FontSize="12"
md:TextFieldAssist.HasClearButton="True"
AcceptsReturn="True"
MaxHeight="100" Style="{StaticResource MaterialDesignOutlinedTextBox}"
Margin="15"
Height="90"
md:HintAssist.IsFloating="False"
md:HintAssist.Hint="IP:PORT&#x0a;IP:PORT:USER:PASS&#x0a;PROTOCOL://IP:PORT:USER:PASS&#x0a;IP:PORT:USER:PASS{ID}
" />
<Button x:Name="AddProxyBtn" ToolTip="CTRL+ENTER" IsDefault="True" Grid.Column="1" Height="90"
Command="{Binding AddProxyCommand}">
<md:PackIcon Kind="Add" />
</Button>
<Button Grid.Column="2" Height="90" Command="{Binding RemoveProxyCommand}" CommandParameter="{x:Null}">
<md:PackIcon Kind="Trash" />
</Button>
</Grid>
<Button x:Name="AddProxyBtn" ToolTip="CTRL+ENTER" IsDefault="True" Grid.Column="1"
Height="90" Command="{Binding AddProxyCommand}">
<md:PackIcon Kind="Add" />
</Button>
<Button Grid.Column="2" Height="90" Command="{Binding RemoveProxyCommand}"
CommandParameter="{x:Null}">
<md:PackIcon Kind="Trash" />
</Button>
</Grid>
</Grid>
</Grid>
</TabItem>
</Grid>
<!-- Tab 2: Bulk Assignment -->
<TabItem Header="{Tr ProxyManagerDialog.TabAssignment}">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Stats counters -->
<UniformGrid Grid.Row="0" Columns="3" Margin="0,8,0,12">
<Border Margin="0,0,4,0" Padding="10,8" CornerRadius="8"
Background="{DynamicResource MaterialDesign.Brush.Card.Background}">
<StackPanel HorizontalAlignment="Center">
<TextBlock FontSize="20" FontWeight="Bold" HorizontalAlignment="Center"
Text="{Binding TotalAccounts, Mode=OneWay}" />
<TextBlock FontSize="11" HorizontalAlignment="Center"
Text="{Tr ProxyManagerDialog.TotalAccounts}"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
</StackPanel>
</Border>
<Border Margin="4,0,4,0" Padding="10,8" CornerRadius="8"
Background="{DynamicResource MaterialDesign.Brush.Card.Background}">
<StackPanel HorizontalAlignment="Center">
<TextBlock FontSize="20" FontWeight="Bold" HorizontalAlignment="Center"
Text="{Binding TotalProxies, Mode=OneWay}" />
<TextBlock FontSize="11" HorizontalAlignment="Center"
Text="{Tr ProxyManagerDialog.TotalProxies}"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
</StackPanel>
</Border>
<Border Margin="4,0,0,0" Padding="10,8" CornerRadius="8"
Background="{DynamicResource MaterialDesign.Brush.Card.Background}">
<StackPanel HorizontalAlignment="Center">
<TextBlock FontSize="20" FontWeight="Bold" HorizontalAlignment="Center"
Text="{Binding AssignedProxies, Mode=OneWay}" />
<TextBlock FontSize="11" HorizontalAlignment="Center"
Text="{Tr ProxyManagerDialog.AssignedProxies}"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
</StackPanel>
</Border>
</UniformGrid>
<!-- Toggle: behaviour for unspecified proxy -->
<StackPanel Grid.Row="1" Margin="0,0,0,10">
<TextBlock Text="{Tr ProxyManagerDialog.UnspecifiedBehavior}" FontSize="13"
Margin="0,0,0,4"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
<StackPanel Orientation="Horizontal">
<RadioButton Content="{Tr ProxyManagerDialog.UnspecifiedRandom}"
IsChecked="{Binding UnspecifiedAssignRandom}"
Margin="0,0,24,0" />
<RadioButton Content="{Tr ProxyManagerDialog.UnspecifiedRemove}"
IsChecked="{Binding UnspecifiedAssignRandom, Converter={StaticResource ReverseBooleanConverter}}" />
</StackPanel>
</StackPanel>
<!-- Input -->
<TextBox Grid.Row="2"
Text="{Binding AssignmentInput, UpdateSourceTrigger=PropertyChanged}"
FontSize="12"
md:TextFieldAssist.HasClearButton="True"
AcceptsReturn="True"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
VerticalScrollBarVisibility="Auto"
md:HintAssist.IsFloating="False"
VerticalContentAlignment="Top"
Cursor="IBeam"
md:HintAssist.Hint="login:IP:PORT&#x0a;login:IP:PORT:USER:PASS&#x0a;login:socks5://USER:PASS@IP:PORT&#x0a;login:ID&#x0a;login" />
<!-- Result HintBox -->
<nebulaAuth:HintBox Grid.Row="3"
Visibility="{Binding AssignmentTip, Converter={StaticResource NullableToVisibilityConverter}}"
Margin="0,8,0,0"
Severity="Info"
FontSize="14"
Text="{Binding AssignmentTip}"
CloseCommand="{Binding ClearAssignmentTipCommand}"
ShowCloseButton="True" />
<!-- Apply button -->
<Button Grid.Row="4" Margin="0,10,0,0" HorizontalAlignment="Right"
Command="{Binding ApplyAssignmentCommand}"
Content="{Tr ProxyManagerDialog.ApplyAssignment}" />
</Grid>
</TabItem>
</TabControl>
</Grid>
</UserControl>
+4 -4
View File
@@ -59,9 +59,9 @@
Content="{Tr SettingsDialog.MinimizeToTray}" />
<CheckBox Margin="0,5,0,0" IsChecked="{Binding LegacyMode}"
Content="{Tr SettingsDialog.LegacyMafileMode}"
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
<CheckBox Margin="0,5,0,0" IsChecked="{Binding ConfirmMafileImport}"
Content="{Tr SettingsDialog.ConfirmMafileImport}"
ToolTip="{Tr SettingsDialog.ConfirmMafileImportHint}" />
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
@@ -196,4 +196,4 @@
</TabControl>
</Grid>
</Grid>
</UserControl>
</UserControl>
+1 -1
View File
@@ -42,7 +42,7 @@ public partial class MainVM : ObservableObject
new MaProxy(kvp.Key, kvp.Value)));
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
QueryGroups();
UpdateManager.CheckForUpdates(false);
UpdateManager.CheckForUpdates();
}
[RelayCommand]
+84 -21
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
@@ -25,6 +26,11 @@ namespace NebulaAuth.ViewModel;
public partial class MainVM //File //TODO: Refactor
{
private record MafileReadResult(
Mafile? Mafile,
bool SdaPasswordPrompted,
SDAEncryptionHelper.Context? SdaContext);
public Settings Settings => Settings.Instance;
@@ -71,10 +77,11 @@ public partial class MainVM //File //TODO: Refactor
: Task.CompletedTask;
}
//TODO: extract flow and refactor, this method is too long and does too many things
public async Task AddMafile(string[] path)
{
bool? confirmOverwrite = null;
var summary = new MafileImportSummary();
var items = new List<MafileImportPlanItem>();
SDAEncryptionHelper.Context? sdaContext = null;
var sdaPasswordPrompted = false;
foreach (var str in path)
@@ -90,16 +97,7 @@ public partial class MainVM //File //TODO: Refactor
continue;
}
var overwrite = confirmOverwrite ?? false;
var res = await Storage.AddNewMafileFromData(mafile, overwrite);
if (res == AddMafileResult.AlreadyExist && confirmOverwrite == null)
{
confirmOverwrite =
await DialogsController.ShowConfirmCancelDialog(GetLocalization("ConfirmMafileOverwrite"));
res = await Storage.AddNewMafileFromData(mafile, confirmOverwrite.Value);
}
summary.Apply(res);
items.Add(new MafileImportPlanItem(mafile, false, HasImportConflict(mafile)));
}
catch (FormatException)
{
@@ -109,18 +107,11 @@ public partial class MainVM //File //TODO: Refactor
{
if (path.Length == 1 && ex.Mafile != null)
{
var mafile = ex.Mafile;
if (await HandleAddMafileWithoutSession(mafile))
{
summary.AddedOne();
}
else
{
summary.ErrorOne();
}
items.Add(new MafileImportPlanItem(ex.Mafile, true, HasImportConflict(ex.Mafile)));
}
else
{
summary.ErrorOne();
SnackbarController.SendSnackbar(
$"{GetLocalization("MafileImportError")} {Path.GetFileName(str)}{GetLocalization("MissingSessionInMafile")}",
TimeSpan.FromSeconds(4));
@@ -128,9 +119,78 @@ public partial class MainVM //File //TODO: Refactor
}
}
if (items.Count == 0)
{
ShowImportSummary(summary);
return;
}
var conflictCount = items.Count(i => i.HasConflict);
var showImportDialog = Settings.ConfirmMafileImport || conflictCount > 0;
MafileImportDialogResult? importOptions = null;
if (showImportDialog)
{
importOptions = await DialogsController.ShowMafileImportDialog(Groups, items.Count, conflictCount);
if (importOptions == null)
{
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Canceled", "Canceled"));
return;
}
}
var importGroup = importOptions?.Group;
var overwriteConflicts = importOptions?.OverwriteConflicts ?? false;
foreach (var item in items)
{
ApplyImportGroup(item.Mafile, importGroup);
if (item.RequiresRelogin)
{
if (item.HasConflict && !overwriteConflicts)
{
summary.SkippedOne();
continue;
}
if (await HandleAddMafileWithoutSession(item.Mafile))
{
summary.AddedOne();
}
else
{
summary.ErrorOne();
}
continue;
}
var res = await Storage.AddNewMafileFromData(item.Mafile, overwriteConflicts);
summary.Apply(res);
}
if (summary.Added > 0)
{
QueryGroups();
if (!string.IsNullOrWhiteSpace(importGroup))
{
SelectedGroup = importGroup;
}
}
ShowImportSummary(summary);
}
private static void ApplyImportGroup(Mafile mafile, string? group)
{
if (string.IsNullOrWhiteSpace(group)) return;
mafile.Group = group;
}
private static bool HasImportConflict(Mafile mafile)
{
var fileName = MafilesStorageHelper.CreateMafileFileName(mafile, Settings.Instance.UseAccountNameAsMafileName);
return File.Exists(Path.Combine(Storage.MafilesDirectory, fileName));
}
private async Task<MafileReadResult> TryReadMafile(string path, SDAEncryptionHelper.Context? sdaContext,
bool sdaPasswordPrompted)
@@ -213,6 +273,7 @@ public partial class MainVM //File //TODO: Refactor
var result = data.SessionData != null;
if (!result) return result;
data.Filename = null;
await Storage.SaveMafileAsync(data);
{
ResetQuery();
@@ -352,4 +413,6 @@ public partial class MainVM //File //TODO: Refactor
Mafile? Mafile,
bool SdaPasswordPrompted,
SDAEncryptionHelper.Context? SdaContext);
}
private record MafileImportPlanItem(Mafile Mafile, bool RequiresRelogin, bool HasConflict);
}
+37 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -38,9 +39,10 @@ public partial class MainVM
}
if (!system && SelectedMafile != null)
{
var oldProxyId = SelectedMafile.Proxy?.Id;
SelectedMafile.Proxy = SelectedProxy;
ProxyAssignmentCache.UpdateAssignment(SelectedMafile.AccountName, oldProxyId, SelectedProxy?.Id);
Storage.UpdateMafile(SelectedMafile);
}
@@ -74,7 +76,9 @@ public partial class MainVM
{
mafile ??= SelectedMafile;
if (mafile?.Proxy == null) return;
var oldProxyId = mafile.Proxy.Id;
mafile.Proxy = null;
ProxyAssignmentCache.UpdateAssignment(mafile.AccountName, oldProxyId, null);
if (mafile == SelectedMafile)
SetProxy(null, false); //Not system, triggered by user
}
@@ -85,6 +89,38 @@ public partial class MainVM
return mafile is {Proxy: not null};
}
[RelayCommand(CanExecute = nameof(AssignFreeProxyCanExecute))]
private void AssignFreeProxy(Mafile? mafile)
{
mafile ??= SelectedMafile;
if (mafile == null) return;
int? defaultProxyId = null;
if (MaClient.DefaultProxy != null)
{
var defKvp = ProxyStorage.Proxies.FirstOrDefault(kvp => kvp.Value.Equals(MaClient.DefaultProxy));
if (defKvp.Value != null)
defaultProxyId = defKvp.Key;
}
var freeProxy = ProxyAssignmentCache.GetRandomFreeProxy(ProxyStorage.Proxies, defaultProxyId);
if (freeProxy == null) return;
var newProxy = new MaProxy(freeProxy.Value.Key, freeProxy.Value.Value);
var oldProxyId = mafile.Proxy?.Id;
mafile.Proxy = newProxy;
ProxyAssignmentCache.UpdateAssignment(mafile.AccountName, oldProxyId, newProxy.Id);
Storage.UpdateMafile(mafile);
if (mafile == SelectedMafile)
SetProxy(newProxy, true); // system=true: update UI only, assignment already saved
}
private bool AssignFreeProxyCanExecute(Mafile? mafile)
{
return ProxyStorage.Proxies.Count > 0;
}
private void CheckProxyExist()
{
if (SelectedProxy == null)
@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AchiesUtilities.Web.Proxy;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLibForked.Models.SteamIds;
namespace NebulaAuth.ViewModel.Other;
public partial class ProxyManagerVM
{
public int TotalAccounts => Storage.MaFiles.Count;
public int TotalProxies => ProxyStorage.Proxies.Count;
public int AssignedProxies => Storage.MaFiles.Count(m => m.Proxy != null);
[ObservableProperty] private string? _assignmentInput;
[ObservableProperty] private string? _assignmentTip;
/// <summary>
/// true = assign a random free proxy to unspecified accounts; false = remove proxy from unspecified accounts.
/// </summary>
[ObservableProperty] private bool _unspecifiedAssignRandom = true;
public void RefreshAssignmentCounters()
{
OnPropertyChanged(nameof(TotalAccounts));
OnPropertyChanged(nameof(TotalProxies));
OnPropertyChanged(nameof(AssignedProxies));
}
[RelayCommand]
private async Task ApplyAssignment()
{
AssignmentTip = null;
var input = AssignmentInput;
if (string.IsNullOrWhiteSpace(input)) return;
var lines = input.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var mafs = Storage.MaFiles.ToList();
var defaultProxyId = GetDefaultProxyId();
var success = 0;
var errors = 0;
var notFound = 0;
foreach (var rawLine in lines)
{
var line = rawLine.Trim();
if (string.IsNullOrWhiteSpace(line)) continue;
try
{
string login;
// resolvedProxy: null means unspecified (apply toggle behaviour)
MaProxy? resolvedProxy;
var colonIdx = line.IndexOf(':');
if (colonIdx >= 0)
{
login = line[..colonIdx].Trim();
var proxyPart = line[(colonIdx + 1)..].Trim();
if (string.IsNullOrEmpty(proxyPart))
{
resolvedProxy = null; // unspecified
}
else if (int.TryParse(proxyPart, out var parsedId))
{
// Mode: login:ID — proxy ID
if (!ProxyStorage.Proxies.TryGetValue(parsedId, out var byId))
{
errors++;
continue;
}
resolvedProxy = new MaProxy(parsedId, byId);
}
else if (ProxyStorage.DefaultScheme.TryParse(proxyPart, out var parsedProxy))
{
// Mode: login:proxy_string — find existing or add
resolvedProxy = FindOrAddProxy(parsedProxy);
}
else
{
errors++;
continue;
}
}
else
{
login = line;
resolvedProxy = null; // unspecified
}
if (string.IsNullOrWhiteSpace(login))
{
errors++;
continue;
}
var maf = FindMafile(mafs, login);
if (maf == null)
{
notFound++;
continue;
}
var newProxy = resolvedProxy ?? (UnspecifiedAssignRandom
? BuildRandomFreeProxy(defaultProxyId)
: null);
var oldProxyId = maf.Proxy?.Id;
maf.Proxy = newProxy;
ProxyAssignmentCache.UpdateAssignment(maf.AccountName, oldProxyId, newProxy?.Id);
await Storage.UpdateMafileAsync(maf);
success++;
}
catch
{
errors++;
}
}
AnyChanges = true;
RefreshAssignmentCounters();
AssignmentTip = BuildAssignmentTip(success, notFound, errors);
}
[RelayCommand]
private void ClearAssignmentTip()
{
AssignmentTip = null;
}
/// <summary>
/// Finds a proxy equal to <paramref name="proxyData" /> in storage.
/// If not found, adds it and returns the new entry.
/// </summary>
private static MaProxy FindOrAddProxy(ProxyData proxyData)
{
var existing = ProxyStorage.Proxies.FirstOrDefault(kvp => kvp.Value.Equals(proxyData));
if (existing.Value != null)
return new MaProxy(existing.Key, existing.Value);
ProxyStorage.SetProxy(null, proxyData);
// Fetch the entry we just added (SetProxy guarantees it was stored)
var added = ProxyStorage.Proxies.First(kvp => kvp.Value.Equals(proxyData));
return new MaProxy(added.Key, added.Value);
}
private static MaProxy? BuildRandomFreeProxy(int? defaultProxyId)
{
var free = ProxyAssignmentCache.GetRandomFreeProxy(ProxyStorage.Proxies, defaultProxyId);
return free.HasValue ? new MaProxy(free.Value.Key, free.Value.Value) : null;
}
private static int? GetDefaultProxyId()
{
if (MaClient.DefaultProxy == null) return null;
var defKvp = ProxyStorage.Proxies.FirstOrDefault(kvp => kvp.Value.Equals(MaClient.DefaultProxy));
return defKvp.Value != null ? defKvp.Key : null;
}
private static Mafile? FindMafile(List<Mafile> mafs, string login)
{
SteamId64? steamId = null;
if (SteamId64.TryParse(login, out var id64))
steamId = id64;
if (steamId != null)
return mafs.FirstOrDefault(m => m.SteamId == steamId || m.AccountName == login);
return mafs.FirstOrDefault(m =>
string.Equals(m.AccountName, login, StringComparison.OrdinalIgnoreCase));
}
private static string BuildAssignmentTip(int success, int notFound, int errors)
{
if (success > 0 && errors == 0 && notFound == 0)
return string.Format(GetLocalizationOrDefault("AssignmentSuccess"), success);
return string.Format(GetLocalizationOrDefault("AssignmentPartialSuccess"), success, notFound, errors);
}
}
+4 -4
View File
@@ -168,10 +168,10 @@ public partial class SettingsVM : ObservableObject
}
}
public bool LegacyMode
public bool ConfirmMafileImport
{
get => Settings.LegacyMode;
set => Settings.LegacyMode = value;
get => Settings.ConfirmMafileImport;
set => Settings.ConfirmMafileImport = value;
}
public bool AllowAutoUpdate
@@ -249,4 +249,4 @@ public partial class SettingsVM : ObservableObject
}
#endregion
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
using SteamLibForked.Models.Core;
using System.Collections.Immutable;
using System.Collections.Immutable;
using SteamLibForked.Models.Core;
namespace SteamLib.Core;
@@ -1,8 +1,8 @@
using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using SteamLib.Core;
using SteamLibForked.Abstractions;
using SteamLibForked.Models.Core;
using System.Diagnostics.CodeAnalysis;
namespace SteamLibForked.Models.Session;
@@ -42,6 +42,7 @@ public class MobileSessionData : SessionData, IMobileSessionData
// See: SteamAuthToken 'TODO' for more details
return MobileToken ?? base.GetToken(domain);
}
return base.GetToken(domain);
}
@@ -52,7 +53,7 @@ public class MobileSessionData : SessionData, IMobileSessionData
if (token.Type != SteamAccessTokenType.Mobile)
throw new ArgumentException("Token must be of type MobileAccess", nameof(token))
{
Data = { { "ActualType", token.Type } }
Data = {{"ActualType", token.Type}}
};
MobileToken = token;
@@ -65,6 +66,6 @@ public class MobileSessionData : SessionData, IMobileSessionData
public override MobileSessionData Clone()
{
return (MobileSessionData)((ISessionData)this).Clone();
return (MobileSessionData) ((ISessionData) this).Clone();
}
}
@@ -4,14 +4,11 @@ namespace SteamLib.Utility.MafileSerialization;
internal class LegacyMafile
{
[JsonProperty("shared_secret")]
public string SharedSecret { get; set; } = null!;
[JsonProperty("shared_secret")] public string SharedSecret { get; set; } = null!;
[JsonProperty("identity_secret")]
public string IdentitySecret { get; set; } = null!;
[JsonProperty("identity_secret")] public string IdentitySecret { get; set; } = null!;
[JsonProperty("device_id")]
public string DeviceId { get; set; } = null!;
[JsonProperty("device_id")] public string DeviceId { get; set; } = null!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = null!;
[JsonProperty("account_name")] public string AccountName { get; set; } = null!;