Compare commits

...

69 Commits

Author SHA1 Message Date
Achies f2daaeb64e Update README.md 2026-06-20 18:00:23 +03:00
github-actions fd3e131d33 chore(release): 1.8.5 2026-06-20 11:46:25 +00:00
Achies f7cc7dcf99 Merge pull request #26 from achiez/dev
Release 1.8.5
2026-06-20 14:45:51 +03:00
achiez 16f0cc2b2d chore: update AssemblyVersion to 1.8.5 and add changelog for new features and fixes 2026-06-20 00:49:26 +03:00
achiez cd0416705f fix: add missing SignAt scheme in Proxy Assignment 2026-06-19 23:36:04 +03:00
Achies 2d06cb81f9 Merge pull request #30 from achiez/feat/website-links
feat(links): fetch website and docs URLs from GitHub at startup
2026-06-19 22:36:21 +03:00
achiez ed5d3c2eda feat(links): fetch website and docs URLs from GitHub at startup
- Add links.json to repo (alongside update.xml) with Website and Documentation URLs.
- Add LinksManager to support link fetching on startup via raw.githubusercontent.com
- Suppress TimeAligner failure instead of blocking app launch
2026-06-19 22:34:37 +03:00
Achies 07eb9f15a9 Merge pull request #29 from achiez/feat/login-again-on-session-expired
feat(session): show LoginAgainDialog on permanent session expiry
2026-06-19 22:06:44 +03:00
achiez c39660fc55 feat(session): show LoginAgainDialog on permanent session expiry
- Replace snackbar with interactive LoginAgainDialog when session cannot be   recovered automatically on user-initiated actions
- Show proxy warning hint in dialog when no proxy assigned to account or set as default
- Pass allowInteractiveLogin: false from MAACRequestHandler to skip dialog in  automated MAAC confirmations
2026-06-19 22:05:49 +03:00
Achies 038f62b4db Merge pull request #28 from achiez/feat/export-zip-and-improvements
feat(export): add zip export, export-all, and login:password input parsing
2026-06-19 21:21:51 +03:00
achiez 5029f78247 feat(export): add zip export, export-all, and login:password input parsing 2026-06-19 21:18:34 +03:00
Achies b9452c4baf Merge pull request #27 from achiez/feat/dialog-header
feat(ui): add DialogHeader component and restyle dialog headers
2026-06-19 20:45:30 +03:00
achiez ec71b9881e feat(ui): add DialogHeader component and restyle dialog headers 2026-06-19 20:43:43 +03:00
achiez 4b727af1fa chore(resharper): perform resharper cleanup and add shared cleanup profile 2026-06-19 19:26:26 +03:00
achiez 9897ca21a6 fix: add missing MafileImportDialogVM from #24 2026-06-19 19:05:58 +03:00
Achies 631068e2f3 Merge pull request #25 from achiez/feat/proxy-signat-scheme
feat(proxy): add SignAtScheme support for improved proxy parsing
2026-06-19 19:03:56 +03:00
Achies 0cc9a3d100 Merge pull request #24 from achiez/feat/group-on-import
feat(import): add group assignment and skip-confirmation setting
2026-06-19 19:03:33 +03:00
Achies 7348bc2673 Merge branch 'dev' into feat/group-on-import 2026-06-19 19:03:16 +03:00
Achies 3a9d929c9a Merge pull request #23 from achiez/fix/mafile-mover-loc
fix(localization): update mafile mover Steam Guard error message
2026-06-19 19:01:40 +03:00
Achies 25818997af Merge pull request #22 from achiez/feat/proxy-assignment
feat(proxy): add bulk assignment and proxy usage stats
2026-06-19 19:00:47 +03:00
achiez 5836ed4508 feat(proxy): add SignAtScheme support for improved proxy parsing 2026-06-19 18:55:39 +03:00
achiez 537d5d907f feat(import): add group assignment and skip-confirmation setting; remove obsolete format setting
- Add ability to assign imported mafiles to a group directly from the import dialog
- Add setting to skip the import confirmation dialog when no conflicts are detected
- Remove the deprecated "legacy mafile format" setting as it is no longer relevant
2026-06-19 18:54:37 +03:00
achiez 0e90f1826b fix(localization): update mafile mover Steam Guard error message 2026-05-19 22:42:58 +03:00
achiez b5d3b4a8af feat(proxy): add bulk assignment and proxy usage stats
- Implement bulk proxy assignment for accounts with flexible input (login:proxy, login:ID, or login)
- Add UI for mass assignment with counters and behavior selection for unspecified proxies
- Show assigned account count badges in proxy list for better visibility
- Enable quick assignment of a free proxy to an account
- Ensure fast and consistent proxy assignment state via in-memory cache
- Perform ReSharper cleanup
2026-05-14 16:44:14 +03:00
achiez 30cf049f23 refactor(auth): redesign Steam audience and web domain authorization model
- Rename AuthorizedDomains to WebDomains
- Introduce explicit Steam audience constants and web audience mappings
- Improve Steam token/domain resolution and cookie installation flow
- Allow mobile-issued tokens with "web" audience to be used for web domains
- Align authorization logic closer to actual Steam audience behavior
- Rename cookie APIs from Add* to Set* where appropriate
2026-05-12 16:36:37 +03:00
achiez ce55005d44 chore: remove outdated README files and update main README with new features and documentation link 2026-05-04 19:37:37 +03:00
github-actions a23d7017a7 chore(release): 1.8.4 2026-03-14 13:36:41 +00:00
achiez c879dd462c fix(build): include NebulaAuth.exe in release package
Fix CI packaging configuration to ensure NebulaAuth.exe is included
in the published release artifacts.
2026-03-14 15:36:02 +02:00
github-actions e681ca07f1 chore(release): 1.8.3 2026-03-13 13:37:20 +00:00
Achies 9227b383bb Merge pull request #20 from achiez/1.8.3
Release 1.8.3
2026-03-13 15:36:47 +02:00
achiez 0130737789 chore(ver): bump version to 1.8.3 and clean solution
- Bump version to 1.8.3
- Add 1.8.3 changelog
- Migrate to .slnx
- Cleanup solution
2026-03-13 15:35:38 +02:00
achiez 1ab2f99783 chore: perform resharper cleanup 2026-03-13 14:54:33 +02:00
achiez da00fa5c96 loc: add es, tr, kk locales and split localization files 2026-03-13 14:51:03 +02:00
Achies 0a46950ca9 Merge pull request #19 from achiez/feature/updates-v2
feat(update): introduce modern update system

- 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:25:04 +02:00
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
Achies a3804dd48d Update README.md
Add copyright disclaimer
2026-03-12 15:41:38 +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
achiez a56757302e Resolved issue where a "Confirmation Error" notification appeared despite the confirmation being successful 2025-07-19 00:11:42 +03:00
achiez 1ad30ba49d 1.7.3 Add automatic ack on confirming Trades accordingly to new update 2025-07-17 22:49:22 +03:00
achiez 04e2188437 bump version to 1.7.2 2025-07-10 13:35:56 +03:00
achiez b4c4f52fd1 Fix: reintroduce bug from incorrect merge, now fully resolved
Previously fixed bug was unintentionally reintroduced due to merging the wrong branch. This commit properly resolves the issue.
2025-07-10 13:31:44 +03:00
achiez f079488840 temporary return old update.xml path 2025-07-09 22:39:08 +03:00
189 changed files with 11170 additions and 3980 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
+220
View File
@@ -0,0 +1,220 @@
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 \
-r win-x64 \
--self-contained false \
-p:EnableWindowsTargeting=true \
-o build
# --------------------------------------------------------
# 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/
-56
View File
@@ -1,56 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33205.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{161BFADE-FCF3-45D1-82EA-8A1B187529F7}"
ProjectSection(SolutionItems) = preProject
changelog\1.3.4.html = changelog\1.3.4.html
changelog\1.4.4.html = changelog\1.4.4.html
changelog\1.4.5.html = changelog\1.4.5.html
changelog\1.4.6.html = changelog\1.4.6.html
changelog\1.4.7.html = changelog\1.4.7.html
changelog\1.4.8.html = changelog\1.4.8.html
changelog\1.4.9.html = changelog\1.4.9.html
changelog\1.5.0.html = changelog\1.5.0.html
changelog\1.5.1.html = changelog\1.5.1.html
changelog\1.5.2.html = changelog\1.5.2.html
changelog\1.5.3.html = changelog\1.5.3.html
changelog\1.5.4.html = changelog\1.5.4.html
changelog\1.5.5.html = changelog\1.5.5.html
changelog\1.5.6.html = changelog\1.5.6.html
changelog\1.7.1.html = changelog\1.7.1.html
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FA34DDD5-BDC0-4C0A-B347-4ECDBF446900}
EndGlobalSection
EndGlobal
+311
View File
@@ -1,3 +1,314 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 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:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpFileLayoutPatterns/Pattern/@EntryValue">&lt;Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"&gt;&#xD;
&lt;TypePattern DisplayName="Non-reorderable types" Priority="99999999"&gt;&#xD;
&lt;TypePattern.Match&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Interface" /&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /&gt;&#xD;
&lt;HasAttribute Name="System.Runtime.InteropServices.ComImport" /&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;Kind Is="Struct" /&gt;&#xD;
&lt;HasAttribute Name="System.Runtime.InteropServices.StructLayoutAttribute" /&gt;&#xD;
&lt;HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" /&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/TypePattern.Match&gt;&#xD;
&lt;/TypePattern&gt;&#xD;
&lt;TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"&gt;&#xD;
&lt;TypePattern.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Class" /&gt;&#xD;
&lt;HasMember&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="Xunit.FactAttribute" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="Xunit.TheoryAttribute" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/HasMember&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/TypePattern.Match&gt;&#xD;
&lt;Entry DisplayName="Fields"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Field" /&gt;&#xD;
&lt;Not&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/Not&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Readonly /&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Constructors"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Kind Is="Constructor" /&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Teardown Methods"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
&lt;ImplementsInterface Name="System.IDisposable" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="All other members" /&gt;&#xD;
&lt;Entry DisplayName="Test Methods" Priority="100"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
&lt;HasAttribute Name="Xunit.FactAttribute" /&gt;&#xD;
&lt;HasAttribute Name="Xunit.TheoryAttribute" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;/TypePattern&gt;&#xD;
&lt;TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"&gt;&#xD;
&lt;TypePattern.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Class" /&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureAttribute" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureSourceAttribute" /&gt;&#xD;
&lt;HasMember&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
&lt;HasAttribute Name="NUnit.Framework.TestAttribute" /&gt;&#xD;
&lt;HasAttribute Name="NUnit.Framework.TestCaseAttribute" /&gt;&#xD;
&lt;HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/HasMember&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/TypePattern.Match&gt;&#xD;
&lt;Entry DisplayName="Setup/Teardown Methods"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.SetUpAttribute" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.TearDownAttribute" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureSetUpAttribute" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.TestFixtureTearDownAttribute" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.OneTimeSetUpAttribute" /&gt;&#xD;
&lt;HasAttribute Inherited="True" Name="NUnit.Framework.OneTimeTearDownAttribute" /&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="All other members" /&gt;&#xD;
&lt;Entry DisplayName="Test Methods" Priority="100"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
&lt;HasAttribute Name="NUnit.Framework.TestAttribute" /&gt;&#xD;
&lt;HasAttribute Name="NUnit.Framework.TestCaseAttribute" /&gt;&#xD;
&lt;HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;/TypePattern&gt;&#xD;
&lt;TypePattern DisplayName="Grains"&gt;&#xD;
&lt;TypePattern.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;ImplementsInterface Name="^Orleans\.IGrain$" /&gt;&#xD;
&lt;ImplementsInterface Name="Orleans.IGrainBase" /&gt;&#xD;
&lt;ImplementsInterface Name="Achiefy.Orleans.Tenancy.Abstractions.ITenantGrain" /&gt;&#xD;
&lt;InheritsFromType Name="Orleans.Grain" /&gt;&#xD;
&lt;InheritsFromType Name="Achiefy.Tenant.Application.Stateful.Core.TenantGrain" /&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;Not&gt;&#xD;
&lt;Abstract /&gt;&#xD;
&lt;/Not&gt;&#xD;
&lt;Kind Is="Class" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/TypePattern.Match&gt;&#xD;
&lt;Entry DisplayName="Public Delegates" Priority="100"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Access Is="Public" /&gt;&#xD;
&lt;Kind Is="Delegate" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Public Enums" Priority="100"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Access Is="Public" /&gt;&#xD;
&lt;Kind Is="Enum" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Nested Records"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Kind Is="Record" /&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Group DisplayName="Group"&gt;&#xD;
&lt;Entry DisplayName="Static Fields and Constants"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;Kind Is="Constant" /&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Field" /&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Kind Is="0" Order="Constant Field" /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Properties, Indexers"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;Kind Is="Property" /&gt;&#xD;
&lt;Kind Is="Indexer" /&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Fields"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Field" /&gt;&#xD;
&lt;Not&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/Not&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Readonly /&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;/Group&gt;&#xD;
&lt;Entry DisplayName="Constructors"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Kind Is="Constructor" /&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Lifecycle Methods"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
&lt;Name Is="^On(Activate|Deactivate)Async.*$" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="All other members" /&gt;&#xD;
&lt;Entry DisplayName="Nested Types"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Kind Is="Type" /&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;/TypePattern&gt;&#xD;
&lt;TypePattern DisplayName="Default Pattern"&gt;&#xD;
&lt;Entry DisplayName="Public Enums" Priority="100"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Access Is="Public" /&gt;&#xD;
&lt;Kind Is="Enum" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Public Delegates" Priority="100"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Access Is="Public" /&gt;&#xD;
&lt;Kind Is="Delegate" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Nested Records"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Kind Is="Record" /&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Group DisplayName="Group"&gt;&#xD;
&lt;Entry DisplayName="Static Fields and Constants"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;Kind Is="Constant" /&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Field" /&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Kind Is="0" Order="Constant Field" /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Properties, Indexers"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;Kind Is="Property" /&gt;&#xD;
&lt;Kind Is="Indexer" /&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Fields"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Field" /&gt;&#xD;
&lt;Not&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/Not&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Readonly /&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;/Group&gt;&#xD;
&lt;Entry DisplayName="Constructors"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Kind Is="Constructor" /&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="All other members" /&gt;&#xD;
&lt;Entry DisplayName="Nested Types"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;Kind Is="Type" /&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;/TypePattern&gt;&#xD;
&lt;/Patterns&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=MAAC/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+29
View File
@@ -0,0 +1,29 @@
<Solution>
<Folder Name="/changelog/">
<File Path="changelog/1.3.4.html" />
<File Path="changelog/1.4.4.html" />
<File Path="changelog/1.4.5.html" />
<File Path="changelog/1.4.6.html" />
<File Path="changelog/1.4.7.html" />
<File Path="changelog/1.4.8.html" />
<File Path="changelog/1.4.9.html" />
<File Path="changelog/1.5.0.html" />
<File Path="changelog/1.5.1.html" />
<File Path="changelog/1.5.2.html" />
<File Path="changelog/1.5.3.html" />
<File Path="changelog/1.5.4.html" />
<File Path="changelog/1.5.5.html" />
<File Path="changelog/1.5.6.html" />
<File Path="changelog/1.7.1.html" />
<File Path="changelog/1.7.2.html" />
<File Path="changelog/1.7.3.html" />
<File Path="changelog/1.7.4.html" />
<File Path="changelog/1.8.0.html" />
<File Path="changelog/1.8.1.html" />
<File Path="changelog/1.8.2.html" />
<File Path="changelog/1.8.3.json" />
<File Path="changelog/1.8.4.json" />
</Folder>
<Project Path="src/NebulaAuth/NebulaAuth.csproj" />
<Project Path="src/SteamLibForked/SteamLibForked.csproj" />
</Solution>
+4
View File
@@ -0,0 +1,4 @@
{
"Website": "https://achiefy.pro/?nebula=true",
"Documentation": "https://achiefy-project.gitbook.io/nebulaauth"
}
+7 -7
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.7.1.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.7.1/NebulaAuth.1.7.1.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.7.1.html</changelog>
<mandatory>false</mandatory>
</item>
<version>1.8.5</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.5/NebulaAuth.1.8.5.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.5.html</changelog>
<mandatory>false</mandatory>
<checksum algorithm="SHA256">7b2d7768ff5497664c0223a535e35006022d7aed62ebf5400fbf9be3d2893bd5</checksum>
</item>
-74
View File
@@ -1,74 +0,0 @@
# NebulaAuth
## Описание
<h3 align="center" style="margin-bottom:0">
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Скачать последнюю версию</a>
</h3>
<h3 align="center">NebulaAuth — это приложение для эмуляции действий из мобильного приложения Steam. Которая заменяет ваш смартфон при работе в Steam.</h3>
<h4 align="center"><a href="https://t.me/nebulaauth">Официальная группа в Telegram</a></h4>
## Основные преимущества
- **Локализация на трёх языках**: английском, русском и украинском.
- **Полная функциональность Steam Desktop Authenticator**, переосмысление [старого приложения](https://github.com/Jessecar96/SteamDesktopAuthenticator)
- **Поддержка прокси** во всех процессах работы с аккаунтом.
- **Группировка мафайлов** для продвинутого контроля.
- **Автоматическое подтверждение трейдов/действий на ТП** для экономии времени.
- **Массовый импорт мафайлов** с помощью Drag'n'Drop или CTRL+V для удобства.
- **Настройка внешнего вида** для персонализации интерфейса.
- **Возможность подтвердить вход в учетную запись без ввода кода** для облегчения доступа.
- **Автообновление** программы для использования новейших функций.
- **Автоматический повторный вход в случае проблем с сессией** для непрерывной работы.
- **Интуитивно понятный интерфейс** с подсказками и удобствами
- **Постоянная поддержка** кода приложения и других функций.
## Установка
1. Если приложение не запускается, необходимо установить [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
2. [Скачать программу из релизов этого репозитория на Github](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest)
* *Для сохранности ваших данных скачивайте приложение только отсюда*
4. Распакуйте ZIP-файл в любую папку.
5. Запустите файл **NebulaAuth.exe**.
## Использование
![gh-main-window-rus](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/6a84f414-0e24-40a4-8adb-f1923fbe8719)
1. Панель управления.
- управление файлами и настройки
- управление аккаунтом (вход, привязка, отвязка)
- группировка
- выбор прокси
- индикатор с подсказкой об используемом прокси (горит либо желтым, либо красным, при наведении отобразит дополнительную информацию)
- таймеры для автоматического подтверждения трейдов/продаж на торговой площадке
- интервал таймера подтверждений (в секундах)
2. Список ваших аккаунтов
3. Код подтверждения входа (нажмите, чтобы скопировать)
4. Главное окно подтверждений
5. Поиск по логину или SteamID (7xxxxxxxxxxxxx)
6. Подтвердить вход на другогом устройстве.
7. Гиперссылка на официальную страницу приложения с указанием авторства.
## Настройки
![gh-settings-rus](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/33246ed1-1e3d-4310-88c5-085e5c50bc6b)
1. Режим фона. Используйте, если вы хотите отключить фон или установить собственный (поместите файл «Background.png» в папку приложения)
2. Язык приложения
3. Отключить таймеры при переключении между аккаунтами
4. Скрывать в трей при сворачивании
5. Индикатор с цветом. Маленький кружочек на значке панели задач с произвольным цветом. Полезно при использовании нескольких окон.
6. Пользовательский цвет приложения.
7. Текущий пароль шифрования. Если установлено, вы можете сохранять зашифрованные пароли в mafile, чтобы облегчить повторный вход в систему при проблемах с сессией. (Не рекомендуется)
8. Режим устаревших файлов. Режим совместимости Mafile с другими клиентами (SDA и т.д.). Если установлено, приложение будет сохранять файлы в старом стандартном формате (по умолчанию: включено).
9. Разрешить автообновление без подтверждения
## [Лицензия](/LICENSE.md)
Коммерческое использование запрещено. При распространении измененного кода необходимо указывать оригинальное авторство.
-71
View File
@@ -1,71 +0,0 @@
# NebulaAuth
## Опис
<h3 align="center" style="margin-bottom:0">
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Завантажити останню версію</a>
</h3>
<h3 align="center">NebulaAuth - це програма для емуляції дій з мобільного додатку Steam. Яка замінює ваш смартфон під час роботи в Steam.</h3>
<h4 align="center"><a href="https://t.me/nebulaauth">Офіційна група в Telegram</a></h4>
## Основні переваги
- **Локалізація трьома мовами**: англійською, російською та українською.
- **Повна функціональність Steam Desktop Authenticator** переосмислення [старої програми](https://github.com/Jessecar96/SteamDesktopAuthenticator)
- **Підтримка проксі** у всіх процесах роботи з обліковим записом.
- **Угруповання мафайлів** для просунутого контролю.
- **Автоматичне підтвердження трейдів/дій на маркеті** для економії часу.
- **Масовий імпорт мафайлів** за допомогою Drag'n'Drop або CTRL+V для зручності.
- **Налаштування зовнішнього вигляду** для персоналізації інтерфейсу.
- **Можливість підтвердити вхід до облікового запису без введення коду** для полегшення доступу.
- **Автооновлення** програми для використання новітніх функцій.
- **Автоматичний повторний вхід у разі проблем із сесією** для безперервної роботи.
- **Інтуїтивно зрозумілий інтерфейс** з підказками та зручностями
- **Постійна підтримка** коду програми та інших функцій.
## Монтаж
1. Якщо програма не запускається, необхідно встановити [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
2. [Завантажити програму з релізів цього репозиторію на Github](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest)
* *Для збереження ваших даних завантажуйте програму тільки звідси*
4. Розпакуйте ZIP-файл у будь-яку папку.
5. Запустіть файл **NebulaAuth.exe**.
## Використання
![gh-main-window-ua](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/bf61ac61-b21c-4589-9b5a-751a6d983120)
1. Панель керування.
- керування файлами та налаштування
- керування акаунтом (вхід, прив'язка, відв'язування)
- угруповання
- вибір проксі
- індикатор з підказкою про проксі (світиться або жовтим, або червоним, при наведенні відобразить додаткову інформацію)
- таймери для автоматичного підтвердження трейдів/продажів на маркеті
- інтервал таймера підтверджень (у секундах)
2. Список ваших облікових записів
3. Код підтвердження входу (натисніть, щоб скопіювати)
4. Головне вікно підтвердження
5. Пошук за логіном або SteamID (7xxxxxxxxxxxxx)
6. Підтвердити вхід на іншому пристрою.
7. Гіперпосилання на офіційну сторінку додатку із зазначенням авторства.
## Налаштування
![gh-settings-ua](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/3f57e58d-d647-42a1-a4c9-ca53ab7b38a3)
1. Режим фону. Використовуйте, якщо ви хочете вимкнути фон або встановити власний (помістіть файл "Background.png" у папку програми)
2. Мова локалізації
3. Вимкнути таймери під час перемикання між обліковими записами
4. Приховувати у трей при згортанні
5. Індикатор із кольором. Маленький кружечок на піктограмі панелі завдань з довільним кольором. Корисно при використанні кількох вікон.
6. Власний колір програми.
7. Поточний пароль шифрування. Якщо встановлено, ви можете зберігати зашифровані паролі в mafile, щоб полегшити повторний вхід до системи у разі проблеми з сесією. (Не рекомендується)
8. Режим застарілих файлів. Режим сумісності Mafile з іншими клієнтами (SDA тощо). Якщо встановлено, програма зберігатиме файли у старому стандартному форматі (за замовчуванням: увімкнено).
9. Дозволити автооновлення без підтвердження
## [Ліцензія](/LICENSE.md)
Комерційне використання заборонено. У разі поширення зміненого коду необхідно вказувати оригінальне авторство.
+25 -46
View File
@@ -1,36 +1,44 @@
# NebulaAuth
* [Русский](README-RU.md)
* [Українська](README-UA.md)
## Description
<h3 align="center" style="margin-bottom:0">
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Download latest version</a>
</h3>
<h3 align="center">NebulaAuth is an application for emulating actions from the Steam Mobile App. Which replaces your smartphone when operating on Steam. </h3>
<h3 align="center">
NebulaAuth is an application for emulating actions from the Steam Mobile App, replacing your smartphone when operating on Steam.
</h3>
<p align="center">
<sub>NebulaAuth is an independent project and is not affiliated with Valve or Steam.</sub>
</p>
<h4 align="center"><a href="https://t.me/nebulaauth">Official Telegram Group</a></h4>
<h4 align="center"><a href="https://achiefy-project.gitbook.io/nebulaauth">Documentation</a></h4>
## Main advantages
- **Localization in three languages**: English, Russian and Ukrainian.
<p align="center">
<img src="misc/main_window.png" width="600"/>
</p>
- **Localization in eight languages**: English, Russian, Ukrainian, Simplified Chinese, French, Spanish, Turkish, and Kazakh.
- **Full functionality of Steam Desktop Authenticator** reimagining [old app](https://github.com/Jessecar96/SteamDesktopAuthenticator)
- **Proxy support** in all account work processes.
- **Mafile grouping** for improved management.
- **Automatic confirmations of trades/market actions** to save time.
- **Bulk import of .mafiles** via Drag'n'Drop or CTRL+V for convenience.
- **Bulk import of .mafiles** via Drag'n'Drop or Ctrl+V, including SDA-encrypted mafiles with automatic manifest detection.
- **Design customization** to personalize the interface.
- **Ability to confirm account login without entering a code** for easier access.
- **Auto-update** program to use the latest features.
- **Auto-update** with SHA256 checksum verification, changelog viewer and flexible update options.
- **Automatic relogin in case of problems with the session** for continuous operation.
- **Intuitive interface** with tips and conveniences
- **Continious support** of application code and other features.
- **Intuitive interface** with tips and conveniences.
- **Continuous support** of application code and other features.
## Documentation
You can find the documentation for the application [here](https://achiefy-project.gitbook.io/nebulaauth). Currently, only the Russian version is available, but the English version will be released soon.
## Installation
1. If the application does not start, you need to install [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
@@ -39,40 +47,11 @@
4. Unpack the .zip file to any folder
5. Run the file **NebulaAuth.exe**
## Usage
![gh-main-window-eng](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/15c0e870-1766-43a5-9e8c-2f34d5436beb)
1. Control panel.
- file management and settings
- account management (login, linking, unlinking)
- grouping
- proxy selection
- an indicator with a hint about the proxy used (lit either yellow or red, when hovered it will display additional information)
- timers for automatic confirmation of trade offers/sale offers on the marketplace
- confirmation timer interval (in seconds)
2. List of your accounts
3. Login confirmation code (click to copy)
4. Main confirmation window
5. Search by login or SteamID (7xxxxxxxxxxxxx)
6. Confirm login on another device
7. Hyperlink to the official application page with attribution
## Project Ownership
## Settings
![gh-settings-eng](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/cd704495-d2df-45a7-a73e-40c19410eb72)
NebulaAuth is the original project by Achiefy™.
1. Background mode. Use it if you want to disable default or set custom background of application (put file 'Background.png' to your application folder)
2. Localization language
3. Disable timers when switching between accounts
4. Hide to tray on minimize
5. Indicator with color. Small ellipse on your task-bar icon with custom color. Useful when using multiple windows
6. Custom background color of application
7. Current encryption password. If set you can save encrypted passwords to mafile to help re-login on session troubles. (Not recommended)
8. Legacy mafile mode. Mafile compability mode for another applications (SDA and etc). If checked application will save mafiles with old standart format (Default: checked)
9. Allow auto-update without confirmation
Please keep attribution and do not present modified versions as official.
## [License](/LICENSE.md)
Commercial use prohibited. When redistributing modified code, you must indicate the original authorship.
AGPL-3.0 — see [LICENSE](/LICENSE).
+4 -4
View File
@@ -76,10 +76,10 @@
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>UPDATE:</b> Added support for a new confirmation type — Purchase.</li>
<li><b>UPDATE:</b> Merged Global 1.7.0 updates into the release version (<a href="https://t.me/nebulaauth/32">patch notes</a>).</li>
<li><b>FIX:</b> Fixed a crash occurring during confirmation loading.</li>
<li><b>ENHANCEMENT:</b> Improved localization and system messages.</li>
<li><b>UPDATE:</b> Added support for a new confirmation type — Purchase.</li>
<li><b>UPDATE:</b> Merged Global 1.7.0 updates into the release version (<a href="https://t.me/nebulaauth/32">patch notes</a>).</li>
<li><b>FIX:</b> Fixed a crash occurring during confirmation loading.</li>
<li><b>ENHANCEMENT:</b> Improved localization and system messages.</li>
</ul>
</div>
</div>
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.7.2</div>
<div class="date">10.07.2025</div>
<div class="description">
<ul>
<li>
<b>NEWS:</b> Official Telegram group now available! Join us at
<b>
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>FIX:</b> Crash on loading trade confirmations</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.7.3</div>
<div class="date">17.07.2025</div>
<div class="description">
<ul>
<li>
<b>NEWS:</b> Official Telegram group now available! Join us at
<b>
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>UPDATE:</b> Added automatic acknowledgment request for sending trade confirmation</li>
<li><b>FIX:</b> Fixed notifications text in auto-confirm timer</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.7.4</div>
<div class="date">19.07.2025</div>
<div class="description">
<ul>
<li>
<b>NEWS:</b> Official Telegram group now available! Join us at
<b>
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
</b>
</li>
<li><b>FIX:</b> Resolved issue where a "Confirmation Error" notification appeared despite the confirmation being successful.</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.8.0</div>
<div class="date">07.11.2025</div>
<div class="description">
<ul>
<li> Major update: Full changelog is available here <a href="https://teletype.in/@achies_raw/nebula-1-8-0-eng">ENG</a> | <a href="https://teletype.in/@achies_raw/nebula-1-8-0-rus">RUS</a></li>
</ul>
</div>
</div>
</div>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<div class="change">
<div class="version">Version 1.8.1</div>
<div class="date">25.01.2026</div>
<div class="description">
<ul>
<li><b>NEW:</b> Added Mafile Export tool with templates, batch export and flexible field filtering.</li>
<li><b>IMPROVEMENT:</b> Reworked auto-confirmation error handling logic (MAAC) — fewer false disables, smarter retries.</li>
<li><b>UPDATE:</b> Added “Unattach Proxy” option to the account context menu.</li>
<li><b>LOCALIZATION:</b> Added Chinese (Simplified) and French languages.</li>
<li><b>FIX:</b> Fixed crashes related to duplicate mafiles, timers, search filtering and import logic.</li>
<li>
<b>DETAILS:</b> Full patch notes:
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-eng">ENG</a> |
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-rus">RUS</a>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+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>
+27
View File
@@ -0,0 +1,27 @@
<!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>
<li><b>NEW:</b> Introduced a redesigned update system with a custom update dialog and integrated changelog viewer <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/19">details</a></li>
<li><b>NEW:</b> Added support for importing SDA-encrypted mafiles with automatic manifest detection <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/17">details</a></li>
<li><b>NEW:</b> Grouped market confirmations can now be expanded to reveal individual items <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/18">details</a></li>
<li><b>SECURITY:</b> Added SHA256 checksum verification for downloaded update packages</li>
<li><b>IMPROVEMENT:</b> Export feature now trims input automatically to prevent issues caused by invisible characters or extra spaces</li>
<li><b>IMPROVEMENT:</b> Improved update experience with 'Remind later' and 'Skip version' options</li>
<li><b>IMPROVEMENT:</b> Added visual update indicator and manual 'Check for updates' action</li>
<li><b>IMPROVEMENT:</b> Expanded localization support with Spanish, Turkish and Kazakh languages</li>
<li><b>INFO:</b> Read the full release notes for NebulaAuth 1.8.3 <a href="https://teletype.in/@achies_raw/o-RDwZKZkAU">details</a></li>
</ul>
</div>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
{
"version": "1.8.3",
"date": "2026-03-13",
"changes": [
{
"type": "NEW",
"text": "Introduced a redesigned update system with a custom update dialog and integrated changelog viewer",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/19"
},
{
"type": "NEW",
"text": "Added support for importing SDA-encrypted mafiles with automatic manifest detection",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/17"
},
{
"type": "NEW",
"text": "Grouped market confirmations can now be expanded to reveal individual items",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/18"
},
{
"type": "SECURITY",
"text": "Added SHA256 checksum verification for downloaded update packages"
},
{
"type": "IMPROVEMENT",
"text": "Export feature now trims input automatically to prevent issues caused by invisible characters or extra spaces"
},
{
"type": "IMPROVEMENT",
"text": "Improved update experience with 'Remind later' and 'Skip version' options"
},
{
"type": "IMPROVEMENT",
"text": "Added visual update indicator and manual 'Check for updates' action"
},
{
"type": "IMPROVEMENT",
"text": "Expanded localization support with Spanish, Turkish and Kazakh languages"
},
{
"type": "INFO",
"text": "Read the full release notes for NebulaAuth 1.8.3",
"link": "https://teletype.in/@achies_raw/o-RDwZKZkAU"
}
]
}
+28
View File
@@ -0,0 +1,28 @@
<!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>
<li><b>FIX:</b> Rebuilt release package to include the missing NebulaAuth.exe file. Version 1.8.4 is functionally identical to 1.8.3 and only fixes the packaging issue that caused the executable to be absent in the previous release.</li>
<li><b>NEW:</b> Introduced a redesigned update system with a custom update dialog and integrated changelog viewer <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/19">details</a></li>
<li><b>NEW:</b> Added support for importing SDA-encrypted mafiles with automatic manifest detection <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/17">details</a></li>
<li><b>NEW:</b> Grouped market confirmations can now be expanded to reveal individual items <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/18">details</a></li>
<li><b>SECURITY:</b> Added SHA256 checksum verification for downloaded update packages</li>
<li><b>IMPROVEMENT:</b> Export feature now trims input automatically to prevent issues caused by invisible characters or extra spaces</li>
<li><b>IMPROVEMENT:</b> Improved update experience with 'Remind later' and 'Skip version' options</li>
<li><b>IMPROVEMENT:</b> Added visual update indicator and manual 'Check for updates' action</li>
<li><b>IMPROVEMENT:</b> Expanded localization support with Spanish, Turkish and Kazakh languages</li>
<li><b>INFO:</b> Read the full release notes for NebulaAuth 1.8.3 <a href="https://teletype.in/@achies_raw/o-RDwZKZkAU">details</a></li>
</ul>
</div>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
{
"version": "1.8.4",
"date": "2026-03-14",
"changes": [
{
"type": "FIX",
"text": "Rebuilt release package to include the missing NebulaAuth.exe file. Version 1.8.4 is functionally identical to 1.8.3 and only fixes the packaging issue that caused the executable to be absent in the previous release."
},
{
"type": "NEW",
"text": "Introduced a redesigned update system with a custom update dialog and integrated changelog viewer",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/19"
},
{
"type": "NEW",
"text": "Added support for importing SDA-encrypted mafiles with automatic manifest detection",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/17"
},
{
"type": "NEW",
"text": "Grouped market confirmations can now be expanded to reveal individual items",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/18"
},
{
"type": "SECURITY",
"text": "Added SHA256 checksum verification for downloaded update packages"
},
{
"type": "IMPROVEMENT",
"text": "Export feature now trims input automatically to prevent issues caused by invisible characters or extra spaces"
},
{
"type": "IMPROVEMENT",
"text": "Improved update experience with 'Remind later' and 'Skip version' options"
},
{
"type": "IMPROVEMENT",
"text": "Added visual update indicator and manual 'Check for updates' action"
},
{
"type": "IMPROVEMENT",
"text": "Expanded localization support with Spanish, Turkish and Kazakh languages"
},
{
"type": "INFO",
"text": "Read the full release notes for NebulaAuth 1.8.3",
"link": "https://teletype.in/@achies_raw/o-RDwZKZkAU"
}
]
}
+29
View File
@@ -0,0 +1,29 @@
<!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>
<li><b>NEW:</b> Bulk proxy assignment: assign proxies to multiple accounts at once via flexible login:proxy, login:ID or login-only input <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22">details</a></li>
<li><b>NEW:</b> Import dialog now allows assigning imported mafiles to a group directly; added option to skip confirmation when no conflicts are detected <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/24">details</a></li>
<li><b>NEW:</b> Assign random free proxy to the account from context menu <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22">details</a></li>
<li><b>NEW:</b> Export: added ZIP archive output, export-all mode when the input field is empty, and support for login:password input format <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/28">details</a></li>
<li><b>IMPROVEMENT:</b> When a session permanently expires during a user-initiated action, a login dialog is now shown instead of a generic error notification <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/29">details</a></li>
<li><b>IMPROVEMENT:</b> Proxy manager: account count badges now show how many accounts are assigned to each proxy entry <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22">details</a></li>
<li><b>IMPROVEMENT:</b> Added login:pass@ip:port proxy format support <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/25">details</a></li>
<li><b>IMPROVEMENT:</b> Improved dialog header design <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/27">details</a></li>
<li><b>IMPROVEMENT:</b> Website and Documentation links are now available from the links window <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/30">details</a></li>
<li><b>FIX:</b> Application no longer fails to start when Steam time synchronization is unavailable (network issues, firewall)</li>
<li><b>FIX:</b> Fixed incorrect Steam Guard error message shown in the Mafile Mover dialog <a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/23">details</a></li>
</ul>
</div>
</body>
</html>
+60
View File
@@ -0,0 +1,60 @@
{
"version": "1.8.5",
"date": "2026-06-19",
"changes": [
{
"type": "NEW",
"text": "Bulk proxy assignment: assign proxies to multiple accounts at once via flexible login:proxy, login:ID or login-only input",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22"
},
{
"type": "NEW",
"text": "Import dialog now allows assigning imported mafiles to a group directly; added option to skip confirmation when no conflicts are detected",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/24"
},
{
"type": "NEW",
"text": "Assign random free proxy to the account from context menu",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22"
},
{
"type": "NEW",
"text": "Export: added ZIP archive output, export-all mode when the input field is empty, and support for login:password input format",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/28"
},
{
"type": "IMPROVEMENT",
"text": "When a session permanently expires during a user-initiated action, a login dialog is now shown instead of a generic error notification",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/29"
},
{
"type": "IMPROVEMENT",
"text": "Proxy manager: account count badges now show how many accounts are assigned to each proxy entry",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/22"
},
{
"type": "IMPROVEMENT",
"text": "Added login:pass@ip:port proxy format support",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/25"
},
{
"type": "IMPROVEMENT",
"text": "Improved dialog header design",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/27"
},
{
"type": "IMPROVEMENT",
"text": "Website and Documentation links are now available from the links window",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/30"
},
{
"type": "FIX",
"text": "Application no longer fails to start when Steam time synchronization is unavailable (network issues, firewall)"
},
{
"type": "FIX",
"text": "Fixed incorrect Steam Guard error message shown in the Mafile Mover dialog",
"link": "https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/pull/23"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.5.6.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.5.6/NebulaAuth.1.5.6.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.5.6.html</changelog>
<mandatory>false</mandatory>
</item>
@@ -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();
}
+3 -27
View File
@@ -1,38 +1,13 @@
<Application x:Class="NebulaAuth.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:NebulaAuth.Converters"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:background="clr-namespace:NebulaAuth.Converters.Background">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<ResourceDictionary>
<converters:CoefficientConverter x:Key="CoefficientConverter" />
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" />
<converters:ProxyTextConverter x:Key="ProxyTextConverter" />
<converters:ProxyDataTextConverter x:Key="ProxyDataTextConverter" />
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter" />
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
<!-- Background converters-->
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
<background:BackgroundSourceConverter x:Key="BackgroundSourceConverter" />
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
<system:Boolean x:Key="True">True</system:Boolean>
<system:Boolean x:Key="False">False</system:Boolean>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Converters/Converters.xaml" />
<ResourceDictionary Source="Theme/Themes/DefaultTheme.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
@@ -58,6 +33,7 @@
</Style>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
+1 -13
View File
@@ -1,8 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Windows;
using AchiesUtilities.Extensions;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Exceptions;
@@ -18,17 +15,8 @@ public partial class App
var splashScreen = new SplashScreen("Theme\\SplashScreen.png");
splashScreen.Show(false, true);
base.OnStartup(e);
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Shell.Initialize();
var files = 0;
if (Directory.Exists(Storage.MAFILE_F))
files = Directory.GetFiles(Storage.MafileFolder)
.Count(f => Path.GetExtension(f).EqualsIgnoreCase(".mafile"));
var threads = files > 0 ? files / 100 + 1 : 1;
await Storage.Initialize(threads);
await Shell.Initialize();
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;
mainWindow.Show();
@@ -3,7 +3,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace NebulaAuth.Theme.Controls;
namespace NebulaAuth;
public class CodeProgressBar : ProgressBar
{
@@ -13,7 +13,7 @@ public class CodeProgressBar : ProgressBar
public static readonly DependencyProperty MaxTimeProperty =
DependencyProperty.Register(nameof(MaxTime), typeof(double), typeof(CodeProgressBar),
new PropertyMetadata(30.0)); // По умолчанию 30 сек
new PropertyMetadata(30.0));
public double TimeRemaining
{
@@ -0,0 +1,49 @@
<UserControl x:Class="NebulaAuth.DialogHeader"
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"
x:Name="Root">
<StackPanel>
<Border Background="{DynamicResource Base300Brush}" Padding="10,6,10,6">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<materialDesign:PackIcon
Kind="{Binding IconKind, ElementName=Root}"
Width="20" Height="20" Margin="0,0,8,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}">
<materialDesign:PackIcon.Style>
<Style TargetType="materialDesign:PackIcon">
<Style.Triggers>
<Trigger Property="Kind" Value="None">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
</materialDesign:PackIcon.Style>
</materialDesign:PackIcon>
<TextBlock VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}"
FontSize="18">
<Run Text="{Binding Title, ElementName=Root}" />
<Run FontWeight="Bold" Text="{Binding TitleAccent, ElementName=Root}" />
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30"
Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right"
IsEnabled="{Binding IsCloseEnabled, ElementName=Root}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{Binding CloseParameter, ElementName=Root}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,63 @@
using System.Windows;
using System.Windows.Controls;
using MaterialDesignThemes.Wpf;
namespace NebulaAuth;
public partial class DialogHeader : UserControl
{
public static readonly DependencyProperty IconKindProperty =
DependencyProperty.Register(nameof(IconKind), typeof(PackIconKind), typeof(DialogHeader),
new PropertyMetadata(PackIconKind.None));
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DialogHeader),
new PropertyMetadata(null));
public static readonly DependencyProperty TitleAccentProperty =
DependencyProperty.Register(nameof(TitleAccent), typeof(string), typeof(DialogHeader),
new PropertyMetadata(null));
public static readonly DependencyProperty CloseParameterProperty =
DependencyProperty.Register(nameof(CloseParameter), typeof(object), typeof(DialogHeader),
new PropertyMetadata(null));
public static readonly DependencyProperty IsCloseEnabledProperty =
DependencyProperty.Register(nameof(IsCloseEnabled), typeof(bool), typeof(DialogHeader),
new PropertyMetadata(true));
public PackIconKind IconKind
{
get => (PackIconKind) GetValue(IconKindProperty);
set => SetValue(IconKindProperty, value);
}
public string? Title
{
get => (string?) GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string? TitleAccent
{
get => (string?) GetValue(TitleAccentProperty);
set => SetValue(TitleAccentProperty, value);
}
public object? CloseParameter
{
get => GetValue(CloseParameterProperty);
set => SetValue(CloseParameterProperty, value);
}
public bool IsCloseEnabled
{
get => (bool) GetValue(IsCloseEnabledProperty);
set => SetValue(IsCloseEnabledProperty, value);
}
public DialogHeader()
{
InitializeComponent();
}
}
+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>
+103
View File
@@ -0,0 +1,103 @@
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;
case HintBoxSeverity.Warning:
IconKind = PackIconKind.WarningOutline;
IconBrush = (Brush) Application.Current.FindResource("WarningBrush")!;
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,
Warning,
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();
}
}
+5 -2
View File
@@ -8,12 +8,14 @@
<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" />
<converters:ProxyAccountCountConverter x:Key="ProxyAccountCountConverter" />
<!-- Background converters-->
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
<background:BackgroundSourceConverter x:Key="BackgroundSourceConverter" />
@@ -21,6 +23,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;
}
}
@@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Windows.Data;
using NebulaAuth.Model;
namespace NebulaAuth.Converters;
/// <summary>
/// Converts a proxy ID (int) to the number of accounts assigned to it.
/// Returns empty string when count is zero so the badge is invisible.
/// </summary>
public class ProxyAccountCountConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not int proxyId) return string.Empty;
var count = ProxyAssignmentCache.GetAccountCount(proxyId);
return count > 0 ? count.ToString() : string.Empty;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
@@ -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();
}
}
+93 -22
View File
@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoUpdaterDotNET;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Update;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
using NebulaAuth.ViewModel.Linker;
@@ -13,32 +15,15 @@ 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)
public static async Task<LoginAgainVM?> ShowLoginAgainDialog(string username, string? currentPassword = null,
bool showNoProxyWarning = false)
{
var vm = new LoginAgainVM
{
UserName = username,
Password = currentPassword ?? string.Empty,
SavePassword = PHandler.IsPasswordSet
SavePassword = PHandler.IsPasswordSet,
ShowNoProxyWarning = showNoProxyWarning
};
var content = new LoginAgainDialog
{
@@ -70,6 +55,42 @@ 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<MafileImportDialogResult?> ShowMafileImportDialog(IEnumerable<string> groups,
int totalCount, int conflictCount)
{
var vm = new MafileImportDialogVM(groups, totalCount, conflictCount);
var content = new MafileImportDialog
{
DataContext = vm
};
var result = await DialogHost.Show(content);
return result is MafileImportDialogVM
? new MafileImportDialogResult(
!vm.AddToGroup || string.IsNullOrWhiteSpace(vm.Group) ? null : vm.Group.Trim(),
vm.OverwriteConflicts)
: null;
}
public static async Task<bool> ShowProxyManager()
{
var vm = new ProxyManagerVM();
@@ -121,4 +142,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
}
+33
View File
@@ -0,0 +1,33 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using NebulaAuth.Model;
using Newtonsoft.Json;
namespace NebulaAuth.Core;
public static class LinksManager
{
private const string LINKS_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/NebulaAuth/links.json";
private static readonly HttpClient HttpClient = new();
public static string? WebsiteUrl { get; private set; }
public static string? DocumentationUrl { get; private set; }
public static async Task FetchAsync()
{
try
{
var json = await HttpClient.GetStringAsync(LINKS_URL).ConfigureAwait(false);
var links = JsonConvert.DeserializeObject<RemoteLinks>(json);
WebsiteUrl = links?.Website;
DocumentationUrl = links?.Documentation;
}
catch (Exception ex)
{
Shell.Logger.Debug(ex, "Failed to fetch remote links");
}
}
}
+16 -6
View File
@@ -11,6 +11,7 @@ public static class LocManager
{
public const string CODE_BEHIND_PATH_PART = "CodeBehind";
public const string COMMON_PATH_PART = "Common";
private const string LOC_DIR = "Localization";
public static void SetApplicationLocalization(LocalizationLanguage language)
{
@@ -30,7 +31,12 @@ public static class LocManager
{
LocalizationLanguage.English => "en",
LocalizationLanguage.Russian => "ru",
LocalizationLanguage.Ukrainian => "ua",
LocalizationLanguage.Ukrainian => "uk",
LocalizationLanguage.ChineseSimplified => "zh",
LocalizationLanguage.French => "fr",
LocalizationLanguage.Spanish => "es",
LocalizationLanguage.Turkish => "tr",
LocalizationLanguage.Kazakh => "kk",
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
};
}
@@ -47,10 +53,9 @@ public static class LocManager
public static void ReloadFiles()
{
var exampleFileFileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
"localization.loc.json");
LocalizationLoader.Instance.ClearAllTranslations();
LocalizationLoader.Instance.AddFile(exampleFileFileName);
var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var path = Path.Combine(baseDir, LOC_DIR);
LocalizationLoader.Instance.AddDirectory(path);
}
public static string? GetCodeBehind(params string[] path)
@@ -107,5 +112,10 @@ public enum LocalizationLanguage
{
English,
Russian,
Ukrainian
Ukrainian,
ChineseSimplified,
French,
Spanish,
Turkish,
Kazakh
}
+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 -47
View File
@@ -1,75 +1,143 @@
using System;
using System.IO;
using System.Net.Http;
using System.Windows;
using AutoUpdaterDotNET;
using NebulaAuth.Model;
using NebulaAuth.Model.Update;
using Newtonsoft.Json;
namespace NebulaAuth.Core;
public static class UpdateManager
{
private const string UPDATE_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/misc/update.xml";
private const string BASE_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/";
public static void CheckForUpdates()
private const string UPDATE_URL = BASE_URL + "NebulaAuth/update.xml";
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; }
static UpdateManager()
{
AutoUpdater.CheckForUpdateEvent += HandleCheckForUpdateEvent;
}
public static event Action? PendingUpdateDetected;
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;
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;
}
}
}
@@ -0,0 +1,628 @@
{
"CodeBehind": {
"ErrorTranslator": {
"Login": {
"CaptchaRequired": {
"en": "Captcha",
"ru": "Капча",
"zh": "验证码",
"uk": "Капча",
"fr": "Captcha",
"es": "Captcha",
"tr": "Captcha",
"kk": "Капча"
},
"InvalidCredentials": {
"en": "Invalid password",
"ru": "Неверный пароль",
"zh": "密码无效",
"uk": "Невірний пароль",
"fr": "Mot de passe invalide",
"es": "Contraseña inválida",
"tr": "Geçersiz şifre",
"kk": "Қате құпиясөз"
},
"EmailAuthRequired": {
"en": "Email code required",
"ru": "Требуется код с почты",
"zh": "需要电子邮件验证码",
"uk": "Потрібен код з пошти",
"fr": "Code email requis",
"es": "Se requiere código de correo electrónico",
"tr": "E-posta kodu gerekli",
"kk": "Электрондық пошта коды қажет"
},
"InvalidEmailAuthCode": {
"en": "Invalid email code",
"ru": "Неверный код с почты",
"zh": "无效的电子邮件验证码",
"uk": "Невірний код з пошти",
"fr": "Code email invalide",
"es": "Código de correo electrónico inválido",
"tr": "Geçersiz e-posta kodu",
"kk": "Қате электрондық пошта коды"
},
"InvalidTwoFactorCode": {
"en": "Invalid two factor code",
"ru": "Неверный двухфакторный код",
"zh": "无效的两步验证码",
"uk": "Невірний двофакторний код",
"fr": "Code 2FA invalide",
"es": "Código de verificación en dos pasos inválido",
"tr": "Geçersiz iki faktörlü kod",
"kk": "Қате екі факторлы код"
},
"UndefinedError": {
"en": "Undefined error",
"ru": "Неизвестная ошибка",
"zh": "未定义的错误",
"uk": "Невідома помилка",
"fr": "Erreur inconnue",
"es": "Error desconocido",
"tr": "Bilinmeyen hata",
"kk": "Белгісіз қате"
}
},
"EResult": {
"InvalidPassword": {
"ru": "Неверный пароль",
"en": "Invalid password",
"zh": "密码错误",
"uk": "Невірний пароль",
"fr": "Mot de passe invalide",
"es": "Contraseña inválida",
"tr": "Geçersiz şifre",
"kk": "Қате құпиясөз"
},
"InvalidState": {
"ru": "Неверное состояние (11)",
"en": "Invalid state (11)",
"zh": "无效状态 (11)",
"uk": "Невірний стан (11)",
"fr": "État invalide (11)",
"es": "Estado inválido (11)",
"tr": "Geçersiz durum (11)",
"kk": "Жарамсыз күй (11)"
},
"Timeout": {
"ru": "Таймаут (16)",
"en": "Timeout (16)",
"zh": "超时 (16)",
"uk": "Таймаут (16)",
"fr": "Timeout (16)",
"es": "Tiempo de espera agotado (16)",
"tr": "Zaman aşımı (16)",
"kk": "Уақыт аяқталды (16)"
},
"Banned": {
"ru": "Забанен (17)",
"en": "Banned (17)",
"zh": "被封禁 (17)",
"uk": "Забанений (17)",
"fr": "Banni (17)",
"es": "Prohibido (17)",
"tr": "Yasaklandı (17)",
"kk": "Бұғатталған (17)"
},
"AccountNotFound": {
"ru": "Аккаунт не найден (18)",
"en": "Account not found (18)",
"zh": "未找到账户 (18)",
"uk": "Акаунт не знайдено (18)",
"fr": "Compte non trouvé (18)",
"es": "Cuenta no encontrada (18)",
"tr": "Hesap bulunamadı (18)",
"kk": "Аккаунт табылмады (18)"
},
"InvalidSteamID": {
"ru": "Неправильный SteamID (19)",
"en": "Invalid SteamID (19)",
"zh": "无效的 SteamID (19)",
"uk": "Неправильний SteamID (19)",
"fr": "SteamID invalide (19)",
"es": "SteamID inválido (19)",
"tr": "Geçersiz SteamID (19)",
"kk": "Жарамсыз SteamID (19)"
},
"ServiceUnavailable": {
"ru": "Сервер не отвечает (20)",
"en": "Service unavailable (20)",
"zh": "服务器无响应 (20)",
"uk": "Сервер не відповідає (20)",
"fr": "Service indisponible (20)",
"es": "Servicio no disponible (20)",
"tr": "Servis kullanılamıyor (20)",
"kk": "Қызмет қолжетімсіз (20)"
},
"Pending": {
"ru": "Ожидание (21)",
"en": "Pending (21)",
"zh": "等待 (21)",
"uk": "Очікування (21)",
"fr": "En attente (21)",
"es": "Pendiente (21)",
"tr": "Beklemede (21)",
"kk": "Күту (21)"
},
"LimitExceeded": {
"ru": "Лимит превышен (25)",
"en": "Limit exceeded (25)",
"zh": "超出限制 (25)",
"uk": "Ліміт перевищено (25)",
"fr": "Limite dépassée (25)",
"es": "Límite excedido (25)",
"tr": "Limit aşıldı (25)",
"kk": "Лимит асылды (25)"
},
"Expired": {
"ru": "Истекло (27)",
"en": "Expired (27)",
"zh": "有效期已过 (27)",
"uk": "Термін дії закінчився (27)",
"fr": "Expiré (27)",
"es": "Expirado (27)",
"tr": "Süresi doldu (27)",
"kk": "Мерзімі аяқталды (27)"
},
"Blocked": {
"ru": "Заблокировано (40)",
"en": "Blocked (40)",
"zh": "已封锁 (40)",
"uk": "Заблоковано (40)",
"fr": "Bloqué (40)",
"es": "Bloqueado (40)",
"tr": "Engellendi (40)",
"kk": "Блокталған (40)"
},
"AccountDisabled": {
"ru": "Аккаунт отключен (43)",
"en": "Account disabled (43)",
"zh": "账户已被禁用 (43)",
"uk": "Акаунт відключено (43)",
"fr": "Compte désactivé (43)",
"es": "Cuenta deshabilitada (43)",
"tr": "Hesap devre dışı (43)",
"kk": "Аккаунт өшірілген (43)"
},
"Suspended": {
"ru": "Отключено (Suspended 51)",
"en": "Suspended (51)",
"zh": "已停用(暂停 51",
"uk": "Відключено (Suspended 51)",
"fr": "Suspendu (51)",
"es": "Suspendido (51)",
"tr": "Askıya alındı (51)",
"kk": "Тоқтатылған (51)"
},
"Cancelled": {
"ru": "Отмена (52)",
"en": "Cancelled (52)",
"zh": "已取消 (52)",
"uk": "Скасовано (52)",
"fr": "Annulé (52)",
"es": "Cancelado (52)",
"tr": "İptal edildi (52)",
"kk": "Бас тартылды (52)"
},
"InvalidLoginAuthCode": {
"ru": "Неверный код авторизации (65)",
"en": "Invalid login auth code (65)",
"zh": "无效的授权码 (65)",
"uk": "Невірний код авторизації (65)",
"fr": "Code d'authentification invalide (65)",
"es": "Código de autenticación inválido (65)",
"tr": "Geçersiz giriş doğrulama kodu (65)",
"kk": "Жарамсыз авторизация коды (65)"
},
"ExpiredLoginAuthCode": {
"ru": "Истекло время кода (71)",
"en": "Expired login auth code (71)",
"zh": "验证码已过期 (71)",
"uk": "Термін дії коду закінчився (71)",
"fr": "Code d'authentification expiré (71)",
"es": "Código de autenticación expirado (71)",
"tr": "Giriş doğrulama kodunun süresi doldu (71)",
"kk": "Авторизация кодының мерзімі аяқталды (71)"
},
"AccountLockedDown": {
"ru": "Аккаунт заблокирован (КТ 73)",
"en": "Account locked down (73)",
"zh": "账户被封锁 (КТ 73)",
"uk": "Акаунт заблоковано (КТ 73)",
"fr": "Compte verrouillé (73)",
"es": "Cuenta bloqueada (73)",
"tr": "Hesap kilitlendi (73)",
"kk": "Аккаунт бұғатталған (73)"
},
"UnexpectedError": {
"ru": "Неожиданная ошибка (79)",
"en": "Unexpected error (79)",
"zh": "不可预料的错误 (79)",
"uk": "Непередбачувана помилка (79)",
"fr": "Erreur inattendue (79)",
"es": "Error inesperado (79)",
"tr": "Beklenmeyen hata (79)",
"kk": "Күтпеген қате (79)"
},
"Disabled": {
"ru": "Отключено (80)",
"en": "Disabled (80)",
"zh": "已关闭 (80)",
"uk": "Відключено (80)",
"fr": "Désactivé (80)",
"es": "Deshabilitado (80)",
"tr": "Devre dışı (80)",
"kk": "Өшірілген (80)"
},
"RegionLocked": {
"ru": "Регион заблокирован (83)",
"en": "Region locked (83)",
"zh": "该地区已被封锁 (83)",
"uk": "Регіон заблоковано (83)",
"fr": "Région bloquée (83)",
"es": "Región bloqueada (83)",
"tr": "Bölge kilitli (83)",
"kk": "Аймақ бұғатталған (83)"
},
"RateLimitExceeded": {
"ru": "Слишком много запросов (84)",
"en": "Rate limit exceeded (84)",
"zh": "请求过多 (84)",
"uk": "Забагато запитів (84)",
"fr": "Limite de connexion dépassée (84)",
"es": "Límite de solicitudes excedido (84)",
"tr": "İstek limiti aşıldı (84)",
"kk": "Сұраныс лимиті асты (84)"
},
"TwoFactorCodeMismatch": {
"ru": "2FA код не подошел (88)",
"en": "2FA code mismatch (88)",
"zh": "双因素代码不正确 (88)",
"uk": "2FA код не підійшов (88)",
"fr": "Code 2FA invalide (88)",
"es": "Código 2FA incorrecto (88)",
"tr": "2FA kodu eşleşmedi (88)",
"kk": "2FA коды сәйкес келмеді (88)"
},
"TwoFactorActivationCodeMismatch": {
"ru": "2FA код не прошел проверку (89)",
"en": "2FA activation code mismatch (89)",
"zh": "双因素认证代码未通过验证 (89)",
"uk": "2FA код не пройшов перевірку (89)",
"fr": "Code d'activation 2FA invalide (89)",
"es": "Código de activación 2FA incorrecto (89)",
"tr": "2FA aktivasyon kodu eşleşmedi (89)",
"kk": "2FA активация коды сәйкес келмеді (89)"
},
"TimeNotSynced": {
"ru": "Время не синхронизировано (93)",
"en": "Time not synced (93)",
"zh": "时间未同步 (93)",
"uk": "Час не синхронізовано (93)",
"fr": "Heure non synchronisée (93)",
"es": "Hora no sincronizada (93)",
"tr": "Saat senkronize değil (93)",
"kk": "Уақыт синхрондалмаған (93)"
},
"SMSCodeFailed": {
"ru": "Ошибка SMS-кода (94)",
"en": "SMS code failed (94)",
"zh": "短信验证码错误 (94)",
"uk": "Помилка SMS-коду (94)",
"fr": "Code SMS échoué (94)",
"es": "Código SMS incorrecto (94)",
"tr": "SMS kodu hatalı (94)",
"kk": "SMS коды қате (94)"
},
"AccountLimitExceeded": {
"ru": "Лимит аккаунта превышен (95)",
"en": "Account limit exceeded (95)",
"zh": "账户已超出限制 (95)",
"uk": "Ліміт акаунта перевищено (95)",
"fr": "Limite de compte dépassée (95)",
"es": "Límite de cuenta excedido (95)",
"tr": "Hesap limiti aşıldı (95)",
"kk": "Аккаунт лимиті асты (95)"
},
"EmailSendFailure": {
"ru": "Не удалось отправить письмо (99)",
"en": "Email send failure (99)",
"zh": "无法发送邮件 (99)",
"uk": "Не вдалося відправити лист (99)",
"fr": "Échec de l'envoi de l'email (99)",
"es": "Error al enviar el correo (99)",
"tr": "E-posta gönderilemedi (99)",
"kk": "Электрондық хат жіберілмеді (99)"
},
"NeedCaptcha": {
"ru": "Капча (101)",
"en": "Need captcha (101)",
"zh": "验证码 (101)",
"uk": "Капча (101)",
"fr": "Captcha requis (101)",
"es": "Captcha requerido (101)",
"tr": "Captcha gerekli (101)",
"kk": "Капча қажет (101)"
},
"IPBanned": {
"ru": "IP заблокирован (105)",
"en": "IP banned (105)",
"zh": "IP被阻止 (105)",
"uk": "IP заблоковано (105)",
"fr": "IP bloquée (105)",
"es": "IP bloqueada (105)",
"tr": "IP yasaklandı (105)",
"kk": "IP бұғатталған (105)"
},
"LimitedUserAccount": {
"ru": "Аккаунт с лимитом (112)",
"en": "Limited user account (112)",
"zh": "限额账户 (112)",
"uk": "Акаунт з лімітом (112)",
"fr": "Compte utilisateur limité (112)",
"es": "Cuenta limitada (112)",
"tr": "Sınırlı kullanıcı hesabı (112)",
"kk": "Шектеулі аккаунт (112)"
},
"AccountDeleted": {
"ru": "Аккаунт удален (114)",
"en": "Account deleted (114)",
"zh": "账户已删除 (114)",
"uk": "Акаунт видалено (114)",
"fr": "Compte supprimé (114)",
"es": "Cuenta eliminada (114)",
"tr": "Hesap silindi (114)",
"kk": "Аккаунт жойылған (114)"
},
"PhoneNumberIsVOIP": {
"ru": "Номера VOIP запрещены (127)",
"en": "Phone number is VOIP (127)",
"zh": "VOIP 号码被禁止 (127)",
"uk": "Номера VOIP заборонені (127)",
"fr": "Numéros VOIP interdits (127)",
"es": "Los números VOIP no están permitidos (127)",
"tr": "VOIP numaralar yasaklı (127)",
"kk": "VOIP нөмірлеріне тыйым салынған (127)"
}
},
"AuthenticatorLinkerError": {
"PhoneAlreadyAttached": {
"ru": "Телефон привязан, попробуйте через 20 минут",
"en": "Phone already attached, try again in 20 minutes",
"zh": "手机已绑定,请在20分钟后重试",
"uk": "Телефон прив'язано, спробуйте через 20 хвилин",
"fr": "Téléphone déjà attaché, réessayer dans 20 minutes",
"es": "El teléfono ya está vinculado, inténtalo de nuevo en 20 minutos",
"tr": "Telefon zaten bağlı, 20 dakika sonra tekrar deneyin",
"kk": "Телефон бұрыннан тіркелген, 20 минуттан кейін қайта көріңіз"
},
"InvalidPhoneNumber": {
"ru": "Неверный номер телефона",
"en": "Invalid phone number",
"zh": "无效的电话号码",
"uk": "Невірний номер телефону",
"fr": "Numéro de téléphone invalide",
"es": "Número de teléfono inválido",
"tr": "Geçersiz telefon numarası",
"kk": "Жарамсыз телефон нөмірі"
},
"CantAttachPhone": {
"ru": "Не удается привязать телефон",
"en": "Can't attach phone",
"zh": "无法绑定手机",
"uk": "Не вдається прив'язати телефон",
"fr": "Impossible d'attacher le téléphone",
"es": "No se puede vincular el teléfono",
"tr": "Telefon bağlanamıyor",
"kk": "Телефонды тіркеу мүмкін емес"
},
"CantConfirmAttachingEmail": {
"ru": "Не удалось подтвердить письмо",
"en": "Can't confirm attaching email",
"zh": "无法确认邮件",
"uk": "Не вдалося підтвердити лист",
"fr": "Impossible de confirmer l'attachement de l'email",
"es": "No se pudo confirmar el correo electrónico",
"tr": "E-posta doğrulanamadı",
"kk": "Электрондық поштаны растау мүмкін болмады"
},
"CantSendSms": {
"ru": "Не удалось отправить SMS",
"en": "Can't send SMS",
"zh": "无法发送短信",
"uk": "Не вдалося відправити SMS",
"fr": "Impossible d'envoyer SMS",
"es": "No se pudo enviar el SMS",
"tr": "SMS gönderilemedi",
"kk": "SMS жіберу мүмкін болмады"
},
"AuthenticatorPresent": {
"ru": "Аутентификатор привязан",
"en": "Authenticator present",
"zh": "身份验证器已绑定",
"uk": "Аутентифікатор прив'язано",
"fr": "Authenticateur présent",
"es": "Autenticador ya vinculado",
"tr": "Kimlik doğrulayıcı zaten bağlı",
"kk": "Аутентификатор бұрыннан тіркелген"
},
"BadConfirmationCode": {
"ru": "Код не подошел",
"en": "Bad confirmation code",
"zh": "代码不正确",
"uk": "Код не підійшов",
"fr": "Code de confirmation invalide",
"es": "Código de confirmación incorrecto",
"tr": "Doğrulama kodu hatalı",
"kk": "Растау коды қате"
},
"UnableToGenerateCorrectCodes": {
"ru": "Не удается сгенерировать коды",
"en": "Unable to generate correct codes",
"zh": "无法生成代码",
"uk": "Не вдається згенерувати коди",
"fr": "Impossible de générer des codes corrects",
"es": "No se pueden generar códigos correctos",
"tr": "Doğru kodlar oluşturulamıyor",
"kk": "Дұрыс кодтарды генерациялау мүмкін емес"
},
"InvalidStateWithStatus2": {
"ru": "Неверное состояние (InvalidState) со статусом 2. Откройте ссылку на руководство сверху этого окна.",
"en": "Invalid state (InvalidState) with status 2. Open link to the troubleshooting guide at the top of this window.",
"zh": "状态无效(InvalidState),状态码为 2. 请打开本窗口顶部的故障排除指南链接。",
"uk": "Невірний стан (InvalidState) зі статусом 2. Відкрийте посилання на посібник з усунення несправностей у верхній частині цього вікна.",
"fr": "État invalide (InvalidState) avec le statut 2. Ouvrez le lien vers le guide de dépannage en haut de cette fenêtre.",
"es": "Estado inválido (InvalidState) con estado 2. Abre el enlace a la guía de solución de problemas en la parte superior de esta ventana.",
"tr": "Geçersiz durum (InvalidState) durum 2 ile. Bu pencerenin üst kısmındaki sorun giderme kılavuzu bağlantısını açın.",
"kk": "Жарамсыз күй (InvalidState), статус 2. Осы терезенің жоғарғы жағындағы ақауларды жою нұсқаулығының сілтемесін ашыңыз."
},
"GeneralFailure": {
"ru": "Общая ошибка",
"en": "General Failure",
"zh": "一般故障",
"uk": "Загальна помилка",
"fr": "Échec général",
"es": "Fallo general",
"tr": "Genel hata",
"kk": "Жалпы қате"
}
}
},
"ExceptionHandler": {
"SessionInvalidException": {
"ru": "Сессия истекла. Попробуйте обновить ее или залогиниться заново (Аккаунт → войти)",
"en": "Session expired. Try to refresh it or login again (Account → Login)",
"zh": "会话已过期。请通过菜单尝试刷新,或重新登录(账户 → 登录)",
"uk": "Сесія прострочена. Спробуйте оновити її через меню або залогінитися знову (Акаунт → Увійти)",
"fr": "Session expirée. Essayez de la rafraîchir ou de vous reconnecter à nouveau (Compte → Se connecter)",
"es": "La sesión ha expirado. Intenta actualizarla o iniciar sesión nuevamente (Cuenta → Iniciar sesión)",
"tr": "Oturum süresi doldu. Yenilemeyi deneyin veya tekrar giriş yapın (Hesap → Giriş)",
"kk": "Сессия мерзімі аяқталды. Оны жаңартып көріңіз немесе қайта кіріңіз (Аккаунт → Кіру)"
},
"SessionExpiredException": {
"ru": "Сессия полностью истекла. Нужно залогиниться заново (Аккаунт → Войти)",
"en": "Session fully expired. Need to login again (Account → Login)",
"zh": "会话已完全过期。需要重新登录(账户 → 登录)",
"uk": "Сесія повніст'ю прострочена'. Потрібно залогінитися знову (Акаунт → Увійти)",
"fr": "Session entièrement expirée. Vous devez vous reconnecter à nouveau (Compte → Se connecter)",
"es": "La sesión ha expirado completamente. Debes iniciar sesión nuevamente (Cuenta → Iniciar sesión)",
"tr": "Oturum tamamen sona erdi. Tekrar giriş yapmanız gerekiyor (Hesap → Giriş)",
"kk": "Сессия толық аяқталды. Қайта кіру қажет (Аккаунт → Кіру)"
},
"TaskCanceledException": {
"ru": "Произошла отмена операции",
"en": "Operation canceled",
"zh": "操作已取消",
"uk": "Операція скасована",
"fr": "Opération annulée",
"es": "Operación cancelada",
"tr": "İşlem iptal edildi",
"kk": "Операция тоқтатылды"
},
"TimeoutException": {
"ru": "Таймаут подключения. Проверьте ваше соединение с Proxy или интернетом",
"en": "Connection timeout. Check your connection with Proxy or internet",
"zh": "连接超时。请检查您的代理或互联网连接。",
"uk": "Таймаут підключення. Перевірте ваше з'єднання з Proxy або інтернетом",
"fr": "Timeout de connexion. Vérifiez votre connexion avec Proxy ou internet",
"es": "Tiempo de espera de conexión agotado. Verifica tu conexión con el proxy o Internet",
"tr": "Bağlantı zaman aşımına uğradı. Proxy veya internet bağlantınızı kontrol edin",
"kk": "Қосылу уақыты аяқталды. Proxy немесе интернет қосылымыңызды тексеріңіз"
},
"UnsupportedResponseException": {
"ru": "Получен неожиданный ответ от Steam. Результат сохранен в log.log. Свяжитесь с разработчиком",
"en": "Unexpected response from Steam. Result saved in log.log. Contact developer",
"zh": "从 Steam 收到意外响应。结果已保存到 log.log。请联系开发者",
"uk": "Отримано неочікувану відповідь від Steam. Результат збережено в log.log. Зв'яжіться з розробником",
"fr": "Réponse inattendue de Steam. Résultat enregistré dans log.log. Contactez le développeur",
"es": "Respuesta inesperada de Steam. El resultado se guardó en log.log. Contacta al desarrollador",
"tr": "Steam'den beklenmeyen bir yanıt alındı. Sonuç log.log dosyasına kaydedildi. Geliştiriciyle iletişime geçin",
"kk": "Steam-нан күтпеген жауап алынды. Нәтиже log.log файлына сақталды. Әзірлеушімен байланысыңыз"
},
"LoginException": {
"ru": "Ошибка входа: ",
"en": "Login error: ",
"zh": "登录错误:",
"uk": "Помилка входу: ",
"fr": "Erreur de connexion: ",
"es": "Error de inicio de sesión: ",
"tr": "Giriş hatası: ",
"kk": "Кіру қатесі: "
},
"UnsupportedAuthTypeException": {
"Mail": {
"ru": "Ошибка: Был запрошен код с почты. Steam Guard не активирован",
"en": "Error: Email code was requested. Steam Guard is not activated",
"zh": "错误:已请求电子邮件验证码。令牌未激活",
"uk": "Помилка: Був запрошений код з пошти. Steam Guard не активовано",
"fr": "Erreur: Code email était demandé. Steam Guard non activé",
"es": "Error: Se solicitó un código por correo electrónico. Steam Guard no está activado",
"tr": "Hata: E-posta kodu istendi. Steam Guard etkin değil",
"kk": "Қате: Электрондық пошта коды сұралды. Steam Guard белсендірілмеген"
},
"Guard": {
"ru": "Ошибка: Был запрошен вход с помощью Steam Guard. На аккаунте уже активирован мобильный аутентификатор",
"en": "Error: Login with Steam Guard was requested. Mobile authenticator is already activated on the account",
"zh": "错误:请求使用令牌登录。该账户已激活移动验证器",
"uk": "Помилка: Був запрошений вхід за допомогою Steam Guard. На акаунті вже активовано мобільний автентифікатор",
"fr": "Erreur: Connexion avec Steam Guard demandée. Authenticateur mobile est déjà activé sur le compte",
"es": "Error: Se solicitó iniciar sesión con Steam Guard. El autenticador móvil ya está activado en la cuenta",
"tr": "Hata: Steam Guard ile giriş istendi. Hesapta mobil doğrulayıcı zaten etkin",
"kk": "Қате: Steam Guard арқылы кіру сұралды. Аккаунтта мобильді аутентификатор әлдеқашан қосылған"
},
"Unknown": {
"ru": "Ошибка: Неизвестный тип аутентификации",
"en": "Error: Unknown authentication type",
"zh": "错误:未知的身份验证类型",
"uk": "Помилка: Невідомий тип автентифікації",
"fr": "Erreur: Type d'authentification inconnu",
"es": "Error: Tipo de autenticación desconocido",
"tr": "Hata: Bilinmeyen kimlik doğrulama türü",
"kk": "Қате: Белгісіз аутентификация түрі"
}
},
"UnknownException": {
"ru": "Неизвестная ошибка: ",
"en": "Unknown error: ",
"zh": "未知错误:",
"uk": "Невідома помилка: ",
"fr": "Erreur inconnue: ",
"es": "Error desconocido: ",
"tr": "Bilinmeyen hata: ",
"kk": "Белгісіз қате: "
},
"CantLoadConfirmationsException": {
"Common": {
"ru": "Не удалось загрузить потверждения из-за ошибки Steam: ",
"en": "Can't load confirmations due to Steam error: ",
"zh": "由于 Steam 错误,无法下载确认信息:",
"uk": "Не вдалося завантажити підтвердження через помилку Steam: ",
"fr": "Impossible de charger les confirmations en raison de l'erreur Steam: ",
"es": "No se pueden cargar las confirmaciones debido a un error de Steam: ",
"tr": "Steam hatası nedeniyle onaylar yüklenemedi: ",
"kk": "Steam қатесіне байланысты растауларды жүктеу мүмкін емес: "
},
"TryAgainLater": {
"ru": "Попробуйте позже",
"en": "Try again later",
"zh": "稍后再试",
"uk": "Спробуйте пізніше",
"fr": "Réessayer plus tard",
"es": "Inténtalo más tarde",
"tr": "Daha sonra tekrar deneyin",
"kk": "Кейінірек қайта көріңіз"
},
"NotSetupToReceiveConfirmations": {
"ru": "Аккаунт не настроен на получение подтверждений",
"en": "Account is not set up to receive confirmations",
"zh": "帐户未设置为接收确认",
"uk": "Акаунт не налаштований на отримання підтверджень",
"fr": "Le compte n'est pas configuré pour recevoir des confirmations",
"es": "La cuenta no está configurada para recibir confirmaciones",
"tr": "Hesap onayları almak için ayarlanmamış",
"kk": "Аккаунт растауларды алу үшін бапталмаған"
}
}
}
}
}
@@ -0,0 +1,168 @@
{
"CodeBehind": {
"SessionHandler": {
"SessionWasRefreshedAutomatically": {
"ru": "Сессия была обновлена автоматически",
"en": "Session was refreshed automatically",
"zh": "会话已自动更新",
"uk": "Сесія була оновлена автоматично",
"fr": "La session a été rafraîchie automatiquement",
"es": "La sesión se actualizó automáticamente",
"tr": "Oturum otomatik olarak yenilendi",
"kk": "Сессия автоматты түрде жаңартылды"
}
},
"MAAC": {
"TimerConfirmed": {
"ru": "Авто: подтверждено ",
"en": "Auto: confirmed ",
"zh": "自动:已确认 ",
"uk": "Авто: підтверджено ",
"fr": "Auto: confirmé ",
"es": "Auto: confirmado ",
"tr": "Otomatik: onaylandı ",
"kk": "Авто: расталды "
},
"TimerNotConfirmed": {
"ru": "Авто: подтверждение не сработало",
"en": "Auto: confirmation was unsuccessful",
"zh": "自动:确认失败",
"uk": "Авто: підтвердження не спрацювало",
"fr": "Auto: la confirmation n'a pas fonctionné",
"es": "Auto: la confirmación falló",
"tr": "Otomatik: doğrulama başarısız oldu",
"kk": "Авто: растау сәтсіз болды"
},
"TimerSessionError": {
"ru": "необходимо обновить сессию или перелогиниться",
"en": "need to refresh session or relogin",
"zh": "需要刷新会话或重新登录",
"uk": "необхідно оновити сесію або перелогінитися",
"fr": "il faut rafraîchir la session ou se reconnecter",
"es": "es necesario actualizar la sesión o volver a iniciar sesión",
"tr": "oturumu yenilemek veya tekrar giriş yapmak gerekiyor",
"kk": "сессияны жаңарту немесе қайта кіру қажет"
},
"TimerPrefix": {
"ru": "Авто ",
"en": "Auto ",
"zh": "自动 ",
"uk": "Авто ",
"fr": "Auto ",
"es": "Auto ",
"tr": "Otomatik ",
"kk": "Авто "
},
"TimerPreventedOverlap": {
"ru": "Авто: таймер подтверждений пытался начать новый цикл, когда предыдущий еще не завершился. Советуем увеличить задержку",
"en": "Auto: confirmation timer tried to start new cycle when previous one was not finished yet. We recommend to increase delay",
"zh": "自动: 确认计时器在前一个周期尚未完成时尝试启动新周期。我们建议增加延迟。",
"uk": "Авто: таймер підтверджень намагався запустити новий цикл, коли попередній ще не закінчився. Рекомендуємо збільшити затримку",
"fr": "Auto: le timer de confirmation a tenté de démarrer un nouveau cycle alors que le précédent n'était pas encore terminé. Nous recommandons d'augmenter le délai",
"es": "Auto: el temporizador de confirmación intentó iniciar un nuevo ciclo cuando el anterior aún no había terminado. Recomendamos aumentar el retraso",
"tr": "Otomatik: onay zamanlayıcısı önceki döngü bitmeden yeni bir döngü başlatmaya çalıştı. Gecikmeyi artırmanızı öneririz",
"kk": "Авто: растау таймері алдыңғы цикл аяқталмай тұрып жаңасын бастауға тырысты. Кідірісті арттыруды ұсынамыз"
},
"FailedToLoadStorage": {
"en": "Failed to load auto-confirm timers. File seems to be corrupted",
"ru": "Не удалось загрузить таймеры авто-подтверждений. Файл, похоже, поврежден",
"zh": "无法加载自动确认计时器。文件似乎已损坏",
"uk": "Не вдалося завантажити таймери авто-підтверджень. Файл, схоже, пошкоджено",
"fr": "Impossible de charger les timers d'auto-confirmation. Le fichier semble être corrompu",
"es": "No se pudieron cargar los temporizadores de auto-confirmación. El archivo parece estar dañado",
"tr": "Otomatik onay zamanlayıcıları yüklenemedi. Dosya bozulmuş görünüyor",
"kk": "Авто-растау таймерлерін жүктеу мүмкін болмады. Файл бүлінген сияқты"
},
"PortableMaClientStatus": {
"SessionError": {
"ru": "Ошибка сессии. Нужно обновить сессию или перелогиниться",
"en": "Session error. Need to refresh session or relogin",
"zh": "会话错误。需要刷新会话或重新登录",
"uk": "Помилка сесії. Потрібно оновити сесію або перелогінитися",
"fr": "Erreur de session. Il faut rafraîchir la session ou se reconnecter",
"es": "Error de sesión. Es necesario actualizar la sesión o volver a iniciar sesión",
"tr": "Oturum hatası. Oturumu yenilemek veya tekrar giriş yapmak gerekiyor",
"kk": "Сессия қатесі. Сессияны жаңарту немесе қайта кіру қажет"
}
}
},
"ProxyStorage": {
"ErrorWhileLoadingProxies": {
"en": "Error while loading proxies",
"ru": "Ошибка при загрузке прокси",
"zh": "加载代理时出错",
"uk": "Помилка при завантаженні проксі",
"fr": "Erreur lors du chargement des proxies",
"es": "Error al cargar los proxies",
"tr": "Proxy yüklenirken hata oluştu",
"kk": "Proxy жүктеу кезінде қате"
}
},
"Settings": {
"ErrorWhileLoadingSettings": {
"en": "Error while loading settings. Settings were reset",
"ru": "Ошибка при загрузке настроек. Настройки были сброшены",
"zh": "加载设置时出错。设置已重置",
"uk": "Помилка при завантаженні налаштувань. Налаштування були скинуті",
"fr": "Erreur lors du chargement des paramètres. Les paramètres ont été réinitialisés",
"es": "Error al cargar la configuración. Los ajustes fueron restablecidos",
"tr": "Ayarlar yüklenirken hata oluştu. Ayarlar sıfırlandı",
"kk": "Баптауларды жүктеу кезінде қате. Баптаулар қалпына келтірілді"
}
},
"MafileExporter": {
"InvalidPath": {
"ru": "Указан некорректный путь для экспорта.",
"en": "The specified export path is invalid.",
"uk": "Вказано некоректний шлях для експорту.",
"fr": "Le chemin dexportation spécifié est invalide.",
"zh": "指定的导出路径无效。",
"es": "La ruta de exportación especificada no es válida.",
"tr": "Belirtilen dışa aktarma yolu geçersiz.",
"kk": "Көрсетілген экспорт жолы жарамсыз."
},
"NebulaUtilizedDirectory": {
"ru": "Невозможно выполнить экспорт в папку, которая используется приложением. Рекомендуется выбрать отдельную директорию.",
"en": "Cannot export to a directory that is currently used by the application. Please choose a separate folder.",
"uk": "Неможливо виконати експорт у папку, яка використовується застосунком. Рекомендується вибрати окрему директорію.",
"fr": "Impossible dexporter vers un dossier utilisé par lapplication. Veuillez choisir un dossier distinct.",
"zh": "无法导出到应用程序正在使用的文件夹。请选择一个单独的目录。",
"es": "No se puede exportar a una carpeta que está siendo usada por la aplicación. Elija una carpeta diferente.",
"tr": "Uygulama tarafından kullanılan bir klasöre dışa aktarma yapılamaz. Lütfen ayrı bir klasör seçin.",
"kk": "Қолданба пайдаланып жатқан қалтаға экспорт жасау мүмкін емес. Басқа қалтаны таңдаңыз."
},
"AccessDenied": {
"ru": "Недостаточно прав для записи в указанную папку. Проверьте права доступа или выберите другой путь.",
"en": "Insufficient permissions to write to the specified directory. Please check access rights or choose another path.",
"uk": "Недостатньо прав для запису в указану папку. Перевірте права доступу або виберіть інший шлях.",
"fr": "Autorisation insuffisante pour écrire dans le dossier spécifié. Vérifiez les droits daccès ou choisissez un autre chemin.",
"zh": "没有权限写入指定的文件夹。请检查访问权限或选择其他路径。",
"es": "Permisos insuficientes para escribir en la carpeta especificada. Verifique los permisos o elija otra ruta.",
"tr": "Belirtilen klasöre yazmak için yeterli izin yok. Lütfen erişim haklarını kontrol edin veya başka bir yol seçin.",
"kk": "Көрсетілген қалтаға жазуға рұқсат жеткіліксіз. Қолжетімділік құқықтарын тексеріңіз немесе басқа жол таңдаңыз."
}
},
"UpdateService": {
"LatestVersion": {
"en": "You are using the latest version",
"ru": "У вас установлена последняя версия",
"zh": "您正在使用最新版本",
"uk": "У вас встановлена остання версія",
"fr": "Vous utilisez la dernière version",
"es": "Estás usando la última versión",
"tr": "En son sürümü kullanıyorsunuz",
"kk": "Сіз соңғы нұсқаны пайдаланып отырсыз"
},
"UpdateIndicatorHint": {
"en": "Update available. Click 'by achies' to check",
"ru": "Доступно обновление. Нажмите 'by achies' для проверки",
"zh": "有可用更新。点击 'by achies' 进行检查",
"uk": "Доступне оновлення. Натисніть 'by achies', щоб перевірити",
"fr": "Une mise à jour est disponible. Cliquez sur 'by achies' pour vérifier",
"es": "Actualización disponible. Haz clic en 'by achies' para comprobar",
"tr": "Güncelleme mevcut. Kontrol etmek için 'by achies' tıklayın",
"kk": "Жаңарту қолжетімді. Тексеру үшін 'by achies' басыңыз"
}
}
}
}
@@ -0,0 +1,54 @@
{
"SetEncryptionPasswordDialog": {
"Title": {
"en": "Encryption password",
"ru": "Пароль шифрования",
"zh": "加密密码",
"uk": "Пароль шифрування",
"fr": "Mot de passe de chiffrement",
"es": "Contraseña de cifrado",
"tr": "Şifreleme parolası",
"kk": "Шифрлау құпиясөзі"
},
"DialogText": {
"en": "You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session.\r\n(Optional)",
"ru": "Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией.\r\n(Не обязательно)",
"zh": "之前您为密码文件设置了加密密码,请输入它,以便应用程序在会话出现问题时可以自动登录。\r\n(非必填)",
"uk": "Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією.\r\n(Не обов'язково)",
"fr": "Vous avez déjà défini un mot de passe de chiffrement pour les mots de passe des mafiles, spécifiez-le pour que l'application puisse se connecter automatiquement en cas de problème avec la session.\r\n(Facultatif)",
"es": "Anteriormente estableciste una contraseña de cifrado para las contraseñas en los mafiles. Introdúcela para que la aplicación pueda iniciar sesión automáticamente si hay problemas con la sesión.\r\n(Opcional)",
"tr": "Daha önce mafile içindeki parolalar için bir şifreleme parolası ayarladınız. Oturumla ilgili sorunlar oluştuğunda uygulamanın otomatik giriş yapabilmesi için bunu belirtin.\r\n(İsteğe bağlı)",
"kk": "Бұрын mafile ішіндегі құпиясөздер үшін шифрлау құпиясөзін орнатқансыз. Сессияда мәселе туындаған жағдайда қолданба автоматты түрде кіруі үшін оны енгізіңіз.\r\n(Міндетті емес)"
},
"Ok": {
"en": "Ok",
"ru": "Ок",
"zh": "确定",
"uk": "Ок",
"fr": "Ok",
"es": "Aceptar",
"tr": "Tamam",
"kk": "Жарайды"
},
"Cancel": {
"en": "Cancel",
"ru": "Отмена",
"zh": "取消",
"uk": "Відміна",
"fr": "Annuler",
"es": "Cancelar",
"tr": "İptal",
"kk": "Бас тарту"
},
"Password": {
"en": "Password",
"ru": "Пароль",
"zh": "密码",
"uk": "Пароль",
"fr": "Mot de passe",
"es": "Contraseña",
"tr": "Şifre",
"kk": "Құпиясөз"
}
}
}
@@ -0,0 +1,324 @@
{
"CodeBehind": {
"MafileExporterVM": {
"Result": {
"Exported": {
"ru": "Экспортировано: {0}",
"en": "Exported: {0}",
"uk": "Експортовано: {0}",
"fr": "Exporté : {0}",
"zh": "已导出:{0}",
"es": "Exportados: {0}",
"tr": "Dışa aktarıldı: {0}",
"kk": "Экспортталды: {0}"
},
"NotFound": {
"ru": ". Не найдено: {0}",
"en": ". Not found: {0}",
"uk": ". Не знайдено: {0}",
"fr": ". Introuvable : {0}",
"zh": "。未找到:{0}",
"es": ". No encontrados: {0}",
"tr": ". Bulunamadı: {0}",
"kk": ". Табылмады: {0}"
},
"Conflict": {
"ru": ". Конфликты: {0}",
"en": ". Conflicts: {0}",
"uk": ". Конфлікти: {0}",
"fr": ". Conflits : {0}",
"zh": "。冲突:{0}",
"es": ". Conflictos: {0}",
"tr": ". Çakışmalar: {0}",
"kk": ". Қайшылықтар: {0}"
}
}
}
},
"ExportDialog": {
"ExportTitle": {
"ru": "Экспорт",
"en": "Export",
"uk": "Експорт",
"fr": "Exportation",
"zh": "导出",
"es": "Exportar",
"tr": "Dışa aktar",
"kk": "Экспорт"
},
"ExportButton": {
"ru": "Экспорт",
"en": "Export",
"uk": "Експорт",
"fr": "Exporter",
"zh": "导出",
"es": "Exportar",
"tr": "Dışa aktar",
"kk": "Экспорт"
},
"TemplateHint": {
"ru": "Шаблон",
"en": "Template",
"uk": "Шаблон",
"fr": "Modèle",
"zh": "模板",
"es": "Plantilla",
"tr": "Şablon",
"kk": "Үлгі"
},
"SelectPathHint": {
"ru": "Выберите путь",
"en": "Select path",
"uk": "Виберіть шлях",
"fr": "Sélectionner le chemin",
"zh": "选择路径",
"es": "Seleccionar ruta",
"tr": "Yol seç",
"kk": "Жолды таңдаңыз"
},
"ExportSettingsHeader": {
"ru": "Настройки экспорта",
"en": "Export settings",
"uk": "Налаштування експорту",
"fr": "Paramètres dexportation",
"zh": "导出设置",
"es": "Configuración de exportación",
"tr": "Dışa aktarma ayarları",
"kk": "Экспорт параметрлері"
},
"ExportSettingsCommon": {
"ru": "Основные:",
"en": "General:",
"uk": "Основні:",
"fr": "Général :",
"zh": "常规:",
"es": "General:",
"tr": "Genel:",
"kk": "Негізгі:"
},
"ExportSettingsNebula": {
"ru": "Nebula:",
"en": "Nebula:",
"uk": "Nebula:",
"fr": "Nebula :",
"zh": "Nebula",
"es": "Nebula:",
"tr": "Nebula:",
"kk": "Nebula:"
},
"ExportOptions": {
"UseLoginAsMafileName": {
"ru": "Использовать логин как имя mafile",
"en": "Use login as mafile name",
"uk": "Використовувати логін як ім’я mafile",
"fr": "Utiliser le login comme nom de mafile",
"zh": "使用登录名作为 mafile 名称",
"es": "Usar login como nombre del mafile",
"tr": "Giriş adını mafile adı olarak kullan",
"kk": "Логинді mafile атауы ретінде қолдану"
},
"IncludeSharedSecret": {
"ru": "Включать shared secret",
"en": "Include shared secret",
"uk": "Включати shared secret",
"fr": "Inclure le secret partagé",
"zh": "包含 shared secret",
"es": "Incluir shared secret",
"tr": "Shared secret dahil et",
"kk": "Shared secret қосу"
},
"IncludeIdentitySecret": {
"ru": "Включать identity secret",
"en": "Include identity secret",
"uk": "Включати identity secret",
"fr": "Inclure le secret didentité",
"zh": "包含 identity secret",
"es": "Incluir identity secret",
"tr": "Identity secret dahil et",
"kk": "Identity secret қосу"
},
"IncludeRCode": {
"ru": "Включать RCode",
"en": "Include RCode",
"uk": "Включати RCode",
"fr": "Inclure RCode",
"zh": "包含 RCode",
"es": "Incluir RCode",
"tr": "RCode dahil et",
"kk": "RCode қосу"
},
"IncludeSessionData": {
"ru": "Включать данные сессии",
"en": "Include session data",
"uk": "Включати дані сесії",
"fr": "Inclure les données de session",
"zh": "包含会话数据",
"es": "Incluir datos de sesión",
"tr": "Oturum verilerini dahil et",
"kk": "Сессия деректерін қосу"
},
"IncludeOtherInfo": {
"ru": "Включать прочую информацию",
"en": "Include other info",
"uk": "Включати іншу інформацію",
"fr": "Inclure dautres informations",
"zh": "包含其他信息",
"es": "Incluir otra información",
"tr": "Diğer bilgileri dahil et",
"kk": "Басқа ақпаратты қосу"
},
"NebulaProxy": {
"ru": "Прокси",
"en": "Proxy",
"uk": "Проксі",
"fr": "Proxy",
"zh": "代理",
"es": "Proxy",
"tr": "Proxy",
"kk": "Прокси"
},
"NebulaPassword": {
"ru": "Пароль",
"en": "Password",
"uk": "Пароль",
"fr": "Mot de passe",
"zh": "密码",
"es": "Contraseña",
"tr": "Şifre",
"kk": "Құпиясөз"
},
"NebulaGroup": {
"ru": "Группа",
"en": "Group",
"uk": "Група",
"fr": "Groupe",
"zh": "组",
"es": "Grupo",
"tr": "Grup",
"kk": "Топ"
},
"ExportToZip": {
"ru": "Экспортировать в ZIP архив",
"en": "Export to ZIP archive",
"uk": "Експортувати в ZIP архів",
"fr": "Exporter en archive ZIP",
"zh": "导出为 ZIP 压缩包",
"es": "Exportar a archivo ZIP",
"tr": "ZIP arşivine aktar",
"kk": "ZIP мұрағатқа экспорттау"
}
},
"Tooltips": {
"UseLoginAsMafileName": {
"ru": "Использовать логин аккаунта в качестве имени mafile.",
"en": "Use the account login as the mafile name.",
"uk": "Використовувати логін акаунта як ім’я mafile.",
"fr": "Utiliser le login du compte comme nom du mafile.",
"zh": "使用账户登录名作为 mafile 的名称。",
"es": "Usar el login de la cuenta como nombre del mafile.",
"tr": "Hesap giriş adını mafile adı olarak kullan.",
"kk": "Аккаунт логинін mafile атауы ретінде қолдану."
},
"IncludeSharedSecret": {
"ru": "Позволяет генерировать коды подтверждения и входить в аккаунт при наличии пароля или активной сессии.",
"en": "Allows generating confirmation codes and logging into the account if a password or active session is available.",
"uk": "Дозволяє генерувати коди підтвердження та входити в акаунт за наявності пароля або активної сесії.",
"fr": "Permet de générer des codes de confirmation et de se connecter au compte si un mot de passe ou une session active est disponible.",
"zh": "允许生成确认代码,并在有密码或活动会话的情况下登录账户。",
"es": "Permite generar códigos de confirmación e iniciar sesión si hay contraseña o una sesión activa.",
"tr": "Parola veya aktif bir oturum varsa onay kodları üretmeye ve hesaba giriş yapmaya izin verir.",
"kk": "Құпиясөз немесе белсенді сессия болса, растау кодтарын жасауға және аккаунтқа кіруге мүмкіндік береді."
},
"IncludeIdentitySecret": {
"ru": "Включает identity_secret и device_id. Необходимы для отправки подтверждений и дают доступ к трейдам и торговой площадке.",
"en": "Includes identity_secret and device_id. Required for confirmations and provides access to trades and the market.",
"uk": "Включає identity_secret і device_id. Потрібні для надсилання підтверджень і надають доступ до трейдів та торгового майданчика.",
"fr": "Inclut identity_secret et device_id. Nécessaires pour les confirmations et donnent accès aux échanges et au marché.",
"zh": "包含 identity_secret 和 device_id。用于发送确认,并可访问交易和市场。",
"es": "Incluye identity_secret y device_id. Necesarios para confirmaciones y acceso a intercambios y mercado.",
"tr": "identity_secret ve device_id içerir. Onaylar için gereklidir ve takaslara ile pazara erişim sağlar.",
"kk": "identity_secret және device_id қосады. Растаулар үшін қажет және трейдтер мен нарыққа қолжетімділік береді."
},
"IncludeRCode": {
"ru": "Код восстановления. Позволяет отвязать Steam Guard от аккаунта в случае потери доступа.",
"en": "Recovery code. Allows removing Steam Guard from the account if access is lost.",
"uk": "Код відновлення. Дозволяє відв’язати Steam Guard від акаунта у разі втрати доступу.",
"fr": "Code de récupération. Permet de supprimer Steam Guard du compte en cas de perte daccès.",
"zh": "恢复代码。在失去访问权限时可将 Steam Guard 与账户解绑。",
"es": "Código de recuperación. Permite eliminar Steam Guard si se pierde el acceso.",
"tr": "Kurtarma kodu. Erişim kaybedilirse Steam Guard’ı hesaptan kaldırmayı sağlar.",
"kk": "Қалпына келтіру коды. Қолжетімділік жоғалған жағдайда Steam Guard-ты аккаунттан алып тастауға мүмкіндік береді."
},
"IncludeSessionData": {
"ru": "Включает данные текущей сессии. Если сессия активна и не истекла, позволяет получить доступ к аккаунту без ввода пароля с помощью mafile.",
"en": "Includes current session data. If the session is active and valid, allows accessing the account without entering a password using the mafile.",
"uk": "Включає дані поточної сесії. Якщо сесія активна й не прострочена, дозволяє отримати доступ до акаунта без введення пароля за допомогою mafile.",
"fr": "Inclut les données de la session actuelle. Si la session est active et valide, permet daccéder au compte sans saisir de mot de passe via le mafile.",
"zh": "包含当前会话数据。如果会话处于活动状态且未过期,可通过 mafile 在无需输入密码的情况下访问账户。",
"es": "Incluye datos de la sesión actual. Si la sesión está activa, permite acceder a la cuenta sin introducir contraseña.",
"tr": "Mevcut oturum verilerini içerir. Oturum aktifse mafile ile parola girmeden erişim sağlar.",
"kk": "Ағымдағы сессия деректерін қосады. Егер сессия белсенді болса, mafile арқылы құпиясөзсіз кіруге мүмкіндік береді."
},
"IncludeOtherInfo": {
"ru": "Включает дополнительные необязательные поля, которые в данный момент не используются Steam, но могут применяться в будущем и содержать чувствительную информацию.",
"en": "Includes additional optional fields not currently used by Steam, but potentially usable in the future and may contain sensitive information.",
"uk": "Включає додаткові необов’язкові поля, які наразі не використовуються Steam, але можуть бути задіяні в майбутньому та містити чутливу інформацію.",
"fr": "Inclut des champs supplémentaires optionnels actuellement non utilisés par Steam, mais susceptibles d’être exploités à lavenir et de contenir des informations sensibles.",
"zh": "包含目前未被 Steam 使用的可选附加字段,这些字段将来可能会被使用,并可能包含敏感信息。",
"es": "Incluye campos opcionales adicionales que Steam no usa actualmente pero podrían usarse en el futuro.",
"tr": "Steam tarafından şu anda kullanılmayan ancak gelecekte kullanılabilecek ek alanları içerir.",
"kk": "Қазіргі уақытта Steam қолданбайтын, бірақ болашақта қолданылуы мүмкін қосымша өрістерді қосады."
},
"NebulaProxy": {
"ru": "Включить Nebula-совместимые данные прокси.",
"en": "Include Nebula-compatible proxy data.",
"uk": "Увімкнути Nebula-сумісні дані проксі.",
"fr": "Inclure les données de proxy compatibles avec Nebula.",
"zh": "包含与 Nebula 兼容的代理数据。",
"es": "Incluir datos de proxy compatibles con Nebula.",
"tr": "Nebula uyumlu proxy verilerini dahil et.",
"kk": "Nebula үйлесімді прокси деректерін қосу."
},
"NebulaPassword": {
"ru": "Включить Nebula-совместимые данные пароля.",
"en": "Include Nebula-compatible password data.",
"uk": "Увімкнути Nebula-сумісні дані пароля.",
"fr": "Inclure les données de mot de passe compatibles avec Nebula.",
"zh": "包含与 Nebula 兼容的密码数据。",
"es": "Incluir datos de contraseña compatibles con Nebula.",
"tr": "Nebula uyumlu parola verilerini dahil et.",
"kk": "Nebula үйлесімді құпиясөз деректерін қосу."
},
"NebulaGroup": {
"ru": "Включить Nebula-совместимые данные группы.",
"en": "Include Nebula-compatible group data.",
"uk": "Увімкнути Nebula-сумісні дані групи.",
"fr": "Inclure les données de groupe compatibles avec Nebula.",
"zh": "包含与 Nebula 兼容的组数据。",
"es": "Incluir datos de grupo compatibles con Nebula.",
"tr": "Nebula uyumlu grup verilerini dahil et.",
"kk": "Nebula үйлесімді топ деректерін қосу."
},
"ExportToZip": {
"ru": "Экспортировать в ZIP архив вместо папки. Архив будет сохранён в выбранной директории с именем вида N_timestamp.zip.",
"en": "Export to a ZIP archive instead of a folder. The archive will be saved in the selected directory as N_timestamp.zip.",
"uk": "Експортувати в ZIP архів замість папки. Архів буде збережено у вибраній директорії з назвою N_timestamp.zip.",
"fr": "Exporter dans une archive ZIP plutôt que dans un dossier. L'archive sera enregistrée dans le répertoire sélectionné sous le nom N_timestamp.zip.",
"zh": "导出为 ZIP 压缩包而非文件夹。压缩包将保存在所选目录中,名称格式为 N_timestamp.zip。",
"es": "Exportar a un archivo ZIP en lugar de una carpeta. El archivo se guardará en el directorio seleccionado como N_timestamp.zip.",
"tr": "Klasör yerine ZIP arşivine aktar. Arşiv, seçilen dizine N_timestamp.zip adıyla kaydedilecektir.",
"kk": "Қалта орнына ZIP мұрағатына экспорттау. Мұрағат таңдалған каталогта N_timestamp.zip атауымен сақталады."
}
},
"ExportAccountsPlaceholder": {
"ru": "Введите логины или SteamID (по одному на строку). Поддерживается формат логин:пароль — пароль будет проигнорирован. Оставьте пустым для экспорта всех аккаунтов.",
"en": "Enter logins or SteamIDs (one per line). Supports login:password format — the password will be ignored. Leave empty to export all accounts.",
"uk": "Введіть логіни або SteamID (по одному на рядок). Підтримується формат логін:пароль — пароль буде проігноровано. Залиште порожнім для експорту всіх акаунтів.",
"fr": "Entrez les identifiants ou SteamID (un par ligne). Supporte le format login:mot_de_passe — le mot de passe sera ignoré. Laissez vide pour exporter tous les comptes.",
"zh": "输入登录名或 SteamID(每行一个)。支持 login:password 格式——密码将被忽略。留空则导出所有账户。",
"es": "Introduce logins o SteamID (uno por línea). Soporta el formato login:contraseña — la contraseña será ignorada. Déjalo vacío para exportar todas las cuentas.",
"tr": "Girişleri veya SteamIDleri girin (her satıra bir tane). login:şifre formatı desteklenir — şifre yok sayılır. Tüm hesapları dışa aktarmak için boş bırakın.",
"kk": "Логиндерді немесе SteamID (әр жолға біреуден) енгізіңіз. логин:құпиясөз форматы қолданылады — құпиясөз еленбейді. Барлық аккаунттарды экспорттау үшін бос қалдырыңыз."
}
}
}
@@ -0,0 +1,308 @@
{
"CodeBehind": {
"LinkVM": {
"EnterLoginAndPassword": {
"ru": "Введите логин и пароль",
"en": "Enter login and password",
"zh": "请输入用户名和密码",
"uk": "Введіть логін і пароль",
"fr": "Entrez le login et le mot de passe",
"es": "Introduce el usuario y la contraseña",
"tr": "Giriş adı ve şifre girin",
"kk": "Логин мен құпиясөзді енгізіңіз"
},
"CantLogin": {
"ru": "Не удалось залогиниться: ",
"en": "Can't login: ",
"zh": "无法登录:",
"uk": "Не вдалося залогінитися: ",
"fr": "Impossible de se connecter: ",
"es": "No se pudo iniciar sesión: ",
"tr": "Giriş yapılamadı: ",
"kk": "Кіру мүмкін болмады: "
},
"UnsupportedGuardType": {
"ru": "Steam Guard уже активирован. Был запрошен код с мобильного устройства",
"en": "Steam Guard already activated. Mobile device code was requested",
"zh": "令牌已激活。已从移动设备请求验证码",
"uk": "Steam Guard вже активовано. Був запрошений код з мобільного пристрою",
"fr": "Steam Guard déjà activé. Code de l'appareil mobile demandé",
"es": "Steam Guard ya está activado. Se solicitó un código del dispositivo móvil",
"tr": "Steam Guard zaten etkin. Mobil cihaz kodu istendi",
"kk": "Steam Guard бұрыннан белсенді. Мобильді құрылғы коды сұралды"
},
"MafileLinked": {
"ru": "RCode: {0}\r\nИмя файла: {1}.mafile",
"en": "RCode: {0}\r\nFile name: {1}.mafile",
"zh": "响应代码: {0}\r\n文件名: {1}.mafile",
"uk": "RCode: {0}\r\nІм'я файлу: {1}.mafile",
"fr": "RCode: {0}\r\nNom du fichier: {1}.mafile",
"es": "RCode: {0}\r\nNombre del archivo: {1}.mafile",
"tr": "RCode: {0}\r\nDosya adı: {1}.mafile",
"kk": "RCode: {0}\r\nФайл атауы: {1}.mafile"
},
"ErrorWithCode": {
"ru": "Ошибка с кодом: ",
"en": "Error with code: ",
"zh": "错误代码:",
"uk": "Помилка з кодом: ",
"fr": "Erreur avec le code: ",
"es": "Error con código: ",
"tr": "Kodlu hata: ",
"kk": "Кодпен қате: "
},
"UnknownError": {
"ru": "Неизвестная ошибка: ",
"en": "Unknown error: ",
"zh": "未知错误:",
"uk": "Невідома помилка: ",
"fr": "Erreur inconnue: ",
"es": "Error desconocido: ",
"tr": "Bilinmeyen hata: ",
"kk": "Белгісіз қате: "
},
"EnterEmailCode": {
"ru": "Введите код, отправленный на вашу почту",
"en": "Enter the code sent to your email",
"zh": "请输入发送到您邮箱的验证码",
"uk": "Введіть код, надісланий на вашу пошту",
"fr": "Entrez le code envoyé à votre email",
"es": "Introduce el código enviado a tu correo",
"tr": "E-postanıza gönderilen kodu girin",
"kk": "Поштаңызға жіберілген кодты енгізіңіз"
},
"ClickOnEmailLink": {
"ru": "На почту было отправлено письмо, откройте ссылку из письма, а затем нажмите — 'Продолжить'",
"en": "An email has been sent. Open the link from the email, then click 'Proceed'",
"zh": "电子邮件已发送。请打开邮件中的链接,然后点击——“继续”",
"uk": "На пошту надіслано лист. Відкрийте посилання з листа, а потім натисніть — 'Продовжити'",
"fr": "Un email a été envoyé. Ouvrez le lien de l'email, puis cliquez sur 'Continuer'",
"es": "Se envió un correo. Abre el enlace del correo y luego haz clic en 'Continuar'",
"tr": "Bir e-posta gönderildi. E-postadaki bağlantıyı açın ve ardından 'Devam et' düğmesine basın",
"kk": "Электрондық хат жіберілді. Хаттағы сілтемені ашып, кейін 'Жалғастыру' түймесін басыңыз"
},
"ClickOnEmailLinkRetry": {
"ru": "Не удалось подтвердить переход по ссылке. Убедитесь, что вы открыли ссылку из письма, затем нажмите — 'Продолжить'",
"en": "We couldnt verify that you clicked the link. Make sure you opened the link from the email, then click 'Proceed'",
"zh": "无法确认通过链接的跳转。请确保您是从邮件中打开的链接,然后点击——“继续”",
"uk": "Не вдалося підтвердити перехід за посиланням. Переконайтесь, що ви відкрили посилання з листа, а потім натисніть — 'Продовжити'",
"fr": "Nous n'avons pas pu vérifier que vous avez cliqué sur le lien. Assurez-vous d'avoir ouvert le lien de l'email, puis cliquez sur 'Continuer'",
"es": "No pudimos verificar que abriste el enlace. Asegúrate de abrirlo desde el correo y luego haz clic en 'Continuar'",
"tr": "Bağlantıya tıkladığınızı doğrulayamadık. E-postadaki bağlantıyı açtığınızdan emin olun ve ardından 'Devam et' düğmesine basın",
"kk": "Сілтемеге өткеніңізді тексере алмадық. Хаттағы сілтемені ашқаныңызға көз жеткізіп, кейін 'Жалғастыру' түймесін басыңыз"
},
"PhoneHint": {
"ru": "На телефон {0} была отправлена СМС",
"en": "SMS was sent to phone {0}",
"zh": "短信已发送到手机 {0}",
"uk": "На телефон {0} було відправлено СМС",
"fr": "SMS envoyé au téléphone {0}",
"es": "Se envió un SMS al teléfono {0}",
"tr": "{0} numaralı telefona SMS gönderildi",
"kk": "{0} телефонына SMS жіберілді"
},
"EnterPhoneNumber": {
"ru": "Введите номер в международном формате, только цифры (без '+', пробелов и скобок), или оставьте поле пустым, чтобы привязать без номера",
"en": "Enter number in international format, digits only (no '+', spaces or brackets), or leave the field empty to link without a number",
"zh": "请输入国际格式的号码,仅数字(不含空格和括号),或留空以绑定无号码",
"uk": "Введіть номер у міжнародному форматі, лише цифри (без '+', пробілів і дужок), або залиште поле порожнім, щоб прив’язати без номера",
"fr": "Entrez un numéro au format international, uniquement des chiffres (sans '+', espaces ou parenthèses), ou laissez le champ vide pour lier sans numéro",
"es": "Introduce el número en formato internacional, solo dígitos (sin '+', espacios ni paréntesis), o deja el campo vacío para vincular sin número",
"tr": "Numarayı uluslararası formatta girin, yalnızca rakamlar ( '+', boşluk veya parantez yok ) veya numarasız bağlamak için alanı boş bırakın",
"kk": "Нөмірді халықаралық форматта енгізіңіз, тек сандар ('+', бос орын және жақша жоқ) немесе нөмірсіз байланыстыру үшін өрісті бос қалдырыңыз"
}
}
},
"LinkerDialog": {
"Title": {
"en": "Linker",
"ru": "Привязка",
"zh": "绑定",
"uk": "Прив'язка",
"fr": "Liaison",
"es": "Vinculación",
"tr": "Bağlayıcı",
"kk": "Байланыстыру"
},
"GotErrorHyperlinkText": {
"en": "(getting error?)",
"ru": "(возникает ошибка?)",
"zh": "(出现错误?)",
"uk": "(виникає помилка?)",
"fr": "(erreur ?)",
"es": "(¿ocurre un error?)",
"tr": "(hata mı alıyorsunuz?)",
"kk": "(қате шығып жатыр ма?)"
},
"Proxy": {
"en": "Proxy",
"ru": "Прокси",
"zh": "代理",
"uk": "Проксі",
"fr": "Proxy",
"es": "Proxy",
"tr": "Proxy",
"kk": "Прокси"
},
"Authorization": {
"en": "Authorization",
"ru": "Вход в аккаунт",
"zh": "账户登录",
"uk": "Вхід в акаунт",
"fr": "Connexion au compte",
"es": "Inicio de sesión",
"tr": "Hesaba giriş",
"kk": "Аккаунтқа кіру"
},
"EmailCode": {
"en": "Email code",
"ru": "Код из письма",
"zh": "来自邮件的代码",
"uk": "Код з листа",
"fr": "Code email",
"es": "Código del correo",
"tr": "E-posta kodu",
"kk": "Email коды"
},
"PhoneNumber": {
"en": "Phone number",
"ru": "Номер телефона",
"zh": "电话号码",
"uk": "Номер телефону",
"fr": "Numéro de téléphone",
"es": "Número de teléfono",
"tr": "Telefon numarası",
"kk": "Телефон нөмірі"
},
"EmailLink": {
"en": "Email link",
"ru": "Ссылка из email",
"zh": "电子邮件中的链接",
"uk": "Посилання з email",
"fr": "Lien email",
"es": "Enlace del correo",
"tr": "E-posta bağlantısı",
"kk": "Email сілтемесі"
},
"SmsOrCode": {
"en": "SMS or code",
"ru": "СМС или код",
"zh": "短信验证码",
"uk": "СМС або код",
"fr": "SMS ou code",
"es": "SMS o código",
"tr": "SMS veya kod",
"kk": "SMS немесе код"
},
"Completed": {
"en": "Completed",
"ru": "Завершено",
"zh": "已完成",
"uk": "Завершено",
"fr": "Terminé",
"es": "Completado",
"tr": "Tamamlandı",
"kk": "Аяқталды"
},
"Message": {
"en": "Message:",
"ru": "Сообщение:",
"zh": "消息:",
"uk": "Повідомлення:",
"fr": "Message:",
"es": "Mensaje:",
"tr": "Mesaj:",
"kk": "Хабар:"
},
"ProceedButton": {
"en": "Proceed",
"ru": "Продолжить",
"zh": "继续",
"uk": "Продовжити",
"fr": "Continuer",
"es": "Continuar",
"tr": "Devam et",
"kk": "Жалғастыру"
},
"CancelButton": {
"en": "Cancel",
"ru": "Отмена",
"zh": "取消",
"uk": "Відміна",
"fr": "Annuler",
"es": "Cancelar",
"tr": "İptal",
"kk": "Бас тарту"
},
"MafileLinked": {
"en": "Mafile successfully linked",
"ru": "Мафайл успешно привязан",
"zh": "文件已成功绑定",
"uk": "Мафайл успішно прив'язано",
"fr": "Mafile lié avec succès",
"es": "Mafile vinculado correctamente",
"tr": "Mafile başarıyla bağlandı",
"kk": "Mafile сәтті байланыстырылды"
},
"FinishButton": {
"en": "Finish",
"ru": "Завершить",
"zh": "完成",
"uk": "Завершити",
"fr": "Terminer",
"es": "Finalizar",
"tr": "Bitir",
"kk": "Аяқтау"
},
"CopyRCodeButton": {
"en": "Copy RCode",
"ru": "Скопировать RCode",
"zh": "复制响应代码",
"uk": "Скопіювати RCode",
"fr": "Copier RCode",
"es": "Copiar RCode",
"tr": "RCode kopyala",
"kk": "RCode көшіру"
},
"Login": {
"en": "Login",
"ru": "Логин",
"zh": "登录",
"uk": "Логін",
"fr": "Identifiant",
"es": "Usuario",
"tr": "Giriş",
"kk": "Логин"
},
"Password": {
"en": "Password",
"ru": "Пароль",
"zh": "密码",
"uk": "Пароль",
"fr": "Mot de passe",
"es": "Contraseña",
"tr": "Şifre",
"kk": "Құпиясөз"
},
"Code": {
"en": "Code",
"ru": "Код",
"zh": "验证码",
"uk": "Код",
"fr": "Code",
"es": "Código",
"tr": "Kod",
"kk": "Код"
},
"SmsCode": {
"en": "SMS code",
"ru": "СМС код",
"zh": "短信验证码",
"uk": "СМС код",
"fr": "Code SMS",
"es": "Código SMS",
"tr": "SMS kodu",
"kk": "SMS коды"
}
}
}
@@ -0,0 +1,101 @@
{
"WaitLoginDialog": {
"Text": {
"en": "Login in progress. Please wait",
"ru": "Производится логин. Ожидайте",
"zh": "正在登录。请稍候...",
"uk": "Виконується вхід. Зачекайте",
"fr": "Connexion en cours. Veuillez patienter",
"es": "Inicio de sesión en curso. Por favor espere",
"tr": "Giriş yapılıyor. Lütfen bekleyin",
"kk": "Кіру орындалуда. Күте тұрыңыз"
}
},
"ConfirmCancelDialog": {
"Title": {
"ru": "Подтвердите действие",
"en": "Confirm action",
"zh": "确认操作",
"uk": "Підтвердіть дію",
"fr": "Confirmer l'action",
"es": "Confirmar acción",
"tr": "İşlemi onayla",
"kk": "Әрекетті растаңыз"
}
},
"TextFieldDialog": {
"Title": {
"ru": "Введите значение",
"en": "Enter value",
"zh": "请输入数值",
"uk": "Введіть значення",
"fr": "Entrer la valeur",
"es": "Introducir valor",
"tr": "Değer girin",
"kk": "Мән енгізіңіз"
}
},
"MafileImportDialog": {
"Title": {
"en": "Mafile import",
"ru": "Импорт мафайлов",
"zh": "导入 mafile",
"uk": "Імпорт mafile",
"fr": "Import de mafiles",
"es": "Importación de mafiles",
"tr": "Mafile içe aktarma",
"kk": "Mafile импорттау"
},
"TotalCount": {
"en": "Mafiles to import",
"ru": "Мафайлов к импорту",
"zh": "要导入的 mafile",
"uk": "Mafile до імпорту:",
"fr": "Mafiles à importer :",
"es": "Mafiles para importar:",
"tr": "İçe aktarılacak mafile:",
"kk": "Импортталатын mafile:"
},
"ConflictCount": {
"en": "Already exist",
"ru": "Уже существуют",
"zh": "已存在:",
"uk": "Уже існують:",
"fr": "Déjà existants :",
"es": "Ya existen:",
"tr": "Zaten mevcut:",
"kk": "Бұрыннан бар:"
},
"OverwriteConflicts": {
"en": "Overwrite conflicting mafiles",
"ru": "Перезаписать конфликтующие мафайлы",
"zh": "覆盖冲突的 mafile",
"uk": "Перезаписати конфліктні mafile",
"fr": "Remplacer les mafiles en conflit",
"es": "Sobrescribir mafiles en conflicto",
"tr": "Çakışan mafile'ların üzerine yaz",
"kk": "Қақтығысатын mafile файлдарын қайта жазу"
},
"AddToGroup": {
"en": "Add imported accounts to a group",
"ru": "Добавить импортированные аккаунты в группу",
"zh": "将导入的账号添加到组",
"uk": "Додати імпортовані акаунти до групи",
"fr": "Ajouter les comptes importés à un groupe",
"es": "Añadir las cuentas importadas a un grupo",
"tr": "İçe aktarılan hesapları bir gruba ekle",
"kk": "Импортталған аккаунттарды топқа қосу"
},
"GroupFieldHint": {
"en": "Existing or new group",
"ru": "Существующая или новая группа",
"zh": "现有或新组",
"uk": "Наявна або нова група",
"fr": "Groupe existant ou nouveau",
"es": "Grupo existente o nuevo",
"tr": "Mevcut veya yeni grup",
"kk": "Бар немесе жаңа топ"
}
}
}
@@ -0,0 +1,94 @@
{
"LoginAgainDialog": {
"Title": {
"en": "Login",
"ru": "Вход",
"zh": "登录",
"uk": "Вхід",
"fr": "Connexion",
"es": "Inicio de sesión",
"tr": "Giriş",
"kk": "Кіру"
},
"LoginFor": {
"en": "Login for",
"ru": "Войти в",
"zh": "登录为",
"uk": "Увійти в",
"fr": "Se connecter à",
"es": "Iniciar sesión en",
"tr": "Şunun için giriş",
"kk": "Мынаға кіру"
},
"LoginButton": {
"en": "Login",
"ru": "Войти",
"zh": "登录",
"uk": "Увійти",
"fr": "Se connecter",
"es": "Iniciar sesión",
"tr": "Giriş yap",
"kk": "Кіру"
},
"CancelButton": {
"en": "Cancel",
"ru": "Отмена",
"zh": "取消",
"uk": "Відміна",
"fr": "Annuler",
"es": "Cancelar",
"tr": "İptal",
"kk": "Болдырмау"
},
"PasswordBox": {
"en": "Password",
"ru": "Пароль",
"zh": "密码",
"uk": "Пароль",
"fr": "Mot de passe",
"es": "Contraseña",
"tr": "Parola",
"kk": "Құпиясөз"
},
"SaveEncryptedPassword": {
"en": "Save encrypted password to mafile",
"ru": "Сохранить зашифрованный пароль в мафайл",
"zh": "将加密密码保存到 mafile",
"uk": "Зберегти зашифрований пароль в мафайлі",
"fr": "Enregistrer le mot de passe chiffré dans le mafile",
"es": "Guardar contraseña cifrada en el mafile",
"tr": "Şifrelenmiş parolayı mafile içine kaydet",
"kk": "Шифрланған құпиясөзді mafile ішіне сақтау"
},
"UseMafileProxy": {
"en": "Use mafile proxy",
"ru": "Использовать прокси из мафайла",
"zh": "使用 mafile 代理",
"uk": "Використовувати проксі з мафайла",
"fr": "Utiliser le proxy du mafile",
"es": "Usar proxy del mafile",
"tr": "Mafile proxy kullan",
"kk": "Mafile проксиді қолдану"
},
"ProxyToolTip": {
"en": "Press 'DEL' to remove",
"ru": "Нажмите 'DEL' для удаления",
"zh": "按 'DEL' 键删除",
"uk": "Натисніть 'DEL' для видалення",
"fr": "Appuyez sur 'DEL' pour supprimer",
"es": "Pulsa 'DEL' para eliminar",
"tr": "Silmek için 'DEL' tuşuna bas",
"kk": "Жою үшін 'DEL' пернесін басыңыз"
},
"NoProxyWarning": {
"en": "No proxy is assigned",
"ru": "Прокси не установлен",
"zh": "未分配代理",
"uk": "Проксі не призначено",
"fr": "Aucun proxy assigné",
"es": "No hay ningún proxy asignado",
"tr": "Proxy atanmamış",
"kk": "Прокси тағайындалмаған"
}
}
}
@@ -0,0 +1,98 @@
{
"CodeBehind": {
"MafileMoverVM": {
"EnterGuardCode": {
"en": "Enter Steam Guard code or confirm login and click 'Proceed'",
"ru": "Введите код Steam Guard либо подтвердите вход и нажмите \"Продолжить\"",
"zh": "请输入令牌代码或确认登录,然后点击“继续”",
"uk": "Введіть код Steam Guard або підтвердіть вхід і натисніть 'Продовжити'",
"fr": "Entrez le code Steam Guard ou confirmez la connexion et cliquez sur 'Continuer'",
"es": "Introduce el código de Steam Guard o confirma el inicio de sesión y haz clic en 'Continuar'",
"tr": "Steam Guard kodunu girin veya girişi onaylayıp 'Devam' butonuna tıklayın",
"kk": "Steam Guard кодын енгізіңіз немесе кіруді растаңыз да 'Жалғастыру' түймесін басыңыз"
},
"EnterGuardCodeRetrying": {
"en": "Failed to confirm login. Enter Steam Guard code or try again",
"ru": "Не удалось подтвердить вход. Введите код Steam Guard или попробуйте еще раз",
"zh": "无法登录。请输入Steam令牌代码或重试",
"uk": "Не вдалося підтвердити вхід. Введіть код Steam Guard або спробуйте ще раз",
"fr": "Impossible de confirmer la connexion. Entrez le code Steam Guard ou réessayez",
"es": "No se pudo confirmar el inicio de sesión. Introduce el código de Steam Guard o inténtalo de nuevo",
"tr": "Giriş doğrulanamadı. Steam Guard kodunu girin veya tekrar deneyin",
"kk": "Кіруді растау мүмкін болмады. Steam Guard кодын енгізіңіз немесе қайтадан көріңіз"
},
"SmsCodeStepTip": {
"en": "Enter the code from the SMS sent to your phone number",
"ru": "Введите код из СМС, отправленный на ваш номер телефона",
"zh": "请输入发送到您手机号码的短信验证码",
"uk": "Введіть код з СМС, надісланий на ваш номер телефону",
"fr": "Entrez le code du SMS envoyé à votre numéro de téléphone",
"es": "Introduce el código del SMS enviado a tu número de teléfono",
"tr": "Telefon numaranıza gönderilen SMS kodunu girin",
"kk": "Телефон нөміріңізге жіберілген SMS кодын енгізіңіз"
},
"DoneStepTip": {
"ru": "RCode: {0}\r\nИмя файла: {1}.mafile",
"en": "RCode: {0}\r\nFile name: {1}.mafile",
"zh": "响应代码: {0}\r\n文件名: {1}.mafile",
"uk": "RCode: {0}\r\nІм'я файлу: {1}.mafile",
"fr": "RCode: {0}\r\nNom du fichier: {1}.mafile",
"es": "RCode: {0}\r\nNombre del archivo: {1}.mafile",
"tr": "RCode: {0}\r\nDosya adı: {1}.mafile",
"kk": "RCode: {0}\r\nФайл атауы: {1}.mafile"
},
"GuardIsNotActive": {
"ru": "Steam Guard НЕ установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"",
"en": "Steam Guard is NOT set on this account. To link mafile to the account, use the 'Link' window",
"zh": "此账户尚未启用令牌。要将 mafile 绑定到此账户,请使用“绑定”窗口。",
"uk": "Steam Guard НЕ встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'",
"fr": "Steam Guard n'est PAS configuré sur ce compte. Pour lier le mafile au compte, utilisez la fenêtre 'Lier'",
"es": "Steam Guard NO está activado en esta cuenta. Para vincular el mafile a la cuenta, usa la ventana 'Vincular'",
"tr": "Steam Guard bu hesapta etkin DEĞİL. Mafile'ı hesaba bağlamak için 'Bağla' penceresini kullanın",
"kk": "Бұл аккаунтта Steam Guard орнатылмаған. Mafile-ды аккаунтқа байланыстыру үшін 'Байланыстыру' терезесін пайдаланыңыз"
},
"SeemsNoPhoneNumber": {
"en": "The account has no phone number linked, returned Fail code (2)",
"ru": "На аккаунте не привязан номер телефона, был возвращен код Fail (2)",
"zh": "账户未绑定电话号码,返回代码失败 (2)",
"uk": "На акаунті не прив'язано номер телефону, повернуто код Fail (2)",
"fr": "Le compte n'a pas de numéro de téléphone lié, code Fail (2) retourné",
"es": "La cuenta no tiene número de teléfono vinculado, se devolvió el código Fail (2)",
"tr": "Hesaba bağlı telefon numarası yok, Fail kodu (2) döndü",
"kk": "Аккаунтқа телефон нөмірі байланыстырылмаған, Fail коды (2) қайтарылды"
},
"SmsCodeFailed": {
"en": "SMS code failed, try again",
"ru": "Ошибка SMS-кода, попробуйте еще раз",
"zh": "短信验证码错误,请再试一次",
"uk": "Помилка SMS-коду, спробуйте ще раз",
"fr": "Code SMS échoué, veuillez réessayer",
"es": "El código SMS falló, inténtalo de nuevo",
"tr": "SMS kodu başarısız, tekrar deneyin",
"kk": "SMS коды қате, қайтадан көріңіз"
}
}
},
"MafileMoverView": {
"Title": {
"en": "Move Steam Guard",
"ru": "Перенести Steam Guard",
"zh": "转移Steam手机令牌",
"uk": "Перенести Steam Guard",
"fr": "Déplacer Steam Guard",
"es": "Mover Steam Guard",
"tr": "Steam Guard'ı taşı",
"kk": "Steam Guard-ты көшіру"
},
"Done": {
"en": "Steam Guard moved",
"ru": "Steam Guard перенесен",
"zh": "Steam手机令牌已迁移",
"uk": "Steam Guard перенесено",
"fr": "Steam Guard déplacé",
"es": "Steam Guard movido",
"tr": "Steam Guard taşındı",
"kk": "Steam Guard көшірілді"
}
}
}
@@ -0,0 +1,208 @@
{
"CodeBehind": {
"ProxyManagerVM": {
"WrongFormatSomeIdsMissing": {
"ru": "Неверный формат. Некоторые прокси не имеют ID",
"en": "Wrong format. Some proxies have no ID",
"zh": "格式不正确。某些代理没有ID",
"uk": "Невірний формат. Деякі проксі не мають ID",
"fr": "Format incorrect. Certains proxies n'ont pas d'ID",
"es": "Formato incorrecto. Algunos proxies no tienen ID",
"tr": "Yanlış format. Bazı proxylerin ID'si yok",
"kk": "Қате формат. Кейбір проксилерде ID жоқ"
},
"DuplicateId": {
"ru": "Прокси с ID {0} добавлен несколько раз",
"en": "Proxy with ID {0} added multiple times",
"zh": "具有 ID {0} 的代理已被多次添加",
"uk": "Проксі з ID {0} додано декілька разів",
"fr": "Proxy avec ID {0} ajouté plusieurs fois",
"es": "El proxy con ID {0} fue añadido varias veces",
"tr": "ID {0} olan proxy birden fazla kez eklendi",
"kk": "{0} ID бар прокси бірнеше рет қосылды"
},
"WrongFormatOnLine": {
"ru": "Прокси на строке {0} имеет неверный формат.",
"en": "Proxy on line {0} has wrong format.",
"zh": "第 {0} 行的代理格式不正确。",
"uk": "Проксі на строці {0} має невірний формат.",
"fr": "Proxy sur la ligne {0} a un format incorrect.",
"es": "El proxy en la línea {0} tiene un formato incorrecto",
"tr": "{0}. satırdaki proxy yanlış formatta",
"kk": "{0} жолындағы прокси қате форматта"
},
"WrongFormat": {
"ru": "Прокси имеет неверный формат.",
"en": "Proxy has wrong format.",
"zh": "代理格式不正确。",
"uk": "Проксі має невірний формат.",
"fr": "Format incorrect du proxy.",
"es": "El proxy tiene un formato incorrecto",
"tr": "Proxy yanlış formatta",
"kk": "Прокси қате форматта"
},
"AssignmentSuccess": {
"ru": "Прокси назначены: {0}",
"en": "Assigned: {0}",
"zh": "已分配:{0}",
"uk": "Проксі призначено: {0}",
"fr": "Attribués: {0}",
"es": "Asignados: {0}",
"tr": "Atandı: {0}",
"kk": "Тағайындалды: {0}"
},
"AssignmentPartialSuccess": {
"ru": "Назначено: {0}, не найдено: {1}, ошибки: {2}",
"en": "Assigned: {0}, not found: {1}, errors: {2}",
"zh": "已分配:{0},未找到:{1},错误:{2}",
"uk": "Призначено: {0}, не знайдено: {1}, помилки: {2}",
"fr": "Attribués: {0}, non trouvés: {1}, erreurs: {2}",
"es": "Asignados: {0}, no encontrados: {1}, errores: {2}",
"tr": "Atandı: {0}, bulunamadı: {1}, hatalar: {2}",
"kk": "Тағайындалды: {0}, табылмады: {1}, қателер: {2}"
}
}
},
"ProxyManagerDialog": {
"Title": {
"en": "Proxy manager",
"ru": "Менеджер прокси",
"zh": "代理管理器",
"uk": "Менеджер проксі",
"fr": "Gestionnaire de proxy",
"es": "Administrador de proxy",
"tr": "Proxy yöneticisi",
"kk": "Прокси менеджері"
},
"DefaultProxy": {
"en": "Default proxy:",
"ru": "Прокси по умолчанию:",
"zh": "默认代理:",
"uk": "Проксі за замовчуванням:",
"fr": "Proxy par défaut:",
"es": "Proxy predeterminado:",
"tr": "Varsayılan proxy:",
"kk": "Әдепкі прокси:"
},
"UseRandomDefaultProxy": {
"en": "Use random default proxy",
"ru": "Cлучайный прокси по умолчанию",
"zh": "默认随机代理",
"uk": "Випадковий проксі за замовчуванням",
"fr": "Proxy aléatoire par défaut",
"es": "Usar proxy predeterminado aleatorio",
"tr": "Rastgele varsayılan proxy kullan",
"kk": "Кездейсоқ әдепкі проксиді қолдану"
},
"DisplayProxyProtocol": {
"en": "Display protocol",
"ru": "Отображать протокол",
"zh": "显示协议",
"uk": "Відображати протокол",
"fr": "Afficher le protocole",
"es": "Mostrar protocolo",
"tr": "Protokolü göster",
"kk": "Протоколды көрсету"
},
"DisplayProxyCredentials": {
"en": "Display credentials",
"ru": "Отображать логин и пароль",
"zh": "显示凭证",
"uk": "Відображати логін та пароль",
"fr": "Afficher le login et le mot de passe",
"es": "Mostrar credenciales",
"tr": "Kimlik bilgilerini göster",
"kk": "Тіркелгі деректерін көрсету"
},
"TabManager": {
"en": "Manager",
"ru": "Менеджер",
"zh": "管理",
"uk": "Менеджер",
"fr": "Gestionnaire",
"es": "Administrador",
"tr": "Yönetici",
"kk": "Менеджер"
},
"TabAssignment": {
"en": "Assignment",
"ru": "Назначение",
"zh": "分配",
"uk": "Призначення",
"fr": "Attribution",
"es": "Asignación",
"tr": "Atama",
"kk": "Тағайындау"
},
"TotalAccounts": {
"en": "Accounts",
"ru": "Аккаунтов",
"zh": "账户",
"uk": "Акаунтів",
"fr": "Comptes",
"es": "Cuentas",
"tr": "Hesaplar",
"kk": "Аккаунттар"
},
"TotalProxies": {
"en": "Proxies",
"ru": "Прокси",
"zh": "代理",
"uk": "Проксі",
"fr": "Proxies",
"es": "Proxies",
"tr": "Proxy'ler",
"kk": "Проксилер"
},
"AssignedProxies": {
"en": "Assigned",
"ru": "Назначено",
"zh": "已分配",
"uk": "Призначено",
"fr": "Attribués",
"es": "Asignados",
"tr": "Atandı",
"kk": "Тағайындалды"
},
"UnspecifiedBehavior": {
"en": "Unspecified proxy:",
"ru": "Не указанный прокси:",
"zh": "未指定代理:",
"uk": "Не вказаний проксі:",
"fr": "Proxy non spécifié:",
"es": "Proxy no especificado:",
"tr": "Belirtilmemiş proxy:",
"kk": "Көрсетілмеген прокси:"
},
"UnspecifiedRandom": {
"en": "Assign random",
"ru": "Назначить случайный",
"zh": "随机分配",
"uk": "Призначити випадковий",
"fr": "Attribuer aléatoirement",
"es": "Asignar aleatorio",
"tr": "Rastgele ata",
"kk": "Кездейсоқ тағайындау"
},
"UnspecifiedRemove": {
"en": "Remove proxy",
"ru": "Удалить прокси",
"zh": "删除代理",
"uk": "Видалити проксі",
"fr": "Supprimer le proxy",
"es": "Eliminar proxy",
"tr": "Proxy'yi kaldır",
"kk": "Проксиді жою"
},
"ApplyAssignment": {
"en": "Apply",
"ru": "Применить",
"zh": "应用",
"uk": "Застосувати",
"fr": "Appliquer",
"es": "Aplicar",
"tr": "Uygula",
"kk": "Қолдану"
}
}
}
@@ -0,0 +1,64 @@
{
"SdaPasswordDialog": {
"Title": {
"en": "SDA encryption password",
"ru": "Пароль шифрования SDA",
"zh": "SDA 加密密码",
"uk": "Пароль шифрування SDA",
"fr": "Mot de passe de chiffrement SDA",
"es": "Contraseña de cifrado SDA",
"tr": "SDA şifreleme parolası",
"kk": "SDA шифрлау құпиясөзі"
},
"Message": {
"en": "This mafile is encrypted with Steam Desktop Authenticator. Enter your SDA encryption password to import it.",
"ru": "Этот mafile зашифрован Steam Desktop Authenticator. Введите пароль шифрования SDA для импорта.",
"zh": "此 mafile 由 Steam Desktop Authenticator 加密。请输入 SDA 加密密码以导入。",
"uk": "Цей mafile зашифровано Steam Desktop Authenticator. Введіть пароль шифрування SDA для імпорту.",
"fr": "Ce mafile est chiffré avec Steam Desktop Authenticator. Entrez votre mot de passe SDA pour l'importer.",
"es": "Este mafile está cifrado con Steam Desktop Authenticator. Introduce tu contraseña SDA para importarlo.",
"tr": "Bu mafile Steam Desktop Authenticator ile şifrelenmiştir. İçe aktarmak için SDA şifreleme parolanızı girin.",
"kk": "Бұл mafile Steam Desktop Authenticator арқылы шифрланған. Импорттау үшін SDA шифрлау құпиясөзін енгізіңіз."
},
"PasswordHint": {
"en": "SDA encryption password",
"ru": "Пароль шифрования SDA",
"zh": "SDA 加密密码",
"uk": "Пароль шифрування SDA",
"fr": "Mot de passe de chiffrement SDA",
"es": "Contraseña de cifrado SDA",
"tr": "SDA şifreleme parolası",
"kk": "SDA шифрлау құпиясөзі"
},
"WrongPassword": {
"en": "Wrong SDA password. Please try again.",
"ru": "Неверный пароль SDA. Попробуйте снова.",
"zh": "SDA 密码错误。请重试。",
"uk": "Невірний пароль SDA. Спробуйте ще раз.",
"fr": "Mauvais mot de passe SDA. Veuillez réessayer.",
"es": "Contraseña SDA incorrecta. Inténtalo de nuevo.",
"tr": "Yanlış SDA parolası. Lütfen tekrar deneyin.",
"kk": "SDA құпиясөзі қате. Қайтадан көріңіз."
},
"ManifestMissing": {
"en": "This mafile looks like it was encrypted by SDA, but manifest.json was not found. Please select your SDA manifest.json to continue.",
"ru": "Похоже, этот mafile зашифрован SDA, но manifest.json не найден. Выберите manifest.json от SDA, чтобы продолжить.",
"zh": "此 mafile 看起来由 SDA 加密,但未找到 manifest.json。请选择 SDA 的 manifest.json 以继续。",
"uk": "Схоже, цей mafile зашифровано SDA, але manifest.json не знайдено. Виберіть manifest.json від SDA, щоб продовжити.",
"fr": "Ce mafile semble chiffré par SDA, mais manifest.json est introuvable. Sélectionnez manifest.json de SDA pour continuer.",
"es": "Este mafile parece estar cifrado por SDA, pero no se encontró manifest.json. Selecciona tu manifest.json de SDA para continuar.",
"tr": "Bu mafile SDA tarafından şifrelenmiş gibi görünüyor ancak manifest.json bulunamadı. Devam etmek için SDA manifest.json dosyanızı seçin.",
"kk": "Бұл mafile SDA арқылы шифрланған сияқты, бірақ manifest.json табылмады. Жалғастыру үшін SDA manifest.json файлын таңдаңыз."
},
"ManifestEntryNotFound": {
"en": "Selected manifest.json does not contain an entry for this mafile.",
"ru": "Выбранный manifest.json не содержит запись для этого mafile.",
"zh": "所选的 manifest.json 不包含此 mafile 的条目。",
"uk": "Вибраний manifest.json не містить запису для цього mafile.",
"fr": "Le manifest.json sélectionné ne contient pas d'entrée pour ce mafile.",
"es": "El manifest.json seleccionado no contiene una entrada para este mafile.",
"tr": "Seçilen manifest.json bu mafile için bir kayıt içermiyor.",
"kk": "Таңдалған manifest.json бұл mafile үшін жазба қамтымайды."
}
}
}
@@ -0,0 +1,88 @@
{
"CodeBehind": {
"SetAccountPasswordsVM": {
"Success": {
"en": "Passwords successfully set for {0} mafiles",
"ru": "Пароли успешно установлены для {0} мафайлов",
"zh": "已成功为 {0} 个文件设置密码",
"uk": "Паролі успішно встановлені для {0} мафайлів",
"fr": "Les mots de passe ont été définis avec succès pour {0} mafiles",
"es": "Contraseñas establecidas correctamente para {0} mafiles",
"tr": "{0} mafile için parolalar başarıyla ayarlandı",
"kk": "{0} mafile үшін құпиясөздер сәтті орнатылды"
},
"PartialSuccess": {
"en": "Success: {0}, Not found: {1}, Errors: {2}",
"ru": "Успешно: {0}, Не найдено: {1}, Ошибки: {2}",
"zh": "成功:{0},未找到:{1},错误:{2}",
"uk": "Успішно: {0}, Не знайдено: {1}, Помилки: {2}",
"fr": "Succès: {0}, Non trouvé: {1}, Erreurs: {2}",
"es": "Éxito: {0}, No encontrado: {1}, Errores: {2}",
"tr": "Başarılı: {0}, Bulunamadı: {1}, Hatalar: {2}",
"kk": "Сәтті: {0}, Табылмады: {1}, Қателер: {2}"
}
}
},
"SetAccountPasswordsView": {
"Title": {
"en": "Set passwords",
"ru": "Назначить пароли",
"zh": "设置密码",
"uk": "Призначити паролі",
"fr": "Définir les mots de passe",
"es": "Establecer contraseñas",
"tr": "Parolaları ayarla",
"kk": "Құпиясөздерді орнату"
},
"EncryptionPassword": {
"en": "Encryption password:",
"ru": "Пароль шифрования:",
"zh": "加密密码:",
"uk": "Пароль шифрування:",
"fr": "Mot de passe de chiffrement:",
"es": "Contraseña de cifrado:",
"tr": "Şifreleme parolası:",
"kk": "Шифрлау құпиясөзі:"
},
"EnterPassword": {
"en": "Enter password",
"ru": "Введите пароль",
"zh": "输入密码",
"uk": "Введіть пароль",
"fr": "Entrer le mot de passe",
"es": "Introducir contraseña",
"tr": "Parolayı gir",
"kk": "Құпиясөзді енгізіңіз"
},
"Status": {
"en": "Status:",
"ru": "Статус:",
"zh": "状态:",
"uk": "Статус:",
"fr": "Statut:",
"es": "Estado:",
"tr": "Durum:",
"kk": "Күйі:"
},
"EncryptionHint": {
"en": "All passwords in mafiles will be encrypted using the encryption password.",
"ru": "Все пароли в мафайлах будут зашифрованы с помощью пароля шифрования.",
"zh": "mafiles 中的所有密码都将使用加密密码加密。",
"uk": "Всі паролі в мафайлах будуть зашифровані за допомогою пароля шифрування.",
"fr": "Tous les mots de passe dans les mafiles seront chiffrés à l'aide du mot de passe de chiffrement.",
"es": "Todas las contraseñas en los mafiles serán cifradas usando la contraseña de cifrado.",
"tr": "Mafile içindeki tüm parolalar şifreleme parolası kullanılarak şifrelenecektir.",
"kk": "Mafile ішіндегі барлық құпиясөздер шифрлау құпиясөзі арқылы шифрланады."
},
"InputFormat": {
"en": "Supported formats:\r\n\r\nlogin:password\r\nsteamid:password",
"ru": "Поддерживаемые форматы:\r\n\r\nlogin:password\r\nsteamid:password",
"zh": "支持的格式:\r\n\r\n登录名:密码\r\nSteamID:密码",
"uk": "Підтримувані формати:\r\n\r\nlogin:password\r\nsteamid:password",
"fr": "Formats pris en charge:\r\n\r\nlogin:password\r\nsteamid:password",
"es": "Formatos compatibles:\r\n\r\nlogin:password\r\nsteamid:password",
"tr": "Desteklenen formatlar:\r\n\r\nlogin:password\r\nsteamid:password",
"kk": "Қолдау көрсетілетін форматтар:\r\n\r\nlogin:password\r\nsteamid:password"
}
}
}
@@ -0,0 +1,336 @@
{
"CodeBehind": {
"SettingsVM": {
"MafileRenaming": {
"Error": {
"en": "Error while renaming mafiles. It was saved in log",
"ru": "Ошибка при переименовании мафайлов. Она была сохранена в log",
"zh": "重命名 mafiles 时出错。已记录在日志中",
"uk": "Помилка при перейменуванні мафайлів. Вона була збережена в log",
"fr": "Erreur lors du renommage des mafiles. Détails enregistrés dans le fichier log",
"es": "Error al renombrar los mafiles. Se guardó en el log",
"tr": "Mafile yeniden adlandırılırken hata oluştu. Log dosyasına kaydedildi",
"kk": "Mafile атауын өзгерту кезінде қате болды. Ол log файлына сақталды"
},
"AllRenamed": {
"ru": "Все мафайлы успешно переименованы ({0}).\r\nБыла создана резервная копия: {1}",
"en": "All mafiles successfully renamed ({0}).\r\nBackup was created: {1}",
"zh": "所有文件已成功重命名({0})。\r\n备份已创建:{1}",
"uk": "Всі мафайли успішно перейменовані ({0}).\r\nБуло створено резервну копію: {1}",
"fr": "Tous les mafiles ont été renommés avec succès ({0}).\r\nUn backup a été créé: {1}",
"es": "Todos los mafiles se renombraron correctamente ({0}).\r\nSe creó una copia de seguridad: {1}",
"tr": "Tüm mafile'lar başarıyla yeniden adlandırıldı ({0}).\r\nYedek oluşturuldu: {1}",
"kk": "Барлық mafile сәтті қайта аталды ({0}).\r\nРезервтік көшірме жасалды: {1}"
},
"PartialSuccess": {
"en": "Total mafiles: {0}\r\nRenamed: {1}\r\nConflicts: {2}\r\nErrors: {3}\r\n\r\nBackup of mafiles saved in file: {4}",
"ru": "Всего мафайлов: {0}\r\nПереименовано: {1}\r\nКонфликты: {2}\r\nОшибок: {3}\r\n\r\nРезервная копия мафайлов сохранена в файле: {4}",
"zh": "总文件数: {0}\r\n重命名: {1}\r\n冲突: {2}\r\n错误: {3}\r\n\r\n文件备份已保存至: {4}",
"uk": "Всього мафайлів: {0}\r\nПерейменовано: {1}\r\nКонфлікти: {2}\r\nПомилок: {3}\r\n\r\nРезервна копія мафайлів збережена у файлі: {4}",
"fr": "Total des mafiles: {0}\r\nRenommés: {1}\r\nConflits: {2}\r\nErreurs: {3}\r\n\r\nBackup des mafiles enregistré dans le fichier: {4}",
"es": "Total de mafiles: {0}\r\nRenombrados: {1}\r\nConflictos: {2}\r\nErrores: {3}\r\n\r\nCopia de seguridad guardada en el archivo: {4}",
"tr": "Toplam mafile: {0}\r\nYeniden adlandırıldı: {1}\r\nÇakışmalar: {2}\r\nHatalar: {3}\r\n\r\nMafile yedeği dosyaya kaydedildi: {4}",
"kk": "Барлық mafile саны: {0}\r\nҚайта аталғаны: {1}\r\nҚайшылықтар: {2}\r\nҚателер: {3}\r\n\r\nMafile резервтік көшірмесі файлға сақталды: {4}"
}
}
}
},
"SettingsDialog": {
"Title": {
"en": "Settings",
"ru": "Настройки",
"zh": "设置",
"uk": "Налаштування",
"fr": "Paramètres",
"es": "Configuración",
"tr": "Ayarlar",
"kk": "Баптаулар"
},
"MainSettings": {
"en": "Main",
"ru": "Основные",
"zh": "主要",
"uk": "Основні",
"fr": "Général",
"es": "Principal",
"tr": "Genel",
"kk": "Негізгі"
},
"ThemeSettings": {
"en": "Theme",
"ru": "Тема",
"zh": "主题",
"uk": "Тема",
"fr": "Thème",
"es": "Tema",
"tr": "Tema",
"kk": "Тақырып"
},
"Theme": {
"BackgroundBlur": {
"en": "Background Blur",
"ru": "Размытие фона",
"zh": "背景模糊",
"uk": "Розмиття фону",
"fr": "Flou de l'arrière-plan",
"es": "Desenfoque de fondo",
"tr": "Arka plan bulanıklığı",
"kk": "Артқы фон бұлыңғырлығы"
},
"BackgroundOpacity": {
"en": "Background Opacity",
"ru": "Непрозрачность фона",
"zh": "背景不透明度",
"uk": "Непрозорість фону",
"fr": "Opacité de l'arrière-plan",
"es": "Opacidad del fondo",
"tr": "Arka plan opaklığı",
"kk": "Артқы фон мөлдірлігі"
},
"Gamma": {
"en": "Gamma",
"ru": "Гамма",
"zh": "伽马",
"uk": "Гама",
"fr": "Gamma",
"es": "Gamma",
"tr": "Gamma",
"kk": "Гамма"
},
"LeftOpacity": {
"en": "Left Opacity",
"ru": "Непрозрачность слева",
"zh": "左透明度",
"uk": "Непрозорість зліва",
"fr": "Opacité à gauche",
"es": "Opacidad izquierda",
"tr": "Sol opaklık",
"kk": "Сол жақ мөлдірлігі"
},
"RightOpacity": {
"en": "Right Opacity",
"ru": "Непрозрачность справа",
"zh": "右透明度",
"uk": "Непрозорість справа",
"fr": "Opacité à droite",
"es": "Opacidad derecha",
"tr": "Sağ opaklık",
"kk": "Оң жақ мөлдірлігі"
},
"CurrentTheme": {
"en": "Current Theme",
"ru": "Текущая тема",
"zh": "当前主题",
"uk": "Поточна тема",
"fr": "Thème actuel",
"es": "Tema actual",
"tr": "Geçerli tema",
"kk": "Ағымдағы тақырып"
},
"DialogBlur": {
"en": "Blur on Dialog",
"ru": "Размытие при открытии диалог. окна",
"zh": "打开对话窗口时的模糊效果",
"uk": "Розмиття при відкритті діалог. вікна",
"fr": "Flou derrière la boîte de dialogue",
"es": "Desenfoque en diálogo",
"tr": "Diyalog açıldığında bulanıklık",
"kk": "Диалог ашылғанда бұлыңғырлау"
},
"RippleDisabled": {
"en": "Disable ripple animations",
"ru": "Отключить анимации «ripple»",
"zh": "禁用波纹动画",
"uk": "Вимкнути анімації «ripple»",
"fr": "Désactiver les animations ripple",
"es": "Desactivar animaciones ripple",
"tr": "Ripple animasyonlarını kapat",
"kk": "Ripple анимацияларын өшіру"
},
"Reset": {
"en": "Reset",
"ru": "Сбросить",
"zh": "重置",
"uk": "Скинути",
"fr": "Réinitialiser",
"es": "Restablecer",
"tr": "Sıfırla",
"kk": "Қалпына келтіру"
}
},
"BackgroundHint": {
"en": "Background",
"ru": "Фон",
"zh": "背景",
"uk": "Фон",
"fr": "Arrière-plan",
"es": "Fondo",
"tr": "Arka plan",
"kk": "Фон"
},
"BackgroundMode": {
"Default": {
"en": "Default",
"ru": "По умолчанию",
"zh": "默认",
"uk": "За замовчуванням",
"fr": "Par défaut",
"es": "Predeterminado",
"tr": "Varsayılan",
"kk": "Әдепкі"
},
"Custom": {
"en": "Custom",
"ru": "Свой",
"zh": "自定义",
"uk": "Свій",
"fr": "Personnalisé",
"es": "Personalizado",
"tr": "Özel",
"kk": "Жеке"
},
"NoBackground": {
"en": "No background",
"ru": "Без фона",
"zh": "无背景",
"uk": "Без фону",
"fr": "Aucun fond",
"es": "Sin fondo",
"tr": "Arka plan yok",
"kk": "Фон жоқ"
}
},
"MinimizeToTray": {
"en": "Minimize to tray",
"ru": "Сворачивать в трей",
"zh": "最小化到托盘",
"uk": "Згортати в трей",
"fr": "Minimiser dans la barre des tâches",
"es": "Minimizar a la bandeja",
"tr": "Tepsiye küçült",
"kk": "Трейге кішірейту"
},
"UseIndicator": {
"en": "Use color indicator in taskbar",
"ru": "Цветной индикатор в панели задач",
"zh": "在任务栏中使用颜色指示器",
"uk": "Кольоровий індикатор в панелі завдань",
"fr": "Utiliser un indicateur de couleur dans la barre des tâches",
"es": "Usar indicador de color en la barra de tareas",
"tr": "Görev çubuğunda renk göstergesi kullan",
"kk": "Тапсырмалар жолағында түс индикаторын қолдану"
},
"UseIndicatorHint": {
"en": "Small color indicator in windows toolbar to identify program instance",
"ru": "Маленький цветной индикатор в панели задач для идентификации экземпляра программы",
"zh": "Windows 工具栏中的小颜色指示器,用于识别程序实例",
"uk": "Малий кольоровий індикатор в панелі завдань для ідентифікації екземпляра програми",
"fr": "Petit indicateur de couleur dans la barre des tâches pour identifier l'instance du programme",
"es": "Pequeño indicador de color en la barra de tareas para identificar la instancia del programa",
"tr": "Program örneğini tanımlamak için görev çubuğunda küçük renk göstergesi",
"kk": "Бағдарлама данасын анықтау үшін тапсырмалар жолағындағы шағын түс индикаторы"
},
"UseCustomColor": {
"en": "Custom window color",
"ru": "Свой цвет окна",
"zh": "自定义窗口颜色",
"uk": "Свій колір вікна",
"fr": "Couleur personnalisée de la fenêtre",
"es": "Color de ventana personalizado",
"tr": "Özel pencere rengi",
"kk": "Терезенің жеке түсі"
},
"PasswordBox": {
"CurrentCryptPassword": {
"en": "Current encryption password",
"ru": "Текущий пароль шифрования",
"zh": "当前加密密码",
"uk": "Поточний пароль шифрування",
"fr": "Mot de passe de chiffrement actuel",
"es": "Contraseña de cifrado actual",
"tr": "Geçerli şifreleme parolası",
"kk": "Ағымдағы шифрлау құпиясөзі"
},
"Hint": {
"en": "Not saved to disk",
"ru": "Не сохраняется в системе",
"zh": "未保存到磁盘",
"uk": "Не зберігається у системі",
"fr": "Non sauvegardé sur le disque",
"es": "No se guarda en el disco",
"tr": "Diske kaydedilmez",
"kk": "Дискке сақталмайды"
}
},
"ConfirmMafileImport": {
"en": "Ask confirmation when importing mafiles",
"ru": "Спрашивать подтверждение при импорте мафайлов",
"zh": "导入 mafile 时请求确认",
"uk": "Запитувати підтвердження під час імпорту mafile",
"fr": "Demander confirmation lors de l'import des mafiles",
"es": "Pedir confirmación al importar mafiles",
"tr": "Mafile içe aktarırken onay sor",
"kk": "Mafile импорттау кезінде растауды сұрау"
},
"ConfirmMafileImportHint": {
"en": "Allows selecting a group when importing maFiles. Does not affect showing the dialog when file conflicts occur",
"ru": "Позволяет выбрать группу при импорте maFiles. Не влияет на показ окна при конфликте файлов",
"zh": "允许在导入 maFiles 时选择分组。不影响发生文件冲突时窗口的显示",
"uk": "Дозволяє вибрати групу під час імпорту maFiles. Не впливає на показ вікна при конфлікті файлів",
"fr": "Permet de choisir un groupe lors de l'import des maFiles. N'affecte pas l'affichage de la fenêtre en cas de conflit de fichiers",
"es": "Permite seleccionar un grupo al importar maFiles. No afecta a la aparición de la ventana cuando hay conflictos de archivos",
"tr": "maFiles içe aktarılırken grup seçilmesine izin verir. Dosya çakışmalarında pencerenin gösterilmesini etkilemez",
"kk": "maFiles импорттау кезінде топ таңдауға мүмкіндік береді. Файл қайшылықтары болғанда терезенің көрсетілуіне әсер етпейді"
},
"AllowAutoUpdate": {
"en": "Allow auto update",
"ru": "Разрешить автообновление",
"zh": "允许自动更新",
"uk": "Дозволити автооновлення",
"fr": "Autoriser la mise à jour auto",
"es": "Permitir actualización automática",
"tr": "Otomatik güncellemeye izin ver",
"kk": "Авто жаңартуға рұқсат беру"
},
"MafileNamingMode": {
"en": "Mafile naming mode",
"ru": "Режим именования мафайлов",
"zh": "Mafile 命名模式",
"uk": "Режим іменування мафайлів",
"fr": "Format du nommage des mafiles",
"es": "Modo de nombre de mafile",
"tr": "Mafile adlandırma modu",
"kk": "Mafile атау режимі"
},
"ApplyNamingMode": {
"en": "Apply and rename",
"ru": "Применить и переименовать",
"zh": "应用并重命名",
"uk": "Застосувати та перейменувати",
"fr": "Appliquer et renommer",
"es": "Aplicar y renombrar",
"tr": "Uygula ve yeniden adlandır",
"kk": "Қолдану және қайта атау"
},
"IgnorePatchTuesdayErrors": {
"en": "Ignore Patch Tuesday errors in timer (exp.)",
"ru": "Игнорировать ошибки Patch Tuesday в таймере (эксп.)",
"zh": "忽略 Patch Tuesday 中计时器的错误(实验性",
"uk": "Ігнорувати помилки Patch Tuesday у таймері (експ.)",
"fr": "Ignorer les erreurs dues à la maintenance Steam du mardi soir dans le timer (exp.)",
"es": "Ignorar errores de Patch Tuesday en el temporizador (exp.)",
"tr": "Patch Tuesday zamanlayıcı hatalarını yok say (deneysel)",
"kk": "Patch Tuesday таймер қателерін елемеу (эксп.)"
},
"IgnorePatchTuesdayErrorsHint": {
"en": "Ignore errors that occur every Tuesday (Wednesday night in GMT) when Steam doing technical works and auto-confirm timers turns off unnecessarily",
"ru": "Игнорировать ошибки, которые возникают каждый вторник (ночь среды по GMT) когда Steam проводит технические работы и автоподтверждения отключаются без надобности",
"zh": "忽略每周三(格林尼治标准时间周三夜间)发生的错误,因为 Steam 进行技术维护时自动确认功能会被无故关闭",
"uk": "Ігнорувати помилки, які виникають щосереди (ніч середи за GMT) коли Steam проводить технічні роботи і автопідтвердження вимикаються без потреби",
"fr": "Ignorer les erreurs dues à la maintenance Steam du mardi soir (Mercredi à minuit en GMT)",
"es": "Ignorar errores que ocurren cada martes (miércoles por la noche GMT) cuando Steam realiza mantenimiento técnico y los temporizadores de auto-confirmación se desactivan innecesariamente",
"tr": "Steam teknik bakım yaparken her Salı (GMT Çarşamba gecesi) oluşan ve otomatik onay zamanlayıcılarını gereksiz kapatan hataları yok say",
"kk": "Steam техникалық жұмыстар жүргізген кезде әр сейсенбіде (GMT бойынша сәрсенбі түні) пайда болатын және авторастау таймерлерін қажетсіз өшіретін қателерді елемеу"
}
}
}
@@ -0,0 +1,116 @@
{
"UpdateDialog": {
"Title": {
"en": "New version available:",
"ru": "Доступна новая версия:",
"zh": "有新版本可用:",
"uk": "Доступна нова версія:",
"fr": "Nouvelle version disponible :",
"es": "Nueva versión disponible:",
"tr": "Yeni sürüm mevcut:",
"kk": "Жаңа нұсқа қолжетімді:"
},
"Changelog": {
"en": "What's new",
"ru": "Что нового",
"zh": "更新内容",
"uk": "Що нового",
"fr": "Quoi de neuf",
"es": "Novedades",
"tr": "Yenilikler",
"kk": "Не жаңалық"
},
"NoChangelog": {
"en": "No changelog available",
"ru": "Нет информации об изменениях",
"zh": "暂无更新日志",
"uk": "Немає інформації про зміни",
"fr": "Aucun journal des modifications disponible",
"es": "No hay registro de cambios disponible",
"tr": "Değişiklik listesi mevcut değil",
"kk": "Өзгерістер тізімі жоқ"
},
"UpdateNow": {
"en": "Update now",
"ru": "Обновить сейчас",
"zh": "立即更新",
"uk": "Оновити зараз",
"fr": "Mettre à jour maintenant",
"es": "Actualizar ahora",
"tr": "Şimdi güncelle",
"kk": "Қазір жаңарту"
},
"RemindLater": {
"en": "Remind later",
"ru": "Напомнить позже",
"zh": "稍后提醒",
"uk": "Нагадати пізніше",
"fr": "Rappeler plus tard",
"es": "Recordar más tarde",
"tr": "Daha sonra hatırlat",
"kk": "Кейін еске салу"
},
"SkipVersion": {
"en": "Skip version",
"ru": "Пропустить версию",
"zh": "跳过此版本",
"uk": "Пропустити версію",
"fr": "Ignorer cette version",
"es": "Omitir versión",
"tr": "Sürümü atla",
"kk": "Бұл нұсқаны өткізіп жіберу"
},
"RemindAfter": {
"en": "Remind after:",
"ru": "Напомнить через:",
"zh": "提醒时间:",
"uk": "Нагадати через:",
"fr": "Rappeler après :",
"es": "Recordar después de:",
"tr": "Şu süreden sonra hatırlat:",
"kk": "Кейін еске салу:"
},
"RemindOptions": {
"1Hour": {
"en": "1 hour",
"ru": "1 час",
"zh": "1 小时",
"uk": "1 година",
"fr": "1 heure",
"es": "1 hora",
"tr": "1 saat",
"kk": "1 сағат"
},
"1Day": {
"en": "1 day",
"ru": "1 день",
"zh": "1 天",
"uk": "1 день",
"fr": "1 jour",
"es": "1 día",
"tr": "1 gün",
"kk": "1 күн"
},
"3Days": {
"en": "3 days",
"ru": "3 дня",
"zh": "3 天",
"uk": "3 дні",
"fr": "3 jours",
"es": "3 días",
"tr": "3 gün",
"kk": "3 күн"
},
"7Days": {
"en": "7 days",
"ru": "7 дней",
"zh": "7 天",
"uk": "7 днів",
"fr": "7 jours",
"es": "7 días",
"tr": "7 gün",
"kk": "7 күн"
}
}
}
}
+160
View File
@@ -0,0 +1,160 @@
{
"LanguageName": {
"en": "English",
"ru": "Русский",
"zh": "简体中文",
"uk": "Українська",
"fr": "Français",
"es": "Español",
"tr": "Türkçe",
"kk": "Қазақша"
},
"LanguageWord": {
"en": "Language/语言",
"ru": "Language/语言",
"zh": "Language/语言",
"uk": "Language/语言",
"fr": "Language/语言",
"es": "Language/语言",
"tr": "Language/语言",
"kk": "Language/语言"
},
"Common": {
"No": {
"en": "No",
"ru": "Нет",
"zh": "否",
"uk": "Ні",
"fr": "Non",
"es": "No",
"tr": "Hayır",
"kk": "Жоқ"
},
"Yes": {
"en": "Yes",
"ru": "Да",
"zh": "是",
"uk": "Так",
"fr": "Oui",
"es": "Sí",
"tr": "Evet",
"kk": "Иә"
},
"Error": {
"en": "Error",
"ru": "Ошибка",
"zh": "错误",
"uk": "Помилка",
"fr": "Erreur",
"es": "Error",
"tr": "Hata",
"kk": "Қате"
},
"RequestError": {
"en": "Request error",
"ru": "Ошибка запроса",
"zh": "请求错误",
"uk": "Помилка запиту",
"fr": "Erreur de requête",
"es": "Error de solicitud",
"tr": "İstek hatası",
"kk": "Сұраныс қатесі"
},
"Canceled": {
"en": "Canceled",
"ru": "Отменено",
"zh": "已取消",
"uk": "Скасовано",
"fr": "Annulé",
"es": "Cancelado",
"tr": "İptal edildi",
"kk": "Болдырылмады"
},
"Proxy": {
"en": "Proxy",
"ru": "Прокси",
"zh": "代理",
"uk": "Проксі",
"fr": "Proxy",
"es": "Proxy",
"tr": "Proxy",
"kk": "Прокси"
},
"Delete": {
"en": "Delete",
"ru": "Удалить",
"zh": "删除",
"uk": "Видалити",
"fr": "Supprimer",
"es": "Eliminar",
"tr": "Sil",
"kk": "Жою"
},
"Cancel": {
"en": "Cancel",
"ru": "Отмена",
"zh": "取消",
"uk": "Скасувати",
"fr": "Annuler",
"es": "Cancelar",
"tr": "İptal",
"kk": "Болдырмау"
},
"OK": {
"en": "OK",
"ru": "ОК",
"zh": "确定",
"uk": "ОК",
"fr": "OK",
"es": "OK",
"tr": "Tamam",
"kk": "OK"
},
"Save": {
"en": "Save",
"ru": "Сохранить",
"zh": "保存",
"uk": "Зберегти",
"fr": "Enregistrer",
"es": "Guardar",
"tr": "Kaydet",
"kk": "Сақтау"
},
"Abbreviations": {
"Time": {
"Seconds": {
"en": "sec",
"ru": "сек",
"zh": "秒",
"uk": "сек",
"fr": "sec",
"es": "seg",
"tr": "sn",
"kk": "сек"
}
},
"Count": {
"Items": {
"en": "pcs",
"ru": "шт",
"zh": "物品",
"uk": "шт",
"fr": "élém",
"es": "uds",
"tr": "adet",
"kk": "дана"
}
}
}
},
"CantAlignTimeError": {
"ru": "Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log",
"en": "Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log",
"zh": "无法与 Steam 对齐时间。可能没有互联网连接或 Steam 不可用。详细错误已保存到 log.log 中",
"uk": "Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log",
"fr": "Impossible de synchroniser l'heure avec Steam. Peut-être qu'il n'y a pas de connexion internet ou Steam est indisponible. Détails de l'erreur enregistrés dans log.log",
"es": "No se puede sincronizar la hora con Steam. Puede que no haya conexión a Internet o que Steam no esté disponible. El error detallado se guardó en log.log",
"tr": "Steam ile zaman senkronize edilemiyor. İnternet bağlantısı olmayabilir veya Steam kullanılamıyor olabilir. Ayrıntılı hata log.log dosyasına kaydedildi",
"kk": "Уақытты Steam-мен синхрондау мүмкін емес. Интернет байланысы жоқ болуы мүмкін немесе Steam қолжетімсіз. Толық қате log.log файлына сақталды"
}
}
@@ -0,0 +1,933 @@
{
"CodeBehind": {
"MainVM": {
"SuccessfulLogin": {
"en": "Successful login",
"ru": "Успешный вход",
"zh": "登录成功",
"uk": "Успішний вхід",
"fr": "Connexion réussie",
"es": "Inicio de sesión exitoso",
"tr": "Giriş başarılı",
"kk": "Сәтті кіру"
},
"SessionRefreshed": {
"en": "Session refreshed",
"ru": "Сессия обновлена",
"zh": "会话已更新",
"uk": "Сесія оновлена",
"fr": "Session actualisée",
"es": "Sesión actualizada",
"tr": "Oturum yenilendi",
"kk": "Сессия жаңартылды"
},
"MissingRCode": {
"en": "Missing RCode",
"ru": "Отсутствует RCode",
"zh": "缺少响应代码",
"uk": "Відсутній RCode",
"fr": "RCode manquant",
"es": "Falta RCode",
"tr": "RCode eksik",
"kk": "RCode жоқ"
},
"ConfirmRemovingAuthenticator": {
"ru": "Вы точно уверены, что хотите удалить аутентификатор?\r\nПосле этого вы не сможете обмениваться, использовать торговую площадку 15 дней. Мафайл будет удалён навсегда",
"en": "Are you sure you want to remove the authenticator?\r\nAfter that, you will not be able to trade or use the marketplace for 15 days. Mafile will be deleted permanently",
"zh": "您确定要移除身份验证器吗?\r\n之后,您将在15天内无法进行交易或使用市场。Mafile 将被永久删除。",
"uk": "Ви точно впевнені, що хочете видалити автентифікатор?\r\nПісля цього ви не зможете обмінюватися, використовувати торгову площадку 15 днів. Мафайл буде видалено назавжди",
"fr": "Êtes-vous sûr de vouloir supprimer l'authenticateur?\r\nAprès cela, vous ne pourrez pas échanger ou utiliser le marché pendant 15 jours. Le mafile sera supprimé définitivement",
"es": "¿Seguro que quieres eliminar el autenticador?\r\nDespués no podrás comerciar ni usar el mercado durante 15 días. El mafile se eliminará permanentemente",
"tr": "Kimlik doğrulayıcıyı kaldırmak istediğinizden emin misiniz?\r\nBundan sonra 15 gün boyunca ticaret yapamaz veya pazarı kullanamazsınız. Mafile kalıcı olarak silinecek",
"kk": "Аутентификаторды жойғыңыз келетініне сенімдісіз бе?\r\nОсыдан кейін 15 күн бойы сауда жасай алмайсыз және нарықты пайдалана алмайсыз. Mafile біржола жойылады"
},
"AuthenticatorRemoved": {
"ru": "Аутентификатор удален",
"en": "Authenticator removed",
"zh": "身份验证器已删除",
"uk": "Аутентифікатор видалено",
"fr": "Authentificateur supprimé",
"es": "Autenticador eliminado",
"tr": "Kimlik doğrulayıcı kaldırıldı",
"kk": "Аутентификатор жойылды"
},
"AuthenticatorNotRemoved": {
"ru": "Что-то пошло не так",
"en": "Something went wrong",
"zh": "出了点问题",
"uk": "Щось пішло не так",
"fr": "Quelque chose s'est mal passé",
"es": "Algo salió mal",
"tr": "Bir şeyler ters gitti",
"kk": "Бірдеңе дұрыс болмады"
},
"ConfirmLoginSuccess": {
"ru": "Вход осуществлен:",
"en": "Login success:",
"zh": "已登录:",
"uk": "Вхід здійснено:",
"fr": "Connexion réussie:",
"es": "Inicio de sesión correcto:",
"tr": "Giriş başarılı:",
"kk": "Кіру сәтті:"
},
"ConfirmLoginFailedNoRequests": {
"ru": "Запросов на вход не обнаружено",
"en": "No login requests found",
"zh": "未检测到输入请求",
"uk": "Запитів на вхід не виявлено",
"fr": "Aucune demande de connexion trouvée",
"es": "No se encontraron solicitudes de inicio de sesión",
"tr": "Giriş isteği bulunamadı",
"kk": "Кіру сұраныстары табылмады"
},
"ConfirmLoginFailedMoreThanOneRequest": {
"ru": "Обнаружено несколько запросов на вход. В целях безопасности операция отменена",
"en": "Multiple login requests found. For security reasons, the operation is canceled",
"zh": "检测到多个输入请求。出于安全考虑,操作已取消",
"uk": "Виявлено декілька запитів на вхід. З метою безпеки операція скасована",
"fr": "Plusieurs demandes de connexion trouvées. Pour des raisons de sécurité, l'opération est annulée",
"es": "Se encontraron múltiples solicitudes de inicio de sesión. Por razones de seguridad, la operación fue cancelada",
"tr": "Birden fazla giriş isteği bulundu. Güvenlik nedeniyle işlem iptal edildi",
"kk": "Бірнеше кіру сұранысы табылды. Қауіпсіздік мақсатында операция тоқтатылды"
},
"ConfirmationError": {
"ru": "Ошибка подтверждения",
"en": "Confirmation error",
"zh": "确认错误",
"uk": "Помилка підтвердження",
"fr": "Erreur de confirmation",
"es": "Error de confirmación",
"tr": "Onay hatası",
"kk": "Растау қатесі"
},
"MafileImportError": {
"ru": "Ошибка при импорте мафайла",
"en": "Mafile import error",
"zh": "导入文件错误",
"uk": "Помилка імпорту мафайла",
"fr": "Erreur d'importation du mafile",
"es": "Error al importar el mafile",
"tr": "Mafile içe aktarma hatası",
"kk": "Mafile импорттау қатесі"
},
"SdaManifestEntryNotFound": {
"en": "The selected manifest.json does not contain an entry for this .mafile.",
"ru": "Выбранный manifest.json не содержит запись для этого .mafile.",
"zh": "所选的 manifest.json 不包含此 .mafile 的条目。",
"uk": "Вибраний manifest.json не містить запису для цього .mafile.",
"fr": "Le manifest.json sélectionné ne contient pas d'entrée pour ce .mafile.",
"es": "El manifest.json seleccionado no contiene una entrada para este .mafile.",
"tr": "Seçilen manifest.json bu .mafile için bir kayıt içermiyor.",
"kk": "Таңдалған manifest.json бұл .mafile үшін жазба қамтымайды."
},
"SdaWrongPassword": {
"en": "Wrong SDA password. Please try again.",
"ru": "Неверный пароль SDA. Попробуйте снова.",
"zh": "SDA 密码错误。请重试。",
"uk": "Невірний пароль SDA. Спробуйте ще раз.",
"fr": "Mauvais mot de passe SDA. Veuillez réessayer.",
"es": "Contraseña SDA incorrecta. Inténtalo de nuevo.",
"tr": "Yanlış SDA parolası. Lütfen tekrar deneyin.",
"kk": "SDA құпиясөзі қате. Қайтадан көріңіз."
},
"MissingSessionInMafile": {
"ru": ". У него отсутствует сессия. Добавьте этот файл отдельно от остальных",
"en": ". It has no session. Add this file separately from the others",
"zh": ". 他没有会话。请将此文件与其他文件分开添加",
"uk": ". В нього відсутня сесія. Додайте цей файл окремо від інших",
"fr": ". Pas de session dans le mafile. Ajoutez ce fichier séparément des autres",
"es": ". No tiene sesión. Añada este archivo por separado de los demás",
"tr": ". Oturumu yok. Bu dosyayı diğerlerinden ayrı ekleyin",
"kk": ". Бұл файлда сессия жоқ. Оны басқа файлдардан бөлек қосыңыз"
},
"Import": {
"ru": "Импорт",
"en": "Import",
"zh": "导入",
"uk": "Імпорт",
"fr": "Importer",
"es": "Importar",
"tr": "İçe aktar",
"kk": "Импорт"
},
"ImportAdded": {
"ru": "добавлено:",
"en": "added:",
"zh": "已添加:",
"uk": "додано:",
"fr": "ajouté:",
"es": "añadido:",
"tr": "eklendi:",
"kk": "қосылды:"
},
"ImportSkipped": {
"ru": "пропущено:",
"en": "skipped:",
"zh": "跳过:",
"uk": "пропущено:",
"fr": "passé:",
"es": "omitido:",
"tr": "atlandı:",
"kk": "өткізілді:"
},
"ImportErrors": {
"ru": "ошибки:",
"en": "errors:",
"zh": "错误:",
"uk": "помилки:",
"fr": "erreurs:",
"es": "errores:",
"tr": "hatalar:",
"kk": "қателер:"
},
"RemoveMafileConfirmation": {
"ru": "Удалить mafile?\r\nМафайл будет перемещен в папку 'mafiles_removed'\r\nЭто действие НЕ удаляет Steam Guard с аккаунта",
"en": "Remove mafile?\r\nMafile will be moved to 'mafiles_removed' directory\r\nThis action will NOT remove the Steam Guard from the account",
"zh": "是否删除mafile\r\nMafile将被移动到“mafiles_removed”目录\r\n此操作不会从账户中移除令牌",
"uk": "Видалити mafile?\r\nМафайл буде переміщено у каталог 'mafiles_removed'\r\nЦя дія НЕ видаляє автентифікатор з облікового запису",
"fr": "Supprimer le mafile?\r\nLe mafile sera déplacé dans le dossier 'mafiles_removed'\r\nCette action NE SUPPRIME PAS le Steam Guard du compte",
"es": "¿Eliminar mafile?\r\nEl mafile se moverá a la carpeta 'mafiles_removed'\r\nEsta acción NO elimina Steam Guard de la cuenta",
"tr": "Mafile silinsin mi?\r\nMafile 'mafiles_removed' klasörüne taşınacak\r\nBu işlem Steam Guard'ı hesaptan kaldırmaz",
"kk": "Mafile жойылсын ба?\r\nMafile 'mafiles_removed' қалтасына көшіріледі\r\nБұл әрекет Steam Guard-ты аккаунттан алып тастамайды"
},
"CantRemoveAlreadyExist": {
"ru": "Не удалось переместить mafile, возможно в папке уже существует мафайл с таким именем.",
"en": "Can't move mafile, maybe mafile with same name already exists in mafiles_removed folder.",
"zh": "无法移动 mafile,可能 mafiles_removed 文件夹中已存在同名的 mafile。",
"uk": "Не вдалося перемістити mafile, можливо в папці вже існує мафайл з таким іменем.",
"fr": "Impossible de déplacer le mafile, probablement déjà existant dans le dossier mafiles_removed.",
"es": "No se pudo mover el mafile, quizá ya existe un mafile con el mismo nombre en mafiles_removed.",
"tr": "Mafile taşınamadı, aynı isimde bir mafile zaten mafiles_removed klasöründe olabilir.",
"kk": "Mafile жылжыту мүмкін болмады, mafiles_removed қалтасында дәл осындай атаумен файл болуы мүмкін."
},
"CantRemoveMafile": {
"ru": "Не удалось удалить (переместить) мафайл:",
"en": "Can't remove mafile:",
"zh": "无法删除(移动)我的文件:",
"uk": "Не вдалося видалити (перемістити) мафайл:",
"fr": "Impossible de supprimer (déplacer) le mafile:",
"es": "No se pudo eliminar (mover) el mafile:",
"tr": "Mafile silinemedi (taşınamadı):",
"kk": "Mafile жою (көшіру) мүмкін болмады:"
},
"TimerTooFast": {
"ru": "Слишком быстрый таймер.",
"en": "Too fast timer.",
"zh": "计时器太快。",
"uk": "Занадто швидкий таймер.",
"fr": "Timer trop rapide.",
"es": "Temporizador demasiado rápido.",
"tr": "Zamanlayıcı çok hızlı.",
"kk": "Таймер тым жылдам."
},
"TimerChanged": {
"en": "Timer changed",
"ru": "Таймер изменен",
"zh": "计时器已更改",
"uk": "Таймер змінено",
"fr": "Timer modifié",
"es": "Temporizador cambiado",
"tr": "Zamanlayıcı değiştirildi",
"kk": "Таймер өзгертілді"
},
"CantRetrieveSteamIDToUpdate": {
"ru": "Не удалось получить SteamID из мафайла и сохранить изменения. Необходимо сделать релогин. Отменено",
"en": "Can't retrieve SteamID from mafile to save changes. Need to relogin. Canceled",
"zh": "无法从配置文件获取 SteamID 并保存更改。需要重新登录。已取消",
"uk": "Не вдалося отримати SteamID з мафайла та зберегти зміни. Потрібно зробити релогін. Скасовано",
"fr": "Impossible de récupérer le SteamID du mafile pour sauvegarder les modifications. Reconnexion nécessaire. Annulé",
"es": "No se pudo obtener el SteamID del mafile para guardar cambios. Se requiere volver a iniciar sesión. Cancelado",
"tr": "Değişiklikleri kaydetmek için mafile'dan SteamID alınamadı. Yeniden giriş yapılmalı. İptal edildi",
"kk": "Өзгерістерді сақтау үшін mafile-дан SteamID алу мүмкін болмады. Қайта кіру қажет. Болдырылды"
},
"LoginCopied": {
"en": "Login copied",
"ru": "Логин скопирован",
"zh": "登录已复制",
"uk": "Логін скопійовано",
"fr": "Login copié",
"es": "Login copiado",
"tr": "Giriş kopyalandı",
"kk": "Логин көшірілді"
},
"SteamIdCopied": {
"en": "SteamID copied",
"ru": "SteamID скопирован",
"zh": "SteamID 已复制",
"uk": "SteamID скопійовано",
"fr": "SteamID copié",
"es": "SteamID copiado",
"tr": "SteamID kopyalandı",
"kk": "SteamID көшірілді"
},
"MafileCopied": {
"en": "Mafile copied",
"ru": "Мафайл скопирован",
"zh": "文件已复制",
"uk": "Мафайл скопійовано",
"fr": "Mafile copié",
"es": "Mafile copiado",
"tr": "Mafile kopyalandı",
"kk": "Mafile көшірілді"
},
"MafileNotCopied": {
"en": "Mafile not copied",
"ru": "Мафайл не скопирован",
"zh": "文件未被复制",
"uk": "Мафайл не скопійовано",
"fr": "Mafile non copié",
"es": "Mafile no copiado",
"tr": "Mafile kopyalanamadı",
"kk": "Mafile көшірілмеді"
},
"PasswordCopied": {
"en": "Password copied",
"ru": "Пароль скопирован",
"zh": "密码已复制",
"uk": "Пароль скопійовано",
"fr": "Mot de passe copié",
"es": "Contraseña copiada",
"tr": "Parola kopyalandı",
"kk": "Құпиясөз көшірілді"
},
"CantDecryptPassword": {
"en": "Can't decrypt password. Maybe password from this mafile was encrypted with another encryption password",
"ru": "Не удалось расшифровать пароль. Возможно пароль из этого мафайла был зашифрован другим паролем шифрования",
"zh": "无法解密密码。可能此文件中的密码是用其他加密密码加密的。",
"uk": "Не вдалося розшифрувати пароль. Можливо пароль з цього мафайла був зашифрований іншим паролем шифрування",
"fr": "Impossible de déchiffrer le mot de passe. Le mot de passe de chiffrement est peut-être différent",
"es": "No se pudo descifrar la contraseña. Quizás fue cifrada con otra contraseña de cifrado",
"tr": "Parola çözülemedi. Bu mafile'daki parola farklı bir şifreleme parolasıyla şifrelenmiş olabilir",
"kk": "Құпиясөзді шифрдан шығару мүмкін болмады. Бұл mafile-дегі құпиясөз басқа шифрлау құпиясөзімен шифрланған болуы мүмкін"
},
"CreateGroupTitle": {
"en": "New group",
"ru": "Новая группа",
"zh": "新群组",
"uk": "Нова група",
"fr": "Nouveau groupe",
"es": "Nuevo grupo",
"tr": "Yeni grup",
"kk": "Жаңа топ"
},
"CreateGroupInput": {
"en": "Enter group name",
"ru": "Введите имя группы",
"zh": "输入群组名称",
"uk": "Введіть назву групи",
"fr": "Entrer le nom du groupe",
"es": "Ingrese el nombre del grupo",
"tr": "Grup adını girin",
"kk": "Топ атауын енгізіңіз"
},
"CreateGroupSuccess": {
"en": "Group created",
"ru": "Группа создана",
"zh": "群组已创建",
"uk": "Групу створено",
"fr": "Groupe créé",
"es": "Grupo creado",
"tr": "Grup oluşturuldu",
"kk": "Топ құрылды"
}
}
},
"MainWindow": {
"Global": {
"DragNDropHint": {
"en": "Release to import mafiles",
"ru": "Отпустите для импорта мафайлов",
"zh": "允许导入文件",
"uk": "Відпустіть для імпорту мафайлів",
"fr": "Lâchez pour importer les mafiles",
"es": "Suelta para importar mafiles",
"tr": "Mafile içe aktarmak için bırakın",
"kk": "Mafile импорттау үшін жіберіңіз"
},
"LoadingHint": {
"en": "Loading...",
"ru": "Загрузка...",
"zh": "加载中...",
"uk": "Завантаження...",
"fr": "Chargement...",
"es": "Cargando...",
"tr": "Yükleniyor...",
"kk": "Жүктелуде..."
}
},
"Menu": {
"File": {
"Caption": {
"en": "Menu",
"ru": "Меню",
"zh": "菜单",
"uk": "Меню",
"fr": "Menu",
"es": "Menú",
"tr": "Menü",
"kk": "Мәзір"
},
"Import": {
"en": "Import",
"ru": "Импорт",
"zh": "导入",
"uk": "Імпорт",
"fr": "Importer",
"es": "Importar",
"tr": "İçe aktar",
"kk": "Импорттау"
},
"Remove": {
"en": "Remove",
"ru": "Удалить",
"zh": "删除",
"uk": "Видалити",
"fr": "Supprimer",
"es": "Eliminar",
"tr": "Kaldır",
"kk": "Жою"
},
"OpenFolder": {
"en": "Open folder",
"ru": "Открыть папку",
"zh": "打开文件夹",
"uk": "Відкрити папку",
"fr": "Ouvrir le dossier",
"es": "Abrir carpeta",
"tr": "Klasörü aç",
"kk": "Қалтаны ашу"
},
"Settings": {
"en": "Settings",
"ru": "Настройки",
"zh": "设置",
"uk": "Налаштування",
"fr": "Paramètres",
"es": "Configuración",
"tr": "Ayarlar",
"kk": "Баптаулар"
},
"ProxyManager": {
"en": "Proxy manager",
"ru": "Менеджер прокси",
"zh": "代理管理器",
"uk": "Менеджер проксі",
"fr": "Gestionnaire de proxy",
"es": "Administrador de proxy",
"tr": "Proxy yöneticisi",
"kk": "Прокси менеджері"
},
"Other": {
"Title": {
"en": "Other",
"ru": "Прочее",
"zh": "其他",
"uk": "Інше",
"fr": "Divers",
"es": "Otros",
"tr": "Diğer",
"kk": "Басқа"
},
"SetPasswords": {
"en": "Set passwords",
"ru": "Назначить пароли",
"zh": "设置密码",
"uk": "Призначити паролі",
"fr": "Définir les mots de passe",
"es": "Establecer contraseñas",
"tr": "Parolaları ayarla",
"kk": "Құпиясөздерді орнату"
},
"OpenExport": {
"ru": "Экспорт",
"en": "Export",
"uk": "Експорт",
"fr": "Exporter",
"zh": "导出",
"es": "Exportar",
"tr": "Dışa aktar",
"kk": "Экспорт"
}
}
},
"Account": {
"Caption": {
"en": "Account",
"ru": "Аккаунт",
"zh": "账户",
"uk": "Акаунт",
"fr": "Compte",
"es": "Cuenta",
"tr": "Hesap",
"kk": "Тіркелгі"
},
"Link": {
"en": "Link",
"ru": "Привязать",
"zh": "绑定",
"uk": "Прив'язати",
"fr": "Lier",
"es": "Vincular",
"tr": "Bağla",
"kk": "Байлау"
},
"MoveSteamGuard": {
"en": "Move Steam Guard",
"ru": "Перенести Steam Guard",
"zh": "令牌",
"uk": "Перенести Steam Guard",
"fr": "Déplacer Steam Guard",
"es": "Mover Steam Guard",
"tr": "Steam Guard'ı taşı",
"kk": "Steam Guard-ты көшіру"
},
"Unlink": {
"en": "Unlink",
"ru": "Отвязать",
"zh": "解绑",
"uk": "Відв'язати",
"fr": "Délier",
"es": "Desvincular",
"tr": "Bağlantıyı kaldır",
"kk": "Байланысты үзу"
},
"RefreshSession": {
"en": "Refresh session",
"ru": "Обновить сессию",
"zh": "刷新",
"uk": "Оновити сесію",
"fr": "Actualiser la session",
"es": "Actualizar sesión",
"tr": "Oturumu yenile",
"kk": "Сессияны жаңарту"
},
"LoginAgain": {
"en": "Login",
"ru": "Войти",
"zh": "登录",
"uk": "Увійти",
"fr": "Se connecter",
"es": "Iniciar sesión",
"tr": "Giriş yap",
"kk": "Кіру"
}
}
},
"AppBar": {
"GroupsHint": {
"en": "Groups",
"ru": "Группы",
"zh": "群组",
"uk": "Групи",
"fr": "Groupes",
"es": "Grupos",
"tr": "Gruplar",
"kk": "Топтар"
},
"GroupToolTip": {
"ru": "Введите новую группу и нажмите Enter. Управление группой - ПКМ на аккаунте",
"en": "Enter new group and press Enter. Group management - RMB on account",
"zh": "输入新组并按回车键。管理组 - 在账户上右键点击",
"uk": "Введіть нову групу і натисніть Enter. Управління групою - ПКМ на акаунті",
"fr": "Entrez un nouveau groupe et appuyez sur Entrée. Gestion du groupe - clic droit sur le compte",
"es": "Introduce un nuevo grupo y pulsa Enter. Gestión del grupo: clic derecho en la cuenta",
"tr": "Yeni bir grup girin ve Enter'a basın. Grup yönetimi - hesap üzerinde sağ tık",
"kk": "Жаңа топ енгізіп, Enter басыңыз. Топты басқару — аккаунтта оң жақ батырма"
},
"Proxy": {
"ProxyHint": {
"en": "Proxy",
"ru": "Прокси",
"zh": "代理",
"uk": "Проксі",
"fr": "Proxy",
"es": "Proxy",
"tr": "Proxy",
"kk": "Прокси"
},
"ProxyOpenManagerHint": {
"en": "Open proxy manager",
"ru": "Открыть менеджер прокси",
"zh": "打开代理管理器",
"uk": "Відкрити менеджер проксі",
"fr": "Ouvrir le gestionnaire de proxy",
"es": "Abrir administrador de proxy",
"tr": "Proxy yöneticisini aç",
"kk": "Прокси менеджерін ашу"
},
"ProxyManipulateToolTip": {
"en": "Right-click to open menu. DEL to remove",
"ru": "Правый клик для открытия меню. DEL для удаления",
"zh": "右键点击以打开菜单。按 DEL 删除",
"uk": "Правий клік для відкриття меню. DEL для видалення",
"fr": "Clic droit pour ouvrir le menu. Touche Suppr pour supprimer",
"es": "Clic derecho para abrir el menú. DEL para eliminar",
"tr": "Menüyü açmak için sağ tık. Silmek için DEL",
"kk": "Мәзірді ашу үшін оң жақ батырма. Жою үшін DEL"
},
"ProxyAlert": {
"DefaultInUse": {
"en": "Default proxy is in use",
"ru": "Используется прокси по умолчанию",
"zh": "使用默认代理",
"uk": "Використовується проксі за замовчуванням",
"fr": "Proxy par défaut en cours d'utilisation",
"es": "Proxy predeterminado en uso",
"tr": "Varsayılan proxy kullanılıyor",
"kk": "Әдепкі прокси қолданылуда"
},
"MafileProxyInUse": {
"en": "Mafile proxy is in use",
"ru": "Используется прокси из мафайла",
"zh": "使用来自 mafile 的代理",
"uk": "Використовується проксі з мафайла",
"fr": "Proxy du mafile en cours d'utilisation",
"es": "Proxy del mafile en uso",
"tr": "Mafile proxy kullanılıyor",
"kk": "Mafile прокси қолданылуда"
}
}
},
"TradeTimerHint": {
"en": "Auto-confirm trades. RMC for mass control",
"ru": "Автоподтверждение трейдов. ПКМ для массового управления",
"zh": "自动确认交易。右键进行批量管理",
"uk": "Автопідтвердження трейдів. ПКМ для масового управління",
"fr": "Confirmation auto des échanges. Clic droit pour le contrôle en masse",
"es": "Confirmación automática de intercambios. Clic derecho para control masivo",
"tr": "Takasları otomatik onayla. Toplu kontrol için sağ tık",
"kk": "Саудаларды авторастау. Жаппай басқару үшін оң жақ батырма"
},
"MarketTimerHint": {
"en": "Auto-confirm market listings. RMC for mass control",
"ru": "Автоподтверждение лотов на маркете. ПКМ для массового управления",
"zh": "市场上拍品的自动确认。右键进行批量管理",
"uk": "Автопідтвердження лотів на маркеті. ПКМ для масового управління",
"fr": "Confirmation auto des offres du marché. Clic droit pour le contrôle en masse",
"es": "Confirmación automática de anuncios del mercado. Clic derecho para control masivo",
"tr": "Pazar ilanlarını otomatik onayla. Toplu kontrol için sağ tık",
"kk": "Маркет лоттарын авторастау. Жаппай басқару үшін оң жақ батырма"
},
"TimersContextMenu": {
"SwitchMAACGroup": {
"en": "Switch in group",
"ru": "Переключить в группе",
"zh": "切换到组",
"uk": "Перемкнути в групі",
"fr": "Basculer dans le groupe",
"es": "Cambiar en el grupo",
"tr": "Grup içinde değiştir",
"kk": "Топ ішінде ауыстыру"
},
"SwitchMAACAll": {
"en": "Switch all",
"ru": "Переключить все",
"zh": "切换全部",
"uk": "Перемкнути всі",
"fr": "Basculer tous",
"es": "Cambiar todo",
"tr": "Hepsini değiştir",
"kk": "Барлығын ауыстыру"
}
},
"ShowAutoConfirmAccountsHint": {
"en": "Show accounts with auto-confirmation enabled.",
"ru": "Показать аккаунты с включенным автоподтверждением.",
"zh": "显示已启用自动确认的账户。",
"uk": "Показати акаунти з включеним автопідтвердженням.",
"fr": "Afficher les comptes avec la confirmation auto activée.",
"es": "Mostrar cuentas con confirmación automática activada.",
"tr": "Otomatik onay etkin olan hesapları göster.",
"kk": "Авторастау қосылған аккаунттарды көрсету."
}
},
"LeftPart": {
"SearchBoxHint": {
"en": "Search",
"ru": "Поиск",
"zh": "搜索",
"uk": "Пошук",
"fr": "Rechercher",
"es": "Buscar",
"tr": "Ara",
"kk": "Іздеу"
},
"NoMafiles": {
"en": "No mafiles found..",
"ru": "Мафайлы не найдены..",
"zh": "未找到文件..",
"uk": "Мафайли не знайдено..",
"fr": "Aucun mafile trouvé..",
"es": "No se encontraron mafiles..",
"tr": "Mafile bulunamadı..",
"kk": "Mafile табылмады.."
}
},
"RightPart": {
"LoadConfirmations": {
"en": "Load confirmations",
"ru": "Загрузить подтверждения",
"zh": "下载确认",
"uk": "Завантажити підтвердження",
"fr": "Charger les confirmations",
"es": "Cargar confirmaciones",
"tr": "Onayları yükle",
"kk": "Растауларды жүктеу"
},
"ConfirmLogin": {
"en": "Confirm login",
"ru": "Подтвердить вход",
"zh": "确认登录",
"uk": "Підтвердити вхід",
"fr": "Confirmer la connexion",
"es": "Confirmar inicio de sesión",
"tr": "Girişi onayla",
"kk": "Кіруді растау"
}
},
"Footer": {
"AccountsCount": {
"en": "Accounts: ",
"ru": "Аккаунты: ",
"zh": "账户:",
"uk": "Акаунти: ",
"fr": "Comptes: ",
"es": "Cuentas: ",
"tr": "Hesaplar: ",
"kk": "Аккаунттар: "
},
"SelectedAccount": {
"en": "Current: ",
"ru": "Текущий: ",
"zh": "群组:",
"uk": "Поточний: ",
"fr": "Actuel: ",
"es": "Actual: ",
"tr": "Geçerli: ",
"kk": "Ағымдағы: "
}
},
"ConfirmationTemplates": {
"TradeWith": {
"en": "Trade with",
"ru": "Трейд с",
"zh": "交易与",
"uk": "Трейд з",
"fr": "Échanger avec",
"es": "Intercambiar con",
"tr": "Şununla takas",
"kk": "Мынамен сауда"
},
"Recovery": {
"en": "Recovery",
"ru": "Восстановление",
"zh": "恢复",
"uk": "Відновлення",
"fr": "Récupération",
"es": "Recuperación",
"tr": "Kurtarma",
"kk": "Қалпына келтіру"
},
"ApiKey": {
"en": "Api key",
"ru": "Api ключ",
"zh": "API 密钥",
"uk": "Api ключ",
"fr": "Clé API",
"es": "Clave API",
"tr": "API anahtarı",
"kk": "API кілті"
},
"Market": {
"en": "Sell",
"ru": "Продажа",
"zh": "售卖",
"uk": "Продаж",
"fr": "Vendre",
"es": "Vender",
"tr": "Sat",
"kk": "Сату"
},
"Purchase": {
"en": "Purchase",
"ru": "Покупка",
"zh": "购买",
"uk": "Покупка",
"fr": "Acheter",
"es": "Comprar",
"tr": "Satın al",
"kk": "Сатып алу"
},
"ConfirmAll": {
"en": "Confirm all",
"ru": "Подтвердить все",
"zh": "全部确认",
"uk": "Підтвердити все",
"fr": "Tout confirmer",
"es": "Confirmar todo",
"tr": "Hepsini onayla",
"kk": "Барлығын растау"
},
"CancelAll": {
"en": "Cancel all",
"ru": "Отклонить все",
"zh": "全部取消",
"uk": "Відхилити все",
"fr": "Tout annuler",
"es": "Cancelar todo",
"tr": "Hepsini iptal et",
"kk": "Барлығын болдырмау"
},
"ConfirmOne": {
"en": "Confirm this item",
"ru": "Подтвердить этот предмет",
"zh": "确认此物品",
"uk": "Підтвердити цей предмет",
"fr": "Confirmer cet objet",
"es": "Confirmar este objeto",
"tr": "Bu öğeyi onayla",
"kk": "Бұл затты растау"
},
"CancelOne": {
"en": "Cancel this item",
"ru": "Отклонить этот предмет",
"zh": "取消此物品",
"uk": "Відхилити цей предмет",
"fr": "Annuler cet objet",
"es": "Cancelar este objeto",
"tr": "Bu öğeyi iptal et",
"kk": "Бұл затты болдырмау"
}
},
"CodeCopied": {
"ru": "Скопировано",
"en": "Copied",
"zh": "已复制",
"uk": "Скопійовано",
"fr": "Copié",
"es": "Copiado",
"tr": "Kopyalandı",
"kk": "Көшірілді"
},
"ContextMenus": {
"Mafile": {
"CopyLogin": {
"en": "Copy login",
"ru": "Скопировать логин",
"zh": "复制登录名",
"uk": "Скопіювати логін",
"fr": "Copier le login",
"es": "Copiar login",
"tr": "Girişi kopyala",
"kk": "Логинді көшіру"
},
"CopyMafile": {
"en": "Copy mafile",
"ru": "Скопировать мафайл",
"zh": "复制我的文件",
"uk": "Скопіювати мафайл",
"fr": "Copier le mafile",
"es": "Copiar mafile",
"tr": "Mafile'ı kopyala",
"kk": "Mafile көшіру"
},
"CopySteamId": {
"en": "Copy SteamID",
"ru": "Скопировать SteamID",
"zh": "复制 Steam ID",
"uk": "Скопіювати SteamID",
"fr": "Copier SteamID",
"es": "Copiar SteamID",
"tr": "SteamID kopyala",
"kk": "SteamID көшіру"
},
"CreateGroup": {
"en": "Create group",
"ru": "Создать группу",
"zh": "创建群组",
"uk": "Створити групу",
"fr": "Créer un groupe",
"es": "Crear grupo",
"tr": "Grup oluştur",
"kk": "Топ құру"
},
"AddToGroup": {
"en": "Add to group",
"ru": "Добавить в группу",
"zh": "添加到群组",
"uk": "Додати в групу",
"fr": "Ajouter au groupe",
"es": "Agregar al grupo",
"tr": "Gruba ekle",
"kk": "Топқа қосу"
},
"RemoveFromGroup": {
"en": "Remove from group",
"ru": "Удалить из группы",
"zh": "从群组中删除",
"uk": "Видалити з групи",
"fr": "Supprimer du groupe",
"es": "Eliminar del grupo",
"tr": "Gruptan kaldır",
"kk": "Топтан шығару"
},
"CopyPassword": {
"en": "Copy password",
"ru": "Скопировать пароль",
"zh": "复制密码",
"uk": "Скопіювати пароль",
"fr": "Copier le mot de passe",
"es": "Copiar contraseña",
"tr": "Parolayı kopyala",
"kk": "Құпиясөзді көшіру"
},
"UnattachProxy": {
"en": "Unattach proxy",
"ru": "Открепить прокси",
"zh": "取消代理绑定",
"uk": "Відкріпити проксі",
"fr": "Détacher le proxy",
"es": "Desvincular proxy",
"tr": "Proxy bağlantısını kaldır",
"kk": "Проксиді ажырату"
},
"AssignFreeProxy": {
"en": "Assign free proxy",
"ru": "Назначить свободный прокси",
"zh": "分配空闲代理",
"uk": "Призначити вільний проксі",
"fr": "Attribuer un proxy libre",
"es": "Asignar proxy libre",
"tr": "Serbest proxy ata",
"kk": "Бос проксиді тағайындау"
}
},
"Proxy": {
"Copy": {
"en": "Copy",
"ru": "Скопировать",
"zh": "复制",
"uk": "Скопіювати",
"fr": "Copier",
"es": "Copiar",
"tr": "Kopyala",
"kk": "Көшіру"
},
"CopyAddress": {
"en": "Copy address",
"ru": "Скопировать адрес",
"zh": "复制地址",
"uk": "Скопіювати адресу",
"fr": "Copier l'adresse",
"es": "Copiar dirección",
"tr": "Adresi kopyala",
"kk": "Мекенжайды көшіру"
}
}
}
}
}
@@ -0,0 +1,34 @@
{
"LinksView": {
"UsefulLinks": {
"en": "Useful links",
"ru": "Полезные ссылки",
"zh": "相关链接",
"uk": "Корисні посилання",
"fr": "Liens utiles",
"es": "Enlaces útiles",
"tr": "Faydalı bağlantılar",
"kk": "Пайдалы сілтемелер"
},
"Documentation": {
"en": "Documentation",
"ru": "Документация",
"zh": "文档",
"uk": "Документація",
"fr": "Documentation",
"es": "Documentación",
"tr": "Dokümantasyon",
"kk": "Құжаттама"
},
"CheckForUpdates": {
"en": "Check for updates",
"ru": "Проверить обновления",
"zh": "检查更新",
"uk": "Перевірити оновлення",
"fr": "Vérifier les mises à jour",
"es": "Buscar actualizaciones",
"tr": "Güncellemeleri kontrol et",
"kk": "Жаңартуларды тексеру"
}
}
}
+238 -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,13 +28,21 @@
<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"
DialogClosed="DialogHost_DialogClosed"
ApplyBlurBackground="{Binding Settings.ApplyBlurBackground}"
BlurRadius="12"
DialogContentUniformCornerRadius="12"
x:Name="DialogHostInstance">
<Grid ZIndex="0">
@@ -57,31 +66,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 +109,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 +141,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 +165,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 +193,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 +270,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 +290,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 +349,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 +370,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 +418,141 @@
<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}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AssignFreeProxy}"
Command="{Binding AssignFreeProxyCommand}"
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>
+69 -5
View File
@@ -2,7 +2,10 @@
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;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
@@ -24,12 +27,50 @@ 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 +86,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 +136,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), false))
: 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)
+41 -96
View File
@@ -11,10 +11,9 @@ using AchiesUtilities.Web.Proxy;
using CommunityToolkit.Mvvm.ComponentModel;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
using SteamLib.Api.Mobile;
using SteamLib.Api.Trade;
using SteamLib.Authentication;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile.Confirmations;
using SteamLib.Web;
using SteamLibForked.Abstractions;
@@ -33,7 +32,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
[ObservableProperty] private bool _autoConfirmMarket;
[ObservableProperty] private bool _autoConfirmTrades;
[ObservableProperty] private bool _isError;
[ObservableProperty] private PortableMaClientStatus _status = PortableMaClientStatus.Ok();
public PortableMaClient(Mafile mafile)
{
@@ -45,6 +44,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
ClientHandler = pair.Handler;
UpdateCookies(mafile.SessionData);
Mafile.PropertyChanged += Mafile_PropertyChanged;
SetStatus(PortableMaClientStatus.Ok());
}
private void Mafile_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -55,42 +55,26 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
}
private void UpdateCookies(IMobileSessionData? sessionData)
{
Application.Current.Dispatcher.Invoke(() => IsError = sessionData == null);
var newStatus = MAACRequestHandler.ClearErrors(Mafile);
if (!newStatus.Equals(Status))
SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies();
if (sessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
ClientHandler.CookieContainer.SetSteamMobileCookies(sessionData);
}
public async Task<int> Confirm()
{
Proxy.SetData(Mafile.Proxy?.Data ?? MaClient.DefaultProxy);
List<Confirmation> conf;
try
{
conf = (await HandleTimerRequest(GetConfirmations)).ToList();
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: Error GetConf in timer.", Mafile.AccountName);
return 0;
}
var conf = (await GetConfirmations()).ToList();
var toConfirm = new List<Confirmation>();
if (AutoConfirmMarket)
{
var market = conf.Where(c =>
var market = conf.Where(c =>
c.ConfType is ConfirmationType.MarketSellTransaction or ConfirmationType.Purchase);
toConfirm.AddRange(market);
}
@@ -102,19 +86,12 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
if (toConfirm.Count == 0) return 0;
try
{
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await HandleTimerRequest(() => SendConfirmations(toConfirm));
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: MultiConf error in Timer.", Mafile.AccountName);
return 0;
}
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await SendConfirmations(toConfirm);
//TODO: handle success == false
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
@@ -126,62 +103,27 @@ public partial class PortableMaClient : ObservableObject, IDisposable
private async Task<bool> SendConfirmations(IEnumerable<Confirmation> confirmations)
{
return await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, confirmations,
var conf = confirmations.ToList();
var res = await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, conf,
Mafile.SessionData!.SteamId, Mafile, true, _cts.Token);
if (!res && conf.Any(c => c.ConfType == ConfirmationType.Trade))
{
Shell.Logger.Warn("Timer {accountName}: Failed to send trade confirmations. Sending ack",
Mafile.AccountName);
await SteamTradeApi.Acknowledge(Client, Mafile.SessionData!.SessionId, _cts.Token);
await Task.Delay(10, _cts.Token);
}
else
{
return res;
}
return await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, conf,
Mafile.SessionData!.SteamId, Mafile, true, _cts.Token);
}
private async Task<T> HandleTimerRequest<T>(Func<Task<T>> func)
{
Exception innerException;
try
{
return await SessionHandler.Handle(func, Mafile, Chp(), GetTimerPrefix());
}
catch (OperationCanceledException ex)
{
innerException = ex; //Ignored
}
catch (SessionInvalidException ex)
{
if (IgnoreTimerErrors())
{
Shell.Logger.Warn(
"Timer {accountName}: Session error while requesting in timer. Ignored due to IgnorePatchTuesdayErrors setting",
Mafile.AccountName);
}
else
{
Shell.Logger.Warn("Timer {accountName}: Session error while requesting in timer. Timer disabled",
Mafile.AccountName);
SetError();
}
innerException = ex;
}
catch (Exception ex) when (ExceptionHandler.Handle(ex, GetTimerPrefix()))
{
innerException = ex;
}
throw new ApplicationException("Swallowed", innerException);
}
private bool IgnoreTimerErrors()
{
if (Settings.Instance.IgnorePatchTuesdayErrors == false) return false;
var pstZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var pstNow = TimeZoneInfo.ConvertTime(DateTime.UtcNow, pstZone);
var startTime = pstNow.Date.AddHours(15).AddMinutes(55); // 15:55 PST //22:55 GMT //00:55 GMT+2
var endTime = pstNow.Date.AddHours(17).AddMinutes(15); // 17:15 PST //00:15 GMT //02:15 GMT+2
return pstNow.DayOfWeek == DayOfWeek.Tuesday && pstNow >= startTime && pstNow <= endTime;
}
private HttpClientHandlerPair Chp()
public HttpClientHandlerPair Chp()
{
return new HttpClientHandlerPair(Client, ClientHandler);
}
@@ -196,11 +138,14 @@ public partial class PortableMaClient : ObservableObject, IDisposable
return GetLocalization("TimerPrefix") + Mafile.AccountName + ": ";
}
public void SetError()
public void SetStatus(PortableMaClientStatus status)
{
Application.Current.Dispatcher.Invoke(() => IsError = true);
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + GetLocalization("TimerSessionError"));
Application.Current.Dispatcher.Invoke(() => Status = status);
if (status.StatusType == PortableMaClientStatusType.Error)
{
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + status.Message);
}
}
public void Dispose()
@@ -0,0 +1,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);
}
}
+42 -24
View File
@@ -8,6 +8,7 @@ using AchiesUtilities.Web.Proxy;
using NebulaAuth.Model.Entities;
using SteamLib.Api.Mobile;
using SteamLib.Api.Services;
using SteamLib.Api.Trade;
using SteamLib.Authentication;
using SteamLib.Exceptions.Authorization;
using SteamLib.ProtoCore.Services;
@@ -19,11 +20,8 @@ namespace NebulaAuth.Model;
public static class MaClient
{
private static HttpClientHandler ClientHandler { get; }
private static HttpClient Client { get; }
private static DynamicProxy Proxy { get; }
public static ProxyData? DefaultProxy { get; set; }
@@ -39,21 +37,9 @@ public static class MaClient
public static void SetAccount(Mafile? account)
{
ClientHandler.CookieContainer.ClearAllCookies();
if (account != null)
{
if (account.SessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
Proxy.SetData(account.Proxy?.Data);
}
if (account == null) return;
ClientHandler.CookieContainer.SetSteamMobileCookies(account.SessionData);
Proxy.SetData(account.Proxy?.Data);
}
public static Task<IEnumerable<Confirmation>> GetConfirmations(Mafile mafile)
@@ -78,26 +64,58 @@ public static class MaClient
return SessionHandler.RefreshMobileToken(new HttpClientHandlerPair(Client, ClientHandler), mafile);
}
public static Task<bool> SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
public static async Task<bool> SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
{
ValidateMafile(mafile);
SetProxy(mafile);
return SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId, mafile,
var res = await SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId,
mafile,
confirm);
if (!res && confirmation.ConfType == ConfirmationType.Trade)
{
Shell.Logger.Warn("Failed to send trade confirmation for {accountName}. Sending ack", mafile.AccountName);
await SteamTradeApi.Acknowledge(Client, mafile.SessionData.SessionId);
await Task.Delay(10);
}
else
{
return res;
}
return await SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId,
mafile,
confirm);
}
public static Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations,
public static async Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations,
bool confirm)
{
var enumerable = confirmations.ToList();
if (enumerable.Count == 0)
{
return Task.FromResult(false);
return false;
}
ValidateMafile(mafile);
SetProxy(mafile);
return SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, enumerable, mafile.SessionData!.SteamId,
var res = await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, enumerable,
mafile.SessionData!.SteamId,
mafile, confirm);
if (!res && enumerable.Any(c => c.ConfType == ConfirmationType.Trade))
{
Shell.Logger.Warn("Failed to send trade confirmations for {accountName}. Sending ack", mafile.AccountName);
await SteamTradeApi.Acknowledge(Client, mafile.SessionData.SessionId);
await Task.Delay(10);
}
else
{
return res;
}
return await SteamMobileConfirmationsApi.SendMultipleConfirmations(Client, enumerable,
mafile.SessionData!.SteamId,
mafile, confirm);
}
@@ -125,7 +143,7 @@ public static class MaClient
if (mafile.SessionData.RefreshToken.IsExpired)
throw new SessionPermanentlyExpiredException();
if (ignoreAccessToken == false)
if (!ignoreAccessToken)
{
var access = mafile.SessionData.GetMobileToken();
if (access == null || access.Value.IsExpired)
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.MafileExport;
public class ExportResult
{
public string? Error { get; }
public Dictionary<Mafile, string>? Exported { get; }
public List<string>? NotFound { get; }
public List<string>? Conflict { get; }
[MemberNotNullWhen(true, nameof(Exported))]
[MemberNotNullWhen(true, nameof(NotFound))]
[MemberNotNullWhen(true, nameof(Conflict))]
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
private ExportResult(string? error, Dictionary<Mafile, string>? exported, List<string>? notFound,
List<string>? conflict)
{
Error = error;
Exported = exported;
NotFound = notFound;
Conflict = conflict;
}
public ExportResult(Dictionary<Mafile, string> exported, List<string> notFound, List<string> conflict)
: this(null, exported, notFound, conflict)
{
}
public ExportResult(string? error)
: this(error, null, null, null)
{
}
}
@@ -0,0 +1,17 @@
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; }
public bool ExportToZip { get; set; }
}
@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using NebulaAuth.Core;
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>();
var toExport = new List<(string key, Mafile mafile)>();
foreach (var key in keys)
{
SteamId? steamId = null;
if (SteamId64.TryParse(key, out var id64))
steamId = id64;
var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId);
if (maf != null)
toExport.Add((key, maf));
else
notFound.Add(key);
}
if (template.ExportToZip)
{
if (toExport.Count > 0)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var zipName = $"{toExport.Count}_{timestamp}.zip";
var zipPath = Path.Combine(template.Path, zipName);
await using var zipStream = File.Create(zipPath);
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
var addedEntries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var (key, maf) in toExport)
{
var (content, fileName) = BuildMafileContent(template, maf);
if (!addedEntries.Add(fileName))
{
conflict.Add(key);
continue;
}
var entry = archive.CreateEntry(fileName);
await using var entryStream = entry.Open();
await using var writer = new StreamWriter(entryStream, Encoding.UTF8);
await writer.WriteAsync(content);
exported[maf] = fileName;
}
}
}
else
{
foreach (var (key, maf) in toExport)
{
var (content, fileName) = BuildMafileContent(template, maf);
var fullPath = Path.Combine(template.Path, fileName);
if (File.Exists(fullPath))
{
conflict.Add(key);
continue;
}
await File.WriteAllTextAsync(fullPath, content);
exported[maf] = fullPath;
}
}
return new ExportResult(exported, notFound, conflict);
}
private static (string content, string fileName) BuildMafileContent(MafileExportTemplate template, Mafile mafile)
{
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);
return (serialized, fileName);
}
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,143 @@
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,21 @@ 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; }
+7 -10
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
{
@@ -83,11 +85,6 @@ public static class NebulaSerializer
public static string SerializeMafile(MobileDataExtended data, Dictionary<string, object?>? properties)
{
if (Settings.Instance.LegacyMode)
{
return MafileSerializer.SerializeLegacy(data, Serializer.Settings.SerializationOptions, properties);
}
return Serializer.Serialize(data);
return MafileSerializer.SerializeLegacy(data, Serializer.Settings.SerializationOptions, properties);
}
}
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AchiesUtilities.Collections;
using AchiesUtilities.Web.Proxy;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model;
/// <summary>
/// In-memory cache tracking which proxy IDs are currently assigned to accounts.
/// Provides fast lookup for free proxy selection without iterating all mafiles.
/// Initialized at startup and kept in sync whenever proxy assignments change.
/// </summary>
public static class ProxyAssignmentCache
{
private static readonly Dictionary<int, HashSet<string>> _proxyToAccounts = new();
public static void Initialize(IEnumerable<Mafile> mafiles)
{
_proxyToAccounts.Clear();
foreach (var maf in mafiles)
{
if (maf.Proxy != null)
AddInternal(maf.Proxy.Id, maf.AccountName);
}
}
/// <summary>
/// Call whenever a proxy assignment on a mafile changes and is persisted to disk.
/// </summary>
public static void UpdateAssignment(string? accountName, int? oldProxyId, int? newProxyId)
{
if (accountName == null) return;
if (oldProxyId.HasValue) RemoveInternal(oldProxyId.Value, accountName);
if (newProxyId.HasValue) AddInternal(newProxyId.Value, accountName);
}
public static bool IsProxyFree(int proxyId)
{
return !_proxyToAccounts.TryGetValue(proxyId, out var set) || set.Count == 0;
}
public static int GetAccountCount(int proxyId)
{
return _proxyToAccounts.TryGetValue(proxyId, out var set) ? set.Count : 0;
}
/// <summary>
/// Returns a random free proxy (not the default proxy), or null if none available.
/// </summary>
public static KeyValuePair<int, ProxyData>? GetRandomFreeProxy(
ObservableDictionary<int, ProxyData> proxies,
int? excludeDefaultProxyId)
{
var free = proxies
.Where(kvp => kvp.Key != excludeDefaultProxyId && IsProxyFree(kvp.Key))
.ToList();
if (free.Count == 0) return null;
return free[Random.Shared.Next(free.Count)];
}
private static void AddInternal(int proxyId, string accountName)
{
if (!_proxyToAccounts.TryGetValue(proxyId, out var set))
{
set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_proxyToAccounts[proxyId] = set;
}
set.Add(accountName);
}
private static void RemoveInternal(int proxyId, string accountName)
{
if (_proxyToAccounts.TryGetValue(proxyId, out var set))
set.Remove(accountName);
}
}
+12 -3
View File
@@ -22,13 +22,20 @@ public static class ProxyStorage
PatternRequirement.Optional,
PatternRequirement.Optional);
public static readonly ProxyParser SignAtScheme = new(
ProxyDefaultFormats.UniversalSignAt, false, ProxyProtocol.HTTP,
ProxyPatternProtocol.All,
ProxyPatternHostFormat.Domain | ProxyPatternHostFormat.IPv4,
PatternRequirement.Required,
PatternRequirement.Required);
public static ObservableDictionary<int, ProxyData> Proxies { get; } = new();
static ProxyStorage()
{
if (File.Exists("proxies.json") == false)
if (!File.Exists("proxies.json"))
return;
try
{
@@ -46,7 +53,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 +102,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);
+7
View File
@@ -0,0 +1,7 @@
namespace NebulaAuth.Model;
public class RemoteLinks
{
public string? Website { get; set; }
public string? Documentation { get; set; }
}
@@ -6,7 +6,9 @@ using AchiesUtilities.Web.Models;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
using NebulaAuth.View.Dialogs;
using NebulaAuth.ViewModel.Other;
using SteamLib.Exceptions.Authorization;
namespace NebulaAuth.Model;
@@ -16,13 +18,13 @@ public static partial class SessionHandler
private static readonly SemaphoreSlim Semaphore = new(1, 1);
public static async Task<T> Handle<T>(Func<Task<T>> func, Mafile mafile,
HttpClientHandlerPair? chp = null, string? snackbarPrefix = null)
HttpClientHandlerPair? chp = null, string? snackbarPrefix = null, bool allowInteractiveLogin = true)
{
chp ??= MaClient.GetHttpClientHandlerPair(mafile);
await Semaphore.WaitAsync();
try
{
return await HandleInternal(func, chp.Value, mafile, snackbarPrefix);
return await HandleInternal(func, chp.Value, mafile, snackbarPrefix, allowInteractiveLogin);
}
finally
{
@@ -31,40 +33,31 @@ public static partial class SessionHandler
}
private static async Task<T> HandleInternal<T>(Func<Task<T>> func, HttpClientHandlerPair chp, Mafile mafile,
string? snackbarPrefix = null)
string? snackbarPrefix = null, bool allowInteractiveLogin = true)
{
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 +70,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 +88,36 @@ public static partial class SessionHandler
}
}
//Nothing to do more, everything is expired
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
}
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 (allowInteractiveLogin && !DialogHost.IsDialogOpen(null))
{
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
var noProxy = mafile.Proxy == null && MaClient.DefaultProxy == null;
var currentPassword = GetPassword(mafile);
Task<LoginAgainVM?> dialogTask = null!;
Application.Current.Dispatcher.Invoke(() =>
{
return PHandler.Decrypt(mafile.Password);
dialogTask = DialogsController.ShowLoginAgainDialog(mafile.AccountName, currentPassword, noProxy);
});
var vm = await dialogTask;
if (vm != null)
{
Shell.Logger.Info("User provided credentials interactively for {name}, attempting login",
mafile.AccountName);
var logged = await LoginAgainInternal(chp, mafile, vm.Password, vm.SavePassword);
if (logged)
{
Shell.Logger.Debug("Interactive login for {name} succeeded, retrying operation",
mafile.AccountName);
return await func();
}
}
}
catch
{
// ignored
}
return null;
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG,
currentException);
}
private static async Task<bool> RefreshInternal(HttpClientHandlerPair chp, Mafile mafile)
{
try
@@ -163,6 +146,7 @@ public static partial class SessionHandler
{
Shell.Logger.Debug(ex, "Failed to relogin mafile {name} {steamid}", mafile.AccountName,
mafile.SessionData?.SteamId);
ExceptionHandler.Handle(ex);
return false;
}
finally
@@ -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,10 +28,9 @@ 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);
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
await Storage.UpdateMafileAsync(mafile);
chp.Handler.CookieContainer.SetSteamMobileCookies(mafile.SessionData);
}
public static async Task LoginAgain(HttpClientHandlerPair chp, Mafile mafile, string password, bool savePassword)
@@ -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;
}
}
+16 -6
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();
}
@@ -86,19 +91,24 @@ public partial class Settings : ObservableObject
[ObservableProperty] private Color? _iconColor;
[ObservableProperty] private bool _isPasswordSet;
[ObservableProperty] private LocalizationLanguage _language = LocalizationLanguage.English;
[ObservableProperty] private bool _legacyMode = true;
[ObservableProperty] private bool _confirmMafileImport = true;
[ObservableProperty] private bool _allowAutoUpdate;
[ObservableProperty] private bool _useAccountNameAsMafileName;
[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
}
+20 -4
View File
@@ -1,6 +1,11 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Core;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.MafileExport;
using NebulaAuth.Model.Mafiles;
using NLog;
using NLog.Extensions.Logging;
using SteamLib.Core;
@@ -14,8 +19,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 +35,21 @@ public static class Shell
try
{
TimeAligner.AlignTime();
await TimeAligner.AlignTimeAsync();
}
catch (Exception ex)
{
throw new CantAlignTimeException("", ex);
Logger.Error(ex, "Failed to align time with Steam");
TimeAligner.SetTimeDifference(0);
}
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
ProxyAssignmentCache.Initialize(Storage.MaFiles);
MAACStorage.Initialize();
MafileExporterStorage.Initialize();
_ = LinksManager.FetchAsync();
ExtensionsLogger.LogDebug("Application started");
}
-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,22 @@
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,57 @@
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}" />
+60 -7
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
@@ -10,7 +10,7 @@
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
<ApplicationIcon>Theme\lock.ico</ApplicationIcon>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AssemblyVersion>1.7.1</AssemblyVersion>
<AssemblyVersion>1.8.5</AssemblyVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@@ -28,7 +28,7 @@
<PackageReference Include="CodingSeb.Localization.WPF" Version="1.4.1" />
<PackageReference Include="CodingSebLocalization.Fody" Version="1.4.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
@@ -36,7 +36,7 @@
<ItemGroup>
<Resource Include="Theme\Background.png">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Resource>
<Resource Include="Theme\Background_Old.jpg">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
@@ -53,16 +53,69 @@
<Compile Update="View\Dialogs\LoginAgainOnImportDialog.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="View\Dialogs\SdaPasswordDialog.xaml.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="NLog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<None Update="Localization\codebehind.errors.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="localization.loc.json">
<None Update="Localization\codebehind.other.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.setpasswords.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.mafilemover.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.export.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.update.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.sdapassword.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.encryptionpassword.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.linker.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.proxymanager.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.loginagain.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.settings.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\dialogs.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\global.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\mainwindow.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\views.loc.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="NLog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Theme\Controls\" />
</ItemGroup>
</Project>

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