Compare commits

...

30 Commits

Author SHA1 Message Date
achiez 5836ed4508 feat(proxy): add SignAtScheme support for improved proxy parsing 2026-06-19 18:55:39 +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
118 changed files with 7034 additions and 2765 deletions
-78
View File
@@ -1,78 +0,0 @@
name: Publish release
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
release:
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version
id: ver
run: echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
shell: bash
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build NebulaAuth
run: dotnet publish src/NebulaAuth/NebulaAuth.csproj -c Release -o build
- name: Package release
run: |
cd build
powershell Compress-Archive * "../NebulaAuth.${env:GITHUB_REF_NAME}.zip"
shell: pwsh
- name: Install pandoc
run: |
choco install pandoc -y
- name: Convert changelog HTML to Markdown (ul-only)
id: changelog
shell: bash
run: |
version="${{ steps.ver.outputs.version }}"
file="changelog/${version}.html"
echo "Looking for file: $file"
if [ -f "$file" ]; then
echo "✅ File found, extracting <ul> section"
sed -n '/<ul>/,/<\/ul>/p' "$file" > body.html
echo "Converting with pandoc..."
pandoc body.html -f html -t markdown --wrap=none -o changelog.md
echo "-------- Converted changelog.md --------"
cat changelog.md
echo "----------------------------------------"
echo "body<<EOF" >> $GITHUB_OUTPUT
cat changelog.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "❌ Changelog not found for version $version"
echo "body=No changelog found for $version" >> $GITHUB_OUTPUT
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: Release ${{ env.RELEASE_NAME }}
body: ${{ steps.changelog.outputs.body }}
files: |
NebulaAuth.${{ steps.ver.outputs.version }}.zip
env:
RELEASE_NAME: ${{ steps.ver.outputs.version }}
-65
View File
@@ -1,65 +0,0 @@
name: Prepare release
on:
push:
branches: [ master ]
paths:
- 'src/NebulaAuth/NebulaAuth.csproj'
workflow_dispatch:
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_TOKEN }}
- name: Read version from csproj
id: ver
shell: bash
run: |
VERSION=$(grep -oPm1 "(?<=<AssemblyVersion>)[^<]+" src/NebulaAuth/NebulaAuth.csproj)
echo "Detected version $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Generate update.xml
run: |
cat > NebulaAuth/update.xml <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>${{ steps.ver.outputs.version }}</version>
<url>https://github.com/${{ github.repository }}/releases/download/${{ steps.ver.outputs.version }}/NebulaAuth.${{ steps.ver.outputs.version }}.zip</url>
<changelog>https://achiez.github.io/${{ github.repository }}/changelog/${{ steps.ver.outputs.version }}.html</changelog>
<mandatory>false</mandatory>
</item>
EOF
echo "Generated NebulaAuth/update.xml:"
cat NebulaAuth/update.xml
- name: Check if update.xml changed
id: diff
run: |
if git diff --exit-code NebulaAuth/update.xml >/dev/null 2>&1; then
echo "no_changes=true" >> $GITHUB_OUTPUT
else
echo "no_changes=false" >> $GITHUB_OUTPUT
fi
- name: Commit and tag
if: steps.diff.outputs.no_changes == 'false'
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git add NebulaAuth/update.xml
git commit -m "chore(release): ${{ steps.ver.outputs.version }}"
git tag -a "${{ steps.ver.outputs.version }}" -m "Release ${{ steps.ver.outputs.version }}"
git push origin HEAD:master --tags
- name: Skip message
if: steps.diff.outputs.no_changes == 'true'
run: echo "No changes in version or update.xml — skipping release preparation."
+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
-60
View File
@@ -1,60 +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
changelog\1.7.2.html = changelog\1.7.2.html
changelog\1.7.3.html = changelog\1.7.3.html
changelog\1.7.4.html = changelog\1.7.4.html
changelog\1.8.0.html = changelog\1.8.0.html
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
+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>
+5 -4
View File
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.8.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.0/NebulaAuth.1.8.0.zip</url>
<changelog>https://achiez.github.io/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.0.html</changelog>
<item>
<version>1.8.4</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.4/NebulaAuth.1.8.4.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.4.html</changelog>
<mandatory>false</mandatory>
<checksum algorithm="SHA256">5ecad7a711bab7e98f5f3bfa92b7a00e21efc18aa92251bb30bd3e50a2f7d2f2</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 six languages**: English, Russian, Ukrainian, Spanish, Turkish and Kazakh.
- **Full functionality of Steam Desktop Authenticator** reimagining [old app](https://github.com/Jessecar96/SteamDesktopAuthenticator)
- **Proxy support** in all account work processes.
- **Mafile grouping** for improved management.
- **Automatic confirmations of trades/market actions** to save time.
- **Bulk import of .mafiles** via Drag'n'Drop or CTRL+V for convenience.
- **Bulk import of .mafiles** via Drag'n'Drop or Ctrl+V, including SDA-encrypted mafiles with automatic manifest detection.
- **Design customization** to personalize the interface.
- **Ability to confirm account login without entering a code** for easier access.
- **Auto-update** program to use the latest features.
- **Auto-update** with SHA256 checksum verification, changelog viewer and flexible update options.
- **Automatic relogin in case of problems with the session** for continuous operation.
- **Intuitive interface** with tips and conveniences
- **Continious support** of application code and other features.
- **Intuitive interface** with tips and conveniences.
- **Continuous support** of application code and other features.
## Documentation
You can find the documentation for the application [here](https://achiefy-project.gitbook.io/nebulaauth). Currently, only the Russian version is available, but the English version will be released soon.
## Installation
1. If the application does not start, you need to install [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
@@ -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).
+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"
}
]
}
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>net8.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();
}
+2 -7
View File
@@ -3,7 +3,6 @@ using System.Windows;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
namespace NebulaAuth;
@@ -16,12 +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 threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
MAACStorage.Initialize();
await Shell.Initialize();
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;
mainWindow.Show();
+1 -1
View File
@@ -12,7 +12,6 @@
<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" />
@@ -23,6 +22,7 @@
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
<system:Boolean x:Key="True">True</system:Boolean>
<system:Boolean x:Key="False">False</system:Boolean>
@@ -1,25 +1,19 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace NebulaAuth.Converters;
public class PortableMaClientStatusToColorConverter : IValueConverter
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is false)
{
return new SolidColorBrush(Color.FromRgb(187, 224, 139));
}
return new SolidColorBrush(Color.FromRgb(224, 139, 139));
return value is bool b && !b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
return value is Visibility v && v != Visibility.Visible;
}
}
+44 -1
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;
@@ -51,6 +53,26 @@ public static class DialogsController
return null;
}
/// <summary>
/// Shows the SDA encryption password dialog for unlocking SDA-encrypted mafiles.
/// </summary>
/// <returns>The entered password if user clicked OK, otherwise null.</returns>
public static async Task<string?> ShowSdaPasswordDialog()
{
var vm = new SdaPasswordDialogVM();
var content = new SdaPasswordDialog
{
DataContext = vm
};
var result = await DialogHost.Show(content);
if (result is true)
{
return vm.Password;
}
return null;
}
public static async Task<bool> ShowProxyManager()
{
var vm = new ProxyManagerVM();
@@ -113,6 +135,27 @@ public static class DialogsController
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)
+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
}
+1 -1
View File
@@ -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();
}
+108 -56
View File
@@ -1,16 +1,38 @@
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/NebulaAuth/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;
@@ -18,6 +40,89 @@ public static class UpdateManager
AutoUpdater.Start(UPDATE_URL);
}
public static void SkipVersion(string version)
{
var settings = UpdateSettings.Load();
settings.SkipVersion(version);
}
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;
if (args.Error != null)
{
if (isManual)
{
Application.Current.Dispatcher.Invoke(() =>
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Request error",
"RequestError")));
}
return;
}
if (!args.IsUpdateAvailable)
{
if (isManual)
{
Application.Current.Dispatcher.Invoke(() =>
SnackbarController.SendSnackbar(
LocManager.GetCodeBehindOrDefault("You are using the latest version", "UpdateService",
"LatestVersion")));
}
return;
}
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
}
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
@@ -35,57 +140,4 @@ public static class UpdateManager
return true;
}
}
//static UpdateManager()
//{
// //AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
//}
//private static async void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
//{
// if (args.Error == null)
// {
// if (args.IsUpdateAvailable)
// {
// DialogResult dialogResult;
// var dialog = new UpdaterView()
// {
// DataContext = new UpdaterVM(args)
// };
// await DialogHost.Show(dialog);
// Application.Current.Shutdown();
// }
// else
// {
// }
// }
// else
// {
// if (args.Error is WebException)
// {
// }
// else
// {
// }
// }
//}
//private static void RunUpdate(UpdateInfoEventArgs args)
//{
// Application.Current.Dispatcher.Invoke(() =>
// {
// if (AutoUpdater.DownloadUpdate(args))
// {
// Application.Current.Shutdown();
// }
// }, DispatcherPriority.ContextIdle);
//}
}
@@ -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,304 @@
{
"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": "Топ"
}
},
"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 үйлесімді топ деректерін қосу."
}
},
"ExportAccountsPlaceholder": {
"ru": "Введите логины аккаунтов или SteamID (по одному на строку) для экспорта в выбранную директорию.",
"en": "Enter account logins or SteamIDs (one per line) to export to the selected directory.",
"uk": "Введіть логіни акаунтів або SteamID (по одному на рядок) для експорту у вибрану директорію.",
"fr": "Entrez les identifiants de compte ou les SteamID (un par ligne) pour exporter vers le répertoire sélectionné.",
"zh": "输入要导出到所选目录的账户登录名或 SteamID(每行一个)。",
"es": "Introduce los logins de las cuentas o SteamID (uno por línea) para exportarlos al directorio seleccionado.",
"tr": "Seçilen dizine dışa aktarmak için hesap girişlerini veya SteamIDleri (her satıra bir tane) girin.",
"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,39 @@
{
"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": "Мән енгізіңіз"
}
}
}
@@ -0,0 +1,84 @@
{
"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' пернесін басыңыз"
}
}
}
@@ -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 set on this account. To link mafile to the account, use the 'Link' window",
"zh": "此账户已启用令牌。要将 mafile 绑定到此账户,请使用“绑定”窗口。",
"uk": "Steam Guard встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'",
"fr": "Steam Guard est configuré sur ce compte. Pour lier mafile au compte, utilisez la fenêtre 'Lien'",
"es": "Steam Guard está activado en esta cuenta. Para vincular el mafile a la cuenta, usa la ventana 'Vincular'",
"tr": "Steam Guard bu hesapta zaten etkin. Mafile'ı hesaba bağlamak için 'Bağla' penceresini kullanın",
"kk": "Бұл аккаунтта Steam Guard орнатылған. Mafile-ды аккаунтқа байланыстыру үшін 'Байланыстыру' терезесін пайдаланыңыз"
},
"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,98 @@
{
"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": "Прокси қате форматта"
}
}
},
"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": "Тіркелгі деректерін көрсету"
}
}
}
@@ -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": "Дискке сақталмайды"
}
},
"LegacyMafileMode": {
"en": "Common mafiles format",
"ru": "Общепринятый формат мафайлов",
"zh": "常见的邮件文件格式",
"uk": "Загальноприйнятий формат мафайлів",
"fr": "Format de mafiles commun",
"es": "Formato común de mafiles",
"tr": "Ortak mafile formatı",
"kk": "Жалпы mafile форматы"
},
"LegacyMafileModeHint": {
"en": "Save mafiles in compatibility format with Steam Desktop Authenticator and other applications",
"ru": "Сохранять мафайлы в старом формате для совместимости со Steam Desktop Authenticator и другими приложениями",
"zh": "以与 Steam 桌面认证器及其他应用程序兼容的格式保存 mafiles",
"uk": "Зберігати мафайли у старому форматі для сумісності зі Steam Desktop Authenticator та іншими додатками",
"fr": "Enregistrer les mafiles dans un format compatible avec Steam Desktop Authenticator et d'autres applications",
"es": "Guardar mafiles en formato compatible con Steam Desktop Authenticator y otras aplicaciones",
"tr": "Mafile'ları Steam Desktop Authenticator ve diğer uygulamalarla uyumlu formatta kaydet",
"kk": "Mafile файлдарын Steam Desktop Authenticator және басқа қолданбалармен үйлесімді форматта сақтау"
},
"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": "Растау қатесі"
},
"ConfirmMafileOverwrite": {
"ru": "Внимание, мафайл(-ы) уже присутствует в папке mafiles.\r\nПерезаписать мафайлы, которые конфликтуют?",
"en": "Attention, mafile(-s) already exists in mafiles folder.\r\nOverwrite conflicting mafiles?",
"zh": "注意,mafile 已经存在于 mafiles 文件夹中。\r\n覆盖冲突的 mafiles 吗?",
"uk": "Увага, мафайл(-и) вже присутній у папці mafiles.\r\nПерезаписати мафайли, які конфліктують?",
"fr": "Attention, mafiles déjà présents dans le dossier mafiles.\r\nRemplacer les mafiles en conflit?",
"es": "Atención, los mafiles ya existen en la carpeta mafiles.\r\n¿Sobrescribir los mafiles en conflicto?",
"tr": "Dikkat, mafile(ler) zaten mafiles klasöründe mevcut.\r\nÇakışan mafile'lar üzerine yazılsın mı?",
"kk": "Назар аударыңыз, mafile(дер) mafiles қалтасында бар.\r\nҚайшылық тудырған mafile файлдарының үстінен жазу керек пе?"
},
"MafileImportError": {
"ru": "Ошибка при импорте мафайла",
"en": "Mafile import error",
"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": "Проксиді ажырату"
}
},
"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": "Жаңартуларды тексеру"
}
}
}
+67 -23
View File
@@ -73,7 +73,7 @@
Kind="FileReplaceOutline" />
</StackPanel>
<ToolBarTray>
<ToolBarTray Grid.Row="0">
<ToolBar ClipToBounds="True" HorizontalContentAlignment="Stretch"
Style="{StaticResource MaterialDesignToolBar}" FontSize="16">
<Menu FontSize="16">
@@ -92,7 +92,9 @@
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.Title}">
<MenuItem Header="{Tr MainWindow.Menu.File.Other.SetPasswords}"
Command="{Binding DebugCommand}" />
Command="{Binding OpenSetPasswordsDialogCommand}" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.OpenExport}"
Command="{Binding OpenExporterDialogCommand}" />
</MenuItem>
</MenuItem>
</Menu>
@@ -292,18 +294,42 @@
<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">
@@ -356,7 +382,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<md:PackIcon Kind="PlusCircle"
VerticalAlignment="Center" />
VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center"
Margin="5,0,0,0" Grid.Column="1"
Text="{Tr MainWindow.ContextMenus.Mafile.CreateGroup}" />
@@ -391,7 +417,9 @@
<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.UnattachProxy}"
Command="{Binding RemoveProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</ContextMenu>
</FrameworkElement.ContextMenu>
@@ -491,16 +519,32 @@
Text="{Binding SelectedMafile.AccountName, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
<!--<Button Command="{Binding DebugCommand}">Debug</Button>-->
</ToolBarPanel>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
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}" />
+45 -5
View File
@@ -4,6 +4,8 @@ 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;
@@ -25,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);
}
@@ -46,7 +86,7 @@ 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);
}
@@ -79,12 +119,12 @@ public partial class MainWindow
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;
@@ -96,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)
-1
View File
@@ -28,7 +28,6 @@ public partial class Mafile : MobileDataExtended
}
private string? _filename;
[JsonIgnore] private PortableMaClient? _linkedClient;
public void SetSessionData(MobileSessionData? sessionData)
@@ -0,0 +1,177 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using SteamLib.Exceptions.Authorization;
namespace NebulaAuth.Model.MAAC;
public static class MAACRequestHandler
{
private const string LOC_PATH = "MAAC";
private static readonly ConcurrentDictionary<Mafile, PortableMaClientErrorData> _errors = new();
public static void Register(Mafile mafile)
{
_errors[mafile] = new PortableMaClientErrorData();
}
public static void Unregister(Mafile mafile)
{
_errors.TryRemove(mafile, out _);
}
public static PortableMaClientStatus ClearErrors(Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data))
{
data.Clear();
}
return PortableMaClientStatus.Ok();
}
/// <summary>
/// Handles a MAAC request with progressive error handling strategy: retries and status management based on error
/// history.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="client"></param>
/// <param name="func"></param>
/// <returns></returns>
public static async Task<T> HandleRequest<T>(PortableMaClient client, Func<Task<T>> func)
{
var result = await client.DoRequest(func, client.Mafile.PreviouslyHadNoErrors());
if (result.IsSuccess)
{
InformRequestSuccessful(client.Mafile);
return result.Data;
}
if (client.Mafile.PreviouslyHadNoErrors())
{
client.SetStatus(PortableMaClientStatus.Warning(GetPortableMaClientStatus("SessionError")));
}
else if (client.Mafile.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval))
{
Shell.Logger.Info("Retrying MAAC request for {name} MAAC account after previous error.",
client.Mafile.AccountName);
result = await client.DoRequest(func);
if (result.IsSuccess)
{
InformRequestSuccessful(client.Mafile);
return result.Data;
}
}
if (client.Mafile.ErrorPersistedFor(Settings.Instance.MaacErrorThreshold))
{
client.SetStatus(PortableMaClientStatus.Error(GetPortableMaClientStatus("SessionError")));
}
AddError(client.Mafile, result.Exception!);
throw result.Exception!;
}
private static async Task<Result<T>> DoRequest<T>(this PortableMaClient client, Func<Task<T>> req,
bool withSessionHandler = true)
{
try
{
return withSessionHandler
? Result<T>.Success(await SessionHandler.Handle(req, client.Mafile, client.Chp(),
GetTimerPrefix(client.Mafile)))
: Result<T>.Success(await req());
}
catch (SessionInvalidException ex)
{
Shell.Logger.Warn("SessionInvalidException caught in MAAC request handler.");
return Result<T>.Error(ex);
}
}
private static void InformRequestSuccessful(Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data) && data.Clear())
{
Shell.Logger.Info("MAAC request for {name} MAAC account succeeded, error history cleared",
mafile.AccountName);
}
var client = mafile.LinkedClient;
if (client != null && client.Status.StatusType != PortableMaClientStatusType.Ok)
{
client.SetStatus(PortableMaClientStatus.Ok());
}
}
private static void AddError(Mafile mafile, Exception ex)
{
if (_errors.TryGetValue(mafile, out var data))
{
Shell.Logger.Info("Registering error for {name} MAAC account: {ex}", mafile.AccountName, ex.Message);
data.AddEntry(ex);
}
}
private static bool ErrorPersistedFor(this Mafile mafile, TimeSpan span)
{
if (_errors.TryGetValue(mafile, out var data))
{
var oldestError = data.GetOldestErrorTime();
if (oldestError.HasValue && DateTime.UtcNow - oldestError.Value > span)
{
return true;
}
}
return false;
}
private static bool PreviouslyHadNoErrors(this Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data))
{
return data.NoErrors;
}
return true;
}
private static bool LastErrorWasAtLeast(this Mafile mafile, TimeSpan timeAgo)
{
if (!_errors.TryGetValue(mafile, out var data)) return false;
var latestError = data.GetTimeFromLastError();
if (latestError.HasValue && latestError.Value > timeAgo)
{
return true;
}
return false;
}
public static bool IsReady(Mafile maf)
{
if (maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Ok}) return true;
return maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Warning}
&& maf.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval);
}
private static string GetTimerPrefix(Mafile mafile)
{
return GetLocalization("TimerPrefix") + mafile.AccountName + ": ";
}
private static string GetLocalization(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
}
private static string GetPortableMaClientStatus(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, "PortableMaClientStatus", key);
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MAAC;
@@ -93,6 +94,7 @@ public static class MAACStorage
public static void Initialize()
{
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
if (!File.Exists("maac.json")) return;
try
{
@@ -107,6 +109,7 @@ public static class MAACStorage
{
mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket;
mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades;
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
Clients[fileName] = storedClient;
}
}
@@ -117,8 +120,6 @@ public static class MAACStorage
SnackbarController.SendSnackbar(
LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage"));
}
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
}
public static void NotifyMafilesRenamed(IDictionary<string, string> oldNewNames)
@@ -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,6 +114,21 @@ 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)
{
@@ -122,6 +137,7 @@ public static class MultiAccountAutoConfirmer
if (Clients.Contains(mafile)) return false;
mafile.LinkedClient = new PortableMaClient(mafile);
Clients.Add(mafile);
MAACRequestHandler.Register(mafile);
return true;
});
}
@@ -134,6 +150,7 @@ public static class MultiAccountAutoConfirmer
Clients.Remove(mafile);
mafile.LinkedClient?.Dispose();
mafile.LinkedClient = null;
MAACRequestHandler.Unregister(mafile);
});
}
+22 -95
View File
@@ -11,11 +11,9 @@ using AchiesUtilities.Web.Proxy;
using CommunityToolkit.Mvvm.ComponentModel;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
using SteamLib.Api.Mobile;
using SteamLib.Api.Trade;
using SteamLib.Authentication;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile.Confirmations;
using SteamLib.Web;
using SteamLibForked.Abstractions;
@@ -34,7 +32,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
[ObservableProperty] private bool _autoConfirmMarket;
[ObservableProperty] private bool _autoConfirmTrades;
[ObservableProperty] private bool _isError;
[ObservableProperty] private PortableMaClientStatus _status = PortableMaClientStatus.Ok();
public PortableMaClient(Mafile mafile)
{
@@ -46,6 +44,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
ClientHandler = pair.Handler;
UpdateCookies(mafile.SessionData);
Mafile.PropertyChanged += Mafile_PropertyChanged;
SetStatus(PortableMaClientStatus.Ok());
}
private void Mafile_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -56,37 +55,21 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
}
private void UpdateCookies(IMobileSessionData? sessionData)
{
Application.Current.Dispatcher.Invoke(() => IsError = sessionData == null);
var newStatus = MAACRequestHandler.ClearErrors(Mafile);
if (!newStatus.Equals(Status))
SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies();
if (sessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
public async Task<int> Confirm()
{
Proxy.SetData(Mafile.Proxy?.Data ?? MaClient.DefaultProxy);
List<Confirmation> conf;
try
{
conf = (await HandleTimerRequest(GetConfirmations)).ToList();
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: Error GetConf in timer.", Mafile.AccountName);
return 0;
}
var conf = (await GetConfirmations()).ToList();
var toConfirm = new List<Confirmation>();
if (AutoConfirmMarket)
@@ -103,20 +86,12 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
if (toConfirm.Count == 0) return 0;
try
{
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await HandleTimerRequest(() => SendConfirmations(toConfirm));
//TODO: handle success == false
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: MultiConf error in Timer.", Mafile.AccountName);
return 0;
}
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await SendConfirmations(toConfirm);
//TODO: handle success == false
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
@@ -148,58 +123,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
Mafile.SessionData!.SteamId, Mafile, true, _cts.Token);
}
private async Task<T> HandleTimerRequest<T>(Func<Task<T>> func)
{
Exception innerException;
try
{
return await SessionHandler.Handle(func, Mafile, Chp(), GetTimerPrefix());
}
catch (OperationCanceledException ex)
{
innerException = ex; //Ignored
}
catch (SessionInvalidException ex)
{
if (IgnoreTimerErrors())
{
Shell.Logger.Warn(
"Timer {accountName}: Session error while requesting in timer. Ignored due to IgnorePatchTuesdayErrors setting",
Mafile.AccountName);
}
else
{
Shell.Logger.Warn("Timer {accountName}: Session error while requesting in timer. Timer disabled",
Mafile.AccountName);
SetError();
}
innerException = ex;
}
catch (Exception ex) when (ExceptionHandler.Handle(ex, GetTimerPrefix()))
{
innerException = ex;
}
throw new ApplicationException("Swallowed", innerException);
}
private bool IgnoreTimerErrors()
{
if (Settings.Instance.IgnorePatchTuesdayErrors == false) return false;
var pstZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var pstNow = TimeZoneInfo.ConvertTime(DateTime.UtcNow, pstZone);
var startTime = pstNow.Date.AddHours(15).AddMinutes(55); // 15:55 PST //22:55 GMT //00:55 GMT+2
var endTime = pstNow.Date.AddHours(17).AddMinutes(15); // 17:15 PST //00:15 GMT //02:15 GMT+2
return pstNow.DayOfWeek == DayOfWeek.Tuesday && pstNow >= startTime && pstNow <= endTime;
}
private HttpClientHandlerPair Chp()
public HttpClientHandlerPair Chp()
{
return new HttpClientHandlerPair(Client, ClientHandler);
}
@@ -214,11 +138,14 @@ public partial class PortableMaClient : ObservableObject, IDisposable
return GetLocalization("TimerPrefix") + Mafile.AccountName + ": ";
}
public void SetError()
public void SetStatus(PortableMaClientStatus status)
{
Application.Current.Dispatcher.Invoke(() => IsError = true);
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + GetLocalization("TimerSessionError"));
Application.Current.Dispatcher.Invoke(() => Status = status);
if (status.StatusType == PortableMaClientStatusType.Error)
{
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + status.Message);
}
}
public void Dispose()
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NebulaAuth.Model.MAAC;
public sealed class PortableMaClientErrorData
{
public SortedDictionary<DateTime, Exception> Values { get; } = new();
public bool NoErrors => Values.Count == 0;
private readonly object _lock = new();
public DateTime? GetOldestErrorTime()
{
if (Values.Count == 0) return null;
return Values.Keys.First();
}
public void AddEntry(Exception ex)
{
lock (_lock)
Values[DateTime.UtcNow] = ex;
}
public bool Clear()
{
if (NoErrors) return false;
lock (_lock)
Values.Clear();
return true;
}
public TimeSpan? GetTimeFromLastError()
{
if (Values.Count == 0) return null;
var lastEntry = Values.Keys.Last();
return DateTime.UtcNow - lastEntry;
}
}
@@ -0,0 +1,26 @@
namespace NebulaAuth.Model.MAAC;
public record PortableMaClientStatus(PortableMaClientStatusType StatusType, string? Message = null)
{
public static PortableMaClientStatus Ok()
{
return new PortableMaClientStatus(PortableMaClientStatusType.Ok);
}
public static PortableMaClientStatus Warning(string? message)
{
return new PortableMaClientStatus(PortableMaClientStatusType.Warning, message);
}
public static PortableMaClientStatus Error(string? message)
{
return new PortableMaClientStatus(PortableMaClientStatusType.Error, message);
}
}
public enum PortableMaClientStatusType
{
Ok,
Warning,
Error
}
+29
View File
@@ -0,0 +1,29 @@
using System;
namespace NebulaAuth.Model.MAAC;
public struct Result<T>
{
public bool IsSuccess { get; }
public T Data => !IsSuccess ? throw new InvalidOperationException("No data available") : _data!;
public Exception? Exception { get; }
private readonly T? _data;
private Result(bool success, T? data, Exception? exception)
{
IsSuccess = success;
_data = data;
Exception = exception;
}
public static Result<T> Success(T data)
{
return new Result<T>(true, data, null);
}
public static Result<T> Error(Exception ex)
{
return new Result<T>(false, default, ex);
}
}
+2 -12
View File
@@ -38,17 +38,7 @@ public static class MaClient
{
ClientHandler.CookieContainer.ClearAllCookies();
if (account == null) return;
if (account.SessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
Proxy.SetData(account.Proxy?.Data);
}
@@ -153,7 +143,7 @@ public static class MaClient
if (mafile.SessionData.RefreshToken.IsExpired)
throw new SessionPermanentlyExpiredException();
if (ignoreAccessToken == false)
if (!ignoreAccessToken)
{
var access = mafile.SessionData.GetMobileToken();
if (access == null || access.Value.IsExpired)
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.MafileExport;
public class ExportResult
{
public string? Error { get; }
public Dictionary<Mafile, string>? Exported { get; }
public List<string>? NotFound { get; }
public List<string>? Conflict { get; }
[MemberNotNullWhen(true, nameof(Exported))]
[MemberNotNullWhen(true, nameof(NotFound))]
[MemberNotNullWhen(true, nameof(Conflict))]
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
private ExportResult(string? error, Dictionary<Mafile, string>? exported, List<string>? notFound,
List<string>? conflict)
{
Error = error;
Exported = exported;
NotFound = notFound;
Conflict = conflict;
}
public ExportResult(Dictionary<Mafile, string> exported, List<string> notFound, List<string> conflict)
: this(null, exported, notFound, conflict)
{
}
public ExportResult(string? error)
: this(error, null, null, null)
{
}
}
@@ -0,0 +1,16 @@
namespace NebulaAuth.Model.MafileExport;
public class MafileExportTemplate
{
public string Name { get; set; }
public bool UseLoginAsMafileName { get; set; }
public bool IncludeSharedSecret { get; set; }
public bool IncludeIdentitySecret { get; set; }
public bool IncludeRCode { get; set; }
public bool IncludeSessionData { get; set; }
public bool IncludeOtherInfo { get; set; }
public bool IncludeNebulaProxy { get; set; }
public bool IncludeNebulaPassword { get; set; }
public bool IncludeNebulaGroup { get; set; }
public string? Path { get; set; }
}
@@ -0,0 +1,166 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLibForked.Models.Session;
using SteamLibForked.Models.SteamIds;
namespace NebulaAuth.Model.MafileExport;
public static class MafileExporter
{
public static async Task<ExportResult> ExportMafiles(IEnumerable<string> keys, MafileExportTemplate template)
{
if (template.Path == null || !PathIsValid(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("InvalidPath", "MafileExporter.InvalidPath"));
}
if (!Directory.Exists(template.Path))
{
Directory.CreateDirectory(template.Path);
}
if (IsNebulaUtilizedDirectory(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("NebulaUtilizedDirectory",
"MafileExporter.NebulaUtilizedDirectory"));
}
if (!IsAllowedToWrite(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("AccessDenied", "MafileExporter.AccessDenied"));
}
var exported = new Dictionary<Mafile, string>();
var notFound = new List<string>();
var conflict = new List<string>();
foreach (var key in keys)
{
SteamId? steamId = null;
if (SteamId64.TryParse(key, out var id64))
{
steamId = id64;
}
var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId);
if (maf != null)
{
var fileName = await ExportMafile(template.Path, template, maf);
if (fileName == null)
{
conflict.Add(key);
continue;
}
exported[maf] = fileName;
}
else
{
notFound.Add(key);
}
}
return new ExportResult(exported, notFound, conflict);
}
private static async Task<string?> ExportMafile(string path, MafileExportTemplate template, Mafile mafile)
{
// We preserve SteamId even if other info is not included
var session = template.IncludeSessionData
? mafile.SessionData
: new MobileSessionData(null!, mafile.SteamId, default, null, tokens: null);
var serializeMaf = new Mafile
{
SharedSecret = template.IncludeSharedSecret ? mafile.SharedSecret : null!,
IdentitySecret = template.IncludeIdentitySecret ? mafile.IdentitySecret : null!,
DeviceId = template.IncludeIdentitySecret ? mafile.DeviceId : null!,
RevocationCode = template.IncludeRCode ? mafile.RevocationCode : null!,
AccountName = mafile.AccountName,
SessionData = session,
SteamId = mafile.SteamId,
ServerTime = template.IncludeOtherInfo ? mafile.ServerTime : 0,
SerialNumber = template.IncludeOtherInfo ? mafile.SerialNumber : 0,
Uri = template.IncludeOtherInfo ? mafile.Uri : null!,
TokenGid = template.IncludeOtherInfo ? mafile.TokenGid : null!,
Secret1 = template.IncludeOtherInfo ? mafile.Secret1 : null!,
Proxy = template.IncludeNebulaProxy ? mafile.Proxy : null!,
Group = template.IncludeNebulaGroup ? mafile.Group : null!,
Password = template.IncludeNebulaPassword ? mafile.Password : null!,
LinkedClient = null,
Filename = null
};
var serialized = NebulaSerializer.SerializeMafile(serializeMaf);
var strategy = template.UseLoginAsMafileName ? MafileNamingStrategy.Login : MafileNamingStrategy.SteamId;
var fileName = strategy.GetMafileName(serializeMaf);
var fullPath = Path.Combine(path, fileName);
if (File.Exists(fullPath))
{
return null;
}
await File.WriteAllTextAsync(fullPath, serialized);
return fullPath;
}
private static bool PathIsValid(string path)
{
if (string.IsNullOrWhiteSpace(path)) return false;
try
{
var full = Path.GetFullPath(path);
var invalidPathChars = Path.GetInvalidPathChars();
if (full.IndexOfAny(invalidPathChars) >= 0) return false;
var invalidFileChars = Path.GetInvalidFileNameChars();
foreach (var part in full.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
{
if (string.IsNullOrEmpty(part)) continue;
if (part is [_, ':']) continue;
if (part.IndexOfAny(invalidFileChars) >= 0) return false;
}
return true;
}
catch
{
return false;
}
}
private static bool IsAllowedToWrite(string path)
{
try
{
var testFilePath = Path.Combine(path, "test.tmp");
using (var fs = File.Create(testFilePath))
{
}
File.Delete(testFilePath);
return true;
}
catch
{
return false;
}
}
private static bool IsNebulaUtilizedDirectory(string path)
{
return Storage.MafilesDirectories.Any(x => Path.GetFullPath(path).EqualsIgnoreCase(x));
}
}
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MafileExport;
public static class MafileExporterStorage
{
private const string FILE_NAME = "export_templates.json";
public static ObservableCollection<MafileExportTemplate> Templates { get; } = new();
public static void Initialize()
{
LoadOrCreateTemplate();
}
public static MafileExportTemplate? GetTemplate(string name)
{
return Templates.FirstOrDefault(x => x.Name == name);
}
public static void AddTemplate(MafileExportTemplate template)
{
Templates.Add(template);
Save();
}
public static void DeleteTemplate(MafileExportTemplate template)
{
if (Templates.Remove(template))
{
Save();
}
}
private static void LoadOrCreateTemplate()
{
if (!File.Exists(FILE_NAME))
{
Templates.Clear();
foreach (var mafileExportTemplate in CreateDefaultTemplate())
{
Templates.Add(mafileExportTemplate);
}
Save();
return;
}
var json = File.ReadAllText(FILE_NAME);
try
{
var loadedTemplates = JsonConvert.DeserializeObject<List<MafileExportTemplate>>(json);
if (loadedTemplates != null)
{
Templates.Clear();
foreach (var tmpl in loadedTemplates)
{
Templates.Add(tmpl);
}
}
}
catch (Exception ex)
{
Shell.Logger.Error(ex, $"Failed to deserialize {FILE_NAME}");
Templates.Clear();
Save();
}
}
public static void Save()
{
var json = JsonConvert.SerializeObject(Templates, Formatting.Indented);
File.WriteAllText(FILE_NAME, json);
}
private static IEnumerable<MafileExportTemplate> CreateDefaultTemplate()
{
return
[
new MafileExportTemplate
{
Name = "Default",
UseLoginAsMafileName = false,
IncludeSharedSecret = true,
IncludeIdentitySecret = true,
IncludeRCode = true,
IncludeSessionData = true,
IncludeOtherInfo = true,
IncludeNebulaProxy = true,
IncludeNebulaPassword = true,
IncludeNebulaGroup = true,
Path = null
},
new MafileExportTemplate
{
Name = "Auth only",
UseLoginAsMafileName = true,
IncludeSharedSecret = true,
IncludeIdentitySecret = false,
IncludeRCode = false,
IncludeSessionData = false,
IncludeOtherInfo = false,
IncludeNebulaProxy = false,
IncludeNebulaPassword = false,
IncludeNebulaGroup = false,
Path = null
}
];
}
}
@@ -0,0 +1,8 @@
namespace NebulaAuth.Model.Mafiles;
public enum AddMafileResult
{
Added,
AlreadyExist,
Error
}
@@ -0,0 +1,50 @@
using System;
namespace NebulaAuth.Model.Mafiles;
public class MafileImportSummary
{
public int Added { get; private set; }
public int NotAdded { get; private set; }
public int Errors { get; private set; }
public void AddedOne()
{
Added++;
}
public void SkippedOne()
{
NotAdded++;
}
public void ErrorOne()
{
Errors++;
}
public void Add(int added = 0, int notAdded = 0, int errors = 0)
{
Added += added;
NotAdded += notAdded;
Errors += errors;
}
public void Apply(AddMafileResult result)
{
switch (result)
{
case AddMafileResult.Added:
AddedOne();
break;
case AddMafileResult.AlreadyExist:
SkippedOne();
break;
case AddMafileResult.Error:
ErrorOne();
break;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
@@ -0,0 +1,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);
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -11,24 +11,30 @@ using AchiesUtilities.Extensions;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.Mafiles;
using SteamLib;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile;
namespace NebulaAuth.Model;
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);
@@ -67,25 +73,46 @@ public static class Storage
/// </summary>
/// <param name="path"></param>
/// <param name="overwrite"></param>
/// <exception cref="FormatException"></exception>
/// <returns> Result of adding mafile</returns>
/// <exception cref="SessionInvalidException"></exception>
/// <exception cref="IOException"></exception>
public static void AddNewMafile(string path, bool overwrite)
public static async Task<AddMafileResult> AddNewMafile(string path, bool overwrite)
{
Mafile data;
try
{
data = ReadMafile(path);
data = await ReadMafileAsync(path);
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
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))
throw new FormatException("File data is not valid. Missing AccountName");
{
Shell.Logger.Warn("Mafile account name is empty");
return AddMafileResult.Error;
}
try
{
@@ -93,22 +120,19 @@ public static class Storage
}
catch (Exception ex)
{
throw new FormatException("Can't generate code on this mafile", ex);
Shell.Logger.Error(ex, "Can't generate code for mafile {accountName}", data.AccountName);
return AddMafileResult.Error;
}
if (overwrite == false && File.Exists(GetOrCreateMafilePath(data)))
data.Filename = null;
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
{
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
return AddMafileResult.AlreadyExist;
}
SaveMafile(data);
}
public static Mafile ReadMafile(string path)
{
var str = File.ReadAllText(path);
return NebulaSerializer.Deserialize(str, path);
await SaveMafileAsync(data);
return AddMafileResult.Added;
}
public static async Task<Mafile> ReadMafileAsync(string path)
@@ -117,66 +141,52 @@ public static class Storage
return NebulaSerializer.Deserialize(str, path);
}
public static void SaveMafile(Mafile data)
{
var path = GetOrCreateMafilePath(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 async Task SaveMafileAsync(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
if (existed != null)
// Replace if exist
for (var i = 0; i < MaFiles.Count; i++)
{
var index = MaFiles.IndexOf(existed);
MaFiles[index] = data;
}
else
{
MaFiles.Add(data);
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 = GetOrCreateMafilePath(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 = GetOrCreateMafilePath(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 = GetOrCreateMafilePath(data);
var destinationPath = Path.Combine(DIR_REMOVED_MAFILES, data.Filename + ".mafile");
var destinationPathFinal = destinationPath;
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 = destinationPath + $" ({i})";
destinationPathFinal = destinationPathBase + $" ({i})";
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
}
File.Copy(sourcePath, destinationPathFinal, false);
@@ -184,24 +194,6 @@ public static class Storage
MaFiles.Remove(data);
}
private static string GetOrCreateMafilePath(Mafile data)
{
if (data.Filename != null)
{
return Path.Combine(MafilesDirectory, data.Filename);
}
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
data.Filename = fileName;
return Path.Combine(MafilesDirectory, fileName);
}
private static string CreateMafileFileName(Mafile data, bool useAccountName)
{
return useAccountName
? MafileNamingStrategy.Login.GetMafileName(data)
: MafileNamingStrategy.SteamId.GetMafileName(data);
}
public static string? TryGetMafilePath(Mafile data)
{
@@ -221,9 +213,10 @@ public static class Storage
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
}
public static async Task<MafileRenameResult> RenameMafiles(bool loginAsFileName, IProgress<double>? progress = null)
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
IProgress<double>? progress = null)
{
if (MaFiles.Count == 0) return new MafileRenameResult();
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);
@@ -255,7 +248,7 @@ public static class Storage
}
var mafiles = MaFiles.ToList();
var res = new MafileRenameResult
var res = new MafilesBulkRenameResult
{
Total = mafiles.Count,
BackupFileName = backupFileName
@@ -266,7 +259,7 @@ public static class Storage
{
try
{
var targetFileName = CreateMafileFileName(mafile, loginAsFileName);
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
if (mafile.Filename == targetFileName || mafile.Filename == null)
{
res.Renamed += 1;
@@ -314,14 +307,4 @@ public static class Storage
res.Errors += 1;
}
}
public class MafileRenameResult
{
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,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; }
+2 -2
View File
@@ -38,7 +38,7 @@ public static class NebulaSerializer
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");
@@ -58,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
{
+8 -1
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
{
@@ -34,37 +34,28 @@ public static partial class SessionHandler
string? snackbarPrefix = null)
{
using var scope = Shell.Logger.PushScopeProperty("Scope", "SessionHandler");
var mobileTokenExpired = MobileTokenExpired(mafile);
var refreshTokenExpired = RefreshTokenExpired(mafile);
var password = GetPassword(mafile);
Exception? currentException = null;
if (!mobileTokenExpired)
if (!mafile.MobileTokenExpired())
{
try
{
return await func();
}
catch (SessionInvalidException ex)
when (refreshTokenExpired == false || password != null)
{
if (ex is SessionPermanentlyExpiredException)
{
Shell.Logger.Debug(ex, "RefreshToken on mafile {name} {steamid} is expired", mafile.AccountName,
mafile.SessionData?.SteamId);
refreshTokenExpired = true;
}
else
{
Shell.Logger.Debug(ex, "MobileToken on mafile {name} {steamid} is expired", mafile.AccountName,
mafile.SessionData?.SteamId);
}
Shell.Logger.Warn(ex, "Session on mafile {name} {steamid} is invalid when mobile token is not expired",
mafile.AccountName, mafile.SessionData?.SteamId);
currentException = ex;
}
}
//State: mobileToken invalid/expired, refreshToken maybe not expired
if (!refreshTokenExpired)
if (!mafile.RefreshTokenExpired())
{
Shell.Logger.Info("Trying to refresh session on mafile {name} {steamid} using refresh token",
mafile.AccountName,
mafile.SessionData?.SteamId);
var refreshed = await RefreshInternal(chp, mafile);
if (refreshed)
{
@@ -77,19 +68,14 @@ public static partial class SessionHandler
}
catch (SessionInvalidException ex)
{
Shell.Logger.Info(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
Shell.Logger.Warn(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
mafile.AccountName, mafile.SessionData?.SteamId);
if (password == null)
throw;
currentException = ex;
}
}
}
Shell.Logger.Debug("Session on mafile {name} {steamid} is invalid/expired", mafile.AccountName,
mafile.SessionData?.SteamId);
//State: mobileToken invalid/expired, refreshToken invalid/expired
if (password != null)
if (mafile.HasPassword(out var password))
{
var logged = await LoginAgainInternal(chp, mafile, password, true);
if (logged)
@@ -100,41 +86,10 @@ public static partial class SessionHandler
}
}
//Nothing to do more, everything is expired
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG,
currentException);
}
private static bool MobileTokenExpired(Mafile mafile)
{
var mobileToken = mafile.SessionData?.GetMobileToken();
return mobileToken == null || mobileToken.Value.IsExpired;
}
private static bool RefreshTokenExpired(Mafile mafile)
{
var refreshToken = mafile.SessionData?.RefreshToken;
return refreshToken == null || refreshToken.Value.IsExpired;
}
private static string? GetPassword(Mafile mafile)
{
try
{
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
{
return PHandler.Decrypt(mafile.Password);
}
}
catch
{
// ignored
}
return null;
}
private static async Task<bool> RefreshInternal(HttpClientHandlerPair chp, Mafile mafile)
{
try
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using AchiesUtilities.Web.Models;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLib.Api.Mobile;
using SteamLib.Authentication;
using SteamLib.Authentication.LoginV2;
@@ -27,7 +28,6 @@ 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);
await Storage.UpdateMafileAsync(mafile);
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
@@ -49,7 +49,6 @@ 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;
@@ -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;
}
}
+4 -2
View File
@@ -25,7 +25,7 @@ public partial class Settings : ObservableObject
static Settings()
{
if (File.Exists("settings.json") == false)
if (!File.Exists("settings.json"))
{
Instance = new Settings();
Instance.PropertyChanged += SettingsOnPropertyChanged;
@@ -94,7 +94,6 @@ public partial class Settings : ObservableObject
[ObservableProperty] private bool _legacyMode = true;
[ObservableProperty] private bool _allowAutoUpdate;
[ObservableProperty] private bool _useAccountNameAsMafileName;
[ObservableProperty] private bool _ignorePatchTuesdayErrors;
[ObservableProperty] private BackgroundMode _backgroundMode = BackgroundMode.Default;
[ObservableProperty] private double _leftOpacity = 0.4;
@@ -108,6 +107,9 @@ public partial class Settings : ObservableObject
[ObservableProperty] private bool _proxyManagerDisplayProtocol;
[ObservableProperty] private bool _proxyManagerDisplayCredentials;
[ObservableProperty] private TimeSpan _maacErrorThreshold = TimeSpan.FromHours(3);
[ObservableProperty] private TimeSpan _maacRetryInterval = TimeSpan.FromMinutes(15);
#endregion
}
+16 -2
View File
@@ -1,6 +1,12 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NebulaAuth.Core;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.MafileExport;
using NebulaAuth.Model.Mafiles;
using NLog;
using NLog.Extensions.Logging;
using SteamLib.Core;
@@ -14,8 +20,11 @@ public static class Shell
public static Logger Logger { get; private set; } = null!;
public static ILogger ExtensionsLogger { get; private set; } = null!;
public static void Initialize()
public static async Task Initialize()
{
File.Delete("log.log");
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Logger = LogManager.GetLogger("Logger");
var lp = new NLogLoggerProvider();
var logger = lp.CreateLogger("SteamLib");
@@ -27,13 +36,18 @@ public static class Shell
try
{
TimeAligner.AlignTime();
await TimeAligner.AlignTimeAsync();
}
catch (Exception ex)
{
throw new CantAlignTimeException("", ex);
}
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
MAACStorage.Initialize();
MafileExporterStorage.Initialize();
ExtensionsLogger.LogDebug("Application started");
}
@@ -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}" />
+53 -5
View File
@@ -10,7 +10,7 @@
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
<ApplicationIcon>Theme\lock.ico</ApplicationIcon>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AssemblyVersion>1.8.0</AssemblyVersion>
<AssemblyVersion>1.8.4</AssemblyVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@@ -28,7 +28,7 @@
<PackageReference Include="CodingSeb.Localization.WPF" Version="1.4.1" />
<PackageReference Include="CodingSebLocalization.Fody" Version="1.4.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
@@ -53,14 +53,62 @@
<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>
+7 -5
View File
@@ -11,13 +11,16 @@ public static class LanguageUtility
{
var userCulture = CultureInfo.CurrentUICulture;
var userLang = userCulture.TwoLetterISOLanguageName;
var userRegion = userCulture.Name;
var userRegion = new RegionInfo(userCulture.Name).TwoLetterISORegionName;
switch (userLang)
{
case "ru": return LocalizationLanguage.Russian;
case "uk": return LocalizationLanguage.Ukrainian;
case "en": return LocalizationLanguage.English;
case "zh": return LocalizationLanguage.ChineseSimplified;
case "fr": return LocalizationLanguage.French;
}
if (userRegion.EndsWith("UA", StringComparison.OrdinalIgnoreCase))
@@ -28,9 +31,8 @@ public static class LanguageUtility
"RU", "BY", "KZ", "KG", "TJ", "TM", "UZ", "AM", "AZ", "GE", "MD"
];
if (cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase)))
return LocalizationLanguage.Russian;
return LocalizationLanguage.English;
return cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase))
? LocalizationLanguage.Russian
: LocalizationLanguage.English;
}
}
+91 -31
View File
@@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
@@ -6,7 +6,6 @@
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
xmlns:vm="clr-namespace:NebulaAuth.ViewModel">
<DataTemplate DataType="{x:Type confirmations:Confirmation}">
<Grid>
<Grid.ColumnDefinitions>
@@ -175,36 +174,97 @@
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type entities:MarketMultiConfirmation}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
Margin="0,0,10,0" />
<TextBlock VerticalAlignment="Center" Grid.Column="1">
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
</TextBlock>
<Expander Padding="0" Background="Transparent" materialDesign:ExpanderAssist.ExpanderButtonPosition="Start"
materialDesign:ExpanderAssist.HorizontalHeaderPadding="0" HorizontalAlignment="Stretch">
<Expander.Header>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
Margin="0,0,10,0" />
<TextBlock VerticalAlignment="Center" Grid.Column="1">
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
</TextBlock>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left" Margin="5,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Check" />
</Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Center" Margin="5,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<!-- Confirm all -->
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="4"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmAll}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="CheckAll" />
</Button>
<!-- Cancel all -->
<Button Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelAll}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</Expander.Header>
<Border Margin="30,6,0,0" Background="{DynamicResource MaterialDesignPaper}" CornerRadius="4">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
MaxHeight="320">
<ItemsControl ItemsSource="{Binding Confirmations}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type confirmations:MarketConfirmation}">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="32"
MaxHeight="32" Margin="0,0,10,0"
Source="{Binding ItemImageUri}" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
</StackPanel>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left"
Margin="8,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Check" />
</Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Expander>
</DataTemplate>
<DataTemplate DataType="{x:Type confirmations:PurchaseConfirmation}">
<Grid>
@@ -58,7 +58,7 @@
materialDesign:HintAssist.IsFloating="False"
materialDesign:TextFieldAssist.LeadingIcon="Key"
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
<CheckBox FontSize="15" Grid.Row="1" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="1" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<Grid Grid.Row="2" Margin="10,10,10,0">
@@ -74,10 +74,10 @@
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
</UIElement.InputBindings>
</ComboBox>
<CheckBox FontSize="15" Grid.Row="2" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="2" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<CheckBox FontSize="15" Grid.Row="3" Margin="10,10,10,0" IsEnabled="{Binding MafileHasProxy}"
<CheckBox FontSize="15" Grid.Row="3" Margin="10,5,10,0" IsEnabled="{Binding MafileHasProxy}"
Content="{Tr LoginAgainDialog.UseMafileProxy}" IsChecked="{Binding UseMafileProxy}" />
<Grid Grid.Row="4" Margin="10,10,10,0">
<Grid.ColumnDefinitions>
@@ -0,0 +1,71 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.SdaPasswordDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
d:DataContext="{d:DesignInstance other:SdaPasswordDialogVM}"
Background="{DynamicResource WindowBackground}">
<Grid MinHeight="200" MinWidth="400" MaxWidth="500">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Lock" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontSize="16" VerticalAlignment="Center" Foreground="{DynamicResource BaseContentBrush}"
Margin="7,0,0,0" HorizontalAlignment="Left"
Text="{Tr SdaPasswordDialog.Title}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<Grid Grid.Row="2" Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Tr SdaPasswordDialog.Message}" TextWrapping="Wrap"
Foreground="{DynamicResource BaseContentBrush}" Margin="10,10,10,8" />
<TextBox Grid.Row="1" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
materialDesign:TextFieldAssist.HasClearButton="True"
Padding="10" FontSize="15" Margin="10,0,10,0"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
materialDesign:HintAssist.Hint="{Tr SdaPasswordDialog.PasswordHint}"
materialDesign:HintAssist.IsFloating="False"
materialDesign:TextFieldAssist.LeadingIcon="Key"
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
<Grid Grid.Row="2" Margin="10,16,10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button IsEnabled="{Binding IsFormValid}" IsDefault="True" Margin="0,5,5,5"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource True}" Content="{Tr Common.OK}" />
<Button IsCancel="True" Grid.Column="1" Margin="5,5,0,5"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource False}" Content="{Tr Common.Cancel}" />
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View.Dialogs;
public partial class SdaPasswordDialog
{
public SdaPasswordDialog()
{
InitializeComponent();
}
}
@@ -0,0 +1,179 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.UpdateDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
d:DataContext="{d:DesignInstance other:UpdateDialogVM}"
Background="{DynamicResource WindowBackground}"
MinWidth="420" MaxWidth="600">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Update" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18">
<Run Text="{Tr UpdateDialog.Title, IsDynamic=False}" />
<Run FontWeight="Bold" Text="{Binding Version, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<!-- Changelog -->
<Grid Grid.Row="2" Margin="12,10,12,4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Tr UpdateDialog.Changelog}" FontSize="14" FontWeight="SemiBold"
Margin="0,0,0,8"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}" />
<!-- JSON changelog items -->
<materialDesign:Card Grid.Row="1" Padding="10">
<ScrollViewer MaxHeight="350" VerticalScrollBarVisibility="Auto"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding Changelog.Changes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,6">
<Border CornerRadius="3" Padding="4,2,4,2" Margin="0,0,8,0"
VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background"
Value="{DynamicResource MaterialDesign.Brush.Primary}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="NEW">
<Setter Property="Background"
Value="{DynamicResource SuccessBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="FIX">
<Setter Property="Background"
Value="{DynamicResource WarningBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="IMPROVEMENT">
<Setter Property="Background"
Value="{DynamicResource AccentBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="SECURITY">
<Setter Property="Background"
Value="{DynamicResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Type}" FontSize="11" FontWeight="Bold"
Foreground="White" />
</Border>
<TextBlock Text="{Binding Text}" TextWrapping="Wrap"
VerticalAlignment="Center" FontSize="14" MaxWidth="420" />
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
Width="20" Height="20"
Padding="2"
Margin="4,0,0,0"
VerticalAlignment="Center"
Command="{Binding DataContext.OpenLinkCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding Link}"
ToolTip="Open Link"
Visibility="{Binding Link, Converter={StaticResource NullableToVisibilityConverter}}">
<materialDesign:PackIcon Kind="OpenInNew" Width="14" Height="14" />
</Button>
</StackPanel>
<Separator Opacity="0.2" Margin="0,2,0,2" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</materialDesign:Card>
<!-- No changelog fallback -->
<TextBlock Grid.Row="1" Text="{Tr UpdateDialog.NoChangelog}" FontSize="13" Opacity="0.6"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
</Grid>
<!-- Buttons -->
<Grid Grid.Row="3" Margin="12,6,12,14">
<!-- Normal buttons -->
<StackPanel
Visibility="{Binding ShowRemindOptions, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
<Button Style="{StaticResource MaterialDesignRaisedButton}"
Command="{Binding UpdateNowCommand}"
HorizontalAlignment="Stretch"
Margin="0,0,0,8"
Content="{Tr UpdateDialog.UpdateNow}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="8" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{Binding ToggleRemindOptionsCommand}"
Content="{Tr UpdateDialog.RemindLater}" />
<Button Grid.Column="2"
Style="{StaticResource MaterialDesignFlatButton}"
Command="{Binding SkipVersionCommand}"
Content="{Tr UpdateDialog.SkipVersion}" />
</Grid>
</StackPanel>
<!-- Remind later options -->
<StackPanel
Visibility="{Binding ShowRemindOptions, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock Text="{Tr UpdateDialog.RemindAfter}" Margin="0,0,0,8" FontSize="14" />
<WrapPanel>
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="1H"
Content="{Tr UpdateDialog.RemindOptions.1Hour}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="1D"
Content="{Tr UpdateDialog.RemindOptions.1Day}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="3D"
Content="{Tr UpdateDialog.RemindOptions.3Days}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="7D"
Content="{Tr UpdateDialog.RemindOptions.7Days}" />
</WrapPanel>
<Button Style="{StaticResource MaterialDesignFlatButton}"
HorizontalAlignment="Left"
Command="{Binding ToggleRemindOptionsCommand}"
Content="{Tr Common.Cancel}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View.Dialogs;
public partial class UpdateDialog
{
public UpdateDialog()
{
InitializeComponent();
}
}
+10 -1
View File
@@ -58,7 +58,7 @@
</Button>
<Button Margin="0,0,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="Website_Click" IsEnabled="False">
Click="Documentation_Click" IsEnabled="False">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Viewbox Width="22" Height="22" Margin="0 0 8 0">
<Canvas Width="22" Height="22">
@@ -72,6 +72,15 @@
</StackPanel>
</Button>
<Button Margin="0,5,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="CheckForUpdates_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<materialDesign:PackIcon Kind="Update" Width="22" Height="22" Margin="0 0 8 0"
Foreground="{DynamicResource MaterialDesign.Brush.Primary}" />
<TextBlock Text="{Tr LinksView.CheckForUpdates}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" IsCancel="True" Width="0"
Height="0" Visibility="Visible" />
</StackPanel>
+8
View File
@@ -1,6 +1,8 @@
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
namespace NebulaAuth.View;
@@ -31,4 +33,10 @@ public partial class LinksView : UserControl
{
Process.Start(new ProcessStartInfo("https://yourwebsite.com") {UseShellExecute = true});
}
private void CheckForUpdates_Click(object sender, RoutedEventArgs e)
{
DialogHost.Close(null);
UpdateManager.CheckForUpdates(true);
}
}
+240
View File
@@ -0,0 +1,240 @@
<UserControl x:Class="NebulaAuth.View.MafileExporterView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
FontFamily="{materialDesign:MaterialDesignFont}"
MinWidth="400"
MaxWidth="400"
Background="{DynamicResource WindowBackground}"
d:DataContext="{d:DesignInstance other:MafileExporterVM}"
d:Background="#141119"
d:Foreground="White"
FontSize="18">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="FileExport" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr ExportDialog.ExportTitle}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsCancel="True">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Background="DarkGray" Opacity="0.4" Grid.Row="1" />
<Grid Grid.Row="2" Margin="15" TextElement.FontSize="14">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ComboBox Grid.Column="0"
Padding="10"
Style="{StaticResource MaterialDesignOutlinedComboBox}"
Margin="0,0,5,0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
ItemsSource="{Binding Templates}"
DisplayMemberPath="Name"
SelectedItem="{Binding CurrentTemplate}"
Visibility="{Binding EditMode, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
<TextBox x:Name="EditTemplateNameTextBox"
Grid.Column="0"
Padding="10"
Margin="0,0,5,0"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
Text="{Binding CurrentTemplate.Name,
FallbackValue=''}"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
Visibility="{Binding EditMode, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBox.InputBindings>
<KeyBinding Key="Enter"
Command="{Binding ToggleEditModeCommand}" />
</TextBox.InputBindings>
</TextBox>
<Button Command="{Binding AddTemplateCommand}" Grid.Column="1">
<materialDesign:PackIcon Kind="Add" />
</Button>
<Button Command="{Binding ToggleEditModeCommand}" Click="EditButton_Click" Grid.Column="2">
<materialDesign:PackIcon Kind="Edit" />
</Button>
<Button Grid.Column="3" Command="{Binding RemoveCurrentTemplateCommand}">
<materialDesign:PackIcon Kind="Trash" />
</Button>
</Grid>
<Grid Margin="0,15,0,0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
MouseDoubleClick="PathTB_DoubleClick" materialDesign:TextFieldAssist.HasClearButton="True"
Text="{Binding CurrentTemplate.Path, FallbackValue=''}" Padding="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}" Grid.Column="0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.SelectPathHint}" />
<Button Command="{Binding OpenSelectDirectoryDialogCommand}" Grid.Column="1" Margin="5,0,0,0">
<materialDesign:PackIcon Kind="FolderOpen" />
</Button>
</Grid>
<Border Grid.Row="2" BorderThickness="1" CornerRadius="4" Margin="0,15,0,0" Padding="2">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseContentColor}" Opacity="0.4" />
</Border.BorderBrush>
<Expander x:Name="TemplateExpander"
Header="{Tr ExportDialog.ExportSettingsHeader}"
materialDesign:ExpanderAssist.HorizontalHeaderPadding="5"
materialDesign:ExpanderAssist.ExpanderButtonPosition="Start"
HorizontalAlignment="Stretch"
Padding="5,0,5,5"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}">
<StackPanel>
<TextBlock FontWeight="DemiBold"
Text="{Tr ExportDialog.ExportSettingsCommon}" />
<CheckBox IsChecked="{Binding CurrentTemplate.UseLoginAsMafileName, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.UseLoginAsMafileName}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.UseLoginAsMafileName}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSharedSecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSharedSecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSharedSecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeIdentitySecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeIdentitySecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeIdentitySecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeRCode, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeRCode}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeRCode}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSessionData, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSessionData}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSessionData}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeOtherInfo, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeOtherInfo}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeOtherInfo}" />
<TextBlock FontWeight="DemiBold"
Margin="0,10,0,0"
Text="{Tr ExportDialog.ExportSettingsNebula}" />
<Grid Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
IsChecked="{Binding CurrentTemplate.IncludeNebulaProxy, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaProxy}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaProxy}" />
<CheckBox Grid.Column="1"
HorizontalAlignment="Center"
IsChecked="{Binding CurrentTemplate.IncludeNebulaPassword, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaPassword}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaPassword}" />
<CheckBox Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CurrentTemplate.IncludeNebulaGroup, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaGroup}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaGroup}" />
</Grid>
</StackPanel>
</Expander>
</Border>
<TextBox GotFocus="ExportTB_GotFocus" PreviewMouseUp="ExportTB_GotFocus" Grid.Row="3"
Style="{StaticResource MaterialDesignOutlinedTextBox}" MaxHeight="600" Margin="0,10,0,0"
MinHeight="250" Height="250" AcceptsReturn="True"
Text="{Binding AccountsText, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ExportCommand.IsRunning, Converter={StaticResource ReverseBooleanConverter}}"
Name="AccountsTB" />
<TextBlock Grid.Row="3"
Text="{Tr ExportDialog.ExportAccountsPlaceholder}"
Margin="18,27,10,10"
Opacity="0.6"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="{Binding Text.IsEmpty,
ElementName=AccountsTB,
Converter={StaticResource BooleanToVisibilityConverter}}" />
<nebulaAuth:HintBox Margin="0,10,0,0" Severity="{Binding HintBoxSeverity}" Grid.Row="4"
Text="{Binding HintText}" />
</Grid>
</Grid>
<Separator Grid.Row="3" />
<Button Command="{Binding ExportCommand}" Style="{StaticResource MaterialDesignFlatDarkBgButton}" Grid.Row="4"
Margin="20,7,20,7" MaxWidth="150" Content="{Tr ExportDialog.ExportButton}" />
</Grid>
</UserControl>
@@ -0,0 +1,32 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
namespace NebulaAuth.View;
public partial class MafileExporterView : UserControl
{
public MafileExporterView()
{
InitializeComponent();
}
private void PathTB_DoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is TextBox textBox)
{
textBox.SelectAll();
}
}
private void ExportTB_GotFocus(object sender, RoutedEventArgs e)
{
TemplateExpander.IsExpanded = false;
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(() => { EditTemplateNameTextBox.Focus(); }, DispatcherPriority.Input);
}
}
+5 -13
View File
@@ -55,27 +55,19 @@
ItemsSource="{Binding Languages}" DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding Language}"
materialDesign:HintAssist.Hint="{Tr LanguageWord}" />
<CheckBox Margin="0,18,0,0" IsChecked="{Binding HideToTray}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding HideToTray}"
Content="{Tr SettingsDialog.MinimizeToTray}" />
<CheckBox Margin="0,18,0,0" IsChecked="{Binding LegacyMode}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding LegacyMode}"
Content="{Tr SettingsDialog.LegacyMafileMode}"
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" BorderBrush="AliceBlue"
Margin="0,18,0,0"
BorderThickness="2"
IsChecked="{Binding IgnorePatchTuesdayErrors}"
ToolTip="{Tr SettingsDialog.IgnorePatchTuesdayErrorsHint}">
<TextBlock TextWrapping="WrapWithOverflow"
Text="{Tr SettingsDialog.IgnorePatchTuesdayErrors}" />
</CheckBox>
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
</CheckBox>-->
<CheckBox Margin="0,20,0,0" IsChecked="{Binding UseIcon}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding UseIcon}"
Content="{Tr SettingsDialog.UseIndicator}"
ToolTip="{Tr SettingsDialog.UseIndicatorHint}" />
<materialDesign:ColorPicker Margin="-5,0,-5,0" IsEnabled="{Binding UseIcon}"
@@ -191,10 +183,10 @@
PreviewMouseUp="Slider_OnMouseUp" />
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
IsChecked="{Binding ApplyBlurBackground}"
FontSize="15" />
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
FontSize="15"
IsChecked="{Binding RippleDisabled}" />
<Button Margin="0,15,0,0" Command="{Binding ResetThemeDefaultsCommand}"
-55
View File
@@ -1,55 +0,0 @@
<UserControl x:Class="NebulaAuth.View.UpdaterView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
mc:Ignorable="d"
Foreground="WhiteSmoke"
FontFamily="{materialDesign:MaterialDesignFont}"
d:DataContext="{d:DesignInstance other:UpdaterVM}"
Background="{DynamicResource WindowBackground}"
Padding="10"
MaxWidth="500">
<d:DesignerProperties.DesignStyle>
<Style TargetType="UserControl">
<Setter Property="Background" Value="{DynamicResource WindowBackground}" />
</Style>
</d:DesignerProperties.DesignStyle>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontSize="24" Text="Обновления" />
<Separator Grid.Row="1" />
<TextBlock FontSize="16" Margin="5,10,0,10" TextWrapping="WrapWithOverflow" Grid.Row="2">
<Run Text="Доступна новая версия:" />
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.CurrentVersion}" />
<Run Text="&#x0a;Вы используете версию" />
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.InstalledVersion}" />
<Run Text="Хотите обновить программу?" />
</TextBlock>
<Expander Header="Что изменилось?" Grid.Row="3">
<!--<wpf:WebView2 HorizontalAlignment="Stretch" Height="300"
Source="{Binding UpdateInfoEventArgs.ChangelogURL}"
/>-->
</Expander>
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Content="OK" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
<Button IsCancel="True" Grid.Column="1" Content="Cancel"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
</Grid>
</Grid>
</UserControl>
-9
View File
@@ -1,9 +0,0 @@
namespace NebulaAuth.View;
public partial class UpdaterView
{
public UpdaterView()
{
InitializeComponent();
}
}
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NLog;
using SteamLib;
@@ -224,7 +225,7 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
Logger.Error(ex, "Error during saving Nebula data to mafile");
}
Storage.SaveMafile(mafile);
await Storage.SaveMafileAsync(mafile);
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString(), login);
}
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.ViewModel.Linker;
using Newtonsoft.Json;
+5 -24
View File
@@ -10,8 +10,8 @@ using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
using SteamLib.SteamMobile;
using SteamLibForked.Exceptions.Authorization;
@@ -22,7 +22,6 @@ public partial class MainVM : ObservableObject
{
[UsedImplicitly] public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
public Mafile? SelectedMafile
{
get => _selectedMafile;
@@ -32,7 +31,7 @@ public partial class MainVM : ObservableObject
public bool IsMafileSelected => SelectedMafile != null;
public DialogHost CurrentDialogHost { get; set; } = null!;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = Storage.MaFiles;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = new(Storage.MaFiles);
private Mafile? _selectedMafile;
@@ -43,16 +42,14 @@ public partial class MainVM : ObservableObject
new MaProxy(kvp.Key, kvp.Value)));
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
QueryGroups();
UpdateManager.CheckForUpdates();
UpdateManager.CheckForUpdates(false);
}
[RelayCommand]
public async Task Debug()
{
await DialogsController.ShowSetAccountsPasswordDialog();
}
private void SetMafile(Mafile? mafile)
{
if (mafile != SelectedMafile)
@@ -71,6 +68,7 @@ public partial class MainVM : ObservableObject
LoginAgainCommand.NotifyCanExecuteChanged();
RemoveAuthenticatorCommand.NotifyCanExecuteChanged();
ConfirmLoginCommand.NotifyCanExecuteChanged();
RemoveProxyCommand.NotifyCanExecuteChanged();
}
}
@@ -114,7 +112,7 @@ public partial class MainVM : ObservableObject
private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
SearchText = string.Empty;
PerformQuery();
}
@@ -134,12 +132,6 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
public async Task LinkAccount()
{
await DialogsController.ShowLinkerDialog();
}
[RelayCommand]
public async Task MoveAccount()
{
@@ -172,7 +164,6 @@ public partial class MainVM : ObservableObject
if (result.Success)
{
Storage.MoveToRemoved(selectedMafile);
MaFiles.Remove(selectedMafile);
}
}
else
@@ -218,16 +209,6 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
private async Task OpenLinksView()
{
CurrentDialogHost.CloseOnClickAway = true;
var view = new LinksView();
await DialogHost.Show(view);
CurrentDialogHost.CloseOnClickAway = false;
}
private static string GetLocalization(string key)
{
const string locPath = "MainVM";
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@@ -76,18 +76,26 @@ public partial class MainVM //Confirmations
return SendConfirmation(SelectedMafile, confirmation, false);
}
private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
{
bool result;
try
{
if (confirmation is MarketMultiConfirmation multi)
switch (confirmation)
{
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
}
else
{
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
case MarketMultiConfirmation multi:
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
break;
case MarketConfirmation market:
result = await MaClient.SendConfirmation(mafile, market, confirm);
break;
default:
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
break;
}
}
catch (Exception ex)
@@ -96,13 +104,49 @@ public partial class MainVM //Confirmations
return;
}
if (result)
{
Confirmations.Remove(confirmation);
}
else
if (!result)
{
SnackbarController.SendSnackbar(GetLocalization("ConfirmationError"));
return;
}
UpdateConfirmationState(confirmation);
}
private void UpdateConfirmationState(Confirmation confirmation)
{
if (confirmation is MarketConfirmation item)
{
var parent = Confirmations
.OfType<MarketMultiConfirmation>()
.FirstOrDefault(p => p.Confirmations.Contains(item));
if (parent == null)
{
Confirmations.Remove(item);
return;
}
parent.Confirmations.Remove(item);
if (parent.Confirmations.Count == 0)
{
Confirmations.Remove(parent);
return;
}
if (parent.Confirmations.Count == 1)
{
var remaining = parent.Confirmations[0];
var idx = Confirmations.IndexOf(parent);
Confirmations[idx] = remaining;
return;
}
parent.Time = parent.Confirmations.FirstOrDefault()?.Time ?? parent.Time;
return;
}
Confirmations.Remove(confirmation);
}
}
+102 -43
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
@@ -13,6 +13,8 @@ using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Model.MafilesLegacy;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
@@ -25,6 +27,7 @@ public partial class MainVM //File //TODO: Refactor
{
public Settings Settings => Settings.Instance;
[RelayCommand]
private void OpenMafileFolder()
{
@@ -57,48 +60,50 @@ public partial class MainVM //File //TODO: Refactor
[RelayCommand]
private Task AddMafile()
{
var openFileDialog = new OpenFileDialog
var dialog = new OpenFileDialog
{
Filter = "Mafile|*.mafile;*.maFile",
Multiselect = false
};
var fs = openFileDialog.ShowDialog();
if (fs != true) return Task.CompletedTask;
var path = openFileDialog.FileName;
return AddMafile([path]);
return dialog.ShowDialog() == true
? AddMafile([dialog.FileName])
: Task.CompletedTask;
}
public async Task AddMafile(string[] path)
{
bool? confirmOverwrite = null;
var added = 0;
var notAdded = 0;
var errors = 0;
var summary = new MafileImportSummary();
SDAEncryptionHelper.Context? sdaContext = null;
var sdaPasswordPrompted = false;
foreach (var str in path)
{
try
{
Storage.AddNewMafile(str, confirmOverwrite ?? false);
added++;
var (mafile, passwordPrompted, context) = await TryReadMafile(str, sdaContext, sdaPasswordPrompted);
sdaContext = context;
sdaPasswordPrompted = passwordPrompted;
if (mafile == null)
{
summary.ErrorOne();
continue;
}
var overwrite = confirmOverwrite ?? false;
var res = await Storage.AddNewMafileFromData(mafile, overwrite);
if (res == AddMafileResult.AlreadyExist && confirmOverwrite == null)
{
confirmOverwrite =
await DialogsController.ShowConfirmCancelDialog(GetLocalization("ConfirmMafileOverwrite"));
res = await Storage.AddNewMafileFromData(mafile, confirmOverwrite.Value);
}
summary.Apply(res);
}
catch (FormatException)
{
errors++;
}
catch (IOException)
{
confirmOverwrite ??=
await DialogsController.ShowConfirmCancelDialog(GetLocalization("ConfirmMafileOverwrite"));
if (confirmOverwrite == true)
{
Storage.AddNewMafile(str, true);
added++;
}
else if (confirmOverwrite == false)
{
notAdded++;
}
summary.ErrorOne();
}
catch (MafileNeedReloginException ex)
{
@@ -107,11 +112,11 @@ public partial class MainVM //File //TODO: Refactor
var mafile = ex.Mafile;
if (await HandleAddMafileWithoutSession(mafile))
{
added++;
summary.AddedOne();
}
else
{
errors++;
summary.ErrorOne();
}
}
else
@@ -123,23 +128,50 @@ public partial class MainVM //File //TODO: Refactor
}
}
var msg = GetLocalization("Import");
if (added > 0)
{
msg += $" {GetLocalization("ImportAdded")} {added}.";
}
ShowImportSummary(summary);
}
if (notAdded > 0)
{
msg += $" {GetLocalization("ImportSkipped")} {notAdded}.";
}
if (errors > 0)
private async Task<MafileReadResult> TryReadMafile(string path, SDAEncryptionHelper.Context? sdaContext,
bool sdaPasswordPrompted)
{
try
{
msg += $" {GetLocalization("ImportErrors")} {errors}.";
}
var content = await File.ReadAllTextAsync(path);
Mafile mafile;
if (!SDAEncryptionHelper.LooksLikeSdaEncryptedBlob(content))
{
mafile = NebulaSerializer.Deserialize(content, path);
return new MafileReadResult(mafile, sdaPasswordPrompted, sdaContext);
}
SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2));
// Looks like encrypted, let's see if we have manifest
sdaContext ??= SDAEncryptionHelper.TryDetect(path, sdaContext?.SdaManifest);
if (sdaContext != null)
{
// We have manifest, but we must ensure we have password
if (sdaContext.Password == null && !sdaPasswordPrompted)
{
sdaPasswordPrompted = true;
var password = await DialogsController.ShowSdaPasswordDialog();
sdaContext = sdaContext.WithPassword(password);
}
var decrypted = SDAEncryptionHelper.TryDecrypt(content, path, sdaContext);
if (decrypted == null) return new MafileReadResult(null, sdaPasswordPrompted, sdaContext);
content = decrypted;
}
// If we are here, it means that either file is not encrypted, or we successfully decrypted it
mafile = NebulaSerializer.Deserialize(content, path);
return new MafileReadResult(mafile, sdaPasswordPrompted, sdaContext);
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
{
Shell.Logger.Warn(ex, "Failed to import mafile");
throw new FormatException($"Failed to read mafile {Path.GetFileName(path)}", ex);
}
}
private async Task<bool> HandleAddMafileWithoutSession(Mafile data)
@@ -190,6 +222,28 @@ public partial class MainVM //File //TODO: Refactor
return result;
}
private static void ShowImportSummary(MafileImportSummary summary)
{
var msg = GetLocalization("Import");
if (summary.Added > 0)
{
msg += $" {GetLocalization("ImportAdded")} {summary.Added}.";
}
if (summary.NotAdded > 0)
{
msg += $" {GetLocalization("ImportSkipped")} {summary.NotAdded}.";
}
if (summary.Errors > 0)
{
msg += $" {GetLocalization("ImportErrors")} {summary.Errors}.";
}
SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2));
}
[RelayCommand]
private async Task RemoveMafile()
{
@@ -239,7 +293,7 @@ public partial class MainVM //File //TODO: Refactor
}
var arr = files.Cast<string>().ToArray();
if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return;
if (arr.All(p => !p.ContainsIgnoreCase("mafile"))) return;
await AddMafile(arr);
@@ -293,4 +347,9 @@ public partial class MainVM //File //TODO: Refactor
if (mafile is not Mafile maf) return false;
return maf.Password != null && PHandler.IsPasswordSet;
}
private record MafileReadResult(
Mafile? Mafile,
bool SdaPasswordPrompted,
SDAEncryptionHelper.Context? SdaContext);
}
+3 -3
View File
@@ -5,8 +5,8 @@ using AchiesUtilities.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel;
@@ -88,7 +88,7 @@ public partial class MainVM //Groups
Storage.UpdateMafile(mafile);
OnPropertyChanged(nameof(SelectedMafile)); //For bindings
QueryGroups();
if (Groups.All(g => g.Equals(mafGroup) == false))
if (Groups.All(g => !g.Equals(mafGroup)))
{
SelectedGroup = null;
}
@@ -104,7 +104,7 @@ public partial class MainVM //Groups
private void QueryGroups()
{
var groups = Storage.MaFiles
.Where(m => string.IsNullOrWhiteSpace(m.Group) == false)
.Where(m => !string.IsNullOrWhiteSpace(m.Group))
.Select(m => m.Group)
.Distinct()
.Order()
+1 -1
View File
@@ -130,7 +130,7 @@ public partial class MainVM //MAAC
{
mafiles = mafiles.ToArray();
var turnOn = mafiles.All(m => m.LinkedClient == null || GetCurrentMode(m.LinkedClient) == false);
var turnOn = mafiles.All(m => m.LinkedClient == null || !GetCurrentMode(m.LinkedClient));
if (turnOn)
{
foreach (var mafile in mafiles)
+38
View File
@@ -0,0 +1,38 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.View;
namespace NebulaAuth.ViewModel;
public partial class MainVM : ObservableObject
{
[RelayCommand]
private Task OpenSetPasswordsDialog()
{
return DialogsController.ShowSetAccountsPasswordDialog();
}
[RelayCommand]
private Task OpenExporterDialog()
{
return DialogsController.ShowExportDialog();
}
[RelayCommand]
public Task LinkAccount()
{
return DialogsController.ShowLinkerDialog();
}
[RelayCommand]
private async Task OpenLinksView()
{
CurrentDialogHost.CloseOnClickAway = true;
var view = new LinksView();
await DialogHost.Show(view);
CurrentDialogHost.CloseOnClickAway = false;
}
}
+14 -6
View File
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel;
@@ -68,13 +69,20 @@ public partial class MainVM
CheckProxyExist();
}
[RelayCommand]
private void RemoveProxy()
[RelayCommand(CanExecute = nameof(RemoveProxyCanExecute))]
private void RemoveProxy(Mafile? mafile)
{
if (SelectedProxy == null) return;
if (SelectedMafile == null) return;
SelectedMafile.Proxy = null;
SetProxy(null, false); //Not system, triggered by user
mafile ??= SelectedMafile;
if (mafile?.Proxy == null) return;
mafile.Proxy = null;
if (mafile == SelectedMafile)
SetProxy(null, false); //Not system, triggered by user
}
private bool RemoveProxyCanExecute(Mafile? mafile)
{
mafile ??= SelectedMafile;
return mafile is {Proxy: not null};
}
private void CheckProxyExist()

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