mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-25 22:31:42 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7348bc2673 | |||
| 3a9d929c9a | |||
| 25818997af | |||
| 537d5d907f | |||
| 0e90f1826b | |||
| b5d3b4a8af | |||
| 30cf049f23 | |||
| ce55005d44 |
@@ -1,74 +0,0 @@
|
||||
# NebulaAuth
|
||||
|
||||
## Описание
|
||||
|
||||
<h3 align="center" style="margin-bottom:0">
|
||||
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Скачать последнюю версию</a>
|
||||
</h3>
|
||||
|
||||
<h3 align="center">NebulaAuth — это приложение для эмуляции действий из мобильного приложения Steam. Которая заменяет ваш смартфон при работе в Steam.</h3>
|
||||
<h4 align="center"><a href="https://t.me/nebulaauth">Официальная группа в Telegram</a></h4>
|
||||
|
||||
|
||||
## Основные преимущества
|
||||
|
||||
- **Локализация на трёх языках**: английском, русском и украинском.
|
||||
- **Полная функциональность Steam Desktop Authenticator**, переосмысление [старого приложения](https://github.com/Jessecar96/SteamDesktopAuthenticator)
|
||||
- **Поддержка прокси** во всех процессах работы с аккаунтом.
|
||||
- **Группировка мафайлов** для продвинутого контроля.
|
||||
- **Автоматическое подтверждение трейдов/действий на ТП** для экономии времени.
|
||||
- **Массовый импорт мафайлов** с помощью Drag'n'Drop или CTRL+V для удобства.
|
||||
- **Настройка внешнего вида** для персонализации интерфейса.
|
||||
- **Возможность подтвердить вход в учетную запись без ввода кода** для облегчения доступа.
|
||||
- **Автообновление** программы для использования новейших функций.
|
||||
- **Автоматический повторный вход в случае проблем с сессией** для непрерывной работы.
|
||||
- **Интуитивно понятный интерфейс** с подсказками и удобствами
|
||||
- **Постоянная поддержка** кода приложения и других функций.
|
||||
|
||||
## Установка
|
||||
|
||||
1. Если приложение не запускается, необходимо установить [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||
2. [Скачать программу из релизов этого репозитория на Github](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest)
|
||||
* *Для сохранности ваших данных скачивайте приложение только отсюда*
|
||||
4. Распакуйте ZIP-файл в любую папку.
|
||||
5. Запустите файл **NebulaAuth.exe**.
|
||||
|
||||
## Использование
|
||||
|
||||

|
||||
|
||||
|
||||
1. Панель управления.
|
||||
- управление файлами и настройки
|
||||
- управление аккаунтом (вход, привязка, отвязка)
|
||||
- группировка
|
||||
- выбор прокси
|
||||
- индикатор с подсказкой об используемом прокси (горит либо желтым, либо красным, при наведении отобразит дополнительную информацию)
|
||||
- таймеры для автоматического подтверждения трейдов/продаж на торговой площадке
|
||||
- интервал таймера подтверждений (в секундах)
|
||||
2. Список ваших аккаунтов
|
||||
3. Код подтверждения входа (нажмите, чтобы скопировать)
|
||||
4. Главное окно подтверждений
|
||||
5. Поиск по логину или SteamID (7xxxxxxxxxxxxx)
|
||||
6. Подтвердить вход на другогом устройстве.
|
||||
7. Гиперссылка на официальную страницу приложения с указанием авторства.
|
||||
|
||||
## Настройки
|
||||

|
||||
|
||||
|
||||
1. Режим фона. Используйте, если вы хотите отключить фон или установить собственный (поместите файл «Background.png» в папку приложения)
|
||||
2. Язык приложения
|
||||
3. Отключить таймеры при переключении между аккаунтами
|
||||
4. Скрывать в трей при сворачивании
|
||||
5. Индикатор с цветом. Маленький кружочек на значке панели задач с произвольным цветом. Полезно при использовании нескольких окон.
|
||||
6. Пользовательский цвет приложения.
|
||||
7. Текущий пароль шифрования. Если установлено, вы можете сохранять зашифрованные пароли в mafile, чтобы облегчить повторный вход в систему при проблемах с сессией. (Не рекомендуется)
|
||||
8. Режим устаревших файлов. Режим совместимости Mafile с другими клиентами (SDA и т.д.). Если установлено, приложение будет сохранять файлы в старом стандартном формате (по умолчанию: включено).
|
||||
9. Разрешить автообновление без подтверждения
|
||||
|
||||
|
||||
|
||||
## [Лицензия](/LICENSE.md)
|
||||
|
||||
Коммерческое использование запрещено. При распространении измененного кода необходимо указывать оригинальное авторство.
|
||||
@@ -1,71 +0,0 @@
|
||||
# NebulaAuth
|
||||
|
||||
## Опис
|
||||
|
||||
<h3 align="center" style="margin-bottom:0">
|
||||
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Завантажити останню версію</a>
|
||||
</h3>
|
||||
|
||||
<h3 align="center">NebulaAuth - це програма для емуляції дій з мобільного додатку Steam. Яка замінює ваш смартфон під час роботи в Steam.</h3>
|
||||
<h4 align="center"><a href="https://t.me/nebulaauth">Офіційна група в Telegram</a></h4>
|
||||
|
||||
## Основні переваги
|
||||
|
||||
- **Локалізація трьома мовами**: англійською, російською та українською.
|
||||
- **Повна функціональність Steam Desktop Authenticator** переосмислення [старої програми](https://github.com/Jessecar96/SteamDesktopAuthenticator)
|
||||
- **Підтримка проксі** у всіх процесах роботи з обліковим записом.
|
||||
- **Угруповання мафайлів** для просунутого контролю.
|
||||
- **Автоматичне підтвердження трейдів/дій на маркеті** для економії часу.
|
||||
- **Масовий імпорт мафайлів** за допомогою Drag'n'Drop або CTRL+V для зручності.
|
||||
- **Налаштування зовнішнього вигляду** для персоналізації інтерфейсу.
|
||||
- **Можливість підтвердити вхід до облікового запису без введення коду** для полегшення доступу.
|
||||
- **Автооновлення** програми для використання новітніх функцій.
|
||||
- **Автоматичний повторний вхід у разі проблем із сесією** для безперервної роботи.
|
||||
- **Інтуїтивно зрозумілий інтерфейс** з підказками та зручностями
|
||||
- **Постійна підтримка** коду програми та інших функцій.
|
||||
|
||||
## Монтаж
|
||||
|
||||
1. Якщо програма не запускається, необхідно встановити [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||
2. [Завантажити програму з релізів цього репозиторію на Github](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest)
|
||||
* *Для збереження ваших даних завантажуйте програму тільки звідси*
|
||||
4. Розпакуйте ZIP-файл у будь-яку папку.
|
||||
5. Запустіть файл **NebulaAuth.exe**.
|
||||
|
||||
## Використання
|
||||
|
||||

|
||||
|
||||
1. Панель керування.
|
||||
- керування файлами та налаштування
|
||||
- керування акаунтом (вхід, прив'язка, відв'язування)
|
||||
- угруповання
|
||||
- вибір проксі
|
||||
- індикатор з підказкою про проксі (світиться або жовтим, або червоним, при наведенні відобразить додаткову інформацію)
|
||||
- таймери для автоматичного підтвердження трейдів/продажів на маркеті
|
||||
- інтервал таймера підтверджень (у секундах)
|
||||
2. Список ваших облікових записів
|
||||
3. Код підтвердження входу (натисніть, щоб скопіювати)
|
||||
4. Головне вікно підтвердження
|
||||
5. Пошук за логіном або SteamID (7xxxxxxxxxxxxx)
|
||||
6. Підтвердити вхід на іншому пристрою.
|
||||
7. Гіперпосилання на офіційну сторінку додатку із зазначенням авторства.
|
||||
|
||||
## Налаштування
|
||||

|
||||
|
||||
1. Режим фону. Використовуйте, якщо ви хочете вимкнути фон або встановити власний (помістіть файл "Background.png" у папку програми)
|
||||
2. Мова локалізації
|
||||
3. Вимкнути таймери під час перемикання між обліковими записами
|
||||
4. Приховувати у трей при згортанні
|
||||
5. Індикатор із кольором. Маленький кружечок на піктограмі панелі завдань з довільним кольором. Корисно при використанні кількох вікон.
|
||||
6. Власний колір програми.
|
||||
7. Поточний пароль шифрування. Якщо встановлено, ви можете зберігати зашифровані паролі в mafile, щоб полегшити повторний вхід до системи у разі проблеми з сесією. (Не рекомендується)
|
||||
8. Режим застарілих файлів. Режим сумісності Mafile з іншими клієнтами (SDA тощо). Якщо встановлено, програма зберігатиме файли у старому стандартному форматі (за замовчуванням: увімкнено).
|
||||
9. Дозволити автооновлення без підтвердження
|
||||
|
||||
|
||||
|
||||
## [Ліцензія](/LICENSE.md)
|
||||
|
||||
Комерційне використання заборонено. У разі поширення зміненого коду необхідно вказувати оригінальне авторство.
|
||||
@@ -1,12 +1,5 @@
|
||||
# NebulaAuth
|
||||
|
||||
* [Русский](README-RU.md)
|
||||
* [Українська](README-UA.md)
|
||||
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
<h3 align="center" style="margin-bottom:0">
|
||||
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Download latest version</a>
|
||||
</h3>
|
||||
@@ -19,24 +12,33 @@ NebulaAuth is an application for emulating actions from the Steam Mobile App, re
|
||||
<sub>NebulaAuth is an independent project and is not affiliated with Valve or Steam.</sub>
|
||||
</p>
|
||||
<h4 align="center"><a href="https://t.me/nebulaauth">Official Telegram Group</a></h4>
|
||||
<h4 align="center"><a href="https://achiefy-project.gitbook.io/nebulaauth">Documentation</a></h4>
|
||||
|
||||
|
||||
## Main advantages
|
||||
|
||||
- **Localization in three languages**: English, Russian and Ukrainian.
|
||||
<p align="center">
|
||||
<img src="misc/main_window.png" width="600"/>
|
||||
</p>
|
||||
|
||||
- **Localization in six languages**: English, Russian, Ukrainian, Spanish, Turkish and Kazakh.
|
||||
- **Full functionality of Steam Desktop Authenticator** reimagining [old app](https://github.com/Jessecar96/SteamDesktopAuthenticator)
|
||||
- **Proxy support** in all account work processes.
|
||||
- **Mafile grouping** for improved management.
|
||||
- **Automatic confirmations of trades/market actions** to save time.
|
||||
- **Bulk import of .mafiles** via Drag'n'Drop or CTRL+V for convenience.
|
||||
- **Bulk import of .mafiles** via Drag'n'Drop or Ctrl+V, including SDA-encrypted mafiles with automatic manifest detection.
|
||||
- **Design customization** to personalize the interface.
|
||||
- **Ability to confirm account login without entering a code** for easier access.
|
||||
- **Auto-update** program to use the latest features.
|
||||
- **Auto-update** with SHA256 checksum verification, changelog viewer and flexible update options.
|
||||
- **Automatic relogin in case of problems with the session** for continuous operation.
|
||||
- **Intuitive interface** with tips and conveniences
|
||||
- **Continious support** of application code and other features.
|
||||
- **Intuitive interface** with tips and conveniences.
|
||||
- **Continuous support** of application code and other features.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find the documentation for the application [here](https://achiefy-project.gitbook.io/nebulaauth). Currently, only the Russian version is available, but the English version will be released soon.
|
||||
|
||||
## Installation
|
||||
|
||||
1. If the application does not start, you need to install [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||
@@ -45,40 +47,11 @@ NebulaAuth is an application for emulating actions from the Steam Mobile App, re
|
||||
4. Unpack the .zip file to any folder
|
||||
5. Run the file **NebulaAuth.exe**
|
||||
|
||||
## Usage
|
||||
|
||||

|
||||
|
||||
1. Control panel.
|
||||
- file management and settings
|
||||
- account management (login, linking, unlinking)
|
||||
- grouping
|
||||
- proxy selection
|
||||
- an indicator with a hint about the proxy used (lit either yellow or red, when hovered it will display additional information)
|
||||
- timers for automatic confirmation of trade offers/sale offers on the marketplace
|
||||
- confirmation timer interval (in seconds)
|
||||
2. List of your accounts
|
||||
3. Login confirmation code (click to copy)
|
||||
4. Main confirmation window
|
||||
5. Search by login or SteamID (7xxxxxxxxxxxxx)
|
||||
6. Confirm login on another device
|
||||
7. Hyperlink to the official application page with attribution
|
||||
## Project Ownership
|
||||
|
||||
## Settings
|
||||

|
||||
NebulaAuth is the original project by Achiefy™.
|
||||
|
||||
1. Background mode. Use it if you want to disable default or set custom background of application (put file 'Background.png' to your application folder)
|
||||
2. Localization language
|
||||
3. Disable timers when switching between accounts
|
||||
4. Hide to tray on minimize
|
||||
5. Indicator with color. Small ellipse on your task-bar icon with custom color. Useful when using multiple windows
|
||||
6. Custom background color of application
|
||||
7. Current encryption password. If set you can save encrypted passwords to mafile to help re-login on session troubles. (Not recommended)
|
||||
8. Legacy mafile mode. Mafile compability mode for another applications (SDA and etc). If checked application will save mafiles with old standart format (Default: checked)
|
||||
9. Allow auto-update without confirmation
|
||||
|
||||
|
||||
Please keep attribution and do not present modified versions as official.
|
||||
|
||||
## [License](/LICENSE.md)
|
||||
|
||||
Commercial use prohibited. When redistributing modified code, you must indicate the original authorship.
|
||||
AGPL-3.0 — see [LICENSE](/LICENSE).
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<item>
|
||||
<version>1.5.6.0</version>
|
||||
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.5.6/NebulaAuth.1.5.6.zip</url>
|
||||
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.5.6.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 бойынша сәрсенбі түні) пайда болатын және авторастау таймерлерін қажетсіз өшіретін қателерді елемеу"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
|
||||
SetStatus(newStatus);
|
||||
|
||||
ClientHandler.CookieContainer.ClearAllCookies();
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookies(sessionData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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='
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='
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
IP:PORT:USER:PASS
PROTOCOL://IP:PORT:USER:PASS
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
IP:PORT:USER:PASS
PROTOCOL://IP:PORT:USER:PASS
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
login:IP:PORT:USER:PASS
login:socks5://USER:PASS@IP:PORT
login:ID
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,33 +54,20 @@ public static class AdmissionHelper
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AddRefreshToken(container, sessionData.RefreshToken);
|
||||
container.SetSteamRefreshToken(sessionData.RefreshToken);
|
||||
|
||||
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, sessionData.SessionId, "/"));
|
||||
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage, "/"));
|
||||
TransferCommunityCookies(container);
|
||||
foreach (var domain in SteamDomains.AuthDomains)
|
||||
foreach (var domain in SteamDomains.WebDomains)
|
||||
{
|
||||
var token = sessionData.GetToken(domain);
|
||||
if (token == null) continue;
|
||||
AddTokenCookie(container, token.Value);
|
||||
container.SetSteamAccessToken(token.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetDomainCookie(this CookieContainer container, SteamDomain domain, SteamAuthToken token)
|
||||
{
|
||||
var uri = SteamDomains.GetDomainUri(domain);
|
||||
foreach (var cookie in container.GetCookies(uri)
|
||||
.Where(c => !c.Expired && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
|
||||
{
|
||||
cookie.Expired = true;
|
||||
}
|
||||
|
||||
AddTokenCookie(container, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear and set new session
|
||||
/// </summary>
|
||||
@@ -95,70 +82,22 @@ public static class AdmissionHelper
|
||||
return;
|
||||
}
|
||||
|
||||
AddRefreshToken(container, mobileSession.RefreshToken);
|
||||
container.SetSteamRefreshToken(mobileSession.RefreshToken);
|
||||
|
||||
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||
container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString()));
|
||||
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId));
|
||||
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
|
||||
TransferCommunityCookies(container);
|
||||
foreach (var domain in SteamDomains.AuthDomains)
|
||||
foreach (var domain in SteamDomains.WebDomains)
|
||||
{
|
||||
var token = mobileSession.GetToken(domain);
|
||||
if (token == null) continue;
|
||||
AddTokenCookie(container, token.Value);
|
||||
var domainUri = SteamDomains.GetDomainUri(domain);
|
||||
container.SetSteamAccessTokenUnsafe(token.Value, domainUri);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear and set new session. Not recommended. Uses <see cref="IMobileSessionData.GetMobileToken()" /> for domain
|
||||
/// <see cref="SteamDomain.Community" /> instead of its own cookie. It's okay to use it only for confirmations. But
|
||||
/// Market, Trading and other pages won't be authorized
|
||||
/// </summary>
|
||||
public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container,
|
||||
IMobileSessionData? mobileSession,
|
||||
string setLanguage = "english")
|
||||
{
|
||||
container.ClearSteamCookies(setLanguage);
|
||||
container.AddMinimalMobileCookies();
|
||||
|
||||
if (mobileSession == null)
|
||||
{
|
||||
TransferCommunityCookies(container);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AddRefreshToken(container, mobileSession.RefreshToken);
|
||||
|
||||
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||
container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString()));
|
||||
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId));
|
||||
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
|
||||
TransferCommunityCookies(container);
|
||||
|
||||
var domainCookieSet = false;
|
||||
foreach (var domain in SteamDomains.AllDomains)
|
||||
{
|
||||
var token = mobileSession.GetToken(domain);
|
||||
if (token == null || token.Value.IsExpired) continue;
|
||||
if (domain == SteamDomain.Community)
|
||||
domainCookieSet = true;
|
||||
AddTokenCookie(container, token.Value);
|
||||
}
|
||||
|
||||
var mobileToken = mobileSession.GetMobileToken();
|
||||
if (!domainCookieSet && mobileToken is {IsExpired: false})
|
||||
{
|
||||
var domain = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, mobileToken.Value.SignedToken)
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
Expires = mobileToken.Value.Expires.ToLocalDateTime()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddMinimalMobileCookies(this CookieContainer container)
|
||||
{
|
||||
@@ -229,27 +168,93 @@ public static class AdmissionHelper
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddRefreshToken(CookieContainer container, SteamAuthToken token)
|
||||
/// <summary>
|
||||
/// Sets a Steam refresh token as a cookie to the specified cookie container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The added cookie will have its expiration set according to the token's expiration time. This
|
||||
/// method is typically used to enable authenticated requests to Steam services that require a refresh
|
||||
/// token.
|
||||
/// </remarks>
|
||||
/// <param name="container">The cookie container to which the Steam refresh token cookie will be added. Cannot be null.</param>
|
||||
/// <param name="token">
|
||||
/// The Steam authentication token to add as a refresh cookie. Must be of type Refresh or
|
||||
/// MobileRefresh.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">Thrown if the token is not of type Refresh or MobileRefresh.</exception>
|
||||
public static void SetSteamRefreshToken(this CookieContainer container, SteamAuthToken token)
|
||||
{
|
||||
if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh))
|
||||
throw new ArgumentException(
|
||||
$"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}",
|
||||
nameof(token));
|
||||
var refreshToken = token.SignedToken;
|
||||
container.Add(SteamLoginUri, new Cookie(REFRESH_COOKIE_NAME, refreshToken)
|
||||
SetSteamRefreshTokenUnsafe(container, token, SteamLoginUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a Steam refresh token as a cookie to the specified cookie container for the given domain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not perform validation on the input parameters. Callers must ensure that the
|
||||
/// provided values are valid and appropriate for use.
|
||||
/// </remarks>
|
||||
/// <param name="container">The cookie container to which the refresh token cookie will be added. Cannot be null.</param>
|
||||
/// <param name="token">
|
||||
/// The Steam authentication token containing the signed token value and expiration information. Cannot
|
||||
/// be null.
|
||||
/// </param>
|
||||
/// <param name="domainUri">The URI of the domain for which the refresh token cookie should be set. Cannot be null.</param>
|
||||
public static void SetSteamRefreshTokenUnsafe(CookieContainer container, SteamAuthToken token, Uri domainUri)
|
||||
{
|
||||
container.Add(domainUri, new Cookie(REFRESH_COOKIE_NAME, token.SignedToken)
|
||||
{
|
||||
Expires = token.Expires.ToLocalDateTime()
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddTokenCookie(CookieContainer container, SteamAuthToken token)
|
||||
/// <summary>
|
||||
/// Sets a Steam access token to the specified cookie container for use with Steam web requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is intended for standard web access tokens bound to a specific Steam web domain.
|
||||
/// Mobile tokens are intentionally rejected, since their audiences are capability-based rather
|
||||
/// than domain-based and may grant access to multiple web domains.
|
||||
/// </remarks>
|
||||
/// <param name="container">The cookie container to which the Steam access token will be added. Cannot be null.</param>
|
||||
/// <param name="token">
|
||||
/// The Steam access token to add. Must be of type AccessToken and associated with a valid Steam
|
||||
/// domain.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">Thrown if the token is not of type AccessToken.</exception>
|
||||
public static void SetSteamAccessToken(this CookieContainer container, SteamAuthToken token)
|
||||
{
|
||||
if (token.Type == SteamAccessTokenType.Mobile)
|
||||
throw new ArgumentException(
|
||||
$"Mobile access tokens cannot be added using this method. Use SetSteamAccessTokenUnsafe instead. Provided token has type: {token.Type}",
|
||||
nameof(token));
|
||||
if (token.Type is not SteamAccessTokenType.AccessToken)
|
||||
throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}",
|
||||
nameof(token));
|
||||
|
||||
var domain = SteamDomains.GetDomainUri(token.Domain);
|
||||
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken)
|
||||
var domainUri = SteamDomains.GetDomainUri(token.Domain);
|
||||
container.SetSteamAccessTokenUnsafe(token, domainUri);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets a Steam access token as a secure, HTTP-only cookie to the specified cookie container for the given domain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not perform validation on the input parameters and should only be used when
|
||||
/// input values are trusted. The added cookie is marked as secure and HTTP-only, and its expiration is set
|
||||
/// according to the token's expiration time.
|
||||
/// </remarks>
|
||||
/// <param name="container">The cookie container to which the Steam access token cookie will be added. Cannot be null.</param>
|
||||
/// <param name="token">The Steam access token to add as a cookie. Must contain a valid signed token and expiration.</param>
|
||||
/// <param name="domainUri">The URI of the domain for which the cookie will be set. Cannot be null.</param>
|
||||
public static void SetSteamAccessTokenUnsafe(this CookieContainer container, SteamAuthToken token, Uri domainUri)
|
||||
{
|
||||
container.Add(domainUri, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken)
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
@@ -257,7 +262,6 @@ public static class AdmissionHelper
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static bool IsSteamCookie(Cookie cookie)
|
||||
{
|
||||
return cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") ||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Immutable;
|
||||
using SteamLibForked.Models.Core;
|
||||
|
||||
namespace SteamLib.Core;
|
||||
@@ -6,44 +6,38 @@ namespace SteamLib.Core;
|
||||
public static class SteamDomains
|
||||
{
|
||||
public static IReadOnlyDictionary<SteamDomain, string> Domains { get; } =
|
||||
new ReadOnlyDictionary<SteamDomain, string>(
|
||||
new Dictionary<SteamDomain, string>
|
||||
{
|
||||
{SteamDomain.Community, SteamConstants.STEAM_COMMUNITY},
|
||||
{SteamDomain.Store, SteamConstants.STEAM_STORE},
|
||||
{SteamDomain.Help, SteamConstants.STEAM_HELP},
|
||||
{SteamDomain.TV, SteamConstants.STEAM_TV},
|
||||
{SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT},
|
||||
{SteamDomain.Login, SteamConstants.STEAM_LOGIN},
|
||||
{SteamDomain.API, SteamConstants.STEAM_API}
|
||||
});
|
||||
new Dictionary<SteamDomain, string>
|
||||
{
|
||||
{SteamDomain.Community, SteamConstants.STEAM_COMMUNITY},
|
||||
{SteamDomain.Store, SteamConstants.STEAM_STORE},
|
||||
{SteamDomain.Help, SteamConstants.STEAM_HELP},
|
||||
{SteamDomain.TV, SteamConstants.STEAM_TV},
|
||||
{SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT},
|
||||
{SteamDomain.Login, SteamConstants.STEAM_LOGIN},
|
||||
{SteamDomain.API, SteamConstants.STEAM_API}
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
public static IReadOnlyDictionary<SteamDomain, Uri> DomainUris { get; }
|
||||
= new ReadOnlyDictionary<SteamDomain, Uri>(
|
||||
Domains.ToDictionary(x => x.Key, x => new Uri(x.Value))
|
||||
);
|
||||
= Domains
|
||||
.ToDictionary(x => x.Key, x => new Uri(x.Value))
|
||||
.ToImmutableDictionary();
|
||||
|
||||
|
||||
public static IEnumerable<SteamDomain> AllDomains { get; } =
|
||||
[
|
||||
SteamDomain.Community,
|
||||
SteamDomain.Store,
|
||||
SteamDomain.Help,
|
||||
SteamDomain.TV,
|
||||
SteamDomain.Checkout,
|
||||
SteamDomain.Login,
|
||||
SteamDomain.API
|
||||
];
|
||||
|
||||
public static IEnumerable<SteamDomain> AuthDomains { get; } =
|
||||
[
|
||||
SteamDomain.Community,
|
||||
SteamDomain.Store,
|
||||
SteamDomain.Help,
|
||||
SteamDomain.TV,
|
||||
SteamDomain.Checkout
|
||||
];
|
||||
/// <summary>
|
||||
/// All known public Steam domains.
|
||||
/// </summary>
|
||||
public static IEnumerable<SteamDomain> AllDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
|
||||
SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout, SteamDomain.Login, SteamDomain.API);
|
||||
|
||||
/// <summary>
|
||||
/// Steam web domains that participate in the standard login authorization flow.
|
||||
/// <para>
|
||||
/// These domains use the <c>web:*</c> audience format and receive
|
||||
/// authentication cookies/tokens during session initialization.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static IReadOnlySet<SteamDomain> WebDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
|
||||
SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout);
|
||||
|
||||
public static Uri GetDomainUri(SteamDomain domain)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using SteamLib.Core;
|
||||
using SteamLibForked.Abstractions;
|
||||
using SteamLibForked.Models.Core;
|
||||
|
||||
@@ -7,7 +8,7 @@ namespace SteamLibForked.Models.Session;
|
||||
|
||||
//WARNING: Any changes here should be reflected in MafileSerializer.cs
|
||||
|
||||
public sealed class MobileSessionData : SessionData, IMobileSessionData
|
||||
public class MobileSessionData : SessionData, IMobileSessionData
|
||||
{
|
||||
public SteamAuthToken? MobileToken { get; private set; }
|
||||
|
||||
@@ -31,9 +32,23 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
|
||||
return MobileToken;
|
||||
}
|
||||
|
||||
public override SteamAuthToken? GetToken(SteamDomain domain)
|
||||
{
|
||||
var isWeb = SteamDomains.WebDomains.Contains(domain);
|
||||
if (isWeb)
|
||||
{
|
||||
// Mobile-issued tokens usually contain the "web" audience,
|
||||
// so we assume they can also be used for all web:* domains.
|
||||
// See: SteamAuthToken 'TODO' for more details
|
||||
return MobileToken ?? base.GetToken(domain);
|
||||
}
|
||||
|
||||
return base.GetToken(domain);
|
||||
}
|
||||
|
||||
|
||||
[MemberNotNull(nameof(MobileToken))]
|
||||
public void SetMobileToken(SteamAuthToken token)
|
||||
public virtual void SetMobileToken(SteamAuthToken token)
|
||||
{
|
||||
if (token.Type != SteamAccessTokenType.Mobile)
|
||||
throw new ArgumentException("Token must be of type MobileAccess", nameof(token))
|
||||
@@ -44,13 +59,13 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
|
||||
MobileToken = token;
|
||||
}
|
||||
|
||||
public override MobileSessionData Clone()
|
||||
{
|
||||
return (MobileSessionData) ((ISessionData) this).Clone();
|
||||
}
|
||||
|
||||
object ICloneable.Clone()
|
||||
{
|
||||
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens);
|
||||
}
|
||||
|
||||
public override MobileSessionData 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!;
|
||||
|
||||
@@ -31,7 +31,7 @@ public static class ClientBuilder
|
||||
}
|
||||
else
|
||||
{
|
||||
container.SetSteamMobileCookiesWithMobileToken(sessionData);
|
||||
container.SetSteamMobileCookies(sessionData);
|
||||
}
|
||||
|
||||
//Nebula tweak:
|
||||
|
||||
Reference in New Issue
Block a user