Compare commits

..

29 Commits

Author SHA1 Message Date
achiez 9ac4ffdc34 feat(update): introduce modern update system with JSON changelog, reminders and checksum validation
- Replace legacy AutoUpdater dialog with a custom update UI integrated into the application theme
- Add support for JSON-based changelog with HTML fallback for backward compatibility
- Implement "Remind later" and "Skip version" options to reduce intrusive update prompts
- Add visual update indicator and manual "Check for updates" action in the links menu
- Persist user update preferences (skipped versions and reminder delays)
- Introduce checksum validation (SHA256) for downloaded update packages
- Migrate changelog source format from HTML to JSON
- Automatically generate HTML changelog and GitHub release notes from JSON via CI
- Consolidate release automation into a single GitHub Actions workflow
- Remove obsolete legacy update dialog and related code
- Expand localization coverage for update-related UI
2026-03-13 00:17:45 +02:00
achiez 17635ccba0 fix(confirmations): roll back the erroneously assigned ConfirmCommand in PurchaseConfirmation 2026-03-12 23:56:57 +02:00
bohdanbtw 69589075e5 feat(import): support SDA-encrypted mafiles (#17)
NebulaAuth previously supported only plain JSON `.mafile` files. Steam Desktop Authenticator (SDA) can encrypt these files and stores per-file salt and IV values in `manifest.json`, which prevented users from importing accounts protected with SDA encryption.

Add support for importing SDA-encrypted mafiles. The importer now detects encrypted blobs, automatically locates the SDA manifest, prompts for the SDA password once, and decrypts files during import.

The import flow was also refactored to unify handling of plain and encrypted mafiles. SDA detection and decryption logic is moved into a dedicated helper, batch imports reuse the entered password, and the ViewModel import logic was simplified with explicit result handling instead of exception-driven control flow.

This allows users to migrate accounts from Steam Desktop Authenticator to NebulaAuth even when their mafiles are encrypted.
2026-03-12 21:22:18 +02:00
bohdanbtw 4b547e19c4 feat(confirmations): reveal items in grouped market sell confirmations (#18)
Grouped market sell confirmations previously appeared as a single row showing only the item count (e.g. "Sell 4 pcs"). This made it difficult to verify which items were being confirmed or their prices.

Add an expandable grouped confirmation that reveals individual items with their icon, name and price. Also allow confirming or canceling items individually while keeping confirm-all and cancel-all actions.

Includes a scrollable list for large batches and minor UI/code cleanup.

Co-authored-by: bohdanbtw [bohdanbtw@DESKTOP-ANGF2KJ](mailto:bohdanbtw@DESKTOP-ANGF2KJ)
2026-03-12 17:27:03 +02:00
achiez 053faec343 feat(export): add input trimming in export feature 2026-03-12 14:57:27 +02:00
github-actions b8ac76d2a9 chore(release): 1.8.2 2026-02-10 14:43:46 +00:00
Achies b81d45765a Merge pull request #15 from achiez/1.8.2
Release 1.8.2
2026-02-10 16:43:35 +02:00
achiez 372b8c6463 fix(maac): portable MacClient status reset and prepare release
- Add changelog/1.8.2.html with details for version 1.8.2
- Client status is properly updates after successful MAAC request
- Bump version to 1.8.2
2026-02-10 16:40:36 +02:00
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
135 changed files with 6832 additions and 2344 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
+218
View File
@@ -0,0 +1,218 @@
name: Build and Publish Release
on:
push:
branches:
- master
paths:
- 'src/NebulaAuth/NebulaAuth.csproj'
- 'changelog/*.json'
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# --------------------------------------------------------
# Checkout
# --------------------------------------------------------
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
# --------------------------------------------------------
# Setup .NET
# --------------------------------------------------------
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
# --------------------------------------------------------
# Extract version via MSBuild
# --------------------------------------------------------
- name: Extract version from csproj
id: ver
run: |
VERSION=$(dotnet msbuild src/NebulaAuth/NebulaAuth.csproj -getProperty:AssemblyVersion)
echo "Detected version $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
# --------------------------------------------------------
# Validate JSON changelog
# --------------------------------------------------------
- name: Validate changelog JSON
run: |
VERSION="${{ steps.ver.outputs.version }}"
FILE="changelog/${VERSION}.json"
echo "Checking changelog file $FILE"
if [ ! -f "$FILE" ]; then
echo "ERROR: changelog JSON not found"
exit 1
fi
jq empty "$FILE"
# --------------------------------------------------------
# Generate HTML changelog (legacy AutoUpdater)
# --------------------------------------------------------
- name: Generate HTML changelog
run: |
VERSION="${{ steps.ver.outputs.version }}"
JSON="changelog/${VERSION}.json"
HTML="changelog/${VERSION}.html"
echo "Generating HTML changelog"
jq -r '
"<!DOCTYPE html>",
"<html>",
"<head>",
"<meta charset=\"UTF-8\">",
"<title>Changelog</title>",
"<style>",
"body { font-family: Segoe UI, sans-serif; background:#eeeeee; padding:20px; }",
".change { background:white; padding:25px; border-radius:10px; }",
"li { margin-bottom:6px; }",
"</style>",
"</head>",
"<body>",
"<div class=\"change\">",
"<ul>",
(.changes[] |
"<li><b>\(.type):</b> \(.text)" +
(if .link then " <a href=\"\(.link)\">details</a>" else "" end) +
"</li>"
),
"</ul>",
"</div>",
"</body>",
"</html>"
' "$JSON" > "$HTML"
echo "Generated $HTML"
# --------------------------------------------------------
# Build application
# --------------------------------------------------------
- name: Build NebulaAuth
run: |
dotnet publish src/NebulaAuth/NebulaAuth.csproj \
-c Release \
-o build \
-p:EnableWindowsTargeting=true
# --------------------------------------------------------
# Package ZIP
# --------------------------------------------------------
- name: Package release
run: |
cd build
zip -r ../NebulaAuth.${{ steps.ver.outputs.version }}.zip *
# --------------------------------------------------------
# Generate SHA256 checksum
# --------------------------------------------------------
- name: Generate checksum
id: checksum
run: |
FILE="NebulaAuth.${{ steps.ver.outputs.version }}.zip"
HASH=$(sha256sum "$FILE" | awk '{print $1}')
echo "checksum=$HASH" >> $GITHUB_OUTPUT
# --------------------------------------------------------
# Generate update.xml
# --------------------------------------------------------
- name: Generate update.xml
run: |
VERSION="${{ steps.ver.outputs.version }}"
CHECKSUM="${{ steps.checksum.outputs.checksum }}"
cat > NebulaAuth/update.xml <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>$VERSION</version>
<url>https://github.com/${{ github.repository }}/releases/download/$VERSION/NebulaAuth.$VERSION.zip</url>
<changelog>https://achiez.github.io/${{ github.event.repository.name }}/changelog/$VERSION.html</changelog>
<mandatory>false</mandatory>
<checksum algorithm="SHA256">$CHECKSUM</checksum>
</item>
EOF
echo "Generated update.xml"
cat NebulaAuth/update.xml
# --------------------------------------------------------
# Generate GitHub Release Notes
# --------------------------------------------------------
- name: Generate release notes
id: notes
run: |
VERSION="${{ steps.ver.outputs.version }}"
FILE="changelog/${VERSION}.json"
BODY=$(jq -r '
.changes[] |
"- **\(.type):** \(.text)" +
(if .link then " (\(.link))" else "" end)
' "$FILE")
echo "body<<EOF" >> $GITHUB_OUTPUT
echo "$BODY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# --------------------------------------------------------
# Commit generated files
# --------------------------------------------------------
- name: Commit generated files
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git add NebulaAuth/update.xml
git add changelog/*.html
git commit -m "chore(release): ${{ steps.ver.outputs.version }}" || echo "No changes"
git push
# --------------------------------------------------------
# Create git tag
# --------------------------------------------------------
- name: Create tag
run: |
VERSION="${{ steps.ver.outputs.version }}"
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "$VERSION"
# --------------------------------------------------------
# Publish GitHub Release
# --------------------------------------------------------
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: Release ${{ steps.ver.outputs.version }}
body: ${{ steps.notes.outputs.body }}
files: |
NebulaAuth.${{ steps.ver.outputs.version }}.zip
+3 -1
View File
@@ -360,4 +360,6 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
todo/
+5 -8
View File
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33205.214
# Visual Studio Version 18
VisualStudioVersion = 18.3.11520.95 d18.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{161BFADE-FCF3-45D1-82EA-8A1B187529F7}"
ProjectSection(SolutionItems) = preProject
@@ -23,12 +23,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{
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
changelog\1.8.2.html = changelog\1.8.2.html
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamLibForked", "src\SteamLibForked\SteamLibForked.csproj", "{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaAuth.LegacyConverter", "src\NebulaAuth.LegacyConverter\NebulaAuth.LegacyConverter.csproj", "{C8235305-E5C4-155B-C718-C0F239CA3AB7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaAuth", "src\NebulaAuth\NebulaAuth.csproj", "{C42F63B6-32F7-ED08-5B86-CD51989761AD}"
EndProject
Global
@@ -41,10 +42,6 @@ Global
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Release|Any CPU.Build.0 = Release|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Release|Any CPU.Build.0 = Release|Any CPU
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+7 -8
View File
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.7.4.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.7.4/NebulaAuth.1.7.4.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.7.4.html</changelog>
<mandatory>false</mandatory>
</item>
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.8.2</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.2/NebulaAuth.1.8.2.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.2.html</changelog>
<mandatory>false</mandatory>
</item>
+1 -1
View File
@@ -76,7 +76,7 @@
<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>
<li><b>FIX:</b> Resolved issue where a "Confirmation Error" notification appeared despite the confirmation being successful.</li>
</ul>
</div>
</div>
+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>
+78
View File
@@ -0,0 +1,78 @@
<!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.2</div>
<div class="date">10.02.2026</div>
<div class="description">
<ul>
<li><b>FIX:</b> AutoConfirm timer now correctly resets the status after a successful request (Warning → Ok).</li>
</ul>
</div>
</div>
</div>
</body>
</html>
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SteamLibForked\SteamLibForked.csproj" />
</ItemGroup>
</Project>
-176
View File
@@ -1,176 +0,0 @@
using System.Reflection;
using AchiesUtilities.Extensions;
using NebulaAuth.LegacyConverter;
using Newtonsoft.Json;
using SteamLib.Utility.MafileSerialization;
try
{
var mafileSerializer = new MafileSerializer(new MafileSerializerSettings
{
DeserializationOptions =
{
AllowDeviceIdGeneration = true,
AllowSessionIdGeneration = true
}
});
var currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (currentPath != null)
Environment.CurrentDirectory = currentPath;
const string toStoreFolder = "ConvertedMafiles";
if (Directory.Exists(toStoreFolder) == false)
{
Directory.CreateDirectory(toStoreFolder);
}
else
{
var isEmpty = Directory.GetFiles(toStoreFolder).Length == 0;
Console.ForegroundColor = ConsoleColor.Yellow;
if (!isEmpty)
{
Console.WriteLine(
"WARNING! 'ConverterdMafiles' folder is not empty. Please backup data from it and then continue.");
Console.ResetColor();
Console.WriteLine("Press Y to continue");
while (Console.ReadKey(true).Key != ConsoleKey.Y)
{
}
}
}
var decryptMode = false;
while (true)
{
Console.WriteLine("Press 'D' to select decrypt mode. Press 'C' to convert mode. Press ESC to exit");
var key = Console.ReadKey(true);
switch (key.Key)
{
case ConsoleKey.D:
decryptMode = true;
break;
case ConsoleKey.C:
break;
case ConsoleKey.Escape:
return;
default:
continue;
}
break;
}
Manifest? manifest = null;
string? password = null;
if (decryptMode)
{
var files = Directory.GetFiles(currentPath, "manifest.json");
var manifestPath = files.FirstOrDefault();
if (manifestPath == null)
{
Console.WriteLine("No manifest.json found in current directory");
return;
}
var manifestText = File.ReadAllText(manifestPath);
try
{
manifest = JsonConvert.DeserializeObject<Manifest>(manifestText)!;
}
catch (Exception ex)
{
Console.WriteLine("Can't read manifest: " + ex);
return;
}
if (manifest.Encrypted == false)
{
Console.WriteLine("Manifest is not encrypted");
return;
}
while (true)
{
Console.WriteLine("Please enter your encryption password: ");
password = Console.ReadLine();
if (string.IsNullOrWhiteSpace(password))
continue;
break;
}
}
Console.WriteLine(currentPath);
foreach (var path in args)
{
if (Path.Exists(path) == false)
{
Console.WriteLine($"NOT VALID PATH: '{path}'");
continue;
}
Console.WriteLine("Reading: " + path);
try
{
var text = File.ReadAllText(path);
if (decryptMode)
{
var fileName = Path.GetFileName(path);
text = DecryptMafile(fileName, text);
if (text == null)
{
Console.WriteLine(path + " not found in manifest. Skipped");
continue;
}
}
var data = mafileSerializer.Deserialize(text);
var maf = data.Data;
var legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented);
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
Write(legacy, fileNameWithoutExtension);
Console.WriteLine("DONE: " + fileNameWithoutExtension);
}
catch (Exception ex)
{
Console.WriteLine($"ERROR: {ex.Message}");
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("-----------------------------------------");
}
}
//Local Functions
void Write(string maf, string name)
{
var path = Path.Combine(toStoreFolder, name + "_legacy.mafile");
File.WriteAllText(path, maf);
}
string? DecryptMafile(string fileName, string cipherText)
{
if (password == null) return null;
var entry = manifest?.Entries.FirstOrDefault(x => x.Filename.EqualsIgnoreCase(fileName));
if (entry == null)
{
return null;
}
var iv = entry.EncryptionIv;
var salt = entry.EncryptionSalt;
return SDAEncryptor.DecryptData(password, salt, iv, cipherText);
}
}
finally
{
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
+1
View File
@@ -33,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();
}
}
+73 -20
View File
@@ -1,8 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoUpdaterDotNET;
using MaterialDesignThemes.Wpf;
using Microsoft.Win32;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Update;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
using NebulaAuth.ViewModel.Linker;
@@ -13,25 +16,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
@@ -70,6 +54,25 @@ public static class DialogsController
return null;
}
/// <summary>
/// Shows the SDA encryption password dialog for unlocking SDA-encrypted mafiles.
/// </summary>
/// <returns>The entered password if user clicked OK, otherwise null.</returns>
public static async Task<string?> ShowSdaPasswordDialog()
{
var vm = new SdaPasswordDialogVM();
var content = new SdaPasswordDialog
{
DataContext = vm
};
var result = await DialogHost.Show(content);
if (result is true)
{
return vm.Password;
}
return null;
}
public static async Task<bool> ShowProxyManager()
{
var vm = new ProxyManagerVM();
@@ -121,4 +124,54 @@ 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);
}
public static async Task ShowUpdateDialog(UpdateInfoEventArgs args, ChangelogEntry? changelog,
string? htmlFallbackUrl)
{
var vm = new UpdateDialogVM(args, changelog, htmlFallbackUrl);
var dialog = new UpdateDialog
{
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();
}
+115 -50
View File
@@ -1,75 +1,140 @@
using System;
using System.IO;
using AutoUpdaterDotNET;
using AutoUpdaterDotNET;
using NebulaAuth.Model;
using NebulaAuth.Model.Update;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net.Http;
using System.Windows;
namespace NebulaAuth.Core;
public static class UpdateManager
{
private const string UPDATE_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/NebulaAuth/update.xml";
private const string BASE_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/";
private const string UPDATE_URL = BASE_URL + "NebulaAuth/update.xml";
public static void CheckForUpdates()
private const string CHANGELOG_BASE_URL = BASE_URL + "changelog/";
private static readonly HttpClient HttpClient = new();
private static bool _isManualCheck;
public static bool HasPendingUpdate { get; private set; }
public static event Action? PendingUpdateDetected;
static UpdateManager()
{
AutoUpdater.CheckForUpdateEvent += HandleCheckForUpdateEvent;
}
public static void CheckForUpdates(bool manual = false)
{
_isManualCheck = manual;
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);
}
public static void SkipVersion(string version)
{
var settings = UpdateSettings.Load();
settings.SkipVersion(version);
}
//static UpdateManager()
//{
// //AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
public static void SetRemindAfter(TimeSpan delay)
{
var settings = UpdateSettings.Load();
settings.SetRemindAfter(DateTime.Now + delay);
}
//}
private static async void HandleCheckForUpdateEvent(UpdateInfoEventArgs args)
{
try
{
var isManual = _isManualCheck;
_isManualCheck = false;
//private static async void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
//{
// if (args.Error == null)
// {
// if (args.IsUpdateAvailable)
// {
// DialogResult dialogResult;
// var dialog = new UpdaterView()
// {
// DataContext = new UpdaterVM(args)
// };
if (args.Error != null)
{
if (isManual)
{
Application.Current.Dispatcher.Invoke(() =>
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Request error", "RequestError")));
}
// await DialogHost.Show(dialog);
// Application.Current.Shutdown();
return;
}
// }
// else
// {
if (!args.IsUpdateAvailable)
{
if (isManual)
{
Application.Current.Dispatcher.Invoke(() =>
SnackbarController.SendSnackbar(
LocManager.GetCodeBehindOrDefault("You are using the latest version", "UpdateService",
"LatestVersion")));
}
// }
// }
// else
// {
// if (args.Error is WebException)
// {
return;
}
// }
// else
// {
var version = args.CurrentVersion.ToString();
var settings = UpdateSettings.Load();
// }
// }
if (!isManual && !settings.ShouldShow(version))
{
HasPendingUpdate = true;
Application.Current.Dispatcher.Invoke(() => PendingUpdateDetected?.Invoke());
return;
}
//}
ChangelogEntry? changelog = null;
try
{
var jsonUrl = $"{CHANGELOG_BASE_URL}{version}.json";
var response = await HttpClient.GetAsync(jsonUrl).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
changelog = JsonConvert.DeserializeObject<ChangelogEntry>(json);
}
}
catch
{
// fallback to HTML changelog URL
}
//private static void RunUpdate(UpdateInfoEventArgs args)
//{
// Application.Current.Dispatcher.Invoke(() =>
// {
// if (AutoUpdater.DownloadUpdate(args))
// {
// Application.Current.Shutdown();
// }
// }, DispatcherPriority.ContextIdle);
//}
var htmlFallbackUrl = changelog == null ? args.ChangelogURL : null;
await Application.Current.Dispatcher.BeginInvoke(async () =>
{
await DialogsController.ShowUpdateDialog(args, changelog, htmlFallbackUrl);
});
}
catch (Exception e)
{
Shell.Logger.Error(e, "Error while checking updates");
}
}
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;
}
}
}
+234 -121
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,109 +417,138 @@
<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">
<Hyperlink
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<md:PackIcon x:Name="UpdateBadgeIcon"
Kind="BellAlert" Width="15" Height="15"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,5,0"
Visibility="Collapsed"
ToolTip="{Tr CodeBehind.UpdateService.UpdateIndicatorHint}">
<md:PackIcon.Foreground>
<SolidColorBrush x:Name="UpdateBadgeBrush" Color="DodgerBlue" />
</md:PackIcon.Foreground>
</md:PackIcon>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
Margin="0,0,10,0">
<Hyperlink
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
Foreground="{DynamicResource AccentBrush}"
Command="{Binding OpenLinksViewCommand}">
by achies
</Hyperlink>
</TextBlock>
Command="{Binding OpenLinksViewCommand}">
<Hyperlink.Foreground>
<SolidColorBrush x:Name="ByAchiesBrush" Color="DodgerBlue" />
</Hyperlink.Foreground>
by achies
</Hyperlink>
</TextBlock>
</StackPanel>
</Grid>
</Border>
<md:Snackbar Grid.Row="1" MessageQueue="{Binding MessageQueue}" />
</Grid>
</md:DialogHost>
</Border>
</w:FontScaleWindow>
</Window>
+75 -12
View File
@@ -1,15 +1,18 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using MaterialDesignThemes.Wpf;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.View.Dialogs;
using NebulaAuth.ViewModel;
using NebulaAuth.ViewModel.Other;
using System;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace NebulaAuth;
@@ -24,12 +27,49 @@ public partial class MainWindow
ThemeManager.InitializeTheme();
Title = Title + " " + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3);
Loaded += OnApplicationStarted;
UpdateManager.PendingUpdateDetected += OnPendingUpdateDetected;
}
private void OnPendingUpdateDetected()
{
Dispatcher.BeginInvoke(StartUpdateBlink);
}
private void StartUpdateBlink()
{
UpdateBadgeIcon.Visibility = Visibility.Visible;
var baseColor = FindResource("AccentBrush") is SolidColorBrush accent
? accent.Color
: ByAchiesBrush.Color;
var duration = new Duration(TimeSpan.FromSeconds(6));
var sb = new Storyboard();
sb.Children.Add(BuildBlinkAnimation(duration, "UpdateBadgeBrush"));
sb.Children.Add(BuildBlinkAnimation(duration, "ByAchiesBrush"));
sb.Completed += (_, _) =>
{
UpdateBadgeIcon.Visibility = Visibility.Collapsed;
ByAchiesBrush.Color = baseColor;
};
sb.Begin(this);
}
private static ColorAnimationUsingKeyFrames BuildBlinkAnimation(Duration duration, string targetName)
{
var anim = new ColorAnimationUsingKeyFrames { Duration = duration };
for (var i = 0; i <= 6; i++)
{
var color = i % 2 == 0 ? Colors.DodgerBlue : Colors.OrangeRed;
anim.KeyFrames.Add(new DiscreteColorKeyFrame(color, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i))));
}
Storyboard.SetTargetName(anim, targetName);
Storyboard.SetTargetProperty(anim, new PropertyPath(SolidColorBrush.ColorProperty));
return anim;
}
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 +85,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 +135,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,177 @@
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) && data.Clear())
{
Shell.Logger.Info("MAAC request for {name} MAAC account succeeded, error history cleared",
mafile.AccountName);
}
var client = mafile.LinkedClient;
if (client != null && client.Status.StatusType != PortableMaClientStatusType.Ok)
{
client.SetStatus(PortableMaClientStatus.Ok());
}
}
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
@@ -58,8 +58,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 +68,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 +114,30 @@ 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)
+22 -95
View File
@@ -11,11 +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;
@@ -34,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)
{
@@ -46,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)
@@ -56,37 +55,21 @@ 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)
@@ -103,20 +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));
//TODO: handle success == false
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;
}
@@ -148,58 +123,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
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);
}
@@ -214,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,41 @@
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 bool Clear()
{
if (NoErrors) return false;
lock (_lock)
Values.Clear();
return true;
}
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);
}
}
+6 -21
View File
@@ -20,11 +20,8 @@ namespace NebulaAuth.Model;
public static class MaClient
{
private static HttpClientHandler ClientHandler { get; }
private static HttpClient Client { get; }
private static DynamicProxy Proxy { get; }
public static ProxyData? DefaultProxy { get; set; }
@@ -40,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)
@@ -99,8 +84,8 @@ public static class MaClient
}
return await SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId,
mafile,
confirm);
mafile,
confirm);
}
public static async Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations,
@@ -158,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,8 @@
namespace NebulaAuth.Model.Mafiles;
public enum AddMafileResult
{
Added,
AlreadyExist,
Error
}
@@ -0,0 +1,50 @@
using System;
namespace NebulaAuth.Model.Mafiles;
public class MafileImportSummary
{
public int Added { get; private set; }
public int NotAdded { get; private set; }
public int Errors { get; private set; }
public void AddedOne()
{
Added++;
}
public void SkippedOne()
{
NotAdded++;
}
public void ErrorOne()
{
Errors++;
}
public void Add(int added = 0, int notAdded = 0, int errors = 0)
{
Added += added;
NotAdded += notAdded;
Errors += errors;
}
public void Apply(AddMafileResult result)
{
switch (result)
{
case AddMafileResult.Added:
AddedOne();
break;
case AddMafileResult.AlreadyExist:
SkippedOne();
break;
case AddMafileResult.Error:
ErrorOne();
break;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, 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);
}
}
+310
View File
@@ -0,0 +1,310 @@
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>
/// <returns> Result of adding mafile</returns>
/// <exception cref="SessionInvalidException"></exception>
public static async Task<AddMafileResult> AddNewMafile(string path, bool overwrite)
{
Mafile data;
try
{
data = await ReadMafileAsync(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);
}
await AddNewMafileInternal(data, overwrite);
return AddMafileResult.Added;
}
/// <summary>
/// </summary>
/// <param name="data"></param>
/// <param name="overwrite"></param>
/// <returns> Result of adding mafile</returns>
/// <exception cref="SessionInvalidException"></exception>
public static Task<AddMafileResult> AddNewMafileFromData(Mafile data, bool overwrite)
{
return AddNewMafileInternal(data, overwrite);
}
private static async Task<AddMafileResult> AddNewMafileInternal(Mafile data, bool overwrite)
{
ArgumentNullException.ThrowIfNull(data);
if (string.IsNullOrWhiteSpace(data.AccountName))
{
Shell.Logger.Warn("Mafile account name is empty");
return AddMafileResult.Error;
}
try
{
_ = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Can't generate code for mafile {accountName}", data.AccountName);
return AddMafileResult.Error;
}
data.Filename = null;
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
{
return AddMafileResult.AlreadyExist;
}
await SaveMafileAsync(data);
return AddMafileResult.Added;
}
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;
}
}
}
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MafilesLegacy;
/// <summary>
/// Helper class for detecting and handling SDA encrypted mafiles.
/// This is used in the legacy mafile handling code to support importing SDA encrypted mafiles without requiring the
/// user to manually decrypt them first.
/// </summary>
public static class SDAEncryptionHelper
{
private const string ManifestFileName = "manifest.json";
public static bool LooksLikeSdaEncryptedBlob(string content)
{
if (string.IsNullOrWhiteSpace(content))
return false;
var trimmed = content.Trim();
if (trimmed.StartsWith('{') || trimmed.Length < 64)
return false;
Span<byte> buffer = stackalloc byte[trimmed.Length];
return Convert.TryFromBase64String(trimmed, buffer, out _);
}
public static Context? TryDetect(string mafilePath, SDAManifest? sdaManifest)
{
if (string.IsNullOrWhiteSpace(mafilePath))
return null;
if (sdaManifest == null)
{
var dir = Path.GetDirectoryName(mafilePath);
if (string.IsNullOrWhiteSpace(dir))
return null;
var manifestPath = FindManifestPath(dir);
if (manifestPath == null)
return null;
sdaManifest = TryReadEncryptedManifest(manifestPath);
}
if (sdaManifest is not {Encrypted: true} || sdaManifest.Entries.Length == 0)
return null;
var fileName = Path.GetFileName(mafilePath);
if (string.IsNullOrWhiteSpace(fileName))
return null;
var entries = sdaManifest.Entries.ToDictionary(x => Path.GetFileName(x.Filename), StringComparer.OrdinalIgnoreCase);
return new Context(sdaManifest, entries, null);
}
private static string? FindManifestPath(string startDirectory)
{
var current = startDirectory;
for (var i = 0; i < 2; i++)
{
var candidate = Path.Combine(current, ManifestFileName);
if (File.Exists(candidate))
return candidate;
var parent = Directory.GetParent(current);
if (parent?.FullName == null)
break;
current = parent.FullName;
}
return null;
}
private static SDAManifest? TryReadEncryptedManifest(string manifestPath)
{
try
{
var manifestText = File.ReadAllText(manifestPath);
var manifest = JsonConvert.DeserializeObject<SDAManifest>(manifestText);
if (manifest is not {Encrypted: true} || manifest.Entries.Length == 0)
return null;
return manifest;
}
catch (Exception)
{
return null;
}
}
public static string? TryDecrypt(string content, string path, Context context)
{
if (string.IsNullOrWhiteSpace(context.Password))
return null;
// If we have password, check if we have entry
var entry = context.GetEntry(path);
if (entry == null)
{
return null;
}
// We have entry, try to decrypt
return SDAEncryptor.TryDecryptData(context.Password, entry.EncryptionSalt, entry.EncryptionIv, content,
out var decrypted)
? decrypted
: null;
}
/// <summary>
/// Result of SDA encrypted mafile detection. When not null, the caller can decrypt
/// using SDAEncryptor.DecryptData(password, Entry.EncryptionSalt, Entry.EncryptionIv, fileContent).
/// </summary>
public sealed class Context
{
public SDAManifest SdaManifest { get; }
public Dictionary<string, SDAManifestEntry> Entries { get; }
public string? Password { get; }
public Context(SDAManifest sdaManifest, Dictionary<string, SDAManifestEntry> entries, string? password)
{
SdaManifest = sdaManifest;
Entries = entries;
Password = password;
}
public Context WithPassword(string? password)
{
return new Context(SdaManifest, Entries, password);
}
public SDAManifestEntry? GetEntry(string path)
{
var fileName = Path.GetFileName(path);
return Entries.GetValueOrDefault(fileName);
}
}
}
@@ -1,7 +1,10 @@
using System.Security.Cryptography;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
namespace NebulaAuth.LegacyConverter;
namespace NebulaAuth.Model.MafilesLegacy;
#pragma warning disable all
#pragma warning disable SYSLIB0023
@@ -83,6 +86,20 @@ public static class SDAEncryptor
}
}
public static bool TryDecryptData(string password, string passwordSalt, string IV, string encryptedData, [NotNullWhen(true)] out string? plaintext)
{
plaintext = null;
try
{
plaintext = DecryptData(password, passwordSalt, IV, encryptedData);
return plaintext != null;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Tries to decrypt and return data given an encrypted base64 encoded string. Must use the same
/// password, salt, IV, and ciphertext that was used during the original encryption of the data.
@@ -4,15 +4,15 @@
//Pragma disable for legacy code
namespace NebulaAuth.LegacyConverter;
namespace NebulaAuth.Model.MafilesLegacy;
public class Manifest
public class SDAManifest
{
[JsonProperty("encrypted")] public bool Encrypted { get; set; }
[JsonProperty("first_run")] public bool FirstRun { get; set; }
[JsonProperty("entries")] public Entry[] Entries { get; set; }
[JsonProperty("entries")] public SDAManifestEntry[] Entries { get; set; }
[JsonProperty("periodic_checking")] public bool PeriodicChecking { get; set; }
@@ -28,7 +28,7 @@ public class Manifest
[JsonProperty("auto_confirm_trades")] public bool AutoConfirmTrades { get; set; }
}
public class Entry
public class SDAManifestEntry
{
[JsonProperty("encryption_iv")] public string EncryptionIv { get; set; }
+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);
}
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NebulaAuth.Model.Update;
public class ChangelogEntry
{
[JsonProperty("version")]
public string Version { get; set; } = string.Empty;
[JsonProperty("date")]
public string Date { get; set; } = string.Empty;
[JsonProperty("changes")]
public List<ChangeItem> Changes { get; set; } = new();
}
public class ChangeItem
{
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
[JsonProperty("text")]
public string Text { get; set; } = string.Empty;
[JsonProperty("link")]
public string? Link { get; set; }
}
@@ -0,0 +1,59 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace NebulaAuth.Model.Update;
public class UpdateSettings
{
private static readonly string FilePath = Path.Combine("settings", "update.json");
[JsonProperty("skippedVersion")]
public string? SkippedVersion { get; set; }
[JsonProperty("remindAfter")]
public DateTime? RemindAfter { get; set; }
public static UpdateSettings Load()
{
try
{
if (!File.Exists(FilePath)) return new UpdateSettings();
var json = File.ReadAllText(FilePath);
return JsonConvert.DeserializeObject<UpdateSettings>(json) ?? new UpdateSettings();
}
catch
{
return new UpdateSettings();
}
}
public void Save()
{
var dir = Path.GetDirectoryName(FilePath)!;
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(FilePath, json);
}
public void SkipVersion(string version)
{
SkippedVersion = version;
RemindAfter = null;
Save();
}
public void SetRemindAfter(DateTime remindAfter)
{
RemindAfter = remindAfter;
Save();
}
public bool ShouldShow(string version)
{
if (SkippedVersion == version) return false;
if (RemindAfter.HasValue && DateTime.Now < RemindAfter.Value) return false;
return true;
}
}
+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}" />
+11 -3
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.4</AssemblyVersion>
<AssemblyVersion>1.8.2</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" />
@@ -53,6 +53,9 @@
<Compile Update="View\Dialogs\LoginAgainOnImportDialog.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="View\Dialogs\SdaPasswordDialog.xaml.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
@@ -65,4 +68,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>
-11
View File
@@ -1,11 +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;
}
}
+92 -34
View File
@@ -1,13 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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 +108,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 +117,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}">
@@ -176,36 +174,96 @@
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type entities:MarketMultiConfirmation}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="ShoppingCartPlus"
Margin="0,0,10,0" />
<TextBlock VerticalAlignment="Center" Grid.Column="1">
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
</TextBlock>
<Expander Padding="0" Background="Transparent" materialDesign:ExpanderAssist.ExpanderButtonPosition="Start" materialDesign:ExpanderAssist.HorizontalHeaderPadding="0" HorizontalAlignment="Stretch">
<Expander.Header>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<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}" />
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
</TextBlock>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left" Margin="5,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Check" />
</Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Center" Margin="5,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<!-- Confirm all -->
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="4"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmAll}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="CheckAll" />
</Button>
<!-- Cancel all -->
<Button Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelAll}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</Expander.Header>
<Border Margin="30,6,0,0" Background="{DynamicResource MaterialDesignPaper}" CornerRadius="4">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
MaxHeight="320">
<ItemsControl ItemsSource="{Binding Confirmations}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type confirmations:MarketConfirmation}">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="32"
MaxHeight="32" Margin="0,0,10,0"
Source="{Binding ItemImageUri}" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
</StackPanel>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left"
Margin="8,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Check" />
</Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Expander>
</DataTemplate>
<DataTemplate DataType="{x:Type confirmations:PurchaseConfirmation}">
<Grid>
@@ -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()
@@ -0,0 +1,71 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.SdaPasswordDialog"
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"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
d:DataContext="{d:DesignInstance other:SdaPasswordDialogVM}"
Background="{DynamicResource WindowBackground}">
<Grid MinHeight="200" MinWidth="400" MaxWidth="500">
<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="Lock" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontSize="16" VerticalAlignment="Center" Foreground="{DynamicResource BaseContentBrush}"
Margin="7,0,0,0" HorizontalAlignment="Left"
Text="{Tr SdaPasswordDialog.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" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Tr SdaPasswordDialog.Message}" TextWrapping="Wrap"
Foreground="{DynamicResource BaseContentBrush}" Margin="10,10,10,8" />
<TextBox Grid.Row="1" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
materialDesign:TextFieldAssist.HasClearButton="True"
Padding="10" FontSize="15" Margin="10,0,10,0"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
materialDesign:HintAssist.Hint="{Tr SdaPasswordDialog.PasswordHint}"
materialDesign:HintAssist.IsFloating="False"
materialDesign:TextFieldAssist.LeadingIcon="Key"
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
<Grid Grid.Row="2" Margin="10,16,10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button IsEnabled="{Binding IsFormValid}" IsDefault="True" Margin="0,5,5,5"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource True}" Content="{Tr Common.OK}" />
<Button IsCancel="True" Grid.Column="1" Margin="5,5,0,5"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource False}" Content="{Tr Common.Cancel}" />
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View.Dialogs;
public partial class SdaPasswordDialog
{
public SdaPasswordDialog()
{
InitializeComponent();
}
}
@@ -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);
}
}
@@ -0,0 +1,172 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.UpdateDialog"
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"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
d:DataContext="{d:DesignInstance other:UpdateDialogVM}"
Background="{DynamicResource WindowBackground}"
MinWidth="420" MaxWidth="600">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<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">
<materialDesign:PackIcon Kind="Update" 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">
<Run Text="{Tr UpdateDialog.Title, IsDynamic=False}" />
<Run FontWeight="Bold" Text="{Binding Version, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right"
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" />
<!-- Changelog -->
<Grid Grid.Row="2" Margin="12,10,12,4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Tr UpdateDialog.Changelog}" FontSize="14" FontWeight="SemiBold"
Margin="0,0,0,8"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}" />
<!-- JSON changelog items -->
<materialDesign:Card Grid.Row="1" Padding="10">
<ScrollViewer MaxHeight="350" VerticalScrollBarVisibility="Auto"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding Changelog.Changes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,6">
<Border CornerRadius="3" Padding="4,2,4,2" Margin="0,0,8,0"
VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{DynamicResource MaterialDesign.Brush.Primary}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="NEW">
<Setter Property="Background" Value="{DynamicResource SuccessBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="FIX">
<Setter Property="Background" Value="{DynamicResource WarningBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="IMPROVEMENT">
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="SECURITY">
<Setter Property="Background" Value="{DynamicResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Type}" FontSize="11" FontWeight="Bold"
Foreground="White" />
</Border>
<TextBlock Text="{Binding Text}" TextWrapping="Wrap"
VerticalAlignment="Center" FontSize="14" MaxWidth="420" />
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
Width="20" Height="20"
Padding="2"
Margin="4,0,0,0"
VerticalAlignment="Center"
Command="{Binding DataContext.OpenLinkCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding Link}"
ToolTip="Open Link"
Visibility="{Binding Link, Converter={StaticResource NullableToVisibilityConverter}}">
<materialDesign:PackIcon Kind="OpenInNew" Width="14" Height="14" />
</Button>
</StackPanel>
<Separator Opacity="0.2" Margin="0,2,0,2"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</materialDesign:Card>
<!-- No changelog fallback -->
<TextBlock Grid.Row="1" Text="{Tr UpdateDialog.NoChangelog}" FontSize="13" Opacity="0.6"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
</Grid>
<!-- Buttons -->
<Grid Grid.Row="3" Margin="12,6,12,14">
<!-- Normal buttons -->
<StackPanel Visibility="{Binding ShowRemindOptions, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
<Button Style="{StaticResource MaterialDesignRaisedButton}"
Command="{Binding UpdateNowCommand}"
HorizontalAlignment="Stretch"
Margin="0,0,0,8"
Content="{Tr UpdateDialog.UpdateNow}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="8" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{Binding ToggleRemindOptionsCommand}"
Content="{Tr UpdateDialog.RemindLater}" />
<Button Grid.Column="2"
Style="{StaticResource MaterialDesignFlatButton}"
Command="{Binding SkipVersionCommand}"
Content="{Tr UpdateDialog.SkipVersion}" />
</Grid>
</StackPanel>
<!-- Remind later options -->
<StackPanel Visibility="{Binding ShowRemindOptions, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock Text="{Tr UpdateDialog.RemindAfter}" Margin="0,0,0,8" FontSize="14" />
<WrapPanel>
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="1H"
Content="{Tr UpdateDialog.RemindOptions.1Hour}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="1D"
Content="{Tr UpdateDialog.RemindOptions.1Day}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="3D"
Content="{Tr UpdateDialog.RemindOptions.3Days}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="7D"
Content="{Tr UpdateDialog.RemindOptions.7Days}" />
</WrapPanel>
<Button Style="{StaticResource MaterialDesignFlatButton}"
HorizontalAlignment="Left"
Command="{Binding ToggleRemindOptionsCommand}"
Content="{Tr Common.Cancel}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View.Dialogs;
public partial class UpdateDialog
{
public UpdateDialog()
{
InitializeComponent();
}
}
@@ -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()
+10 -1
View File
@@ -58,7 +58,7 @@
</Button>
<Button Margin="0,0,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="Website_Click" IsEnabled="False">
Click="Documentation_Click" IsEnabled="False">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Viewbox Width="22" Height="22" Margin="0 0 8 0">
<Canvas Width="22" Height="22">
@@ -72,6 +72,15 @@
</StackPanel>
</Button>
<Button Margin="0,5,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="CheckForUpdates_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<materialDesign:PackIcon Kind="Update" Width="22" Height="22" Margin="0 0 8 0"
Foreground="{DynamicResource MaterialDesign.Brush.Primary}" />
<TextBlock Text="{Tr LinksView.CheckForUpdates}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" IsCancel="True" Width="0"
Height="0" Visibility="Visible" />
</StackPanel>
+9 -4
View File
@@ -1,12 +1,11 @@
using System.Diagnostics;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace NebulaAuth.View;
/// <summary>
/// Логика взаимодействия для LinksView.xaml
/// </summary>
public partial class LinksView : UserControl
{
public LinksView()
@@ -34,4 +33,10 @@ public partial class LinksView : UserControl
{
Process.Start(new ProcessStartInfo("https://yourwebsite.com") {UseShellExecute = true});
}
private void CheckForUpdates_Click(object sender, RoutedEventArgs e)
{
DialogHost.Close(null);
UpdateManager.CheckForUpdates(manual: true);
}
}
+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();
}
}

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