Compare commits

..

26 Commits

Author SHA1 Message Date
github-actions 5de26381bb chore(release): 1.8.1 2026-01-25 17:25:47 +00:00
Achies 16fd4992f8 Merge pull request #14 from achiez/1.8.1
Release 1.8.1 → Merge into master
2026-01-25 19:25:39 +02:00
achiez 7835a53717 Prepare 1.8.1
- Added changelog/1.8.1.html with detailed update notes and links
- Improved MAAC auto-confirmation logic: now uses IsReady() for smarter account readiness checks and retries
- Added GetReadyAccounts() to MultiAccountAutoConfirmer for flexible account selection
- Fixed localization lookup in GetPortableMaClientStatus
- Set PortableMaClient initial status to Ok instead of test error
2026-01-25 19:21:50 +02:00
achiez f15632f2d0 Refactored MAAC error/status handling and UI feedback
- Introduced centralized MAACRequestHandler for error tracking, retries, and status escalation (Ok/Warning/Error) per account
- PortableMaClient now uses new Status property (PortableMaClientStatus) instead of IsError; error history tracked in PortableMaClientErrorData
- All MAAC requests now routed through MAACRequestHandler, providing unified error handling and retry logic
- MultiAccountAutoConfirmer now filters accounts by Status.StatusType == Ok
- Updated MainWindow UI: account name color reflects status (Success/Warning/Error); added "Unattach proxy" to context menu
- Removed obsolete PortableMaClientStatusToColorConverter and related bindings
- Settings: removed IgnorePatchTuesdayErrors, added MaacErrorThreshold and MaacRetryInterval for error escalation control
- Refactored SessionHandler: split into logic, API, and helpers; improved exception propagation and session management
- AdmissionHelper methods now handle null sessions and always transfer community cookies if session is missing
- Mafile export: always preserves SteamId, even if session data is not exported
- ViewModel: RemoveProxy command now parameterized and only enabled if Mafile has a proxy
- Enabled concurrent log writing in NLog config
- Minor code cleanups and improved exception signatures
2026-01-25 18:48:11 +02:00
achiez b56c0e578d Fix checkbox margin accordingly to new MaterialDesignInXaml update 2026-01-19 21:36:42 +02:00
achiez 969590d9f2 Add MafileExport feature and Perform Resharper Cleanup 2026-01-19 21:28:44 +02:00
achiez 3e87a0d50e Improve localization structure and add missing translations
Thanks to @77155331 for Chinese translation (Issue #11)
Thanks to @klNuno for French localization (PR #13)
2026-01-02 22:43:24 +02:00
achiez 690c98527f Update mafiles removal logic and changelog URL generation
- Fixed crash: MaFiles collection in MainVM is now a new ObservableCollection. Manual removal from collection is omitted
- Improved logic for moving mafiles to "removed": file extension is now always correct and name conflicts are handled more reliably
- Github workflow: Changelog URL in update.xml now uses repository name only for correct links
- On MaFiles collection change, PerformQuery is called to update search results
- Fixed parameter order in mafiles renaming result string to match localization
2026-01-02 22:09:31 +02:00
achiez 5242b0009b Refactor mafile storage: async, modular, bulk rename
- Fix crash: Updated SaveMafileAsync to properly update or add to MaFiles collection
- Fix naming logic: Added Filename reset logic to adding new mafile to prevent inconsistent and duplicating import
- Extracted mafile path and naming logic to MafilesStorageHelper for better modularity
- Refactored Storage methods to use MafilesStorageHelper
- Replaced all sync mafile save/add calls with async/await versions throughout the codebase
- Performed code cleanups and improved maintainability for mafile-related operations
2026-01-02 21:23:11 +02:00
Achies 1f14bd77e7 chore(update.xml) Fix changelog URL in update.xml 2025-11-14 22:35:39 +02:00
achiez f2b1cdfcc9 Fix MAACStorage.cs
- fixed collection was untracked if no `maac.json` file was created
- fixed incorrect change tracking for persisted accounts after reboot
2025-11-07 21:58:52 +02:00
github-actions ad1df3227b chore(release): 1.8.0 2025-11-07 14:32:28 +00:00
Achies 05f132ab1b Merge pull request #10 from achiez/1.8.0
1.8.0
2025-11-07 16:32:21 +02:00
achiez ee858921dc (chore) Prepare 1.8.0 Release 2025-11-07 16:31:37 +02:00
achiez 750292bfba Prepare for 1.8.0 release: Simplified CI/CD and localization updates
- Replaced old build-and-release.yml with a streamlined process in build-release.yml, simplifying release creation with dotnet publish, changelog conversion, and GitHub release automation.
- Added prepare-release.yml to automate release preparation, including version extraction, update.xml generation, and tagging.
- Introduced changelog for version 1.8.0 with a new HTML file (1.8.0.html) and external links to detailed updates.
- Updated NebulaAuth.sln to include the new changelog file.
- Refactored SetAccountPasswordsVM.cs to use localization for success messages and added a helper method for retrieving localized strings.
- Enhanced localization.loc.json with new strings for SetAccountPasswordsVM and improved formatting by removing duplicates.
- Improved changelog HTML structure and styles for better readability and mobile responsiveness.
- Performed minor code cleanups and formatting adjustments.
2025-11-07 16:31:01 +02:00
achiez 982bb4d0c8 [chore] Perform ReSharper cleanup 2025-11-07 10:35:10 +02:00
achiez 8e5fea54cd Added MAAC persistence support and password management features
- Introduced `MAACStorage` for managing multi-account auto-confirmers persistence
- Added `SetAccountPasswordsView` dialog for managing account passwords setup
- Enhanced `Storage` with async methods for file operations
- Added `BoolToValueConverter` for flexible XAML data bindings
- Enhanced user experience in dialogs with `IsDefault` and `IsCancel` properties
2025-11-07 10:27:16 +02:00
achiez 882d39b8f3 Refactor and enhance UI, localization, and file name handling
- Refactored file handling: added backup directories, improved mafile naming strategy, and introduced renaming functionality.
- Enhanced UI: updated `MainWindow`, `SettingsView`, and dialogs with new controls, commands, and improved layouts.
- Introduced `TextFieldDialog` for user input with localization support.
- Improved localization: added new strings for buttons, errors, and tooltips. Removed all user-observed not localized strings
- Fixed clipboard handling logic in `ClipboardHelper`.
- Added 'Create Group' button in mafile's context menu and corresponding dialog view
- Implemented `RequiresAdminAccess` check before updating program and requesting previlegies
2025-11-06 18:44:43 +02:00
achiez ffcc7405a7 Refactor and enhance proxy and mafile handling
- Introduced `Filename` property in `Mafile` for better file management.
- Refactored `Storage` methods to support `Filename` and simplify `.mafile` handling.
- Enhanced `ProxyManagerView` with new controls for protocol and credentials display.
- Implemented `HintBox` control for displaying errors and tips in UI.
- Updated `MainWindow` with improved UI elements and event handling.
- Improved command execution checks in `MainVM` for better UX.
- Added localization strings for new features like proxy display options.
- General code cleanups and formatting improvements.
2025-11-03 22:44:46 +02:00
achiez b3e7436e95 Remove FontScaleWindow.cs and residual related code 2025-10-29 11:17:25 +02:00
achiez d319fc19f9 Refine UX, fix auth crash, and revert to .NET 8.0
- Reverted target framework to .NET 8.0 for stability.
- Added autofocus to all dialog input fields (e.g. password dialogs).
- Fixed crash when relogging into accounts without Steam Guard and added proper error message.
- Unified and improved “Confirm Action” dialog design and text clarity.
- Added setting to disable ripple effect animations for better performance.
- Reduced minimum auto-confirmation timer interval from 10s to 5s.
- App now auto-selects the most appropriate language on first launch.
- Added trimming to proxy parse input
- Fixed incorrect hint about maFile binding when using login-based mode.
- Added ESC key behavior to remove focus from account search
- Minor UI polish and cleanup across dialogs.
2025-10-08 18:08:38 +03:00
achiez a56757302e Resolved issue where a "Confirmation Error" notification appeared despite the confirmation being successful 2025-07-19 00:11:42 +03:00
achiez 1ad30ba49d 1.7.3 Add automatic ack on confirming Trades accordingly to new update 2025-07-17 22:49:22 +03:00
achiez 04e2188437 bump version to 1.7.2 2025-07-10 13:35:56 +03:00
achiez b4c4f52fd1 Fix: reintroduce bug from incorrect merge, now fully resolved
Previously fixed bug was unintentionally reintroduced due to merging the wrong branch. This commit properly resolves the issue.
2025-07-10 13:31:44 +03:00
achiez f079488840 temporary return old update.xml path 2025-07-09 22:39:08 +03:00
126 changed files with 5780 additions and 1941 deletions
-108
View File
@@ -1,108 +0,0 @@
name: Build and Release
on:
push:
tags:
- '*'
jobs:
build:
if: github.ref == 'refs/heads/master'
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: "8.x"
- name: Restore dependencies
run: dotnet restore NebulaAuth.sln
- name: Build
run: dotnet build NebulaAuth.sln --configuration Release
- name: Get version from assembly
id: get-version
shell: pwsh
run: |
$content = Get-Content -Path "NebulaAuth/NebulaAuth.csproj" -Raw
$version = [regex]::Match($content, '<AssemblyVersion>(.*?)<\/AssemblyVersion>').Groups[1].Value
Write-Output "VERSION=$version" >> $env:GITHUB_ENV
- name: Check if tag exists
id: tag_exists
run: |
if (git tag -l | Select-String -Pattern "^${env:VERSION}$") {
Write-Output "Version $env:VERSION already exists."
exit 1
}
- name: Check changelog
run: |
if (-not (Test-Path "changelog/${env:VERSION}.html")) {
Write-Output "Changelog file changelog/${env:VERSION}.html does not exist."
exit 1
}
- name: Insert date into changelog
run: |
$date = Get-Date -Format "dd.MM.yyyy"
(Get-Content "changelog/${env:VERSION}.html") -replace '(?<=<div class="date">).*?(?=</div>)', $date | Set-Content "changelog/${env:VERSION}.html"
- name: Extract changelog description
id: extract_description
run: |
$description = (Get-Content "changelog/${env:VERSION}.html" | Select-String -Pattern '(?<=<div class="description">).*?(?=</div>)' | ForEach-Object { $_.Matches.Value }) -replace '<br\/>', "`n"
Write-Output "DESCRIPTION=$description" >> $env:GITHUB_ENV
- name: Create ZIP
run: |
New-Item -ItemType Directory -Path release -Force
Compress-Archive -Path NebulaAuth/bin/Release -DestinationPath release/NebulaAuth.${env:VERSION}.zip
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.VERSION }}
release_name: NebulaAuth ${{ env.VERSION }}
body: |
${{ env.DESCRIPTION }}
draft: false
prerelease: false
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: release/NebulaAuth.${{ env.VERSION }}.zip
asset_name: NebulaAuth.${{ env.VERSION }}.zip
asset_content_type: application/zip
- name: Update XML and Changelog html
shell: pwsh
run: |
$xmlContent = @"
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>${env:VERSION}.0</version>
<url>https://github.com/${env:GITHUB_REPOSITORY}/releases/download/${env:VERSION}/NebulaAuth.${env:VERSION}.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/${env:VERSION}.html</changelog>
<mandatory>false</mandatory>
</item>
"@
$xmlContent | Out-File -FilePath update.xml -Encoding UTF8 -Force
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git add changelog/${env:VERSION}.html update.xml
git commit -m "Update version to ${env:VERSION} and add changelog"
git push origin master
+78
View File
@@ -0,0 +1,78 @@
name: Publish release
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
release:
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version
id: ver
run: echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
shell: bash
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build NebulaAuth
run: dotnet publish src/NebulaAuth/NebulaAuth.csproj -c Release -o build
- name: Package release
run: |
cd build
powershell Compress-Archive * "../NebulaAuth.${env:GITHUB_REF_NAME}.zip"
shell: pwsh
- name: Install pandoc
run: |
choco install pandoc -y
- name: Convert changelog HTML to Markdown (ul-only)
id: changelog
shell: bash
run: |
version="${{ steps.ver.outputs.version }}"
file="changelog/${version}.html"
echo "Looking for file: $file"
if [ -f "$file" ]; then
echo "✅ File found, extracting <ul> section"
sed -n '/<ul>/,/<\/ul>/p' "$file" > body.html
echo "Converting with pandoc..."
pandoc body.html -f html -t markdown --wrap=none -o changelog.md
echo "-------- Converted changelog.md --------"
cat changelog.md
echo "----------------------------------------"
echo "body<<EOF" >> $GITHUB_OUTPUT
cat changelog.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "❌ Changelog not found for version $version"
echo "body=No changelog found for $version" >> $GITHUB_OUTPUT
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: Release ${{ env.RELEASE_NAME }}
body: ${{ steps.changelog.outputs.body }}
files: |
NebulaAuth.${{ steps.ver.outputs.version }}.zip
env:
RELEASE_NAME: ${{ steps.ver.outputs.version }}
+65
View File
@@ -0,0 +1,65 @@
name: Prepare release
on:
push:
branches: [ master ]
paths:
- 'src/NebulaAuth/NebulaAuth.csproj'
workflow_dispatch:
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_TOKEN }}
- name: Read version from csproj
id: ver
shell: bash
run: |
VERSION=$(grep -oPm1 "(?<=<AssemblyVersion>)[^<]+" src/NebulaAuth/NebulaAuth.csproj)
echo "Detected version $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Generate update.xml
run: |
cat > NebulaAuth/update.xml <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>${{ steps.ver.outputs.version }}</version>
<url>https://github.com/${{ github.repository }}/releases/download/${{ steps.ver.outputs.version }}/NebulaAuth.${{ steps.ver.outputs.version }}.zip</url>
<changelog>https://achiez.github.io/${{ github.event.repository.name }}/changelog/${{ steps.ver.outputs.version }}.html</changelog>
<mandatory>false</mandatory>
</item>
EOF
echo "Generated NebulaAuth/update.xml:"
cat NebulaAuth/update.xml
- name: Check if update.xml changed
id: diff
run: |
if git diff --exit-code NebulaAuth/update.xml >/dev/null 2>&1; then
echo "no_changes=true" >> $GITHUB_OUTPUT
else
echo "no_changes=false" >> $GITHUB_OUTPUT
fi
- name: Commit and tag
if: steps.diff.outputs.no_changes == 'false'
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git add NebulaAuth/update.xml
git commit -m "chore(release): ${{ steps.ver.outputs.version }}"
git tag -a "${{ steps.ver.outputs.version }}" -m "Release ${{ steps.ver.outputs.version }}"
git push origin HEAD:master --tags
- name: Skip message
if: steps.diff.outputs.no_changes == 'true'
run: echo "No changes in version or update.xml - skipping release preparation."
+3 -1
View File
@@ -360,4 +360,6 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
todo/
+5
View File
@@ -20,6 +20,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{
changelog\1.5.5.html = changelog\1.5.5.html
changelog\1.5.6.html = changelog\1.5.6.html
changelog\1.7.1.html = changelog\1.7.1.html
changelog\1.7.2.html = changelog\1.7.2.html
changelog\1.7.3.html = changelog\1.7.3.html
changelog\1.7.4.html = changelog\1.7.4.html
changelog\1.8.0.html = changelog\1.8.0.html
changelog\1.8.1.html = changelog\1.8.1.html
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamLibForked", "src\SteamLibForked\SteamLibForked.csproj", "{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}"
+7 -8
View File
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.7.1.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.7.1/NebulaAuth.1.7.1.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.7.1.html</changelog>
<mandatory>false</mandatory>
</item>
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.8.1</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.1/NebulaAuth.1.8.1.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.1.html</changelog>
<mandatory>false</mandatory>
</item>
+4 -4
View File
@@ -76,10 +76,10 @@
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>UPDATE:</b> Added support for a new confirmation type — Purchase.</li>
<li><b>UPDATE:</b> Merged Global 1.7.0 updates into the release version (<a href="https://t.me/nebulaauth/32">patch notes</a>).</li>
<li><b>FIX:</b> Fixed a crash occurring during confirmation loading.</li>
<li><b>ENHANCEMENT:</b> Improved localization and system messages.</li>
<li><b>UPDATE:</b> Added support for a new confirmation type — Purchase.</li>
<li><b>UPDATE:</b> Merged Global 1.7.0 updates into the release version (<a href="https://t.me/nebulaauth/32">patch notes</a>).</li>
<li><b>FIX:</b> Fixed a crash occurring during confirmation loading.</li>
<li><b>ENHANCEMENT:</b> Improved localization and system messages.</li>
</ul>
</div>
</div>
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.7.2</div>
<div class="date">10.07.2025</div>
<div class="description">
<ul>
<li>
<b>NEWS:</b> Official Telegram group now available! Join us at
<b>
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>FIX:</b> Crash on loading trade confirmations</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.7.3</div>
<div class="date">17.07.2025</div>
<div class="description">
<ul>
<li>
<b>NEWS:</b> Official Telegram group now available! Join us at
<b>
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>UPDATE:</b> Added automatic acknowledgment request for sending trade confirmation</li>
<li><b>FIX:</b> Fixed notifications text in auto-confirm timer</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.7.4</div>
<div class="date">19.07.2025</div>
<div class="description">
<ul>
<li>
<b>NEWS:</b> Official Telegram group now available! Join us at
<b>
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>FIX:</b> Resolved issue where a "Confirmation Error" notification appeared despite the confirmation being successful.</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.8.0</div>
<div class="date">07.11.2025</div>
<div class="description">
<ul>
<li> Major update: Full changelog is available here <a href="https://teletype.in/@achies_raw/nebula-1-8-0-eng">ENG</a> | <a href="https://teletype.in/@achies_raw/nebula-1-8-0-rus">RUS</a></li>
</ul>
</div>
</div>
</div>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<div class="change">
<div class="version">Version 1.8.1</div>
<div class="date">25.01.2026</div>
<div class="description">
<ul>
<li><b>NEW:</b> Added Mafile Export tool with templates, batch export and flexible field filtering.</li>
<li><b>IMPROVEMENT:</b> Reworked auto-confirmation error handling logic (MAAC) — fewer false disables, smarter retries.</li>
<li><b>UPDATE:</b> Added “Unattach Proxy” option to the account context menu.</li>
<li><b>LOCALIZATION:</b> Added Chinese (Simplified) and French languages.</li>
<li><b>FIX:</b> Fixed crashes related to duplicate mafiles, timers, search filtering and import logic.</li>
<li>
<b>DETAILS:</b> Full patch notes:
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-eng">ENG</a> |
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-rus">RUS</a>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+3 -27
View File
@@ -1,38 +1,13 @@
<Application x:Class="NebulaAuth.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:NebulaAuth.Converters"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:background="clr-namespace:NebulaAuth.Converters.Background">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<ResourceDictionary>
<converters:CoefficientConverter x:Key="CoefficientConverter" />
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" />
<converters:ProxyTextConverter x:Key="ProxyTextConverter" />
<converters:ProxyDataTextConverter x:Key="ProxyDataTextConverter" />
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter" />
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
<!-- Background converters-->
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
<background:BackgroundSourceConverter x:Key="BackgroundSourceConverter" />
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
<system:Boolean x:Key="True">True</system:Boolean>
<system:Boolean x:Key="False">False</system:Boolean>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Converters/Converters.xaml" />
<ResourceDictionary Source="Theme/Themes/DefaultTheme.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
@@ -58,6 +33,7 @@
</Style>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
+1 -13
View File
@@ -1,8 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Windows;
using AchiesUtilities.Extensions;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Exceptions;
@@ -18,17 +15,8 @@ public partial class App
var splashScreen = new SplashScreen("Theme\\SplashScreen.png");
splashScreen.Show(false, true);
base.OnStartup(e);
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Shell.Initialize();
var files = 0;
if (Directory.Exists(Storage.MAFILE_F))
files = Directory.GetFiles(Storage.MafileFolder)
.Count(f => Path.GetExtension(f).EqualsIgnoreCase(".mafile"));
var threads = files > 0 ? files / 100 + 1 : 1;
await Storage.Initialize(threads);
await Shell.Initialize();
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;
mainWindow.Show();
@@ -3,7 +3,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace NebulaAuth.Theme.Controls;
namespace NebulaAuth;
public class CodeProgressBar : ProgressBar
{
@@ -13,7 +13,7 @@ public class CodeProgressBar : ProgressBar
public static readonly DependencyProperty MaxTimeProperty =
DependencyProperty.Register(nameof(MaxTime), typeof(double), typeof(CodeProgressBar),
new PropertyMetadata(30.0)); // По умолчанию 30 сек
new PropertyMetadata(30.0));
public double TimeRemaining
{
+52
View File
@@ -0,0 +1,52 @@
<UserControl x:Class="NebulaAuth.HintBox"
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"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance nebulaAuth:HintBox}"
d:DesignHeight="100" d:DesignWidth="400">
<Border Padding="5"
BorderThickness="1"
CornerRadius="12"
Visibility="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource NullableToVisibilityConverter}}">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseShadowColor}" Opacity="0.5" />
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.2" />
</Border.Background>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon Height="22" Width="22"
VerticalAlignment="Center"
Margin="5,0,0,0"
Foreground="{Binding IconBrush, RelativeSource={RelativeSource AncestorType=UserControl}}"
Kind="{Binding IconKind, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<TextBlock Grid.Column="1"
FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="WrapWithOverflow"
VerticalAlignment="Center"
Margin="10,10,10,10"
Text="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<Button Grid.Column="2"
Visibility="{Binding ShowCloseButton, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource BooleanToVisibilityConverter}}"
Command="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
Style="{StaticResource MaterialDesignToolButton}"
Margin="0,0,5,0"
Padding="2">
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
</Button>
</Grid>
</Border>
</UserControl>
+98
View File
@@ -0,0 +1,98 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using CommunityToolkit.Mvvm.Input;
using MaterialDesignThemes.Wpf;
namespace NebulaAuth;
public partial class HintBox : UserControl
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(HintBox));
public static readonly DependencyProperty SeverityProperty =
DependencyProperty.Register(nameof(Severity), typeof(HintBoxSeverity), typeof(HintBox),
new PropertyMetadata(HintBoxSeverity.Info, OnSeverityChanged));
public static readonly DependencyProperty ShowCloseButtonProperty =
DependencyProperty.Register(nameof(ShowCloseButton), typeof(bool), typeof(HintBox),
new PropertyMetadata(false));
public static readonly DependencyProperty CloseCommandProperty =
DependencyProperty.Register(nameof(CloseCommand), typeof(ICommand), typeof(HintBox),
new PropertyMetadata(null));
public string Text
{
get => (string) GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public HintBoxSeverity Severity
{
get => (HintBoxSeverity) GetValue(SeverityProperty);
set => SetValue(SeverityProperty, value);
}
public ICommand CloseCommand
{
get => (ICommand) GetValue(CloseCommandProperty) ?? InternalCloseCommand;
set => SetValue(CloseCommandProperty, value);
}
public bool ShowCloseButton
{
get => (bool) GetValue(ShowCloseButtonProperty);
set => SetValue(ShowCloseButtonProperty, value);
}
private ICommand InternalCloseCommand { get; }
public PackIconKind IconKind { get; private set; }
public Brush IconBrush { get; private set; }
public HintBox()
{
InitializeComponent();
ApplySeverityVisuals();
InternalCloseCommand = new RelayCommand(Close);
}
public event RoutedEventHandler Closed;
private static void OnSeverityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((HintBox) d).ApplySeverityVisuals();
}
private void ApplySeverityVisuals()
{
switch (Severity)
{
case HintBoxSeverity.Error:
IconKind = PackIconKind.ErrorOutline;
IconBrush = (Brush) Application.Current.FindResource("ErrorBrush")!;
break;
default:
IconKind = PackIconKind.InfoCircleOutline;
IconBrush = (Brush) Application.Current.FindResource("InfoBrush")!;
break;
}
}
private void Close()
{
Visibility = Visibility.Collapsed;
Closed?.Invoke(this, new RoutedEventArgs());
}
}
public enum HintBoxSeverity
{
Info,
Error
}
@@ -0,0 +1,20 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace NebulaAuth.Converters;
public class BoolToMafileNamingConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not bool boolValue)
return null;
return boolValue ? "Login" : "Steam ID";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,28 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace NebulaAuth.Converters;
public class BoolToValueConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not bool b) return Binding.DoNothing;
if (parameter is Array)
{
var arr = (Array) parameter;
if (arr.Length == 0) return Binding.DoNothing;
var first = arr.GetValue(0);
var second = arr.Length > 1 ? arr.GetValue(1) : null;
return b ? first : second;
}
return b ? parameter : null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
+4 -2
View File
@@ -8,11 +8,12 @@
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" />
<converters:ProxyTextConverter x:Key="ProxyTextConverter" />
<converters:ProxyDataTextConverter x:Key="ProxyDataTextConverter" />
<converters:ProxyDataTextMultiConverter x:Key="ProxyDataTextMultiConverter" />
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter" />
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
<converters:BoolToValueConverter x:Key="BoolToValueConverter" />
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
<!-- Background converters-->
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
@@ -21,6 +22,7 @@
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
<system:Boolean x:Key="True">True</system:Boolean>
<system:Boolean x:Key="False">False</system:Boolean>
@@ -1,25 +1,19 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace NebulaAuth.Converters;
public class PortableMaClientStatusToColorConverter : IValueConverter
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is false)
{
return new SolidColorBrush(Color.FromRgb(187, 224, 139));
}
return new SolidColorBrush(Color.FromRgb(224, 139, 139));
return value is bool b && !b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
return value is Visibility v && v != Visibility.Visible;
}
}
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Text;
using System.Windows.Data;
using AchiesUtilities.Web.Proxy;
using NebulaAuth.Model.Entities;
@@ -33,11 +34,50 @@ public class ProxyDataTextConverter : IValueConverter
return string.Empty;
}
return $"{p.Address}:{p.Port}";
return $"{p.Protocol.ToString().ToLowerInvariant()}://{p.Address}:{p.Port}";
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
public class ProxyDataTextMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var value = values[0] as ProxyData;
var displayProtocol = values.Length > 1 && values[1] is bool dp && dp;
var displayCredentials = values.Length > 2 && values[2] is bool dc && dc;
if (value == null)
{
return string.Empty;
}
var sb = new StringBuilder();
if (displayProtocol)
{
sb.Append(value.Protocol.ToString().ToLowerInvariant());
sb.Append("://");
}
sb.Append(value.Address);
sb.Append(':');
sb.Append(value.Port);
if (displayCredentials && value.AuthEnabled)
{
sb.Append(':');
sb.Append(value.Username);
sb.Append(':');
sb.Append(value.Password);
}
return sb.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
+39 -19
View File
@@ -13,25 +13,6 @@ namespace NebulaAuth.Core;
public static class DialogsController
{
#region CommonDialogs
public static async Task<bool> ShowConfirmCancelDialog(string? msg = null)
{
var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
var result = await DialogHost.Show(content);
return result != null && (bool) result;
}
//public static async Task<string?> ShowTextFieldDialog(string? msg = null)
//{
// var content = msg == null ? new TextFieldDialog() : new TextFieldDialog(msg);
// var result = await DialogHost.Show(content);
// return result as string;
//}
#endregion
public static async Task<LoginAgainVM?> ShowLoginAgainDialog(string username, string? currentPassword = null)
{
var vm = new LoginAgainVM
@@ -121,4 +102,43 @@ public static class DialogsController
vm?.Dispose();
}
}
public static async Task ShowSetAccountsPasswordDialog()
{
var vm = new SetAccountPasswordsVM();
var dialog = new SetAccountPasswordsView
{
DataContext = vm
};
await DialogHost.Show(dialog);
}
public static async Task ShowExportDialog()
{
var vm = new MafileExporterVM();
var dialog = new MafileExporterView
{
DataContext = vm
};
await DialogHost.Show(dialog);
}
#region CommonDialogs
public static async Task<bool> ShowConfirmCancelDialog(string? msg = null)
{
var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
var result = await DialogHost.Show(content);
return result != null && (bool) result;
}
public static async Task<string?> ShowTextFieldDialog(string? title = null, string? msg = null)
{
var content = new TextFieldDialog(title, msg);
var result = await DialogHost.Show(content);
return result as string;
}
#endregion
}
+5 -1
View File
@@ -31,6 +31,8 @@ public static class LocManager
LocalizationLanguage.English => "en",
LocalizationLanguage.Russian => "ru",
LocalizationLanguage.Ukrainian => "ua",
LocalizationLanguage.ChineseSimplified => "zh",
LocalizationLanguage.French => "fr",
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
};
}
@@ -107,5 +109,7 @@ public enum LocalizationLanguage
{
English,
Russian,
Ukrainian
Ukrainian,
ChineseSimplified,
French
}
+2 -2
View File
@@ -29,7 +29,7 @@ public static class TrayManager
var contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("Выйти", null!, OnExitClick);
contextMenu.Items.Add("Exit", null!, OnExitClick);
_notifyIcon.ContextMenuStrip = contextMenu;
@@ -69,7 +69,7 @@ public static class TrayManager
private static void HideMainWindow()
{
if (!Init) return;
if (IsEnabled == false) return;
if (!IsEnabled) return;
_notifyIcon.Visible = true;
MainWindow.Hide();
}
+20 -4
View File
@@ -1,25 +1,41 @@
using System;
using System.IO;
using AutoUpdaterDotNET;
using NebulaAuth.Model;
namespace NebulaAuth.Core;
public static class UpdateManager
{
private const string UPDATE_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/misc/update.xml";
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/NebulaAuth/update.xml";
public static void CheckForUpdates()
{
var jsonPath = Path.Combine(Environment.CurrentDirectory, "update-settings.json");
AutoUpdater.PersistenceProvider = new JsonFilePersistenceProvider(jsonPath);
AutoUpdater.ShowSkipButton = false;
if (Settings.Instance.AllowAutoUpdate)
AutoUpdater.UpdateMode = Mode.ForcedDownload;
AutoUpdater.RunUpdateAsAdmin = RequiresAdminAccess();
AutoUpdater.Start(UPDATE_URL);
}
private static bool RequiresAdminAccess()
{
try
{
var testFile = Path.Combine(Environment.CurrentDirectory, "test.tmp");
using (File.Create(testFile))
{
}
File.Delete(testFile);
return false;
}
catch (UnauthorizedAccessException)
{
return true;
}
}
//static UpdateManager()
//{
+211 -114
View File
@@ -1,24 +1,25 @@
<w:FontScaleWindow x:Class="NebulaAuth.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:w="clr-namespace:NebulaAuth.Theme"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:windowStyle="clr-namespace:NebulaAuth.Theme.WindowStyle"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:NebulaAuth.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
xmlns:viewModel="clr-namespace:NebulaAuth.ViewModel"
xmlns:controls="clr-namespace:NebulaAuth.Theme.Controls"
WindowStartupLocation="CenterScreen"
MinHeight="500" MinWidth="500" DefaultFontSize="18" ScaleCoefficient="0.4"
Title="NebulaAuth" Height="800" Width="730"
Style="{StaticResource MainWindow}"
RenderOptions.BitmapScalingMode="HighQuality"
FontFamily="{md:MaterialDesignFont}"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance viewModel:MainVM}">
<Window x:Class="NebulaAuth.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:windowStyle="clr-namespace:NebulaAuth.Theme.WindowStyle"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:NebulaAuth.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
xmlns:viewModel="clr-namespace:NebulaAuth.ViewModel"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
WindowStartupLocation="CenterScreen"
MinHeight="500" MinWidth="500"
Title="NebulaAuth" Height="800" Width="730"
Style="{StaticResource MainWindow}"
RenderOptions.BitmapScalingMode="HighQuality"
FontFamily="{md:MaterialDesignFont}"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance viewModel:MainVM}"
md:RippleAssist.IsDisabled="{Binding Settings.RippleDisabled}"
FontSize="18">
<b:Interaction.Behaviors>
<windowStyle:WindowChromeRenderedBehavior />
</b:Interaction.Behaviors>
@@ -27,7 +28,14 @@
<KeyBinding Command="{Binding Path=PasteMafilesFromClipboardCommand}"
Key="V"
Modifiers="Control" />
<KeyBinding Command="{x:Static ApplicationCommands.Find}"
Key="F"
Modifiers="Control" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Find"
Executed="FocusSearchBox" />
</Window.CommandBindings>
<Border Name="DragNDropBorder" Panel.ZIndex="3" Opacity="1" AllowDrop="True"
DragDrop.DragEnter="Rectangle_DragEnter" DragLeave="Rectangle_DragLeave" Drop="Rectangle_Drop">
<md:DialogHost DialogOpened="DialogHost_DialogOpened"
@@ -57,31 +65,40 @@
<Rectangle Name="DragNDropOverlay" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="1" Fill="#242123"
Opacity="0.9" Visibility="Hidden" />
<StackPanel Name="DragNDropPanel" Grid.Row="0" ZIndex="2" w:FontScaleWindow.Scale="1"
w:FontScaleWindow.ResizeFont="True" Grid.RowSpan="3" Visibility="Hidden"
<StackPanel Name="DragNDropPanel" Grid.Row="0" ZIndex="2" Grid.RowSpan="3" Visibility="Hidden"
VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock FontWeight="Bold" Foreground="#9a65b8" Text="{Tr MainWindow.Global.DragNDropHint}" />
<TextBlock FontSize="24" FontWeight="Bold" Foreground="#9a65b8"
Text="{Tr MainWindow.Global.DragNDropHint}" />
<md:PackIcon Foreground="#9a65b8" HorizontalAlignment="Center" Width="36" Height="36"
Kind="FileReplaceOutline" />
</StackPanel>
<ToolBarTray>
<ToolBarTray Grid.Row="0">
<ToolBar ClipToBounds="True" HorizontalContentAlignment="Stretch"
Style="{StaticResource MaterialDesignToolBar}" FontSize="16">
<Menu FontSize="16">
<MenuItem Header="{Tr MainWindow.Menu.File.Caption}">
<MenuItem Header="{Tr MainWindow.Menu.File.Import}"
Command="{Binding AddMafileCommand}" />
<MenuItem IsEnabled="{Binding IsMafileSelected}"
Header="{Tr MainWindow.Menu.File.Remove}"
<MenuItem Header="{Tr MainWindow.Menu.File.Remove}"
Command="{Binding RemoveMafileCommand}" />
<MenuItem Header="{Tr MainWindow.Menu.File.OpenFolder}"
Command="{Binding OpenMafileFolderCommand}" />
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
<MenuItem Header="{Tr MainWindow.Menu.File.ProxyManager}"
Command="{Binding OpenProxyManagerCommand}" />
<MenuItem Header="{Tr MainWindow.Menu.File.Settings}"
Command="{Binding OpenSettingsDialogCommand}" />
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.Title}">
<MenuItem Header="{Tr MainWindow.Menu.File.Other.SetPasswords}"
Command="{Binding OpenSetPasswordsDialogCommand}" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.OpenExport}"
Command="{Binding OpenExporterDialogCommand}" />
</MenuItem>
</MenuItem>
</Menu>
<Menu w:FontScaleWindow.Scale="0.68" w:FontScaleWindow.ResizeFont="True">
<Menu>
<MenuItem Header="{Tr MainWindow.Menu.Account.Caption}">
<MenuItem Header="{Tr MainWindow.Menu.Account.Link}"
Command="{Binding LinkAccountCommand}" />
@@ -91,18 +108,18 @@
Command="{Binding RemoveAuthenticatorCommand}" />
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
<MenuItem IsEnabled="{Binding IsMafileSelected}"
Header="{Tr MainWindow.Menu.Account.RefreshSession}"
<MenuItem Header="{Tr MainWindow.Menu.Account.RefreshSession}"
Command="{Binding RefreshSessionCommand}" />
<MenuItem IsEnabled="{Binding IsMafileSelected}"
Header="{Tr MainWindow.Menu.Account.LoginAgain}"
<MenuItem Header="{Tr MainWindow.Menu.Account.LoginAgain}"
Command="{Binding LoginAgainCommand}" />
</MenuItem>
</Menu>
<Separator />
<ComboBox ToolTip="{Tr MainWindow.AppBar.GroupToolTip}"
md:HintAssist.Hint="{Tr MainWindow.AppBar.GroupsHint}"
MinWidth="100" Margin="8,0,8,0" VerticalAlignment="Center" IsEditable="True"
MinWidth="100"
MaxWidth="200"
Margin="8,0,8,0" VerticalAlignment="Center" IsEditable="True"
ItemsSource="{Binding Groups}"
SelectedValue="{Binding SelectedGroup}"
md:TextFieldAssist.HasClearButton="True">
@@ -123,6 +140,7 @@
<ComboBox ToolTip="{Tr MainWindow.AppBar.Proxy.ProxyManipulateToolTip}"
MinWidth="80"
MaxWidth="200"
Margin="8,0,8,0"
VerticalAlignment="Center"
md:HintAssist.Hint="{Tr MainWindow.AppBar.Proxy.ProxyHint}"
@@ -146,6 +164,7 @@
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
</UIElement.InputBindings>
</ComboBox>
<md:PackIcon Kind="StopAlert" VerticalAlignment="Center" HorizontalAlignment="Center"
Foreground="{DynamicResource ErrorBrush}" Margin="3"
ToolTipService.InitialShowDelay="300">
@@ -173,6 +192,7 @@
ToolTip="{Tr MainWindow.AppBar.Proxy.ProxyAlert.DefaultInUse}"
ToolTipService.InitialShowDelay="300"
Visibility="{Binding IsDefaultProxy, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Separator />
<ToggleButton ToolTip="{Tr MainWindow.AppBar.MarketTimerHint}"
IsEnabled="{Binding IsMafileSelected}" Margin="2"
Style="{StaticResource MaterialDesignFlatToggleButton}"
@@ -249,9 +269,17 @@
TextWrapping="WrapWithOverflow" FontSize="16"
Text="{Tr MainWindow.LeftPart.NoMafiles}" />
<ListBox w:FontScaleWindow.Scale="0.8" w:FontScaleWindow.ResizeFont="True" Margin="2"
<ListBox FontSize="17" Margin="2"
x:Name="MafileListBox"
SelectionMode="Single"
ItemsSource="{Binding MaFiles}"
SelectedValue="{Binding SelectedMafile}">
SelectedValue="{Binding SelectedMafile}"
PreviewMouseDown="MafileListBox_OnPreviewMouseDown">
<ListBox.Resources>
<DiscreteObjectKeyFrame x:Key="groupsProxy" Value="{Binding Groups}" />
<DiscreteObjectKeyFrame x:Key="listBoxProxy"
Value="{Binding ElementName=MafileListBox}" />
</ListBox.Resources>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem"
BasedOn="{StaticResource MaterialDesignListBoxItem}">
@@ -261,23 +289,47 @@
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid ToolTip="{Binding Filename}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Stretch" Text="{Binding AccountName}">
<TextBlock.Resources>
<SolidColorBrush x:Key="BaseContentBrushProxy"
Color="{DynamicResource BaseContentColor}" />
</TextBlock.Resources>
<TextBlock HorizontalAlignment="Stretch"
Text="{Binding AccountName}"
ToolTip="{Binding LinkedClient.Status.Message, FallbackValue={x:Null}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground"
Value="{DynamicResource BaseContentBrush}" />
<Style.Triggers>
<DataTrigger
Binding="{Binding LinkedClient.Status.StatusType}"
Value="Ok">
<Setter Property="Foreground"
Value="{DynamicResource SuccessBrush}" />
</DataTrigger>
<DataTrigger
Binding="{Binding LinkedClient.Status.StatusType}"
Value="Warning">
<Setter Property="Foreground"
Value="{DynamicResource WarningBrush}" />
</DataTrigger>
<DataTrigger
Binding="{Binding LinkedClient.Status.StatusType}"
Value="Error">
<Setter Property="Foreground"
Value="{DynamicResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
<TextBlock.Foreground>
<Binding Path="LinkedClient.IsError"
Converter="{StaticResource PortableMaClientStatusToColorConverter}"
FallbackValue="{StaticResource BaseContentBrushProxy}" />
</TextBlock.Foreground>
</TextBlock>
<StackPanel
Visibility="{Binding LinkedClient, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Column="1" Orientation="Horizontal">
@@ -296,13 +348,20 @@
<KeyBinding Key="X" Modifiers="Control"
Command="{Binding DataContext.CopyMafileCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}" />
<KeyBinding Modifiers="Control+Shift" Key="C"
Command="{Binding DataContext.CopyPasswordCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}" />
</ListBox.InputBindings>
<FrameworkElement.ContextMenu>
<ContextMenu>
<ContextMenu Tag="{Binding Groups}">
<MenuItem InputGestureText="Ctrl+C"
Header="{Tr MainWindow.ContextMenus.Mafile.CopyLogin}"
Command="{Binding CopyLoginCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopyPassword}"
Command="{Binding Path=CopyPasswordCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
InputGestureText="CTRL+⇧+C" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopySteamId}"
Command="{Binding CopySteamIdCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
@@ -310,8 +369,33 @@
Header="{Tr MainWindow.ContextMenus.Mafile.CopyMafile}"
Command="{Binding CopyMafileCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AddToGroup}"
ItemsSource="{Binding Groups}">
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AddToGroup}">
<MenuItem.ItemsSource>
<CompositeCollection>
<MenuItem
Command="{Binding CreateGroupCommand}"
CommandParameter="{Binding Value.SelectedValue, Source={StaticResource listBoxProxy}}">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<md:PackIcon Kind="PlusCircle"
VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center"
Margin="5,0,0,0" Grid.Column="1"
Text="{Tr MainWindow.ContextMenus.Mafile.CreateGroup}" />
</Grid>
</MenuItem.Header>
</MenuItem>
<Separator />
<CollectionContainer
Collection="{Binding Value, Source={StaticResource groupsProxy}}" />
</CompositeCollection>
</MenuItem.ItemsSource>
<ItemsControl.ItemContainerStyle>
<Style BasedOn="{StaticResource MaterialDesignMenuItem}"
TargetType="{x:Type MenuItem}">
@@ -333,97 +417,110 @@
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}"
Command="{Binding Path=RemoveGroupCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopyPassword}"
Command="{Binding Path=CopyPasswordCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
Command="{Binding RemoveProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</ContextMenu>
</FrameworkElement.ContextMenu>
</ListBox>
<TextBox Style="{StaticResource MaterialDesignFloatingHintTextBox}"
md:TextFieldAssist.HasClearButton="True" w:FontScaleWindow.Scale="0.7"
w:FontScaleWindow.ResizeFont="True" Grid.Row="1" Margin="10,5,10,10"
FontSize="16"
md:TextFieldAssist.HasClearButton="True" Grid.Row="1" Margin="10,5,10,10"
md:HintAssist.Hint="{Tr MainWindow.LeftPart.SearchBoxHint}"
Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
x:Name="SearchField"
KeyDown="SearchField_OnKeyDown" />
</Grid>
</Border>
</Grid>
<md:Card BorderThickness="1" Style="{StaticResource MaterialDesignOutlinedCard}" Grid.Column="1"
Margin="10,10,15,10" UniformCornerRadius="12" Opacity="{Binding Settings.RightOpacity}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="115*" />
<RowDefinition Height="436*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Row="0">
<Grid Grid.Column="1">
<Border Background="{DynamicResource MaterialDesign.Brush.Card.Background}"
Opacity="{Binding Settings.RightOpacity}"
BorderThickness="1"
Margin="10,10,15,10" CornerRadius="12" VerticalAlignment="Stretch">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.3" />
</Border.BorderBrush>
</Border>
<md:Card Margin="10,10,15,10" Padding="2" UniformCornerRadius="12"
Background="Transparent">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="115*" />
<RowDefinition Height="436*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox w:FontScaleWindow.Scale="1.1" w:FontScaleWindow.ResizeFont="True"
IsReadOnly="True" HorizontalContentAlignment="Center" Background="#00FFFFFF"
<Grid Row="0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox FontSize="24"
IsReadOnly="True" HorizontalContentAlignment="Center"
Background="#00FFFFFF"
Style="{StaticResource MaterialDesignTextBox}"
Style="{StaticResource MaterialDesignTextBox}"
Text="{Binding Code, FallbackValue=Code}">
<TextBox.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding CopyCodeCommand}" />
</TextBox.InputBindings>
</TextBox>
<controls:CodeProgressBar
Visibility="{Binding SelectedMafile, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="1" Height="4" Style="{StaticResource MaterialDesignLinearProgressBar}"
MaxTime="30" TimeRemaining="{Binding CodeProgress, Mode=OneWay}" />
Text="{Binding Code, FallbackValue=Code}">
<TextBox.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding CopyCodeCommand}" />
</TextBox.InputBindings>
</TextBox>
<nebulaAuth:CodeProgressBar
Visibility="{Binding SelectedMafile, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="1" Height="4"
Style="{StaticResource MaterialDesignLinearProgressBar}"
MaxTime="30" TimeRemaining="{Binding CodeProgress, Mode=OneWay}" />
</Grid>
<Button IsEnabled="{Binding IsMafileSelected}"
FontSize="14"
Grid.Row="1"
Style="{StaticResource MaterialDesignOutlinedButton}"
Margin="0,10,0,10"
HorizontalAlignment="Center"
Content="{Tr MainWindow.RightPart.LoadConfirmations}"
Command="{Binding GetConfirmationsCommand}"
md:ButtonProgressAssist.IsIndeterminate="True"
md:ButtonProgressAssist.IsIndicatorVisible="{Binding GetConfirmationsCommand.IsRunning}"
Width="{Binding Path=ActualWidth, Converter={StaticResource CoefficientConverter}, ConverterParameter=0.6, RelativeSource={RelativeSource AncestorType=md:Card}}" />
<ItemsControl md:RippleAssist.IsDisabled="True" Focusable="False" Grid.Row="2"
FontSize="16"
Margin="10,10,10,10"
Visibility="{Binding ConfirmationsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding Confirmations}" Grid.RowSpan="2" />
<Button FontSize="16"
Style="{StaticResource MaterialDesignFlatButton}" Grid.Row="4"
Command="{Binding ConfirmLoginCommand}"
Content="{Tr MainWindow.RightPart.ConfirmLogin}" />
</Grid>
<Button IsEnabled="{Binding IsMafileSelected}" w:FontScaleWindow.Scale="0.7"
w:FontScaleWindow.ResizeFont="True" Grid.Row="1"
Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,10,0,10"
HorizontalAlignment="Center"
Content="{Tr MainWindow.RightPart.LoadConfirmations}"
Command="{Binding GetConfirmationsCommand}"
Width="{Binding Path=ActualWidth, Converter={StaticResource CoefficientConverter}, ConverterParameter=0.6, RelativeSource={RelativeSource AncestorType=md:Card}}" />
<ItemsControl md:RippleAssist.IsDisabled="True" Focusable="False" Grid.Row="2"
w:FontScaleWindow.Scale="0.72" w:FontScaleWindow.ResizeFont="True"
Margin="10,10,10,10"
Visibility="{Binding ConfirmationsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding Confirmations}" Grid.RowSpan="2" />
<Button Style="{StaticResource MaterialDesignFlatButton}" Grid.Row="4"
w:FontScaleWindow.Scale="0.8" w:FontScaleWindow.ResizeFont="True"
Command="{Binding ConfirmLoginCommand}"
Content="{Tr MainWindow.RightPart.ConfirmLogin}" />
</Grid>
</md:Card>
</md:Card>
</Grid>
</Grid>
<Border Grid.Row="2" BorderBrush="#1e1e24">
<Border.Background>
<SolidColorBrush Opacity="1" Color="{DynamicResource Base300Color}" />
</Border.Background>
<Grid>
<ToolBarPanel TextElement.Foreground="{DynamicResource SecondaryContentBrush}"
Orientation="Horizontal" Margin="5" w:FontScaleWindow.Scale="0.73"
w:FontScaleWindow.ResizeFont="True">
<TextBlock FontWeight="Bold" Text="{Tr MainWindow.Footer.Account}" />
<TextBlock Text="{Binding SelectedMafile.AccountName}" />
<TextBlock Text="|" Margin="10,0,10,0" />
<TextBlock FontWeight="Bold" Text="{Tr MainWindow.Footer.Group}" />
<TextBlock FontWeight="Normal"
Text="{Binding SelectedMafile.Group, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
Orientation="Horizontal" Margin="5">
<TextBlock FontSize="15" Text="{Tr MainWindow.Footer.AccountsCount}" />
<TextBlock FontSize="15" Text="{Binding MaFiles.Count}" />
<TextBlock FontSize="15" Text="|" Margin="10,0,10,0" />
<TextBlock FontSize="15" Text="{Tr MainWindow.Footer.SelectedAccount}" />
<TextBlock FontSize="15"
Text="{Binding SelectedMafile.AccountName, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
<!--<Button Command="{Binding DebugCommand}">Debug</Button>-->
</ToolBarPanel>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" w:FontScaleWindow.Scale="0.7"
w:FontScaleWindow.ResizeFont="True" Margin="0,0,10,0">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
Margin="0,0,10,0">
<Hyperlink
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
@@ -438,4 +535,4 @@
</Grid>
</md:DialogHost>
</Border>
</w:FontScaleWindow>
</Window>
+29 -5
View File
@@ -2,6 +2,7 @@
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using MaterialDesignThemes.Wpf;
@@ -29,7 +30,7 @@ public partial class MainWindow
private async void OnApplicationStarted(object? sender, EventArgs e)
{
((MainVM) DataContext).CurrentDialogHost = DialogHostInstance;
if (Settings.Instance.IsPasswordSet == false) return;
if (!Settings.Instance.IsPasswordSet) return;
Topmost = false;
await Dispatcher.InvokeAsync(ShowSetPasswordDialog, DispatcherPriority.ContextIdle);
}
@@ -45,22 +46,45 @@ public partial class MainWindow
var result = await DialogHost.Show(dialog);
var pass = vm.Password;
if (result is true && string.IsNullOrWhiteSpace(pass) == false)
if (result is true && !string.IsNullOrWhiteSpace(pass))
{
PHandler.SetPassword(pass);
}
}
private void SearchField_OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Escape || !SearchField.IsFocused) return;
Keyboard.ClearFocus();
}
private void MafileListBox_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (!Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) return;
if (ItemsControl.ContainerFromElement((ListBox) sender, (DependencyObject) e.OriginalSource) is ListBoxItem
{
IsSelected: true
})
{
e.Handled = true;
}
}
private void FocusSearchBox(object sender, ExecutedRoutedEventArgs e)
{
SearchField.Focus();
}
#region Dran'n'Drop
private void UIElement_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (int.TryParse(e.Text, out _) == false) e.Handled = true;
if (!int.TryParse(e.Text, out _)) e.Handled = true;
}
private void Rectangle_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Handled = true;
return;
@@ -72,7 +96,7 @@ public partial class MainWindow
private async void Rectangle_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
var filePaths = (string[]) e.Data.GetData(DataFormats.FileDrop)!;
if (filePaths.Length == 0) return;
if (DataContext is MainVM mainVm)
+8 -1
View File
@@ -20,8 +20,15 @@ public partial class Mafile : MobileDataExtended
set => SetProperty(ref _linkedClient, value);
}
[JsonIgnore] private PortableMaClient? _linkedClient;
[JsonIgnore]
public string? Filename
{
get => _filename;
set => SetProperty(ref _filename, value);
}
private string? _filename;
[JsonIgnore] private PortableMaClient? _linkedClient;
public void SetSessionData(MobileSessionData? sessionData)
{
@@ -0,0 +1,174 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using SteamLib.Exceptions.Authorization;
namespace NebulaAuth.Model.MAAC;
public static class MAACRequestHandler
{
private const string LOC_PATH = "MAAC";
private static readonly ConcurrentDictionary<Mafile, PortableMaClientErrorData> _errors = new();
public static void Register(Mafile mafile)
{
_errors[mafile] = new PortableMaClientErrorData();
}
public static void Unregister(Mafile mafile)
{
_errors.TryRemove(mafile, out _);
}
public static PortableMaClientStatus ClearErrors(Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data))
{
data.Clear();
}
return PortableMaClientStatus.Ok();
}
/// <summary>
/// Handles a MAAC request with progressive error handling strategy: retries and status management based on error
/// history.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="client"></param>
/// <param name="func"></param>
/// <returns></returns>
public static async Task<T> HandleRequest<T>(PortableMaClient client, Func<Task<T>> func)
{
var result = await client.DoRequest(func, client.Mafile.PreviouslyHadNoErrors());
if (result.IsSuccess)
{
InformRequestSuccessful(client.Mafile);
return result.Data;
}
if (client.Mafile.PreviouslyHadNoErrors())
{
client.SetStatus(PortableMaClientStatus.Warning(GetPortableMaClientStatus("SessionError")));
}
else if (client.Mafile.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval))
{
Shell.Logger.Info("Retrying MAAC request for {name} MAAC account after previous error.",
client.Mafile.AccountName);
result = await client.DoRequest(func);
if (result.IsSuccess)
{
InformRequestSuccessful(client.Mafile);
return result.Data;
}
}
if (client.Mafile.ErrorPersistedFor(Settings.Instance.MaacErrorThreshold))
{
client.SetStatus(PortableMaClientStatus.Error(GetPortableMaClientStatus("SessionError")));
}
AddError(client.Mafile, result.Exception!);
throw result.Exception!;
}
private static async Task<Result<T>> DoRequest<T>(this PortableMaClient client, Func<Task<T>> req,
bool withSessionHandler = true)
{
try
{
return withSessionHandler
? Result<T>.Success(await SessionHandler.Handle(req, client.Mafile, client.Chp(),
GetTimerPrefix(client.Mafile)))
: Result<T>.Success(await req());
}
catch (SessionInvalidException ex)
{
Shell.Logger.Warn("SessionInvalidException caught in MAAC request handler.");
return Result<T>.Error(ex);
}
}
private static void InformRequestSuccessful(Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data))
{
Shell.Logger.Info("MAAC request for {name} MAAC account succeeded, clearing error history.",
mafile.AccountName);
data.Clear();
}
}
private static void AddError(Mafile mafile, Exception ex)
{
if (_errors.TryGetValue(mafile, out var data))
{
Shell.Logger.Info("Registering error for {name} MAAC account: {ex}", mafile.AccountName, ex.Message);
data.AddEntry(ex);
}
}
private static bool ErrorPersistedFor(this Mafile mafile, TimeSpan span)
{
if (_errors.TryGetValue(mafile, out var data))
{
var oldestError = data.GetOldestErrorTime();
if (oldestError.HasValue && DateTime.UtcNow - oldestError.Value > span)
{
return true;
}
}
return false;
}
private static bool PreviouslyHadNoErrors(this Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data))
{
return data.NoErrors;
}
return true;
}
private static bool LastErrorWasAtLeast(this Mafile mafile, TimeSpan timeAgo)
{
if (!_errors.TryGetValue(mafile, out var data)) return false;
var latestError = data.GetTimeFromLastError();
if (latestError.HasValue && latestError.Value > timeAgo)
{
return true;
}
return false;
}
public static bool IsReady(Mafile maf)
{
if (maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Ok}) return true;
return maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Warning}
&& maf.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval);
}
private static string GetTimerPrefix(Mafile mafile)
{
return GetLocalization("TimerPrefix") + mafile.AccountName + ": ";
}
private static string GetLocalization(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
}
private static string GetPortableMaClientStatus(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, "PortableMaClientStatus", key);
}
}
+145
View File
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MAAC;
public static class MAACStorage
{
private static Dictionary<string, StoredClient> Clients { get; set; } = [];
static MAACStorage()
{
}
private static void ClientsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
Clients.Clear();
}
else if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (item is Mafile {Filename: not null} mafile)
{
if (mafile.LinkedClient != null)
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
Clients.Add(mafile.Filename, new StoredClient
{
AutoConfirmMarket = mafile.LinkedClient?.AutoConfirmMarket ?? false,
AutoConfirmTrades = mafile.LinkedClient?.AutoConfirmTrades ?? false
});
}
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
if (item is Mafile {Filename: not null} mafile)
{
if (mafile.LinkedClient != null)
mafile.LinkedClient.PropertyChanged -= LinkedClientOnPropertyChanged;
Clients.Remove(mafile.Filename);
}
}
}
Save();
}
private static void LinkedClientOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is not PortableMaClient client) return;
if (client.Mafile.Filename == null) return;
var anyChanges = false;
if (!Clients.TryGetValue(client.Mafile.Filename, out var storedClient))
{
client.PropertyChanged -= LinkedClientOnPropertyChanged;
return;
}
if (e.PropertyName == nameof(PortableMaClient.AutoConfirmMarket))
{
anyChanges = storedClient.AutoConfirmMarket != client.AutoConfirmMarket;
storedClient.AutoConfirmMarket = client.AutoConfirmMarket;
}
else if (e.PropertyName == nameof(PortableMaClient.AutoConfirmTrades))
{
anyChanges = storedClient.AutoConfirmTrades != client.AutoConfirmTrades;
storedClient.AutoConfirmTrades = client.AutoConfirmTrades;
}
if (anyChanges)
Save();
}
public static void Save()
{
var json = JsonConvert.SerializeObject(Clients, Formatting.Indented);
File.WriteAllText("maac.json", json);
}
public static void Initialize()
{
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
if (!File.Exists("maac.json")) return;
try
{
var json = File.ReadAllText("maac.json");
var clients = JsonConvert.DeserializeObject<Dictionary<string, StoredClient>>(json) ?? [];
foreach (var (fileName, storedClient) in clients)
{
var mafile = Storage.MaFiles.FirstOrDefault(x => x.Filename == fileName);
if (mafile == null) continue;
if (storedClient is {AutoConfirmMarket: false, AutoConfirmTrades: false}) continue;
if (MultiAccountAutoConfirmer.TryAddToConfirm(mafile) && mafile.LinkedClient != null)
{
mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket;
mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades;
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
Clients[fileName] = storedClient;
}
}
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Failed to load MAAC storage");
SnackbarController.SendSnackbar(
LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage"));
}
}
public static void NotifyMafilesRenamed(IDictionary<string, string> oldNewNames)
{
var updatedClients = new Dictionary<string, StoredClient>();
foreach (var (oldName, newName) in oldNewNames)
{
if (Clients.Remove(oldName, out var storedClient))
{
updatedClients[newName] = storedClient;
}
}
Clients = updatedClients;
Save();
}
private class StoredClient
{
public bool AutoConfirmMarket { get; set; }
public bool AutoConfirmTrades { get; set; }
}
}
@@ -25,7 +25,7 @@ public static class MultiAccountAutoConfirmer
Clients = [];
Timer = new Timer(TimerConfirm);
Settings.Instance.PropertyChanged += SettingsOnPropertyChanged;
UpdateTimer();
UpdateTimer(true);
}
// ReSharper disable once AsyncVoidMethod //Already safe
@@ -59,7 +59,7 @@ public static class MultiAccountAutoConfirmer
private static async Task TimerConfirmInternal()
{
var clients = Lock.ReadLock(() => Clients.ToArray());
var enabledClients = clients.Where(x => x.LinkedClient is {IsError: false}).ToArray();
var enabledClients = GetReadyAccounts();
enabledClients = DistributeEvenly(enabledClients).ToArray();
var confirmed = 0;
await Task.Run(async () =>
@@ -69,7 +69,8 @@ public static class MultiAccountAutoConfirmer
var conf = 0;
try
{
conf = await client.LinkedClient!.Confirm();
conf = await MAACRequestHandler.HandleRequest(client.LinkedClient!,
() => client.LinkedClient!.Confirm());
}
catch (ObjectDisposedException)
{
@@ -114,14 +115,29 @@ public static class MultiAccountAutoConfirmer
}
}
private static IEnumerable<Mafile> GetReadyAccounts()
{
var clients = Lock.ReadLock(() => Clients.ToArray());
var res = new List<Mafile>();
foreach (var maf in clients)
{
if(maf.LinkedClient == null) continue;
if (MAACRequestHandler.IsReady(maf))
{
res.Add(maf);
}
}
return res;
}
public static bool TryAddToConfirm(Mafile mafile)
{
return Lock.WriteLock(() =>
{
if (Clients.Contains(mafile)) return false;
Clients.Add(mafile);
mafile.LinkedClient = new PortableMaClient(mafile);
Clients.Add(mafile);
MAACRequestHandler.Register(mafile);
return true;
});
}
@@ -131,9 +147,10 @@ public static class MultiAccountAutoConfirmer
{
Lock.WriteLock(() =>
{
Clients.Remove(mafile);
mafile.LinkedClient?.Dispose();
mafile.LinkedClient = null;
Clients.Remove(mafile);
MAACRequestHandler.Unregister(mafile);
});
}
@@ -144,11 +161,17 @@ public static class MultiAccountAutoConfirmer
UpdateTimer();
}
private static void UpdateTimer()
private static void UpdateTimer(bool firstStart = false)
{
var timerInterval = Settings.Instance.TimerSeconds;
var intervalTimeSpan = TimeSpan.FromSeconds(timerInterval);
Timer.Change(intervalTimeSpan, intervalTimeSpan);
var dueTime = intervalTimeSpan;
if (firstStart)
{
dueTime = timerInterval >= 30 ? intervalTimeSpan : TimeSpan.FromSeconds(30);
}
Timer.Change(dueTime, intervalTimeSpan);
}
private static string GetLocalization(string key)
+41 -96
View File
@@ -11,10 +11,9 @@ using AchiesUtilities.Web.Proxy;
using CommunityToolkit.Mvvm.ComponentModel;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
using SteamLib.Api.Mobile;
using SteamLib.Api.Trade;
using SteamLib.Authentication;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile.Confirmations;
using SteamLib.Web;
using SteamLibForked.Abstractions;
@@ -33,7 +32,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
[ObservableProperty] private bool _autoConfirmMarket;
[ObservableProperty] private bool _autoConfirmTrades;
[ObservableProperty] private bool _isError;
[ObservableProperty] private PortableMaClientStatus _status = PortableMaClientStatus.Ok();
public PortableMaClient(Mafile mafile)
{
@@ -45,6 +44,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
ClientHandler = pair.Handler;
UpdateCookies(mafile.SessionData);
Mafile.PropertyChanged += Mafile_PropertyChanged;
SetStatus(PortableMaClientStatus.Ok());
}
private void Mafile_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -55,42 +55,26 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
}
private void UpdateCookies(IMobileSessionData? sessionData)
{
Application.Current.Dispatcher.Invoke(() => IsError = sessionData == null);
var newStatus = MAACRequestHandler.ClearErrors(Mafile);
if (!newStatus.Equals(Status))
SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies();
if (sessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
public async Task<int> Confirm()
{
Proxy.SetData(Mafile.Proxy?.Data ?? MaClient.DefaultProxy);
List<Confirmation> conf;
try
{
conf = (await HandleTimerRequest(GetConfirmations)).ToList();
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: Error GetConf in timer.", Mafile.AccountName);
return 0;
}
var conf = (await GetConfirmations()).ToList();
var toConfirm = new List<Confirmation>();
if (AutoConfirmMarket)
{
var market = conf.Where(c =>
var market = conf.Where(c =>
c.ConfType is ConfirmationType.MarketSellTransaction or ConfirmationType.Purchase);
toConfirm.AddRange(market);
}
@@ -102,19 +86,12 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
if (toConfirm.Count == 0) return 0;
try
{
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await HandleTimerRequest(() => SendConfirmations(toConfirm));
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: MultiConf error in Timer.", Mafile.AccountName);
return 0;
}
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await SendConfirmations(toConfirm);
//TODO: handle success == false
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
@@ -126,62 +103,27 @@ public partial class PortableMaClient : ObservableObject, IDisposable
private async Task<bool> SendConfirmations(IEnumerable<Confirmation> confirmations)
{
return await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, confirmations,
var conf = confirmations.ToList();
var res = await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, conf,
Mafile.SessionData!.SteamId, Mafile, true, _cts.Token);
if (!res && conf.Any(c => c.ConfType == ConfirmationType.Trade))
{
Shell.Logger.Warn("Timer {accountName}: Failed to send trade confirmations. Sending ack",
Mafile.AccountName);
await SteamTradeApi.Acknowledge(Client, Mafile.SessionData!.SessionId, _cts.Token);
await Task.Delay(10, _cts.Token);
}
else
{
return res;
}
return await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, conf,
Mafile.SessionData!.SteamId, Mafile, true, _cts.Token);
}
private async Task<T> HandleTimerRequest<T>(Func<Task<T>> func)
{
Exception innerException;
try
{
return await SessionHandler.Handle(func, Mafile, Chp(), GetTimerPrefix());
}
catch (OperationCanceledException ex)
{
innerException = ex; //Ignored
}
catch (SessionInvalidException ex)
{
if (IgnoreTimerErrors())
{
Shell.Logger.Warn(
"Timer {accountName}: Session error while requesting in timer. Ignored due to IgnorePatchTuesdayErrors setting",
Mafile.AccountName);
}
else
{
Shell.Logger.Warn("Timer {accountName}: Session error while requesting in timer. Timer disabled",
Mafile.AccountName);
SetError();
}
innerException = ex;
}
catch (Exception ex) when (ExceptionHandler.Handle(ex, GetTimerPrefix()))
{
innerException = ex;
}
throw new ApplicationException("Swallowed", innerException);
}
private bool IgnoreTimerErrors()
{
if (Settings.Instance.IgnorePatchTuesdayErrors == false) return false;
var pstZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var pstNow = TimeZoneInfo.ConvertTime(DateTime.UtcNow, pstZone);
var startTime = pstNow.Date.AddHours(15).AddMinutes(55); // 15:55 PST //22:55 GMT //00:55 GMT+2
var endTime = pstNow.Date.AddHours(17).AddMinutes(15); // 17:15 PST //00:15 GMT //02:15 GMT+2
return pstNow.DayOfWeek == DayOfWeek.Tuesday && pstNow >= startTime && pstNow <= endTime;
}
private HttpClientHandlerPair Chp()
public HttpClientHandlerPair Chp()
{
return new HttpClientHandlerPair(Client, ClientHandler);
}
@@ -196,11 +138,14 @@ public partial class PortableMaClient : ObservableObject, IDisposable
return GetLocalization("TimerPrefix") + Mafile.AccountName + ": ";
}
public void SetError()
public void SetStatus(PortableMaClientStatus status)
{
Application.Current.Dispatcher.Invoke(() => IsError = true);
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + GetLocalization("TimerSessionError"));
Application.Current.Dispatcher.Invoke(() => Status = status);
if (status.StatusType == PortableMaClientStatusType.Error)
{
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + status.Message);
}
}
public void Dispose()
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NebulaAuth.Model.MAAC;
public sealed class PortableMaClientErrorData
{
public SortedDictionary<DateTime, Exception> Values { get; } = new();
public bool NoErrors => Values.Count == 0;
private readonly object _lock = new();
public DateTime? GetOldestErrorTime()
{
if (Values.Count == 0) return null;
return Values.Keys.First();
}
public void AddEntry(Exception ex)
{
lock (_lock)
Values[DateTime.UtcNow] = ex;
}
public void Clear()
{
lock (_lock)
Values.Clear();
}
public TimeSpan? GetTimeFromLastError()
{
if (Values.Count == 0) return null;
var lastEntry = Values.Keys.Last();
return DateTime.UtcNow - lastEntry;
}
}
@@ -0,0 +1,26 @@
namespace NebulaAuth.Model.MAAC;
public record PortableMaClientStatus(PortableMaClientStatusType StatusType, string? Message = null)
{
public static PortableMaClientStatus Ok()
{
return new PortableMaClientStatus(PortableMaClientStatusType.Ok);
}
public static PortableMaClientStatus Warning(string? message)
{
return new PortableMaClientStatus(PortableMaClientStatusType.Warning, message);
}
public static PortableMaClientStatus Error(string? message)
{
return new PortableMaClientStatus(PortableMaClientStatusType.Error, message);
}
}
public enum PortableMaClientStatusType
{
Ok,
Warning,
Error
}
+29
View File
@@ -0,0 +1,29 @@
using System;
namespace NebulaAuth.Model.MAAC;
public struct Result<T>
{
public bool IsSuccess { get; }
public T Data => !IsSuccess ? throw new InvalidOperationException("No data available") : _data!;
public Exception? Exception { get; }
private readonly T? _data;
private Result(bool success, T? data, Exception? exception)
{
IsSuccess = success;
_data = data;
Exception = exception;
}
public static Result<T> Success(T data)
{
return new Result<T>(true, data, null);
}
public static Result<T> Error(Exception ex)
{
return new Result<T>(false, default, ex);
}
}
+42 -24
View File
@@ -8,6 +8,7 @@ using AchiesUtilities.Web.Proxy;
using NebulaAuth.Model.Entities;
using SteamLib.Api.Mobile;
using SteamLib.Api.Services;
using SteamLib.Api.Trade;
using SteamLib.Authentication;
using SteamLib.Exceptions.Authorization;
using SteamLib.ProtoCore.Services;
@@ -19,11 +20,8 @@ namespace NebulaAuth.Model;
public static class MaClient
{
private static HttpClientHandler ClientHandler { get; }
private static HttpClient Client { get; }
private static DynamicProxy Proxy { get; }
public static ProxyData? DefaultProxy { get; set; }
@@ -39,21 +37,9 @@ public static class MaClient
public static void SetAccount(Mafile? account)
{
ClientHandler.CookieContainer.ClearAllCookies();
if (account != null)
{
if (account.SessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
Proxy.SetData(account.Proxy?.Data);
}
if (account == null) return;
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
Proxy.SetData(account.Proxy?.Data);
}
public static Task<IEnumerable<Confirmation>> GetConfirmations(Mafile mafile)
@@ -78,26 +64,58 @@ public static class MaClient
return SessionHandler.RefreshMobileToken(new HttpClientHandlerPair(Client, ClientHandler), mafile);
}
public static Task<bool> SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
public static async Task<bool> SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
{
ValidateMafile(mafile);
SetProxy(mafile);
return SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId, mafile,
var res = await SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId,
mafile,
confirm);
if (!res && confirmation.ConfType == ConfirmationType.Trade)
{
Shell.Logger.Warn("Failed to send trade confirmation for {accountName}. Sending ack", mafile.AccountName);
await SteamTradeApi.Acknowledge(Client, mafile.SessionData.SessionId);
await Task.Delay(10);
}
else
{
return res;
}
return await SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId,
mafile,
confirm);
}
public static Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations,
public static async Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations,
bool confirm)
{
var enumerable = confirmations.ToList();
if (enumerable.Count == 0)
{
return Task.FromResult(false);
return false;
}
ValidateMafile(mafile);
SetProxy(mafile);
return SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, enumerable, mafile.SessionData!.SteamId,
var res = await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, enumerable,
mafile.SessionData!.SteamId,
mafile, confirm);
if (!res && enumerable.Any(c => c.ConfType == ConfirmationType.Trade))
{
Shell.Logger.Warn("Failed to send trade confirmations for {accountName}. Sending ack", mafile.AccountName);
await SteamTradeApi.Acknowledge(Client, mafile.SessionData.SessionId);
await Task.Delay(10);
}
else
{
return res;
}
return await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, enumerable,
mafile.SessionData!.SteamId,
mafile, confirm);
}
@@ -125,7 +143,7 @@ public static class MaClient
if (mafile.SessionData.RefreshToken.IsExpired)
throw new SessionPermanentlyExpiredException();
if (ignoreAccessToken == false)
if (!ignoreAccessToken)
{
var access = mafile.SessionData.GetMobileToken();
if (access == null || access.Value.IsExpired)
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.MafileExport;
public class ExportResult
{
public string? Error { get; }
public Dictionary<Mafile, string>? Exported { get; }
public List<string>? NotFound { get; }
public List<string>? Conflict { get; }
[MemberNotNullWhen(true, nameof(Exported))]
[MemberNotNullWhen(true, nameof(NotFound))]
[MemberNotNullWhen(true, nameof(Conflict))]
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
private ExportResult(string? error, Dictionary<Mafile, string>? exported, List<string>? notFound,
List<string>? conflict)
{
Error = error;
Exported = exported;
NotFound = notFound;
Conflict = conflict;
}
public ExportResult(Dictionary<Mafile, string> exported, List<string> notFound, List<string> conflict)
: this(null, exported, notFound, conflict)
{
}
public ExportResult(string? error)
: this(error, null, null, null)
{
}
}
@@ -0,0 +1,16 @@
namespace NebulaAuth.Model.MafileExport;
public class MafileExportTemplate
{
public string Name { get; set; }
public bool UseLoginAsMafileName { get; set; }
public bool IncludeSharedSecret { get; set; }
public bool IncludeIdentitySecret { get; set; }
public bool IncludeRCode { get; set; }
public bool IncludeSessionData { get; set; }
public bool IncludeOtherInfo { get; set; }
public bool IncludeNebulaProxy { get; set; }
public bool IncludeNebulaPassword { get; set; }
public bool IncludeNebulaGroup { get; set; }
public string? Path { get; set; }
}
@@ -0,0 +1,166 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLibForked.Models.Session;
using SteamLibForked.Models.SteamIds;
namespace NebulaAuth.Model.MafileExport;
public static class MafileExporter
{
public static async Task<ExportResult> ExportMafiles(IEnumerable<string> keys, MafileExportTemplate template)
{
if (template.Path == null || !PathIsValid(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("InvalidPath", "MafileExporter.InvalidPath"));
}
if (!Directory.Exists(template.Path))
{
Directory.CreateDirectory(template.Path);
}
if (IsNebulaUtilizedDirectory(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("NebulaUtilizedDirectory",
"MafileExporter.NebulaUtilizedDirectory"));
}
if (!IsAllowedToWrite(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("AccessDenied", "MafileExporter.AccessDenied"));
}
var exported = new Dictionary<Mafile, string>();
var notFound = new List<string>();
var conflict = new List<string>();
foreach (var key in keys)
{
SteamId? steamId = null;
if (SteamId64.TryParse(key, out var id64))
{
steamId = id64;
}
var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId);
if (maf != null)
{
var fileName = await ExportMafile(template.Path, template, maf);
if (fileName == null)
{
conflict.Add(key);
continue;
}
exported[maf] = fileName;
}
else
{
notFound.Add(key);
}
}
return new ExportResult(exported, notFound, conflict);
}
private static async Task<string?> ExportMafile(string path, MafileExportTemplate template, Mafile mafile)
{
// We preserve SteamId even if other info is not included
var session = template.IncludeSessionData
? mafile.SessionData
: new MobileSessionData(null!, mafile.SteamId, default, null, tokens: null);
var serializeMaf = new Mafile
{
SharedSecret = template.IncludeSharedSecret ? mafile.SharedSecret : null!,
IdentitySecret = template.IncludeIdentitySecret ? mafile.IdentitySecret : null!,
DeviceId = template.IncludeIdentitySecret ? mafile.DeviceId : null!,
RevocationCode = template.IncludeRCode ? mafile.RevocationCode : null!,
AccountName = mafile.AccountName,
SessionData = session,
SteamId = mafile.SteamId,
ServerTime = template.IncludeOtherInfo ? mafile.ServerTime : 0,
SerialNumber = template.IncludeOtherInfo ? mafile.SerialNumber : 0,
Uri = template.IncludeOtherInfo ? mafile.Uri : null!,
TokenGid = template.IncludeOtherInfo ? mafile.TokenGid : null!,
Secret1 = template.IncludeOtherInfo ? mafile.Secret1 : null!,
Proxy = template.IncludeNebulaProxy ? mafile.Proxy : null!,
Group = template.IncludeNebulaGroup ? mafile.Group : null!,
Password = template.IncludeNebulaPassword ? mafile.Password : null!,
LinkedClient = null,
Filename = null
};
var serialized = NebulaSerializer.SerializeMafile(serializeMaf);
var strategy = template.UseLoginAsMafileName ? MafileNamingStrategy.Login : MafileNamingStrategy.SteamId;
var fileName = strategy.GetMafileName(serializeMaf);
var fullPath = Path.Combine(path, fileName);
if (File.Exists(fullPath))
{
return null;
}
await File.WriteAllTextAsync(fullPath, serialized);
return fullPath;
}
private static bool PathIsValid(string path)
{
if (string.IsNullOrWhiteSpace(path)) return false;
try
{
var full = Path.GetFullPath(path);
var invalidPathChars = Path.GetInvalidPathChars();
if (full.IndexOfAny(invalidPathChars) >= 0) return false;
var invalidFileChars = Path.GetInvalidFileNameChars();
foreach (var part in full.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
{
if (string.IsNullOrEmpty(part)) continue;
if (part is [_, ':']) continue;
if (part.IndexOfAny(invalidFileChars) >= 0) return false;
}
return true;
}
catch
{
return false;
}
}
private static bool IsAllowedToWrite(string path)
{
try
{
var testFilePath = Path.Combine(path, "test.tmp");
using (var fs = File.Create(testFilePath))
{
}
File.Delete(testFilePath);
return true;
}
catch
{
return false;
}
}
private static bool IsNebulaUtilizedDirectory(string path)
{
return Storage.MafilesDirectories.Any(x => Path.GetFullPath(path).EqualsIgnoreCase(x));
}
}
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MafileExport;
public static class MafileExporterStorage
{
private const string FILE_NAME = "export_templates.json";
public static ObservableCollection<MafileExportTemplate> Templates { get; } = new();
public static void Initialize()
{
LoadOrCreateTemplate();
}
public static MafileExportTemplate? GetTemplate(string name)
{
return Templates.FirstOrDefault(x => x.Name == name);
}
public static void AddTemplate(MafileExportTemplate template)
{
Templates.Add(template);
Save();
}
public static void DeleteTemplate(MafileExportTemplate template)
{
if (Templates.Remove(template))
{
Save();
}
}
private static void LoadOrCreateTemplate()
{
if (!File.Exists(FILE_NAME))
{
Templates.Clear();
foreach (var mafileExportTemplate in CreateDefaultTemplate())
{
Templates.Add(mafileExportTemplate);
}
Save();
return;
}
var json = File.ReadAllText(FILE_NAME);
try
{
var loadedTemplates = JsonConvert.DeserializeObject<List<MafileExportTemplate>>(json);
if (loadedTemplates != null)
{
Templates.Clear();
foreach (var tmpl in loadedTemplates)
{
Templates.Add(tmpl);
}
}
}
catch (Exception ex)
{
Shell.Logger.Error(ex, $"Failed to deserialize {FILE_NAME}");
Templates.Clear();
Save();
}
}
public static void Save()
{
var json = JsonConvert.SerializeObject(Templates, Formatting.Indented);
File.WriteAllText(FILE_NAME, json);
}
private static IEnumerable<MafileExportTemplate> CreateDefaultTemplate()
{
return
[
new MafileExportTemplate
{
Name = "Default",
UseLoginAsMafileName = false,
IncludeSharedSecret = true,
IncludeIdentitySecret = true,
IncludeRCode = true,
IncludeSessionData = true,
IncludeOtherInfo = true,
IncludeNebulaProxy = true,
IncludeNebulaPassword = true,
IncludeNebulaGroup = true,
Path = null
},
new MafileExportTemplate
{
Name = "Auth only",
UseLoginAsMafileName = true,
IncludeSharedSecret = true,
IncludeIdentitySecret = false,
IncludeRCode = false,
IncludeSessionData = false,
IncludeOtherInfo = false,
IncludeNebulaProxy = false,
IncludeNebulaPassword = false,
IncludeNebulaGroup = false,
Path = null
}
];
}
}
@@ -0,0 +1,89 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
namespace NebulaAuth.Model.Mafiles;
public interface IMafileNamingStrategy
{
bool CanNameMafile(Mafile mafile);
string GetMafileName(Mafile mafile);
}
public class MafileNamingStrategy : IMafileNamingStrategy
{
public const string DEF_EXTENSION = ".mafile";
public static readonly IDictionary<string, Func<Mafile, string?>> Placeholders =
new Dictionary<string, Func<Mafile, string?>>
{
{"{steamid}", GetSteamId},
{"{login}", GetLogin},
{"{group}", GetGroup},
{"{servertime}", GetServerTime}
}.ToFrozenDictionary();
public static MafileNamingStrategy SteamId { get; } = new("{steamid}");
public static MafileNamingStrategy Login { get; } = new("{login}");
public string Pattern { get; }
public bool IncludeExtension { get; }
public MafileNamingStrategy(string pattern, bool includeExtension = true)
{
Pattern = pattern;
IncludeExtension = includeExtension;
}
public bool CanNameMafile(Mafile mafile)
{
if (string.IsNullOrWhiteSpace(Pattern)) return false;
var fileName = ApplyPatternSubstitution(mafile, Pattern, IncludeExtension);
return FileNameValidator.IsValidFileName(fileName);
}
public string GetMafileName(Mafile mafile)
{
if (!CanNameMafile(mafile))
throw new InvalidOperationException($"Cannot name mafile with the current pattern {Pattern}");
return ApplyPatternSubstitution(mafile, Pattern, IncludeExtension);
}
private static string ApplyPatternSubstitution(Mafile mafile, string pattern, bool includeExtension)
{
var result = pattern;
foreach (var placeholder in Placeholders)
{
if (result.Contains(placeholder.Key, StringComparison.OrdinalIgnoreCase))
{
var value = placeholder.Value(mafile);
result = result.Replace(placeholder.Key, value, StringComparison.OrdinalIgnoreCase);
}
}
return includeExtension ? result + DEF_EXTENSION : result;
}
private static string GetSteamId(Mafile mafile)
{
return mafile.SteamId.Steam64.ToString();
}
private static string GetLogin(Mafile mafile)
{
return mafile.AccountName;
}
private static string? GetGroup(Mafile mafile)
{
return mafile.Group;
}
private static string GetServerTime(Mafile mafile)
{
return mafile.ServerTime.ToString();
}
}
@@ -0,0 +1,11 @@
namespace NebulaAuth.Model.Mafiles;
public class MafilesBulkRenameResult
{
public int Total { get; set; }
public int Renamed { get; set; }
public int NotRenamed => Errors + Conflict;
public int Errors { get; set; }
public int Conflict { get; set; }
public string BackupFileName { get; set; }
}
@@ -0,0 +1,38 @@
using System.IO;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.Mafiles;
internal static class MafilesStorageHelper
{
/// <summary>
/// Returns <see cref="Mafile.Filename" /> or creates a new one and updates the property.
/// <see cref="Mafile.Filename" /> is always not <see langword="null" /> after this method call.
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static string GetOrUpdateMafilePath(Mafile data)
{
if (data.Filename != null)
{
return Path.Combine(Storage.MafilesDirectory, data.Filename);
}
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
data.Filename = fileName;
return Path.Combine(Storage.MafilesDirectory, fileName);
}
/// <summary>
/// Creates mafile file name according to the current settings.
/// </summary>
/// <param name="data"></param>
/// <param name="useAccountName"></param>
/// <returns></returns>
public static string CreateMafileFileName(Mafile data, bool useAccountName)
{
return useAccountName
? MafileNamingStrategy.Login.GetMafileName(data)
: MafileNamingStrategy.SteamId.GetMafileName(data);
}
}
+287
View File
@@ -0,0 +1,287 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using SteamLib;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile;
namespace NebulaAuth.Model.Mafiles;
public static class Storage
{
public const string DIR_MAFILES = "maFiles";
public const string DIR_REMOVED_MAFILES = "maFiles_removed";
public const string DIR_BACKUP_MAFILES = "maFiles_backup";
public static readonly string[] MafilesDirectories;
public static string MafilesDirectory { get; } = Path.GetFullPath(DIR_MAFILES);
public static string RemovedMafilesDirectory { get; } = Path.GetFullPath(DIR_REMOVED_MAFILES);
public static string BackupMafilesDirectory { get; } = Path.GetFullPath(DIR_BACKUP_MAFILES);
public static ObservableCollection<Mafile> MaFiles { get; private set; } = [];
static Storage()
{
MafilesDirectories = [MafilesDirectory, RemovedMafilesDirectory, BackupMafilesDirectory];
}
public static async Task Initialize(int threadCount, CancellationToken token = default)
{
Directory.CreateDirectory(MafilesDirectory);
Directory.CreateDirectory(RemovedMafilesDirectory);
Directory.CreateDirectory(BackupMafilesDirectory);
var files = Directory
.GetFiles(MafilesDirectory)
.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile"))
.ToList();
var localList = new ConcurrentBag<Mafile>();
await Task.Run(() =>
{
return Parallel.ForEachAsync(files,
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
async (file, ct) =>
{
try
{
var data = await ReadMafileAsync(file);
localList.Add(data);
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Can't load mafile {file}", Path.GetFileName(file));
}
});
}, token);
MaFiles = new ObservableCollection<Mafile>(localList.OrderBy(m => m.AccountName));
}
/// <summary>
/// </summary>
/// <param name="path"></param>
/// <param name="overwrite"></param>
/// <exception cref="FormatException"></exception>
/// <exception cref="SessionInvalidException"></exception>
/// <exception cref="IOException"></exception>
public static async Task AddNewMafile(string path, bool overwrite)
{
Mafile data;
try
{
data = await ReadMafileAsync(path);
data.Filename = null;
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
{
Shell.Logger.Warn(ex, "Can't load mafile");
throw new FormatException("File data is not valid", ex);
}
if (string.IsNullOrWhiteSpace(data.AccountName))
throw new FormatException("File data is not valid. Missing AccountName");
try
{
_ = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
}
catch (Exception ex)
{
throw new FormatException("Can't generate code on this mafile", ex);
}
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
{
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
}
await SaveMafileAsync(data);
}
public static async Task<Mafile> ReadMafileAsync(string path)
{
var str = await File.ReadAllTextAsync(path);
return NebulaSerializer.Deserialize(str, path);
}
public static async Task SaveMafileAsync(Mafile data)
{
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
// Replace if exist
for (var i = 0; i < MaFiles.Count; i++)
{
var existed = MaFiles[i];
if (ReferenceEquals(existed, data) || (existed.Filename != null && existed.Filename == data.Filename))
{
MaFiles[i] = data;
return;
}
}
MaFiles.Add(data);
}
public static void UpdateMafile(Mafile data)
{
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
}
public static async Task UpdateMafileAsync(Mafile data)
{
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
}
public static void MoveToRemoved(Mafile data)
{
var sourcePath = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var fileName = Path.GetFileNameWithoutExtension(data.Filename!);
var destinationPathBase = Path.Combine(DIR_REMOVED_MAFILES, fileName);
var destinationPathFinal = Path.ChangeExtension(destinationPathBase, MafileNamingStrategy.DEF_EXTENSION);
var i = 0;
while (File.Exists(destinationPathFinal))
{
i++;
destinationPathFinal = destinationPathBase + $" ({i})";
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
}
File.Copy(sourcePath, destinationPathFinal, false);
File.Delete(sourcePath);
MaFiles.Remove(data);
}
public static string? TryGetMafilePath(Mafile data)
{
if (data.Filename == null) return null;
return Path.Combine(MafilesDirectory, data.Filename);
}
public static void WriteBackup(MobileDataExtended data)
{
var json = NebulaSerializer.SerializeMafile(data, null);
WriteBackup(data.AccountName, json);
}
public static void WriteBackup(string accountName, string data)
{
Directory.CreateDirectory(DIR_BACKUP_MAFILES);
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
}
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
IProgress<double>? progress = null)
{
if (MaFiles.Count == 0) return new MafilesBulkRenameResult();
var now = DateTime.Now;
var backupFileName = $"rename_backup_{now:yyyy-MM-dd_HH-mm-ss}.zip";
var zipPath = Path.Combine(BackupMafilesDirectory, backupFileName);
await using (var zipStream = new FileStream(zipPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, false);
var files = Directory
.EnumerateFiles(MafilesDirectory, "*.*", SearchOption.TopDirectoryOnly)
.Where(f => Path.GetExtension(f).Equals(".mafile", StringComparison.OrdinalIgnoreCase))
.ToList();
var counter = 0;
foreach (var file in files)
{
counter++;
var entry = archive.CreateEntry(Path.GetFileName(file), CompressionLevel.Optimal);
await using var entryStream = entry.Open();
await using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096,
true);
await fileStream.CopyToAsync(entryStream);
if (counter % 5 == 0)
{
progress?.Report(counter / (double) files.Count);
await Task.Delay(10);
}
}
}
var mafiles = MaFiles.ToList();
var res = new MafilesBulkRenameResult
{
Total = mafiles.Count,
BackupFileName = backupFileName
};
var dic = new Dictionary<string, string>();
foreach (var mafile in mafiles)
{
try
{
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
if (mafile.Filename == targetFileName || mafile.Filename == null)
{
res.Renamed += 1;
continue;
}
var sourceFileName = mafile.Filename;
var fullSourcePath = Path.Combine(MafilesDirectory, sourceFileName);
var fullTargetPath = Path.Combine(MafilesDirectory, targetFileName);
if (!File.Exists(fullSourcePath))
{
Shell.Logger.Warn("Can't rename mafile {old} to {new} because source file not found",
mafile.Filename, targetFileName);
IncErrors();
continue;
}
if (File.Exists(fullTargetPath))
{
Shell.Logger.Warn("Can't rename mafile {old} to {new} because target file already exist",
mafile.Filename, targetFileName);
res.Conflict += 1;
continue;
}
File.Move(fullSourcePath, fullTargetPath);
dic[sourceFileName] = targetFileName;
res.Renamed += 1;
mafile.Filename = targetFileName;
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Error renaming mafile {file} {accountName}", mafile.Filename,
mafile.AccountName);
IncErrors();
}
}
MAACStorage.NotifyMafilesRenamed(dic);
return res;
void IncErrors()
{
res.Errors += 1;
}
}
}
+6 -4
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using Newtonsoft.Json;
@@ -32,21 +33,22 @@ public static class NebulaSerializer
}
public static Mafile Deserialize(string cont)
public static Mafile Deserialize(string cont, string path)
{
var data = Serializer.Deserialize(cont);
var mobileData = data.Data;
var info = data.Info;
if (info.IsExtended == false)
if (!info.IsExtended)
throw new FormatException("Mafile is not extended data");
var props = info.UnusedProperties ?? new Dictionary<string, JProperty>();
var props = info.UnusedProperties ?? [];
var proxy = GetPropertyValue<MaProxy>("Proxy", props);
var group = GetPropertyValue<string>("Group", props);
var password = GetPropertyValue<string>("Password", props);
var mafile = Mafile.FromMobileDataExtended((MobileDataExtended) mobileData, proxy, group, password);
mafile.Filename = Path.GetFileName(path);
if (!info.SteamIdValid)
throw new MafileNeedReloginException(mafile);
@@ -56,7 +58,7 @@ public static class NebulaSerializer
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
{
if (dictionary.TryGetValue(name, out var prop) == false) return default;
if (!dictionary.TryGetValue(name, out var prop)) return default;
var value = prop.Value;
try
{
+5 -3
View File
@@ -28,7 +28,7 @@ public static class ProxyStorage
static ProxyStorage()
{
if (File.Exists("proxies.json") == false)
if (!File.Exists("proxies.json"))
return;
try
{
@@ -46,7 +46,9 @@ public static class ProxyStorage
}
catch (Exception ex)
{
SnackbarController.SendSnackbar("Ошибка при загрузке прокси");
Shell.Logger.Error(ex, "Error while loading proxies");
SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("Error when loading proxies",
"ProxyStorage", "ErrorWhileLoadingProxies"));
SnackbarController.SendSnackbar(ex.Message);
}
}
@@ -93,7 +95,7 @@ public static class ProxyStorage
Save();
}
public static void OrderCollection() //RETHINK: maybe there is a better way to handle it
public static void SortCollection() //RETHINK: maybe there is a better way to handle it
{
var proxies = Proxies.OrderBy(p => p.Key)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
@@ -34,37 +34,28 @@ public static partial class SessionHandler
string? snackbarPrefix = null)
{
using var scope = Shell.Logger.PushScopeProperty("Scope", "SessionHandler");
var mobileTokenExpired = MobileTokenExpired(mafile);
var refreshTokenExpired = RefreshTokenExpired(mafile);
var password = GetPassword(mafile);
Exception? currentException = null;
if (!mobileTokenExpired)
if (!mafile.MobileTokenExpired())
{
try
{
return await func();
}
catch (SessionInvalidException ex)
when (refreshTokenExpired == false || password != null)
{
if (ex is SessionPermanentlyExpiredException)
{
Shell.Logger.Debug(ex, "RefreshToken on mafile {name} {steamid} is expired", mafile.AccountName,
mafile.SessionData?.SteamId);
refreshTokenExpired = true;
}
else
{
Shell.Logger.Debug(ex, "MobileToken on mafile {name} {steamid} is expired", mafile.AccountName,
mafile.SessionData?.SteamId);
}
Shell.Logger.Warn(ex, "Session on mafile {name} {steamid} is invalid when mobile token is not expired",
mafile.AccountName, mafile.SessionData?.SteamId);
currentException = ex;
}
}
//State: mobileToken invalid/expired, refreshToken maybe not expired
if (!refreshTokenExpired)
if (!mafile.RefreshTokenExpired())
{
Shell.Logger.Info("Trying to refresh session on mafile {name} {steamid} using refresh token",
mafile.AccountName,
mafile.SessionData?.SteamId);
var refreshed = await RefreshInternal(chp, mafile);
if (refreshed)
{
@@ -77,19 +68,14 @@ public static partial class SessionHandler
}
catch (SessionInvalidException ex)
{
Shell.Logger.Info(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
Shell.Logger.Warn(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
mafile.AccountName, mafile.SessionData?.SteamId);
if (password == null)
throw;
currentException = ex;
}
}
}
Shell.Logger.Debug("Session on mafile {name} {steamid} is invalid/expired", mafile.AccountName,
mafile.SessionData?.SteamId);
//State: mobileToken invalid/expired, refreshToken invalid/expired
if (password != null)
if (mafile.HasPassword(out var password))
{
var logged = await LoginAgainInternal(chp, mafile, password, true);
if (logged)
@@ -100,41 +86,10 @@ public static partial class SessionHandler
}
}
//Nothing to do more, everything is expired
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG,
currentException);
}
private static bool MobileTokenExpired(Mafile mafile)
{
var mobileToken = mafile.SessionData?.GetMobileToken();
return mobileToken == null || mobileToken.Value.IsExpired;
}
private static bool RefreshTokenExpired(Mafile mafile)
{
var refreshToken = mafile.SessionData?.RefreshToken;
return refreshToken == null || refreshToken.Value.IsExpired;
}
private static string? GetPassword(Mafile mafile)
{
try
{
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
{
return PHandler.Decrypt(mafile.Password);
}
}
catch
{
// ignored
}
return null;
}
private static async Task<bool> RefreshInternal(HttpClientHandlerPair chp, Mafile mafile)
{
try
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using AchiesUtilities.Web.Models;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLib.Api.Mobile;
using SteamLib.Authentication;
using SteamLib.Authentication.LoginV2;
@@ -27,9 +28,8 @@ public partial class SessionHandler //API
mafile.SessionData.SetMobileToken(newToken);
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
mafile.SetSessionData(mafile.SessionData);
Storage.UpdateMafile(mafile);
await Storage.UpdateMafileAsync(mafile);
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
}
@@ -49,10 +49,9 @@ public partial class SessionHandler //API
AdmissionHelper.TransferCommunityCookies(chp.Handler.CookieContainer);
//Triggers PropertyChanged event for PortableMaClient handling session updated from MaClient
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
mafile.SetSessionData((MobileSessionData) result);
if (PHandler.IsPasswordSet)
mafile.Password = savePassword ? PHandler.Encrypt(password) : null;
Storage.UpdateMafile(mafile);
await Storage.UpdateMafileAsync(mafile);
}
}
@@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model;
public partial class SessionHandler //Helpers
{
private static bool MobileTokenExpired(this Mafile mafile)
{
var mobileToken = mafile.SessionData?.GetMobileToken();
return mobileToken == null || mobileToken.Value.IsExpired;
}
private static bool RefreshTokenExpired(this Mafile mafile)
{
var refreshToken = mafile.SessionData?.RefreshToken;
return refreshToken == null || refreshToken.Value.IsExpired;
}
private static bool HasPassword(this Mafile mafile, [NotNullWhen(true)] out string? plainPassword)
{
plainPassword = GetPassword(mafile);
return !string.IsNullOrWhiteSpace(plainPassword);
}
private static string? GetPassword(Mafile mafile)
{
try
{
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
{
return PHandler.Decrypt(mafile.Password);
}
}
catch
{
// ignored
}
return null;
}
}
+15 -5
View File
@@ -5,6 +5,7 @@ using System.IO;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using NebulaAuth.Core;
using NebulaAuth.Utility;
using Newtonsoft.Json;
namespace NebulaAuth.Model;
@@ -24,10 +25,11 @@ public partial class Settings : ObservableObject
static Settings()
{
if (File.Exists("settings.json") == false)
if (!File.Exists("settings.json"))
{
Instance = new Settings();
Instance.PropertyChanged += SettingsOnPropertyChanged;
Instance.Language = LanguageUtility.DetectPreferredLanguage();
return;
}
@@ -39,7 +41,8 @@ public partial class Settings : ObservableObject
}
catch (Exception ex)
{
SnackbarController.SendSnackbar("Ошибка при загрузке настроек. Настройки были сброшены");
SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("Error when loading settings", "Settings",
"ErrorWhileLoadingSettings"));
SnackbarController.SendSnackbar(ex.Message);
Instance = new Settings();
}
@@ -47,6 +50,7 @@ public partial class Settings : ObservableObject
Instance.PropertyChanged += SettingsOnPropertyChanged;
}
private static void SettingsOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Save();
@@ -68,8 +72,9 @@ public partial class Settings : ObservableObject
Instance.BackgroundOpacity = 1.0;
Instance.BackgroundGamma = 0.0;
Instance.LeftOpacity = 0.4;
Instance.RightOpacity = 0.8;
Instance.RightOpacity = 0.4;
Instance.ApplyBlurBackground = true;
Instance.RippleDisabled = false;
Save();
}
@@ -89,16 +94,21 @@ public partial class Settings : ObservableObject
[ObservableProperty] private bool _legacyMode = true;
[ObservableProperty] private bool _allowAutoUpdate;
[ObservableProperty] private bool _useAccountNameAsMafileName;
[ObservableProperty] private bool _ignorePatchTuesdayErrors;
[ObservableProperty] private BackgroundMode _backgroundMode = BackgroundMode.Default;
[ObservableProperty] private double _leftOpacity = 0.4;
[ObservableProperty] private double _rightOpacity = 1.0;
[ObservableProperty] private double _rightOpacity = 0.4;
[ObservableProperty] private double _backgroundBlur;
[ObservableProperty] private double _backgroundOpacity = 1;
[ObservableProperty] private double _backgroundGamma;
[ObservableProperty] private bool _applyBlurBackground = true;
[ObservableProperty] private ThemeType _themeType = ThemeType.Default;
[ObservableProperty] private bool _rippleDisabled;
[ObservableProperty] private bool _proxyManagerDisplayProtocol;
[ObservableProperty] private bool _proxyManagerDisplayCredentials;
[ObservableProperty] private TimeSpan _maacErrorThreshold = TimeSpan.FromHours(3);
[ObservableProperty] private TimeSpan _maacRetryInterval = TimeSpan.FromMinutes(15);
#endregion
}
+16 -2
View File
@@ -1,6 +1,12 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NebulaAuth.Core;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.MafileExport;
using NebulaAuth.Model.Mafiles;
using NLog;
using NLog.Extensions.Logging;
using SteamLib.Core;
@@ -14,8 +20,11 @@ public static class Shell
public static Logger Logger { get; private set; } = null!;
public static ILogger ExtensionsLogger { get; private set; } = null!;
public static void Initialize()
public static async Task Initialize()
{
File.Delete("log.log");
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Logger = LogManager.GetLogger("Logger");
var lp = new NLogLoggerProvider();
var logger = lp.CreateLogger("SteamLib");
@@ -27,13 +36,18 @@ public static class Shell
try
{
TimeAligner.AlignTime();
await TimeAligner.AlignTimeAsync();
}
catch (Exception ex)
{
throw new CantAlignTimeException("", ex);
}
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
MAACStorage.Initialize();
MafileExporterStorage.Initialize();
ExtensionsLogger.LogDebug("Application started");
}
-287
View File
@@ -1,287 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using SteamLib;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile;
namespace NebulaAuth.Model;
public static class Storage
{
public const string MAFILE_F = "maFiles";
public const string REMOVED_F = "maFiles_removed";
private static int _duplicateFound;
public static int DuplicateFound => _duplicateFound;
public static string MafileFolder { get; } = Path.GetFullPath(MAFILE_F);
public static string RemovedMafileFolder { get; } = Path.GetFullPath(REMOVED_F);
public static ObservableCollection<Mafile> MaFiles { get; private set; } = new();
static Storage()
{
}
public static async Task Initialize(int threadCount, CancellationToken token = default)
{
if (!Directory.Exists(MafileFolder))
Directory.CreateDirectory(MafileFolder);
if (!Directory.Exists(RemovedMafileFolder))
Directory.CreateDirectory(RemovedMafileFolder);
var comparer = new MafileNameComparer(Settings.Instance.UseAccountNameAsMafileName);
var files = Directory
.GetFiles(MafileFolder)
.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile"))
.Order(comparer)
.ToList();
var hashNames = new ConcurrentDictionary<string, byte>();
var hashIds = new ConcurrentDictionary<SteamId, byte>();
var localList = new ConcurrentBag<Mafile>();
var processed = 0;
await Task.Run(() =>
{
return Parallel.ForEachAsync(files,
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
async (file, ct) =>
{
try
{
var data = await ReadMafileAsync(file);
if (!hashNames.TryAdd(data.AccountName, 0) ||
(data.SessionData != null && !hashIds.TryAdd(data.SteamId, 0)))
{
Interlocked.Increment(ref _duplicateFound);
Shell.Logger.Error("Duplicate mafile {file}", Path.GetFileName(file));
}
localList.Add(data);
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Can't load mafile {file}", Path.GetFileName(file));
}
});
}, token);
MaFiles = new ObservableCollection<Mafile>(localList.OrderBy(m => m.AccountName));
}
/// <summary>
/// </summary>
/// <param name="path"></param>
/// <param name="overwrite"></param>
/// <exception cref="FormatException"></exception>
/// <exception cref="SessionInvalidException"></exception>
/// <exception cref="IOException"></exception>
public static void AddNewMafile(string path, bool overwrite)
{
Mafile data;
try
{
data = ReadMafile(path);
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
{
Shell.Logger.Warn(ex, "Can't load mafile");
throw new FormatException("File data is not valid", ex);
}
if (string.IsNullOrWhiteSpace(data.AccountName))
throw new FormatException("File data is not valid. Missing AccountName");
try
{
_ = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
}
catch (Exception ex)
{
throw new FormatException("Can't generate code on this mafile", ex);
}
if (overwrite == false && File.Exists(CreatePathForMafile(data)))
{
throw new IOException("File already exist and overwrite is False");
}
SaveMafile(data);
}
public static Mafile ReadMafile(string path)
{
var str = File.ReadAllText(path);
return NebulaSerializer.Deserialize(str);
}
public static async Task<Mafile> ReadMafileAsync(string path)
{
var str = await File.ReadAllTextAsync(path);
return NebulaSerializer.Deserialize(str);
}
public static void SaveMafile(Mafile data)
{
var path = CreatePathForMafile(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
if (existed != null)
{
var index = MaFiles.IndexOf(existed);
MaFiles[index] = data;
}
else
{
MaFiles.Add(data);
}
}
public static void UpdateMafile(Mafile data)
{
var path = CreatePathForMafile(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
}
public static void MoveToRemoved(Mafile data)
{
var path = CreatePathForMafile(data);
var copyPath = Path.Combine(REMOVED_F, data.SteamId + ".mafile");
var copyPathCompleted = copyPath;
var i = 0;
while (File.Exists(copyPathCompleted))
{
i++;
copyPathCompleted = copyPath + $" ({i})";
}
File.Copy(path, copyPathCompleted, false);
File.Delete(path);
MaFiles.Remove(data);
}
private static string CreatePathForMafile(Mafile data)
{
var fileName = Settings.Instance.UseAccountNameAsMafileName
? CreateFileNameWithAccountName(data.AccountName)
: CreateFileNameWithSteamId(data.SteamId);
return Path.Combine(MafileFolder, fileName);
}
private static string CreateFileNameWithAccountName(string accountName)
{
return accountName + ".mafile";
}
private static string CreateFileNameWithSteamId(SteamId steamId)
{
return steamId.Steam64.Id + ".mafile";
}
public static string? TryFindMafilePath(Mafile data)
{
var pathFileName = Path.Combine(MafileFolder, CreateFileNameWithAccountName(data.AccountName));
string? pathSteamId = null;
if (data.SessionData != null)
{
pathSteamId = Path.Combine(MafileFolder, CreateFileNameWithSteamId(data.SteamId));
}
var steamIdExist = pathSteamId != null && File.Exists(pathSteamId);
var accountNameExist = File.Exists(pathFileName);
if (steamIdExist && accountNameExist)
{
return Settings.Instance.UseAccountNameAsMafileName ? pathFileName : pathSteamId;
}
if (steamIdExist ^ accountNameExist)
{
return steamIdExist ? pathSteamId : pathFileName;
}
return null;
}
public static void BackupHandler(MobileDataExtended data)
{
if (Directory.Exists("mafiles_backup") == false)
{
Directory.CreateDirectory("mafiles_backup");
}
var json = NebulaSerializer.SerializeMafile(data, null);
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"),
json);
}
public static void BackupHandlerStr(string accountName, string data)
{
if (Directory.Exists("mafiles_backup") == false)
{
Directory.CreateDirectory("mafiles_backup");
}
File.WriteAllText(Path.Combine("mafiles_backup", accountName + ".mafile"),
data);
}
}
//TODO: Refactor
//TODO: use numeric orderer when .net 10 released
internal class MafileNameComparer : IComparer<string>
{
private const string MAF_64_START = "765";
private static readonly IComparer<string> DefaultComparer = Comparer<string>.Default;
public bool MafileNameMode { get; }
public MafileNameComparer(bool mafileNameMode)
{
MafileNameMode = mafileNameMode;
}
public int Compare(string? x, string? y)
{
if (x == null && y == null) return 0;
if (x == null) return -1;
if (y == null) return 1;
var xisSteamId = Path.GetFileName(x).StartsWith(MAF_64_START);
var yisSteamId = Path.GetFileName(y).StartsWith(MAF_64_START);
if (xisSteamId ^ yisSteamId)
{
if (MafileNameMode)
{
return xisSteamId ? 1 : -1;
}
return yisSteamId ? 1 : -1;
}
return DefaultComparer.Compare(x, y);
}
}
+5 -2
View File
@@ -5,8 +5,11 @@
throwExceptions="true">
<targets>
<target xsi:type="File" name="File" fileName="log.log"
deleteOldFileOnStartup="true">
<target xsi:type="File"
name="File"
fileName="${basedir}/logs/log-${shortdate}.log"
concurrentWrites="true"
keepFileOpen="false">
<layout xsi:type='CompoundLayout'>
<layout xsi:type="JsonLayout" includeEventProperties="true" IncludeScopeProperties="true">
<attribute name="time" layout="${longdate}" />
+9 -4
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
@@ -10,7 +10,7 @@
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
<ApplicationIcon>Theme\lock.ico</ApplicationIcon>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AssemblyVersion>1.7.1</AssemblyVersion>
<AssemblyVersion>1.8.1</AssemblyVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@@ -28,7 +28,7 @@
<PackageReference Include="CodingSeb.Localization.WPF" Version="1.4.1" />
<PackageReference Include="CodingSebLocalization.Fody" Version="1.4.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
@@ -36,7 +36,7 @@
<ItemGroup>
<Resource Include="Theme\Background.png">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Resource>
<Resource Include="Theme\Background_Old.jpg">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
@@ -65,4 +65,9 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Theme\Controls\" />
</ItemGroup>
</Project>
@@ -2,5 +2,6 @@
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=components/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=viewmodel_005Clinker_005Csteps/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
-14
View File
@@ -1,14 +0,0 @@
• Есть люди которым нужна функция просмотра и завершения сессий аккаунта. Скорее всего в следующей версии это будет реализовано
• Сейчас, при наличии новой версии, отображается стандартное окно обновлений из библиотеки. Планируется сделать кастомный дизайн, для соответствия общему стилю.
• В приложении реализованы "совмещённые" подтверждения для торговой площадки. Т.е. при наличии более одного предмета, ожидающего подтверждение, можно либо отменить, либо подтвердить все. В планах добавить расширенный функционал, с возможностью точечного управления.
• Добавить кнопку "открыть мафайл" в меню "Файл" по аналогии с открытием папки
• Ускорить появление подсказок в интерфейсе
• Добавить запоминание пароля при привязке мафайла
• Функция переноса гуарда с мобильного устройства на ПК с созданием мафайла
• Добавить полное шифрование мафайлов по аналогии с SDA
• Сделать автоматический билд и загрузку обновления на Github при изменении в мастер ветке
• Антик-окно как в MarketApp, возможность сразу открыть браузер напр. на CefSharp с залогиненым аккаунтом
• Стабильность авто-подтверждений, возможность задать пользователю порог времени когда льются ошибки и только тогда выключать авто-подтверждение
• Безопасное сохранение мафайлов через .tmp / .bak
• Создание механизма накопления ошибок, чтобы исправить Patch Tuesday, условно аккаунт может игнорировать ошибки 1-2 часа (настриавемо)
-220
View File
@@ -1,220 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows;
namespace NebulaAuth.Theme;
public class FontScaleWindow : Window
{
// Using a DependencyProperty as the backing store for ScaleCoefficient. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ScaleCoefficientProperty =
DependencyProperty.Register("ScaleCoefficient", typeof(double), typeof(FontScaleWindow),
new PropertyMetadata(1d));
public static readonly DependencyProperty DefaultFontSizeProperty =
DependencyProperty.Register("DefaultFontSize", typeof(double), typeof(FontScaleWindow),
new PropertyMetadata(20d));
private static readonly DependencyPropertyKey ParentScaleWindowKey
= DependencyProperty.RegisterAttachedReadOnly(
"ParentScaleWindow",
typeof(FontScaleWindow), typeof(FontScaleWindow),
new FrameworkPropertyMetadata(default(FontScaleWindow),
FrameworkPropertyMetadataOptions.None));
public static readonly DependencyProperty ParentScaleWindowProperty
= ParentScaleWindowKey.DependencyProperty;
public static readonly DependencyProperty ResizeFontProperty = DependencyProperty.RegisterAttached(
"ResizeFont", typeof(bool), typeof(FontScaleWindow), new FrameworkPropertyMetadata(false, ResizeCallBack));
//<-------------------Window-------------------->
//<-------------------Scaling-------------------->
public static readonly DependencyProperty ScaleProperty = DependencyProperty.RegisterAttached(
"Scale", typeof(double), typeof(FontScaleWindow),
new FrameworkPropertyMetadata(0.9999d, FrameworkPropertyMetadataOptions.Inherits, ScalePropertyCallback));
//<-------------------Window-------------------->
public double DefaultFontSize
{
get => (double) GetValue(DefaultFontSizeProperty);
set => SetValue(DefaultFontSizeProperty, value);
}
public double ScaleCoefficient
{
get => (double) GetValue(ScaleCoefficientProperty);
set => SetValue(ScaleCoefficientProperty, value);
}
private readonly HashSet<FrameworkElement> _cachedObjects = new();
private readonly Func<double, double, double> _diagonal = (h, w) => Math.Sqrt(h * h + w * w);
private double _currentDiagonal;
private double _defaultDiagonal = 1;
private bool _loaded;
public FontScaleWindow()
{
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var w = (FontScaleWindow) sender;
w._defaultDiagonal = _diagonal(MinHeight, MinWidth);
w.SizeChanged += OnSizeChanged;
w.Loaded -= OnLoaded;
_loaded = true;
w.Width += 1;
w.Width -= 1;
}
private static FontScaleWindow? GetParentScaleWindow(DependencyObject element)
{
return (FontScaleWindow) element.GetValue(ParentScaleWindowProperty);
}
private static void SetParentScaleWindow(DependencyObject element, FontScaleWindow value)
{
element.SetValue(ParentScaleWindowKey, value);
}
public static void SetResizeFont(DependencyObject element, bool value)
{
element.SetValue(ResizeFontProperty, value);
}
public static bool GetResizeFont(DependencyObject element)
{
return (bool) element.GetValue(ResizeFontProperty);
}
private static bool SetWindow(FrameworkElement el)
{
var w = GetWindow(el);
if (w is not FontScaleWindow fsWindow) return false;
SetParentScaleWindow(el, fsWindow);
if (fsWindow._cachedObjects.Contains(el)) return true;
fsWindow._cachedObjects.Add(el);
el.Unloaded += ObjOnUnloaded;
return true;
}
private static void ObjOnLoaded(object sender, RoutedEventArgs e)
{
var el = (FrameworkElement) sender;
if (GetParentScaleWindow(el) == null)
{
if (!SetWindow(el))
{
el.Loaded -= ObjOnLoaded;
return;
}
}
ResizeCallBack(sender, new DependencyPropertyChangedEventArgs());
}
private static void ResizeCallBack(object? sender, DependencyPropertyChangedEventArgs eventArgs)
{
if (sender is not FrameworkElement obj) return;
var window = GetParentScaleWindow(obj);
if (window != null && !window._cachedObjects.Contains(obj))
{
if (!SetWindow(obj)) return;
}
else if (window == null)
{
obj.Loaded += ObjOnLoaded;
}
if (window is not {_loaded: true}) return;
CalculateFontSizeResized(obj);
}
private static void ObjOnUnloaded(object sender, RoutedEventArgs e)
{
var obj = (FrameworkElement) sender;
var w = GetParentScaleWindow(obj)!;
w._cachedObjects.Remove(obj);
obj.Unloaded -= ObjOnUnloaded;
obj.Loaded -= ObjOnLoaded;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
_currentDiagonal = _diagonal(e.NewSize.Width, e.NewSize.Height);
foreach (var cached in _cachedObjects)
{
CalculateFontSizeResized(cached);
}
}
public static void SetScale(DependencyObject element, double value)
{
element.SetValue(ScaleProperty, value);
}
public static double GetScale(DependencyObject element)
{
return (double) element.GetValue(ScaleProperty);
}
private static void ScalePropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var window = GetParentScaleWindow(d);
if (window == null)
{
var w = GetWindow(d);
if (w is FontScaleWindow fsWindow)
{
SetParentScaleWindow(d, fsWindow);
}
}
ResizeElement(d);
}
public static void ResizeElement(DependencyObject d)
{
var set = d.ReadLocalValue(ScaleProperty) != DependencyProperty.UnsetValue;
if (set)
{
if (GetResizeFont(d))
{
CalculateFontSizeResized(d);
}
else
{
CalculateFontSize(d);
}
}
}
public static void CalculateFontSize(DependencyObject d)
{
var window = GetParentScaleWindow(d);
if (window == null) return;
var fontSize = GetScale(d) * window._defaultDiagonal;
d.SetValue(FontSizeProperty, fontSize);
}
public static void CalculateFontSizeResized(DependencyObject d)
{
var window = GetParentScaleWindow(d);
if (window == null) return;
var windowsScale = Math.Pow(window._currentDiagonal / window._defaultDiagonal, window.ScaleCoefficient);
var elScale = GetScale(d);
var fontSize = window.DefaultFontSize * elScale * windowsScale;
d.SetValue(FontSizeProperty, fontSize);
}
}
@@ -2,9 +2,6 @@
namespace NebulaAuth.Theme.WindowStyle;
/// <summary>
/// Логика взаимодействия для WindowStyle.xaml
/// </summary>
partial class WindowStyle
{
public WindowStyle()
+17 -35
View File
@@ -1,6 +1,6 @@
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Forms;
using NebulaAuth.Core;
using NebulaAuth.Model;
@@ -10,24 +10,15 @@ public class ClipboardHelper
{
public static bool Set(string text)
{
var i = 0;
while (i < 20)
try
{
try
{
Clipboard.SetText(text);
return true;
}
catch (Exception ex)
{
if (i == 19)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
}
i++;
Clipboard.SetText(text);
return true;
}
catch (Exception ex)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
return false;
@@ -35,24 +26,15 @@ public class ClipboardHelper
public static bool SetFiles(StringCollection files)
{
var i = 0;
while (i < 20)
try
{
try
{
Clipboard.SetFileDropList(files);
return true;
}
catch (Exception ex)
{
if (i == 19)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
}
i++;
Clipboard.SetFileDropList(files);
return true;
}
catch (Exception ex)
{
Shell.Logger.Error(ex);
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
}
return false;
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NebulaAuth.Core;
@@ -6,6 +7,7 @@ using NebulaAuth.Model;
using SteamLib.Exceptions;
using SteamLib.Exceptions.Authorization;
using SteamLib.Exceptions.General;
using SteamLib.ProtoCore.Enums;
using SteamLibForked.Exceptions.Authorization;
namespace NebulaAuth.Utility;
@@ -94,6 +96,18 @@ public static class ExceptionHandler
return "LoginException".GetCodeBehindLocalization() + ": " +
ErrorTranslatorHelper.TranslateLoginError(e.Error);
}
case UnsupportedAuthTypeException e:
{
var requestedType = e.AllowedGuardTypes.First();
var msgType = requestedType switch
{
EAuthSessionGuardType.DeviceCode or EAuthSessionGuardType.DeviceConfirmation => "Guard",
EAuthSessionGuardType.EmailCode => "Mail",
_ => "Unknown"
};
return GetCodeBehindLocalization("UnsupportedAuthTypeException", msgType);
}
case not null when handleAllExceptions:
{
return "UnknownException".GetCodeBehindLocalization() + exception.Message;
@@ -112,4 +126,11 @@ public static class ExceptionHandler
{
return LocManager.GetCodeBehindOrDefault(key, EXCEPTION_HANDLER_LOC_PATH, key);
}
private static string GetCodeBehindLocalization(params string[] path)
{
var def = path.Length == 0 ? "" : path.Last();
var newArr = path.Prepend(EXCEPTION_HANDLER_LOC_PATH).ToArray();
return LocManager.GetCodeBehindOrDefault(def, newArr);
}
}
@@ -0,0 +1,22 @@
using System.IO;
using System.Linq;
namespace NebulaAuth.Utility;
public static class FileNameValidator
{
public static bool IsValidFileName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return false;
var invalidChars = Path.GetInvalidFileNameChars();
return !name.Any(c => invalidChars.Contains(c));
}
public static string GetInvalidChars(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
return new string(name.Where(c => invalidChars.Contains(c)).Distinct().ToArray());
}
}
+38
View File
@@ -0,0 +1,38 @@
using System;
using System.Globalization;
using System.Linq;
using NebulaAuth.Core;
namespace NebulaAuth.Utility;
public static class LanguageUtility
{
public static LocalizationLanguage DetectPreferredLanguage()
{
var userCulture = CultureInfo.CurrentUICulture;
var userLang = userCulture.TwoLetterISOLanguageName;
var userRegion = new RegionInfo(userCulture.Name).TwoLetterISORegionName;
switch (userLang)
{
case "ru": return LocalizationLanguage.Russian;
case "uk": return LocalizationLanguage.Ukrainian;
case "en": return LocalizationLanguage.English;
case "zh": return LocalizationLanguage.ChineseSimplified;
case "fr": return LocalizationLanguage.French;
}
if (userRegion.EndsWith("UA", StringComparison.OrdinalIgnoreCase))
return LocalizationLanguage.Ukrainian;
string[] cisRegions =
[
"RU", "BY", "KZ", "KG", "TJ", "TM", "UZ", "AM", "AZ", "GE", "MD"
];
return cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase))
? LocalizationLanguage.Russian
: LocalizationLanguage.English;
}
}
@@ -2,12 +2,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:theme="clr-namespace:NebulaAuth.Theme"
xmlns:converters="clr-namespace:NebulaAuth.Converters"
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
xmlns:vm="clr-namespace:NebulaAuth.ViewModel">
<DataTemplate DataType="{x:Type confirmations:Confirmation}">
<Grid>
<Grid.ColumnDefinitions>
@@ -110,7 +109,7 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="ShoppingCart"
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
Margin="0,0,10,0" />
<Border Grid.Column="1" Background="{DynamicResource MaterialDesignPaper}" MaxHeight="36"
HorizontalAlignment="Center" BorderBrush="{DynamicResource PrimaryHueMidBrush}"
@@ -119,7 +118,7 @@
Source="{Binding ItemImageUri}" />
</Border>
<Grid Grid.Column="2">
<TextBlock VerticalAlignment="Center" x:Name="ItemName" theme:FontScaleWindow.ResizeFont="True"
<TextBlock VerticalAlignment="Center" x:Name="ItemName"
TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
<TextBlock Foreground="LightGray" Text="{Binding PriceString}">
@@ -184,7 +183,7 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="ShoppingCartPlus"
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
Margin="0,0,10,0" />
<TextBlock VerticalAlignment="Center" Grid.Column="1">
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
@@ -4,42 +4,69 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:theme="clr-namespace:NebulaAuth.Theme"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
Height="Auto" Width="Auto" MaxWidth="500"
Background="{DynamicResource WindowBackground}">
<Grid Margin="15">
<Grid MinHeight="140" MinWidth="350">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock theme:FontScaleWindow.Scale="0.8"
theme:FontScaleWindow.ResizeFont="True"
HorizontalAlignment="Center" Margin="10,10,10,15"
Text="Подтвердите действие"
x:Name="ConfirmTextBlock"
TextWrapping="WrapWithOverflow" />
<Grid Grid.Row="1" Margin="10,0,10,5">
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button DockPanel.Dock="Left"
<StackPanel Orientation="Horizontal">
<!--<materialDesign:PackIcon Kind="WarningCircleOutline" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />-->
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr ConfirmCancelDialog.Title}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<Grid Grid.Row="2" Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<nebulaAuth:HintBox FontSize="14" x:Name="ConfirmHint" Text="Confirm?" Margin="10" />
<Grid Grid.Row="1" Margin="10,0,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button
Width="130"
HorizontalAlignment="Left"
Margin="2,0,4,0"
Style="{StaticResource MaterialDesignRaisedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource True}"
FontSize="{Binding ElementName=ConfirmTextBlock, Path=FontSize,
Converter={StaticResource CoefficientConverter},
ConverterParameter=0.85}">
ОК
</Button>
<Button Grid.Column="1" DockPanel.Dock="Right"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource False}"
FontSize="{Binding ElementName=ConfirmTextBlock, Path=FontSize,
Converter={StaticResource CoefficientConverter},
ConverterParameter=0.85}">
Отмена
</Button>
Content="{Tr Common.OK}"
IsDefault="True" />
<Button Grid.Column="1"
HorizontalAlignment="Right"
Width="130"
Margin="0,0,2,0"
IsCancel="True"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource False}"
Content="{Tr Common.Cancel}" />
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -1,8 +1,5 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для ConfirmCancelDialog.xaml
/// </summary>
public partial class ConfirmCancelDialog
{
public ConfirmCancelDialog()
@@ -13,6 +10,6 @@ public partial class ConfirmCancelDialog
public ConfirmCancelDialog(string msg)
{
InitializeComponent();
ConfirmTextBlock.Text = msg;
ConfirmHint.Text = msg;
}
}
@@ -45,7 +45,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
<TextBox TabIndex="0" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
materialDesign:TextFieldAssist.HasClearButton="True"
Padding="10"
@@ -58,7 +58,7 @@
materialDesign:HintAssist.IsFloating="False"
materialDesign:TextFieldAssist.LeadingIcon="Key"
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
<CheckBox FontSize="15" Grid.Row="1" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="1" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<Grid Grid.Row="2" Margin="10,10,10,0">
@@ -1,8 +1,5 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для LoginAgainDialog.xaml
/// </summary>
public partial class LoginAgainDialog
{
public LoginAgainDialog()
@@ -49,7 +49,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
<TextBox TabIndex="0" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
materialDesign:TextFieldAssist.HasClearButton="True"
Padding="10"
@@ -74,10 +74,10 @@
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
</UIElement.InputBindings>
</ComboBox>
<CheckBox FontSize="15" Grid.Row="2" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="2" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<CheckBox FontSize="15" Grid.Row="3" Margin="10,10,10,0" IsEnabled="{Binding MafileHasProxy}"
<CheckBox FontSize="15" Grid.Row="3" Margin="10,5,10,0" IsEnabled="{Binding MafileHasProxy}"
Content="{Tr LoginAgainDialog.UseMafileProxy}" IsChecked="{Binding UseMafileProxy}" />
<Grid Grid.Row="4" Margin="10,10,10,0">
<Grid.ColumnDefinitions>
@@ -1,8 +1,5 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для LoginAgainDialog.xaml
/// </summary>
public partial class LoginAgainOnImportDialog
{
public LoginAgainOnImportDialog()
@@ -4,7 +4,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:theme="clr-namespace:NebulaAuth.Theme"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance other:LoginAgainVM}">
@@ -29,7 +28,7 @@
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr SetEncryptedPasswordDialog.Title}" />
VerticalAlignment="Center" FontSize="18" Text="{Tr SetEncryptionPasswordDialog.Title}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
@@ -40,12 +39,13 @@
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<TextBlock Grid.Row="2" IsHitTestVisible="False" TextWrapping="Wrap" FontWeight="Normal"
Text="{Tr SetEncryptedPasswordDialog.DialogText}" theme:FontScaleWindow.Scale="0.8"
theme:FontScaleWindow.ResizeFont="True" Margin="10" HorizontalAlignment="Left" />
<PasswordBox FontSize="16"
FontSize="16"
Text="{Tr SetEncryptionPasswordDialog.DialogText}"
Margin="10" HorizontalAlignment="Left" />
<PasswordBox TabIndex="0" FontSize="16" x:Name="PasswordBox"
materialDesign:PasswordBoxAssist.Password="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
Margin="10" Grid.Row="3" Style="{StaticResource MaterialDesignFloatingHintRevealPasswordBox}"
materialDesign:HintAssist.Hint="{Tr SetEncryptedPasswordDialog.Password}" />
materialDesign:HintAssist.Hint="{Tr SetEncryptionPasswordDialog.Password}" />
<Grid Grid.Row="4" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition />
@@ -53,11 +53,11 @@
</Grid.ColumnDefinitions>
<Button IsDefault="True" Margin="10,5,5,5" Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource True}" Content="{Tr SetEncryptedPasswordDialog.Ok}" />
CommandParameter="{StaticResource True}" Content="{Tr SetEncryptionPasswordDialog.Ok}" />
<Button IsCancel="True" Grid.Column="1" Margin="5,5,10,5"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource False}" Content="{Tr SetEncryptedPasswordDialog.Cancel}" />
CommandParameter="{StaticResource False}" Content="{Tr SetEncryptionPasswordDialog.Cancel}" />
</Grid>
</Grid>
</UserControl>
@@ -1,8 +1,5 @@
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для SetCryptPasswordDialog.xaml
/// </summary>
public partial class SetCryptPasswordDialog
{
public SetCryptPasswordDialog()
@@ -0,0 +1,70 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.TextFieldDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Height="Auto" Width="Auto" MaxWidth="500"
Background="{DynamicResource WindowBackground}">
<Grid MinHeight="140" MinWidth="350">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<!--<materialDesign:PackIcon Kind="WarningCircleOutline" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />-->
<TextBlock x:Name="TitleTextBlock" FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr TextFieldDialog.Title}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" CommandParameter="{x:Null}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<Grid Grid.Row="2" Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox x:Name="TextField" Margin="10" materialDesign:HintAssist.Hint="" />
<Grid Grid.Row="1" Margin="10,0,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button
Width="130"
HorizontalAlignment="Left"
Margin="2,0,4,0"
Style="{StaticResource MaterialDesignRaisedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{Binding Text, ElementName=TextField}"
Content="{Tr Common.OK}"
IsDefault="True" />
<Button Grid.Column="1"
HorizontalAlignment="Right"
Width="130"
Margin="0,0,2,0"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsCancel="True"
CommandParameter="{x:Null}"
Content="{Tr Common.Cancel}" />
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,22 @@
using MaterialDesignThemes.Wpf;
namespace NebulaAuth.View.Dialogs;
public partial class TextFieldDialog
{
public TextFieldDialog()
{
InitializeComponent();
}
public TextFieldDialog(string? title, string? msg)
{
InitializeComponent();
if (!string.IsNullOrWhiteSpace(title))
TitleTextBlock.Text = title;
if (!string.IsNullOrEmpty(msg))
HintAssist.SetHint(TextField, msg);
}
}
@@ -3,9 +3,7 @@
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:theme="clr-namespace:NebulaAuth.Theme"
mc:Ignorable="d"
theme:FontScaleWindow.ResizeFont="True" theme:FontScaleWindow.Scale="1"
Foreground="WhiteSmoke"
Background="{DynamicResource WindowBackground}">
<Grid Margin="20">
@@ -19,7 +17,8 @@
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}" IsIndeterminate="True" />
<TextBlock Margin="10,0,0,0" Grid.Column="1" Text="{Tr WaitLoginDialog.Text}" VerticalAlignment="Center" />
<TextBlock FontSize="17" Margin="10,0,0,0" Grid.Column="1" Text="{Tr WaitLoginDialog.Text}"
VerticalAlignment="Center" />
</Grid>
<Grid x:Name="CaptchaGrid" Margin="0,25,0,0" Visibility="Collapsed" Grid.Row="1">
<Grid.RowDefinitions>
@@ -28,7 +27,8 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image x:Name="CaptchaImage" Stretch="Uniform" />
<TextBox Grid.Row="1" Style="{StaticResource MaterialDesignFloatingHintTextBox}" x:Name="CaptchaTB" />
<TextBox TabIndex="0" Grid.Row="1" Style="{StaticResource MaterialDesignFloatingHintTextBox}"
x:Name="CaptchaTB" />
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
@@ -3,9 +3,6 @@ using System.Windows;
namespace NebulaAuth.View.Dialogs;
/// <summary>
/// Логика взаимодействия для WaitLoginDialog.xaml
/// </summary>
public partial class WaitLoginDialog
{
private TaskCompletionSource<string> _tcs = new();
@@ -11,6 +11,7 @@
<TextBlock Margin="5,0,0,0" Text="{Tr LinkerDialog.Authorization}" />
</StackPanel>
<TextBox
TabIndex="0"
Padding="8"
FontSize="14"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
@@ -22,6 +23,7 @@
Margin="0,10,0,0" />
<TextBox
Padding="8"
TabIndex="1"
FontSize="14"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
+10 -36
View File
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:linker="clr-namespace:NebulaAuth.ViewModel.Linker"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
FontFamily="{materialDesign:MaterialDesignFont}"
@@ -29,8 +30,6 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="materialDesign:PackIcon">
@@ -81,45 +80,20 @@
</Button>
</Grid>
<Separator Background="DarkGray" Opacity="0.4" Grid.Row="1" />
<Border Visibility="{Binding Tip, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="2" Margin="10,10,10,0" Padding="5"
BorderThickness="1"
CornerRadius="12">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseShadowColor}" Opacity="0.5" />
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.2" />
</Border.Background>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="InfoCircleOutline" Foreground="{DynamicResource InfoBrush}" Height="22"
Width="22" VerticalAlignment="Center" Margin="5,0,0,0" />
<TextBlock VerticalAlignment="Center" MaxWidth="330" FontSize="14" TextWrapping="WrapWithOverflow"
Margin="10,10,10,10" Text="{Binding Tip}" />
</StackPanel>
</Border>
<nebulaAuth:HintBox FontSize="14"
Visibility="{Binding Tip, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="2" Margin="10,10,10,0"
Text="{Binding Tip}" MaxWidth="330" />
<ContentControl
Grid.Row="3" Margin="20"
d:DataContext="{d:DesignInstance Type=linker:DesignLinkAccountAuthStepVM, IsDesignTimeCreatable=True}"
Content="{Binding CurrentStep}" />
<Border Visibility="{Binding Error, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="4" Margin="10,0,10,10" Padding="5"
BorderThickness="1"
CornerRadius="12">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseShadowColor}" Opacity="0.5" />
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.2" />
</Border.Background>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="ErrorOutline" Foreground="{DynamicResource ErrorBrush}" Height="22"
Width="22" VerticalAlignment="Center" Margin="5,0,0,0" />
<TextBlock VerticalAlignment="Center" MaxWidth="330" FontSize="14" TextWrapping="WrapWithOverflow"
Margin="10,10,10,10" Text="{Binding Error}" />
</StackPanel>
</Border>
<nebulaAuth:HintBox FontSize="14"
Visibility="{Binding Error, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="4" Margin="10,0,10,10"
Severity="Error"
Text="{Binding Error}" />
</Grid>
</UserControl>
-3
View File
@@ -1,8 +1,5 @@
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для LinkerView.xaml
/// </summary>
public partial class LinkerView
{
public LinkerView()
-3
View File
@@ -4,9 +4,6 @@ using System.Windows.Controls;
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для LinksView.xaml
/// </summary>
public partial class LinksView : UserControl
{
public LinksView()
+240
View File
@@ -0,0 +1,240 @@
<UserControl x:Class="NebulaAuth.View.MafileExporterView"
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"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
FontFamily="{materialDesign:MaterialDesignFont}"
MinWidth="400"
MaxWidth="400"
Background="{DynamicResource WindowBackground}"
d:DataContext="{d:DesignInstance other:MafileExporterVM}"
d:Background="#141119"
d:Foreground="White"
FontSize="18">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="FileExport" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr ExportDialog.ExportTitle}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsCancel="True">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Background="DarkGray" Opacity="0.4" Grid.Row="1" />
<Grid Grid.Row="2" Margin="15" TextElement.FontSize="14">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ComboBox Grid.Column="0"
Padding="10"
Style="{StaticResource MaterialDesignOutlinedComboBox}"
Margin="0,0,5,0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
ItemsSource="{Binding Templates}"
DisplayMemberPath="Name"
SelectedItem="{Binding CurrentTemplate}"
Visibility="{Binding EditMode, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
<TextBox x:Name="EditTemplateNameTextBox"
Grid.Column="0"
Padding="10"
Margin="0,0,5,0"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
Text="{Binding CurrentTemplate.Name,
FallbackValue=''}"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
Visibility="{Binding EditMode, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBox.InputBindings>
<KeyBinding Key="Enter"
Command="{Binding ToggleEditModeCommand}" />
</TextBox.InputBindings>
</TextBox>
<Button Command="{Binding AddTemplateCommand}" Grid.Column="1">
<materialDesign:PackIcon Kind="Add" />
</Button>
<Button Command="{Binding ToggleEditModeCommand}" Click="EditButton_Click" Grid.Column="2">
<materialDesign:PackIcon Kind="Edit" />
</Button>
<Button Grid.Column="3" Command="{Binding RemoveCurrentTemplateCommand}">
<materialDesign:PackIcon Kind="Trash" />
</Button>
</Grid>
<Grid Margin="0,15,0,0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
MouseDoubleClick="PathTB_DoubleClick" materialDesign:TextFieldAssist.HasClearButton="True"
Text="{Binding CurrentTemplate.Path, FallbackValue=''}" Padding="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}" Grid.Column="0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.SelectPathHint}" />
<Button Command="{Binding OpenSelectDirectoryDialogCommand}" Grid.Column="1" Margin="5,0,0,0">
<materialDesign:PackIcon Kind="FolderOpen" />
</Button>
</Grid>
<Border Grid.Row="2" BorderThickness="1" CornerRadius="4" Margin="0,15,0,0" Padding="2">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseContentColor}" Opacity="0.4" />
</Border.BorderBrush>
<Expander x:Name="TemplateExpander"
Header="{Tr ExportDialog.ExportSettingsHeader}"
materialDesign:ExpanderAssist.HorizontalHeaderPadding="5"
materialDesign:ExpanderAssist.ExpanderButtonPosition="Start"
HorizontalAlignment="Stretch"
Padding="5,0,5,5"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}">
<StackPanel>
<TextBlock FontWeight="DemiBold"
Text="{Tr ExportDialog.ExportSettingsCommon}" />
<CheckBox IsChecked="{Binding CurrentTemplate.UseLoginAsMafileName, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.UseLoginAsMafileName}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.UseLoginAsMafileName}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSharedSecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSharedSecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSharedSecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeIdentitySecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeIdentitySecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeIdentitySecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeRCode, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeRCode}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeRCode}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSessionData, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSessionData}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSessionData}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeOtherInfo, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeOtherInfo}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeOtherInfo}" />
<TextBlock FontWeight="DemiBold"
Margin="0,10,0,0"
Text="{Tr ExportDialog.ExportSettingsNebula}" />
<Grid Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
IsChecked="{Binding CurrentTemplate.IncludeNebulaProxy, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaProxy}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaProxy}" />
<CheckBox Grid.Column="1"
HorizontalAlignment="Center"
IsChecked="{Binding CurrentTemplate.IncludeNebulaPassword, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaPassword}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaPassword}" />
<CheckBox Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CurrentTemplate.IncludeNebulaGroup, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaGroup}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaGroup}" />
</Grid>
</StackPanel>
</Expander>
</Border>
<TextBox GotFocus="ExportTB_GotFocus" PreviewMouseUp="ExportTB_GotFocus" Grid.Row="3"
Style="{StaticResource MaterialDesignOutlinedTextBox}" MaxHeight="600" Margin="0,10,0,0"
MinHeight="250" Height="250" AcceptsReturn="True"
Text="{Binding AccountsText, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ExportCommand.IsRunning, Converter={StaticResource ReverseBooleanConverter}}"
Name="AccountsTB" />
<TextBlock Grid.Row="3"
Text="{Tr ExportDialog.ExportAccountsPlaceholder}"
Margin="18,27,10,10"
Opacity="0.6"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="{Binding Text.IsEmpty,
ElementName=AccountsTB,
Converter={StaticResource BooleanToVisibilityConverter}}" />
<nebulaAuth:HintBox Margin="0,10,0,0" Severity="{Binding HintBoxSeverity}" Grid.Row="4"
Text="{Binding HintText}" />
</Grid>
</Grid>
<Separator Grid.Row="3" />
<Button Command="{Binding ExportCommand}" Style="{StaticResource MaterialDesignFlatDarkBgButton}" Grid.Row="4"
Margin="20,7,20,7" MaxWidth="150" Content="{Tr ExportDialog.ExportButton}" />
</Grid>
</UserControl>
@@ -0,0 +1,32 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
namespace NebulaAuth.View;
public partial class MafileExporterView : UserControl
{
public MafileExporterView()
{
InitializeComponent();
}
private void PathTB_DoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is TextBox textBox)
{
textBox.SelectAll();
}
}
private void ExportTB_GotFocus(object sender, RoutedEventArgs e)
{
TemplateExpander.IsExpanded = false;
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(() => { EditTemplateNameTextBox.Focus(); }, DispatcherPriority.Input);
}
}
@@ -13,6 +13,7 @@
</StackPanel>
<TextBox
TabIndex="0"
Padding="8"
FontSize="14"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
@@ -25,6 +26,7 @@
<TextBox
Padding="8"
FontSize="14"
TabIndex="1"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
materialDesign:HintAssist.Hint="{Tr LinkerDialog.Password}"
+9 -35
View File
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mafileMover="clr-namespace:NebulaAuth.ViewModel.MafileMover"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
FontFamily="{materialDesign:MaterialDesignFont}"
@@ -76,43 +77,16 @@
</Button>
</Grid>
<Separator Background="DarkGray" Opacity="0.4" Grid.Row="1" />
<Border Visibility="{Binding Tip, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="2" Margin="10,10,10,0" Padding="5"
BorderThickness="1"
CornerRadius="12">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseShadowColor}" Opacity="0.5" />
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.2" />
</Border.Background>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="InfoCircleOutline" Foreground="{DynamicResource InfoBrush}" Height="22"
Width="22" VerticalAlignment="Center" Margin="5,0,0,0" />
<TextBlock VerticalAlignment="Center" MaxWidth="330" FontSize="14" TextWrapping="WrapWithOverflow"
Margin="10,10,10,10" Text="{Binding Tip}" />
</StackPanel>
</Border>
<nebulaAuth:HintBox FontSize="14"
Visibility="{Binding Tip, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="2" Margin="10,10,10,0"
Text="{Binding Tip}" />
<ContentControl
Grid.Row="3" Margin="20" Content="{Binding CurrentStep}" />
<Border Visibility="{Binding Error, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="4" Margin="10,0,10,10" Padding="5"
BorderThickness="1"
CornerRadius="12">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseShadowColor}" Opacity="0.5" />
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.2" />
</Border.Background>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="ErrorOutline" Foreground="{DynamicResource ErrorBrush}" Height="22"
Width="22" VerticalAlignment="Center" Margin="5,0,0,0" />
<TextBlock VerticalAlignment="Center" MaxWidth="330" FontSize="14" TextWrapping="WrapWithOverflow"
Margin="10,10,10,10" Text="{Binding Error}" />
</StackPanel>
</Border>
<nebulaAuth:HintBox FontSize="14"
Visibility="{Binding Error, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Row="4" Margin="10,0,10,10"
Text="{Binding Error}" />
</Grid>
</UserControl>
@@ -2,9 +2,6 @@
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для MafileMoverView.xaml
/// </summary>
public partial class MafileMoverView : UserControl
{
public MafileMoverView()
+101 -35
View File
@@ -5,14 +5,19 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="400"
FontFamily="{md:MaterialDesignFont}"
MinHeight="500"
MinWidth="400"
MaxHeight="550"
d:DataContext="{d:DesignInstance other:ProxyManagerVM}"
Background="Transparent">
d:Background="#141119"
d:Foreground="White"
FontFamily="{md:MaterialDesignFont}"
MinHeight="700"
MinWidth="600"
MaxHeight="700"
MaxWidth="600"
Background="Transparent"
FontSize="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -41,21 +46,42 @@
</Grid>
<Separator Grid.Row="1" />
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Stretch"
Margin="15" FontSize="16">
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" />
<Run Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" />
<Run
Text="{Binding DefaultProxy.Value, Converter='{StaticResource ProxyDataTextConverter}', Mode=OneWay, FallbackValue=''}" />
</TextBlock>
<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>
<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>
<md:Card Grid.Row="3" Margin="10">
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" FontSize="14"
@@ -77,6 +103,9 @@
<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>
@@ -95,8 +124,18 @@
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">
<Run Text="{Binding Key, Mode=OneWay}" /><Run Text=": " />
<Run
Text="{Binding Value, Mode=OneWay, Converter={StaticResource ProxyDataTextConverter}}" />
<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>
<Button Style="{StaticResource MaterialDesignIconButton}" Padding="0" Width="24"
@@ -114,20 +153,47 @@
</ListBox>
</md:Card>
<Grid Grid.Row="4" Margin="5,0,15,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding AddProxyField}" FontSize="12" md:TextFieldAssist.HasClearButton="True"
AcceptsReturn="True" MaxHeight="100" Style="{StaticResource MaterialDesignOutlinedTextBox}"
Margin="15" md:HintAssist.Hint="IP:PORT:USER:PASS{ID}" />
<Button IsDefault="True" Grid.Column="1" Command="{Binding AddProxyCommand}">
<md:PackIcon Kind="Add" />
</Button>
<Button Grid.Column="2" Command="{Binding RemoveProxyCommand}">
<md:PackIcon Kind="Trash" />
</Button>
<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>
</Grid>
+12 -4
View File
@@ -1,12 +1,20 @@
namespace NebulaAuth.View;
using System.Windows.Controls;
using System.Windows.Input;
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для ProxyManagerView.xaml
/// </summary>
public partial class ProxyManagerView
{
public ProxyManagerView()
{
InitializeComponent();
}
private void ProxyInput_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || !Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) return;
var tb = sender as TextBox;
tb?.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
AddProxyBtn.Command.Execute(null);
}
}
@@ -0,0 +1,152 @@
<UserControl x:Class="NebulaAuth.View.SetAccountPasswordsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="400"
d:DataContext="{d:DesignInstance other:SetAccountPasswordsVM}"
d:Background="#141119"
d:Foreground="White"
FontFamily="{md:MaterialDesignFont}"
MinHeight="500"
MaxHeight="500"
MinWidth="400"
MaxWidth="400"
Background="Transparent"
FontSize="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- ===== HEADER ===== -->
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="ShieldKey" Width="20" Height="20"
Margin="0,0,5,0" VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock Text="{Tr SetAccountPasswordsView.Title}"
FontSize="18"
VerticalAlignment="Center"
Foreground="{DynamicResource BaseContentBrush}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30"
Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right"
Command="{x:Static md:DialogHost.CloseDialogCommand}"
IsCancel="True">
<md:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" />
<!-- ===== ENCRYPTION PASSWORD SECTION ===== -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<md:Card Margin="15,15,15,15" BorderThickness="1" BorderBrush="Gray" Padding="12">
<StackPanel>
<TextBlock Text="{Tr SetAccountPasswordsView.EncryptionPassword}" FontSize="16"
Margin="0,0,0,5"
Foreground="{DynamicResource PrimaryContentBrush}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
md:HintAssist.Hint="{Tr SetAccountPasswordsView.EnterPassword}"
md:HintAssist.IsFloating="False"
Margin="0,0,10,0"
Text="{Binding EncryptionPassword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="1" Command="{Binding SetEncryptionPasswordCommand}">
<md:PackIcon Kind="ContentSave" />
</Button>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<StackPanel.Resources>
<x:Array x:Key="BoolToIcon" Type="{x:Type md:PackIconKind}">
<md:PackIconKind>CheckBox</md:PackIconKind>
<md:PackIconKind>CloseBox</md:PackIconKind>
</x:Array>
<x:Array x:Key="BoolToColor" Type="{x:Type SolidColorBrush}">
<SolidColorBrush Color="{DynamicResource SuccessColor}" />
<SolidColorBrush Color="{DynamicResource ErrorColor}" />
</x:Array>
</StackPanel.Resources>
<TextBlock Text="{Tr SetAccountPasswordsView.Status}" VerticalAlignment="Center" />
<md:PackIcon Width="20" Height="20"
VerticalAlignment="Center"
Foreground="{Binding IsEncryptionPasswordSet, Converter={StaticResource BoolToValueConverter}, ConverterParameter={StaticResource BoolToColor}}"
Kind="{Binding IsEncryptionPasswordSet, Converter={StaticResource BoolToValueConverter}, ConverterParameter={StaticResource BoolToIcon}}" />
</StackPanel>
</StackPanel>
</md:Card>
</Grid>
<!-- ===== ACCOUNTS INPUT ===== -->
<TextBox Grid.Row="3"
Margin="15,0,15,10"
Text="{Binding AccountsPasswords, UpdateSourceTrigger=PropertyChanged}"
FontSize="13"
AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
md:TextFieldAssist.HasClearButton="True"
md:HintAssist.IsFloating="False"
md:HintAssist.Hint="{Tr SetAccountPasswordsView.InputFormat}"
md:TextFieldAssist.TextBoxIsMultiLine="True"
md:TextFieldAssist.TextBoxViewVerticalAlignment="Top"
VerticalContentAlignment="Top" />
<!-- ===== INFO TEXT ===== -->
<TextBlock Grid.Row="4"
Margin="20,0,20,15"
TextWrapping="Wrap"
FontSize="12"
Foreground="{DynamicResource BaseContentBrush}"
Text="{Tr SetAccountPasswordsView.EncryptionHint}" />
<Button Grid.Row="5"
Margin="10,0,0,10"
Width="120"
Content="{Tr Common.Save}"
Command="{Binding SetPasswordsCommand}"
Style="{StaticResource MaterialDesignOutlinedButton}" Cursor="Hand" />
<!-- ===== HINT / RESULT BOX ===== -->
<nebulaAuth:HintBox Grid.Row="6"
Margin="15,0,15,10"
Visibility="{Binding Tip, Converter={StaticResource NullableToVisibilityConverter}}"
CloseCommand="{Binding ClearTipCommand}"
Text="{Binding Tip}"
FontSize="13"
Severity="Info"
ShowCloseButton="True" />
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View;
public partial class SetAccountPasswordsView
{
public SetAccountPasswordsView()
{
InitializeComponent();
}
}
+71 -41
View File
@@ -5,14 +5,16 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
d:DesignHeight="650"
FontFamily="{materialDesign:MaterialDesignFont}"
MinHeight="600"
MinWidth="400"
MaxWidth="400"
MinHeight="400"
MinWidth="480"
MaxWidth="480"
d:DataContext="{d:DesignInstance other:SettingsVM}"
Background="Transparent">
Background="Transparent"
FontSize="15">
<Grid>
<Grid>
<Grid.RowDefinitions>
@@ -37,6 +39,7 @@
<Button Grid.Column="1" Width="30" Height="30"
Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsEnabled="{Binding ApplyRenameSettingCommand.IsRunning, Converter={StaticResource ReverseBooleanConverter}, Mode=OneWay}"
IsCancel="True">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
@@ -46,25 +49,36 @@
<TabControl materialDesign:ColorZoneAssist.Background="{DynamicResource Base100Brush}"
Style="{StaticResource MaterialDesignUniformTabControl}" Grid.Row="2">
<TabItem Header="{Tr SettingsDialog.MainSettings}">
<StackPanel Width="340" Margin="10,30,10,10" Orientation="Vertical" HorizontalAlignment="Center">
<StackPanel Width="400" Margin="10,18,10,40" Orientation="Vertical" HorizontalAlignment="Center">
<ComboBox Style="{StaticResource MaterialDesignFloatingHintComboBox}" Margin="0,15,0,0"
FontSize="16"
ItemsSource="{Binding Languages}" DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding Language}"
materialDesign:HintAssist.Hint="{Tr LanguageWord}" />
<CheckBox Margin="0,15,0,0" FontSize="16" IsChecked="{Binding HideToTray}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding HideToTray}"
Content="{Tr SettingsDialog.MinimizeToTray}" />
<CheckBox Margin="0,15,0,0" FontSize="16" IsChecked="{Binding UseIcon}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding LegacyMode}"
Content="{Tr SettingsDialog.LegacyMafileMode}"
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
</CheckBox>-->
<CheckBox Margin="0,5,0,0" IsChecked="{Binding UseIcon}"
Content="{Tr SettingsDialog.UseIndicator}"
ToolTip="{Tr SettingsDialog.UseIndicatorHint}" />
<materialDesign:ColorPicker IsEnabled="{Binding UseIcon}" Color="{Binding IconColor, Delay=50}" />
<Grid Margin="0,20,0,10">
<materialDesign:ColorPicker Margin="-5,0,-5,0" IsEnabled="{Binding UseIcon}"
Color="{Binding IconColor, Delay=50}" />
<Grid Margin="0,10,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<PasswordBox MinWidth="250" materialDesign:PasswordBoxAssist.Password="{Binding Password}"
Height="Auto" VerticalAlignment="Center" FontSize="16"
Height="Auto" VerticalAlignment="Center"
Style="{StaticResource MaterialDesignFloatingHintRevealPasswordBox}"
materialDesign:HintAssist.Hint="{Tr SettingsDialog.PasswordBox.CurrentCryptPassword}"
materialDesign:HintAssist.HelperText="{Tr SettingsDialog.PasswordBox.Hint}" />
@@ -73,46 +87,60 @@
<materialDesign:PackIcon Kind="ContentSave" />
</Button>
</Grid>
<CheckBox Margin="0,15,0,0" FontSize="16" IsChecked="{Binding LegacyMode}"
Content="{Tr SettingsDialog.LegacyMafileMode}"
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
<CheckBox Margin="0,15,0,0" FontSize="16" IsChecked="{Binding UseAccountNameAsMafileName}">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
</CheckBox>
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" BorderBrush="AliceBlue"
BorderThickness="2" IsChecked="{Binding IgnorePatchTuesdayErrors}" Margin="0,15,0,0"
FontSize="16"
ToolTip="{Tr SettingsDialog.IgnorePatchTuesdayErrorsHint}">
<TextBlock TextWrapping="WrapWithOverflow"
Text="{Tr SettingsDialog.IgnorePatchTuesdayErrors}" />
</CheckBox>
<ComboBox materialDesign:HintAssist.Hint="{Tr SettingsDialog.MafileNamingMode}"
Margin="0,25,0,0"
Style="{StaticResource MaterialDesignFloatingHintComboBox}"
SelectedItem="{Binding UseAccountNameAsMafileNamePreview}">
<ComboBox.ItemsSource>
<x:Array Type="{x:Type sys:Boolean}"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<sys:Boolean>True</sys:Boolean>
<sys:Boolean>False</sys:Boolean>
</x:Array>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource BoolToMafileNamingConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Margin="0,10,0,0"
materialDesign:ButtonProgressAssist.Maximum="1"
materialDesign:ButtonProgressAssist.Value="{Binding RenameMafilesProgress}"
materialDesign:ButtonProgressAssist.IsIndicatorVisible="{Binding ApplyRenameSettingCommand.IsRunning}"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{Binding ApplyRenameSettingCommand}"
Content="{Tr SettingsDialog.ApplyNamingMode}" />
<nebulaAuth:HintBox Margin="0,10,0,0" Text="{Binding RenameResultText}"
FontSize="14"
Visibility="{Binding RenameResultText, Converter={StaticResource NullableToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</TabItem>
<TabItem IsEnabled="True" Header="{Tr SettingsDialog.ThemeSettings}">
<StackPanel Width="340" Margin="10,30,10,10" Orientation="Vertical" HorizontalAlignment="Center">
<TabItem TextElement.FontSize="13" IsEnabled="True" Header="{Tr SettingsDialog.ThemeSettings}">
<StackPanel Width="340" Margin="10,20,10,10" Orientation="Vertical" HorizontalAlignment="Center">
<ComboBox Style="{StaticResource MaterialDesignOutlinedComboBox}"
Padding="8"
Margin="0,15,0,0"
FontSize="16"
FontSize="15"
ItemsSource="{Binding ThemeTypes}" DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding ThemeType}"
materialDesign:HintAssist.Hint="{Tr SettingsDialog.Theme.CurrentTheme}" />
<ComboBox Style="{StaticResource MaterialDesignOutlinedComboBox}"
Padding="8"
Margin="0,15,0,0"
FontSize="15"
ItemsSource="{Binding BackgroundModes}" DisplayMemberPath="Value"
SelectedValuePath="Key"
SelectedValue="{Binding BackgroundMode}"
materialDesign:HintAssist.Hint="{Tr SettingsDialog.BackgroundHint}" />
<!-- Theme input -->
<ComboBox Style="{StaticResource MaterialDesignOutlinedComboBox}"
Padding="8"
Margin="0,15,0,0"
FontSize="16"
ItemsSource="{Binding ThemeTypes}" DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding ThemeType}"
materialDesign:HintAssist.Hint="{Tr SettingsDialog.Theme.CurrentTheme}" />
<TextBlock Text="{Tr SettingsDialog.Theme.BackgroundBlur}" Margin="0,15,0,0" />
<Slider materialDesign:SliderAssist.HideActiveTrack="True"
TickFrequency="5"
Minimum="0" Maximum="100" Value="{Binding BackgroundBlur, Mode=TwoWay}"
materialDesign:HintAssist.Hint="Blur Amount"
@@ -155,15 +183,17 @@
PreviewMouseUp="Slider_OnMouseUp" />
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
IsChecked="{Binding ApplyBlurBackground}" />
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
IsChecked="{Binding ApplyBlurBackground}"
FontSize="15" />
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
FontSize="15"
IsChecked="{Binding RippleDisabled}" />
<Button Margin="0,15,0,0" Command="{Binding ResetThemeDefaultsCommand}"
Content="{Tr SettingsDialog.Theme.Reset}" />
</StackPanel>
</TabItem>
</TabControl>
</Grid>
</Grid>
</UserControl>
-3
View File
@@ -5,9 +5,6 @@ using MaterialDesignThemes.Wpf;
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для SettingsView.xaml
/// </summary>
public partial class SettingsView
{
private readonly DialogHost? _dialogHost;
-3
View File
@@ -1,8 +1,5 @@
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для UpdaterView.xaml
/// </summary>
public partial class UpdaterView
{
public UpdaterView()
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -13,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NLog;
using SteamLib;
@@ -177,7 +177,7 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
var linkOptions = new LinkOptions(_client, StaticLoginConsumer.Instance, this,
this, this, Storage.BackupHandler, Logger2);
this, this, Storage.WriteBackup, Logger2);
var linker = new SteamAuthenticatorLinker(linkOptions);
MobileDataExtended result;
try
@@ -225,11 +225,94 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
Logger.Error(ex, "Error during saving Nebula data to mafile");
}
Storage.SaveMafile(mafile);
File.Delete(Path.Combine("mafiles_backup", mafile.AccountName + ".mafile"));
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString());
await Storage.SaveMafileAsync(mafile);
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString(), login);
}
private void SetCurrentStep(LinkAccountStepVM step)
{
Dispatcher.CurrentDispatcher.Invoke(() =>
{
CurrentStep = step;
Tip = CurrentStep.Tip;
});
}
[RelayCommand]
private void OpenTroubleshooting()
{
const string troubleshootingURI =
"https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/docs/{0}/LinkingTroubleshooting";
var localized = string.Format(troubleshootingURI, LocManager.GetCurrentLanguageCode());
Process.Start(new ProcessStartInfo(new Uri(localized).ToString())
{
UseShellExecute = true
});
}
private static string GetLocalizationOrDefault(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key);
}
private static string GetLocalizationCommon(string key)
{
return LocManager.GetCommonOrDefault(key, key);
}
public void Dispose()
{
_client.Dispose();
_handler.Dispose();
try
{
_cts.Cancel();
}
catch
{
//Ignored, may be cancelled or disposed
}
_cts.Dispose();
}
// @formatter:off
#region Step 2: Email Code
public bool IsSupportedGuardType(ILoginConsumer consumer, EAuthSessionGuardType type)
{
return type == EAuthSessionGuardType.EmailCode;
}
// Step 2: Email Code
public async Task UpdateAuthSession(HttpClient authClient, ILoginConsumer loginConsumer,
UpdateAuthSessionModel model,
CancellationToken cancellationToken = default)
{
while (true)
{
try
{
var step = new LinkAccountEmailAuthStepVM();
SetCurrentStep(step);
var res = await step.GetResultAsync();
var req = AuthRequestHelper.CreateEmailCodeRequest(res, model.ClientId, model.SteamId);
await AuthenticationServiceApi.UpdateAuthSessionWithSteamGuardCode(authClient, req, cancellationToken);
Error = null;
break;
}
catch (SteamStatusCodeException ex)
when (ex.StatusCode.Equals(SteamStatusCode.InvalidLoginAuthCode))
{
Error = ExceptionHandler.GetExceptionString(ex);
}
}
}
#endregion
#region Step 3: Phone Number
// Step 3: Phone number
@@ -280,95 +363,14 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
#region Step 7: Done
public async Task Done(string rCode, string steamId)
public async Task Done(string rCode, string steamId, string login)
{
var step = new LinkAccountDoneStepVM(rCode, steamId);
var filename = Settings.Instance.UseAccountNameAsMafileName ? login : steamId;
var step = new LinkAccountDoneStepVM(rCode, filename);
SetCurrentStep(step);
await step.GetResultAsync();
}
#endregion
private void SetCurrentStep(LinkAccountStepVM step)
{
Dispatcher.CurrentDispatcher.Invoke(() =>
{
CurrentStep = step;
Tip = CurrentStep.Tip;
});
}
[RelayCommand]
private void OpenTroubleshooting()
{
const string troubleshootingURI =
"https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/docs/{0}/LinkingTroubleshooting";
var localized = string.Format(troubleshootingURI, LocManager.GetCurrentLanguageCode());
Process.Start(new ProcessStartInfo(new Uri(localized).ToString())
{
UseShellExecute = true
});
}
private static string GetLocalizationOrDefault(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key);
}
private static string GetLocalizationCommon(string key)
{
return LocManager.GetCommonOrDefault(key, key);
}
public void Dispose()
{
_client.Dispose();
_handler.Dispose();
try
{
_cts.Cancel();
}
catch
{
//Ignored, may be cancelled or disposed
}
_cts.Dispose();
}
#region Step 2: Email Code
public bool IsSupportedGuardType(ILoginConsumer consumer, EAuthSessionGuardType type)
{
return type == EAuthSessionGuardType.EmailCode;
}
// Step 2: Email Code
public async Task UpdateAuthSession(HttpClient authClient, ILoginConsumer loginConsumer,
UpdateAuthSessionModel model,
CancellationToken cancellationToken = default)
{
while (true)
{
try
{
var step = new LinkAccountEmailAuthStepVM();
SetCurrentStep(step);
var res = await step.GetResultAsync();
var req = AuthRequestHelper.CreateEmailCodeRequest(res, model.ClientId, model.SteamId);
await AuthenticationServiceApi.UpdateAuthSessionWithSteamGuardCode(authClient, req, cancellationToken);
Error = null;
break;
}
catch (SteamStatusCodeException ex)
when (ex.StatusCode.Equals(SteamStatusCode.InvalidLoginAuthCode))
{
Error = ExceptionHandler.GetExceptionString(ex);
}
}
}
#endregion
// @formatter:on
}
@@ -1,9 +1,7 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Utility;
namespace NebulaAuth.ViewModel.Linker;
@@ -13,10 +11,10 @@ public partial class LinkAccountDoneStepVM : LinkAccountStepVM
private readonly TaskCompletionSource _doneTcs = new();
private readonly string _rCode;
public LinkAccountDoneStepVM(string rCode, string steamId)
public LinkAccountDoneStepVM(string rCode, string fileName)
{
var tipStr = LocManager.GetCodeBehindOrDefault("MafileLinked", LinkAccountVM.LOCALIZATION_KEY, "MafileLinked");
InnerTip = string.Format(tipStr, rCode, steamId);
InnerTip = string.Format(tipStr, rCode, fileName);
_rCode = rCode;
}
@@ -44,13 +42,6 @@ public partial class LinkAccountDoneStepVM : LinkAccountStepVM
[RelayCommand]
private void CopyCode()
{
try
{
Clipboard.SetText(_rCode);
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Error whily copying RCode");
}
ClipboardHelper.Set(_rCode);
}
}
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -13,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.ViewModel.Linker;
using Newtonsoft.Json;
@@ -236,7 +236,7 @@ public partial class MafileMoverVM : ObservableObject, IAuthProvider, IDisposabl
var t = res.ReplacementToken;
var j = JsonConvert.SerializeObject(t);
Storage.BackupHandlerStr(login, j);
Storage.WriteBackup(login, j);
var mobileData = t.ToMobileDataExtended(SteamAuthenticatorLinkerApi.GenerateDeviceId(), msd);
@@ -253,34 +253,10 @@ public partial class MafileMoverVM : ObservableObject, IAuthProvider, IDisposabl
Logger.Error(ex, "Error during saving Nebula data to mafile");
}
Storage.SaveMafile(mafile);
File.Delete(Path.Combine("mafiles_backup", mafile.AccountName + ".mafile"));
await Storage.SaveMafileAsync(mafile);
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString());
}
#region Step 3: Sms code
// Step 3: Sms code
public Task<int> GetSms()
{
var step = new MafileMoverSmsStepVM();
SetCurrentStep(step);
return step.GetResultAsync();
}
#endregion
#region Step 4: Done
public async Task Done(string rCode, string steamId)
{
var step = new MafileMoverDoneStepVM(rCode, steamId);
SetCurrentStep(step);
await step.GetResultAsync();
}
#endregion
private void SetCurrentStep(MafileMoverStepVM step)
{
Dispatcher.CurrentDispatcher.Invoke(() =>
@@ -335,13 +311,15 @@ public partial class MafileMoverVM : ObservableObject, IAuthProvider, IDisposabl
_cts.Dispose();
}
#region Step 2: Guard Code
public bool IsSupportedGuardType(ILoginConsumer consumer, EAuthSessionGuardType type)
{
return type == EAuthSessionGuardType.DeviceCode;
}
// @formatter:off
#region Step 2: Guard Code
// Step 2: Guard Code
public async Task UpdateAuthSession(HttpClient authClient, ILoginConsumer loginConsumer,
UpdateAuthSessionModel model,
@@ -374,4 +352,29 @@ public partial class MafileMoverVM : ObservableObject, IAuthProvider, IDisposabl
}
#endregion
#region Step 3: Sms code
// Step 3: Sms code
public Task<int> GetSms()
{
var step = new MafileMoverSmsStepVM();
SetCurrentStep(step);
return step.GetResultAsync();
}
#endregion
#region Step 4: Done
public async Task Done(string rCode, string steamId)
{
var step = new MafileMoverDoneStepVM(rCode, steamId);
SetCurrentStep(step);
await step.GetResultAsync();
}
#endregion
// @formatter:on
}
+24 -46
View File
@@ -10,8 +10,8 @@ using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
using SteamLib.SteamMobile;
using SteamLibForked.Exceptions.Authorization;
@@ -22,7 +22,6 @@ public partial class MainVM : ObservableObject
{
[UsedImplicitly] public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
public Mafile? SelectedMafile
{
get => _selectedMafile;
@@ -32,7 +31,7 @@ public partial class MainVM : ObservableObject
public bool IsMafileSelected => SelectedMafile != null;
public DialogHost CurrentDialogHost { get; set; } = null!;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = Storage.MaFiles;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = new(Storage.MaFiles);
private Mafile? _selectedMafile;
@@ -44,21 +43,13 @@ public partial class MainVM : ObservableObject
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
QueryGroups();
UpdateManager.CheckForUpdates();
if (Storage.DuplicateFound > 0)
{
SnackbarController.SendSnackbar(
GetLocalization("DuplicateMafilesFound") + " " + Storage.DuplicateFound,
TimeSpan.FromSeconds(4));
}
}
[RelayCommand]
public async Task Debug()
{
Shell.Logger.Info("test");
}
private void SetMafile(Mafile? mafile)
{
if (mafile != SelectedMafile)
@@ -73,20 +64,23 @@ public partial class MainVM : ObservableObject
OnPropertyChanged(nameof(IsDefaultProxy));
if (mafile != null) Code = SteamGuardCodeGenerator.GenerateCode(mafile.SharedSecret);
OnPropertyChanged(nameof(IsMafileSelected));
RefreshSessionCommand.NotifyCanExecuteChanged();
LoginAgainCommand.NotifyCanExecuteChanged();
RemoveAuthenticatorCommand.NotifyCanExecuteChanged();
ConfirmLoginCommand.NotifyCanExecuteChanged();
RemoveProxyCommand.NotifyCanExecuteChanged();
}
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(IsMafileSelected))]
public async Task LoginAgain()
{
if (SelectedMafile == null)
{
return;
}
var currentPassword = PHandler.DecryptPassword(SelectedMafile.Password);
var loginAgainVm = await DialogsController.ShowLoginAgainDialog(SelectedMafile.AccountName, currentPassword);
var selectedMafile = SelectedMafile;
if (selectedMafile == null) return;
var currentPassword = PHandler.DecryptPassword(selectedMafile.Password);
var loginAgainVm = await DialogsController.ShowLoginAgainDialog(selectedMafile.AccountName, currentPassword);
if (loginAgainVm == null)
{
return;
@@ -97,7 +91,7 @@ public partial class MainVM : ObservableObject
var wait = DialogHost.Show(waitDialog);
try
{
await MaClient.LoginAgain(SelectedMafile, password, loginAgainVm.SavePassword);
await MaClient.LoginAgain(selectedMafile, password, loginAgainVm.SavePassword);
SnackbarController.SendSnackbar(GetLocalization("SuccessfulLogin"));
}
catch (LoginException ex)
@@ -119,17 +113,18 @@ public partial class MainVM : ObservableObject
private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
SearchText = string.Empty;
PerformQuery();
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(IsMafileSelected))]
private async Task RefreshSession()
{
if (SelectedMafile == null) return;
var selectedMafile = SelectedMafile;
if (selectedMafile == null) return;
try
{
await MaClient.RefreshSession(SelectedMafile);
await MaClient.RefreshSession(selectedMafile);
SnackbarController.SendSnackbar(GetLocalization("SessionRefreshed"));
}
catch (Exception ex) when (ExceptionHandler.Handle(ex))
@@ -138,19 +133,13 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
public async Task LinkAccount()
{
await DialogsController.ShowLinkerDialog();
}
[RelayCommand]
public async Task MoveAccount()
{
await DialogsController.ShowMafileMoverDialog();
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(IsMafileSelected))]
private async Task RemoveAuthenticator()
{
var selectedMafile = SelectedMafile;
@@ -176,7 +165,6 @@ public partial class MainVM : ObservableObject
if (result.Success)
{
Storage.MoveToRemoved(selectedMafile);
MaFiles.Remove(selectedMafile);
}
}
else
@@ -191,14 +179,14 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(IsMafileSelected))]
private async Task ConfirmLogin()
{
if (SelectedMafile == null) return;
var selectedMafile = SelectedMafile;
if (selectedMafile == null) return;
try
{
var res = await SessionHandler.Handle(() => MaClient.ConfirmLoginRequest(SelectedMafile), SelectedMafile);
var res = await SessionHandler.Handle(() => MaClient.ConfirmLoginRequest(selectedMafile), selectedMafile);
if (res.Success)
{
SnackbarController.SendSnackbar($"{GetLocalization("ConfirmLoginSuccess")} {res.IP} ({res.Country})");
@@ -222,16 +210,6 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
private async Task OpenLinksView()
{
CurrentDialogHost.CloseOnClickAway = true;
var view = new LinksView();
await DialogHost.Show(view);
CurrentDialogHost.CloseOnClickAway = false;
}
private static string GetLocalization(string key)
{
const string locPath = "MainVM";
@@ -14,7 +14,7 @@ namespace NebulaAuth.ViewModel;
public partial class MainVM //Confirmations
{
public ObservableCollection<Confirmation> Confirmations { get; } = new();
public ObservableCollection<Confirmation> Confirmations { get; } = [];
public bool ConfirmationsVisible => SelectedMafile == _confirmationsLoadedForMafile;
private Mafile? _confirmationsLoadedForMafile;
+8 -7
View File
@@ -13,6 +13,7 @@ using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
@@ -30,11 +31,11 @@ public partial class MainVM //File //TODO: Refactor
{
var mafile = SelectedMafile;
var path = Storage.MafileFolder;
var path = Storage.MafilesDirectory;
string? mafilePath = null;
if (mafile != null)
{
mafilePath = Storage.TryFindMafilePath(mafile);
mafilePath = Storage.TryGetMafilePath(mafile);
}
if (mafilePath != null)
@@ -78,7 +79,7 @@ public partial class MainVM //File //TODO: Refactor
{
try
{
Storage.AddNewMafile(str, confirmOverwrite ?? false);
await Storage.AddNewMafile(str, confirmOverwrite ?? false);
added++;
}
catch (FormatException)
@@ -92,7 +93,7 @@ public partial class MainVM //File //TODO: Refactor
if (confirmOverwrite == true)
{
Storage.AddNewMafile(str, true);
await Storage.AddNewMafile(str, true);
added++;
}
else if (confirmOverwrite == false)
@@ -181,7 +182,7 @@ public partial class MainVM //File //TODO: Refactor
var result = data.SessionData != null;
if (!result) return result;
Storage.SaveMafile(data);
await Storage.SaveMafileAsync(data);
{
ResetQuery();
SearchText = data.AccountName ?? string.Empty;
@@ -239,7 +240,7 @@ public partial class MainVM //File //TODO: Refactor
}
var arr = files.Cast<string>().ToArray();
if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return;
if (arr.All(p => !p.ContainsIgnoreCase("mafile"))) return;
await AddMafile(arr);
@@ -266,7 +267,7 @@ public partial class MainVM //File //TODO: Refactor
private void CopyMafile(object? mafile)
{
if (mafile is not Mafile maf) return;
var path = Storage.TryFindMafilePath(maf);
var path = Storage.TryGetMafilePath(maf);
if (ClipboardHelper.SetFiles([path]))
SnackbarController.SendSnackbar(GetLocalization("MafileCopied"));
}
+20 -5
View File
@@ -1,10 +1,12 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Model;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel;
@@ -33,9 +35,18 @@ public partial class MainVM //Groups
[ObservableProperty] private ObservableCollection<string> _groups = [];
private string _searchText = string.Empty;
private string? _selectedGroup;
[RelayCommand]
private async Task CreateGroup(Mafile? mafile)
{
if (mafile == null) return;
var res = await DialogsController.ShowTextFieldDialog(GetLocalization("CreateGroupTitle"),
GetLocalization("CreateGroupInput"));
if (string.IsNullOrWhiteSpace(res)) return;
AddToGroup([res, mafile]);
SnackbarController.SendSnackbar(GetLocalization("CreateGroupSuccess"));
}
[RelayCommand]
private void AddGroup(string? value)
@@ -68,7 +79,7 @@ public partial class MainVM //Groups
OnPropertyChanged(nameof(SelectedMafile));
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(RemoveGroupCanExecute))]
private void RemoveGroup(Mafile? mafile)
{
if (mafile?.Group == null) return;
@@ -77,7 +88,7 @@ public partial class MainVM //Groups
Storage.UpdateMafile(mafile);
OnPropertyChanged(nameof(SelectedMafile)); //For bindings
QueryGroups();
if (Groups.All(g => g.Equals(mafGroup) == false))
if (Groups.All(g => !g.Equals(mafGroup)))
{
SelectedGroup = null;
}
@@ -85,11 +96,15 @@ public partial class MainVM //Groups
PerformQuery();
}
private bool RemoveGroupCanExecute(Mafile? mafile)
{
return mafile is {Group: not null};
}
private void QueryGroups()
{
var groups = Storage.MaFiles
.Where(m => string.IsNullOrWhiteSpace(m.Group) == false)
.Where(m => !string.IsNullOrWhiteSpace(m.Group))
.Select(m => m.Group)
.Distinct()
.Order()

Some files were not shown because too many files have changed in this diff Show More