mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-26 14:51:42 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0130737789 | |||
| 1ab2f99783 | |||
| da00fa5c96 | |||
| 0a46950ca9 | |||
| 9ac4ffdc34 | |||
| 17635ccba0 | |||
| 69589075e5 | |||
| 4b547e19c4 | |||
| 053faec343 | |||
| b8ac76d2a9 | |||
| b81d45765a | |||
| 372b8c6463 | |||
| 5de26381bb | |||
| 16fd4992f8 | |||
| 7835a53717 | |||
| f15632f2d0 | |||
| b56c0e578d | |||
| 969590d9f2 | |||
| 3e87a0d50e | |||
| 690c98527f | |||
| 5242b0009b | |||
| 1f14bd77e7 | |||
| f2b1cdfcc9 | |||
| ad1df3227b | |||
| 05f132ab1b | |||
| ee858921dc | |||
| 750292bfba | |||
| 982bb4d0c8 | |||
| 8e5fea54cd | |||
| 882d39b8f3 | |||
| ffcc7405a7 | |||
| b3e7436e95 | |||
| d319fc19f9 |
@@ -1,108 +0,0 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref == 'refs/heads/master'
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: "8.x"
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore NebulaAuth.sln
|
||||
|
||||
- name: Build
|
||||
run: dotnet build NebulaAuth.sln --configuration Release
|
||||
|
||||
- name: Get version from assembly
|
||||
id: get-version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$content = Get-Content -Path "NebulaAuth/NebulaAuth.csproj" -Raw
|
||||
$version = [regex]::Match($content, '<AssemblyVersion>(.*?)<\/AssemblyVersion>').Groups[1].Value
|
||||
Write-Output "VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Check if tag exists
|
||||
id: tag_exists
|
||||
run: |
|
||||
if (git tag -l | Select-String -Pattern "^${env:VERSION}$") {
|
||||
Write-Output "Version $env:VERSION already exists."
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Check changelog
|
||||
run: |
|
||||
if (-not (Test-Path "changelog/${env:VERSION}.html")) {
|
||||
Write-Output "Changelog file changelog/${env:VERSION}.html does not exist."
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Insert date into changelog
|
||||
run: |
|
||||
$date = Get-Date -Format "dd.MM.yyyy"
|
||||
(Get-Content "changelog/${env:VERSION}.html") -replace '(?<=<div class="date">).*?(?=</div>)', $date | Set-Content "changelog/${env:VERSION}.html"
|
||||
|
||||
- name: Extract changelog description
|
||||
id: extract_description
|
||||
run: |
|
||||
$description = (Get-Content "changelog/${env:VERSION}.html" | Select-String -Pattern '(?<=<div class="description">).*?(?=</div>)' | ForEach-Object { $_.Matches.Value }) -replace '<br\/>', "`n"
|
||||
Write-Output "DESCRIPTION=$description" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Create ZIP
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path release -Force
|
||||
Compress-Archive -Path NebulaAuth/bin/Release -DestinationPath release/NebulaAuth.${env:VERSION}.zip
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ env.VERSION }}
|
||||
release_name: NebulaAuth ${{ env.VERSION }}
|
||||
body: |
|
||||
${{ env.DESCRIPTION }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: release/NebulaAuth.${{ env.VERSION }}.zip
|
||||
asset_name: NebulaAuth.${{ env.VERSION }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Update XML and Changelog html
|
||||
shell: pwsh
|
||||
run: |
|
||||
$xmlContent = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>${env:VERSION}.0</version>
|
||||
<url>https://github.com/${env:GITHUB_REPOSITORY}/releases/download/${env:VERSION}/NebulaAuth.${env:VERSION}.zip</url>
|
||||
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/${env:VERSION}.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
||||
"@
|
||||
$xmlContent | Out-File -FilePath update.xml -Encoding UTF8 -Force
|
||||
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git add changelog/${env:VERSION}.html update.xml
|
||||
git commit -m "Update version to ${env:VERSION} and add changelog"
|
||||
git push origin master
|
||||
@@ -0,0 +1,218 @@
|
||||
name: Build and Publish Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'src/NebulaAuth/NebulaAuth.csproj'
|
||||
- 'changelog/*.json'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Checkout
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Setup .NET
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Extract version via MSBuild
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Extract version from csproj
|
||||
id: ver
|
||||
run: |
|
||||
VERSION=$(dotnet msbuild src/NebulaAuth/NebulaAuth.csproj -getProperty:AssemblyVersion)
|
||||
echo "Detected version $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Validate JSON changelog
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Validate changelog JSON
|
||||
run: |
|
||||
VERSION="${{ steps.ver.outputs.version }}"
|
||||
FILE="changelog/${VERSION}.json"
|
||||
|
||||
echo "Checking changelog file $FILE"
|
||||
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "ERROR: changelog JSON not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jq empty "$FILE"
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Generate HTML changelog (legacy AutoUpdater)
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Generate HTML changelog
|
||||
run: |
|
||||
VERSION="${{ steps.ver.outputs.version }}"
|
||||
JSON="changelog/${VERSION}.json"
|
||||
HTML="changelog/${VERSION}.html"
|
||||
|
||||
echo "Generating HTML changelog"
|
||||
|
||||
jq -r '
|
||||
"<!DOCTYPE html>",
|
||||
"<html>",
|
||||
"<head>",
|
||||
"<meta charset=\"UTF-8\">",
|
||||
"<title>Changelog</title>",
|
||||
"<style>",
|
||||
"body { font-family: Segoe UI, sans-serif; background:#eeeeee; padding:20px; }",
|
||||
".change { background:white; padding:25px; border-radius:10px; }",
|
||||
"li { margin-bottom:6px; }",
|
||||
"</style>",
|
||||
"</head>",
|
||||
"<body>",
|
||||
"<div class=\"change\">",
|
||||
"<ul>",
|
||||
(.changes[] |
|
||||
"<li><b>\(.type):</b> \(.text)" +
|
||||
(if .link then " <a href=\"\(.link)\">details</a>" else "" end) +
|
||||
"</li>"
|
||||
),
|
||||
"</ul>",
|
||||
"</div>",
|
||||
"</body>",
|
||||
"</html>"
|
||||
' "$JSON" > "$HTML"
|
||||
|
||||
echo "Generated $HTML"
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Build application
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Build NebulaAuth
|
||||
run: |
|
||||
dotnet publish src/NebulaAuth/NebulaAuth.csproj \
|
||||
-c Release \
|
||||
-o build \
|
||||
-p:EnableWindowsTargeting=true
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Package ZIP
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Package release
|
||||
run: |
|
||||
cd build
|
||||
zip -r ../NebulaAuth.${{ steps.ver.outputs.version }}.zip *
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Generate SHA256 checksum
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Generate checksum
|
||||
id: checksum
|
||||
run: |
|
||||
FILE="NebulaAuth.${{ steps.ver.outputs.version }}.zip"
|
||||
HASH=$(sha256sum "$FILE" | awk '{print $1}')
|
||||
echo "checksum=$HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Generate update.xml
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Generate update.xml
|
||||
run: |
|
||||
VERSION="${{ steps.ver.outputs.version }}"
|
||||
CHECKSUM="${{ steps.checksum.outputs.checksum }}"
|
||||
|
||||
cat > NebulaAuth/update.xml <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>$VERSION</version>
|
||||
<url>https://github.com/${{ github.repository }}/releases/download/$VERSION/NebulaAuth.$VERSION.zip</url>
|
||||
<changelog>https://achiez.github.io/${{ github.event.repository.name }}/changelog/$VERSION.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
<checksum algorithm="SHA256">$CHECKSUM</checksum>
|
||||
</item>
|
||||
EOF
|
||||
|
||||
echo "Generated update.xml"
|
||||
cat NebulaAuth/update.xml
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Generate GitHub Release Notes
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Generate release notes
|
||||
id: notes
|
||||
run: |
|
||||
VERSION="${{ steps.ver.outputs.version }}"
|
||||
FILE="changelog/${VERSION}.json"
|
||||
|
||||
BODY=$(jq -r '
|
||||
.changes[] |
|
||||
"- **\(.type):** \(.text)" +
|
||||
(if .link then " (\(.link))" else "" end)
|
||||
' "$FILE")
|
||||
|
||||
echo "body<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$BODY" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Commit generated files
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Commit generated files
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "actions@github.com"
|
||||
|
||||
git add NebulaAuth/update.xml
|
||||
git add changelog/*.html
|
||||
|
||||
git commit -m "chore(release): ${{ steps.ver.outputs.version }}" || echo "No changes"
|
||||
git push
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Create git tag
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Create tag
|
||||
run: |
|
||||
VERSION="${{ steps.ver.outputs.version }}"
|
||||
git tag -a "$VERSION" -m "Release $VERSION"
|
||||
git push origin "$VERSION"
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Publish GitHub Release
|
||||
# --------------------------------------------------------
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.ver.outputs.version }}
|
||||
name: Release ${{ steps.ver.outputs.version }}
|
||||
body: ${{ steps.notes.outputs.body }}
|
||||
files: |
|
||||
NebulaAuth.${{ steps.ver.outputs.version }}.zip
|
||||
+3
-1
@@ -360,4 +360,6 @@ MigrationBackup/
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
FodyWeavers.xsd
|
||||
|
||||
todo/
|
||||
@@ -1,59 +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
|
||||
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
|
||||
@@ -0,0 +1,28 @@
|
||||
<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" />
|
||||
</Folder>
|
||||
<Project Path="src/NebulaAuth/NebulaAuth.csproj" />
|
||||
<Project Path="src/SteamLibForked/SteamLibForked.csproj" />
|
||||
</Solution>
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<item>
|
||||
<version>1.7.4.0</version>
|
||||
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.7.4/NebulaAuth.1.7.4.zip</url>
|
||||
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.7.4.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>1.8.2</version>
|
||||
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.2/NebulaAuth.1.8.2.zip</url>
|
||||
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.2.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<a href="https://t.me/nebulaauth">t.me/nebulaauth</a>
|
||||
</b>
|
||||
</li>
|
||||
<li><b>FIX:</b> Resolved issue where a "Confirmation Error" notification appeared despite the confirmation being successful.</li>
|
||||
<li><b>FIX:</b> Resolved issue where a "Confirmation Error" notification appeared despite the confirmation being successful.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Changelog</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #eeeeee;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.changelog-container {
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 25px;
|
||||
margin: 25px auto;
|
||||
max-width: 800px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.change {
|
||||
margin-bottom: 20px;
|
||||
padding: 0;
|
||||
border-left: 4px solid #a50ec7;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-weight: 600;
|
||||
font-size: 1.5em;
|
||||
color: #a50ec7;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-style: italic;
|
||||
color: #888;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1em;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.description ul {
|
||||
list-style: inside square;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.changelog-container {
|
||||
width: 90%;
|
||||
margin: 25px auto;
|
||||
padding: 25px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="changelog-container">
|
||||
<!-- Changelog entry -->
|
||||
<div class="change">
|
||||
<div class="version">Version 1.8.0</div>
|
||||
<div class="date">07.11.2025</div>
|
||||
<div class="description">
|
||||
<ul>
|
||||
<li> Major update: Full changelog is available here <a href="https://teletype.in/@achies_raw/nebula-1-8-0-eng">ENG</a> | <a href="https://teletype.in/@achies_raw/nebula-1-8-0-rus">RUS</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SteamLibForked\SteamLibForked.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
@@ -18,17 +15,8 @@ public partial class App
|
||||
var splashScreen = new SplashScreen("Theme\\SplashScreen.png");
|
||||
splashScreen.Show(false, true);
|
||||
base.OnStartup(e);
|
||||
LocManager.Init();
|
||||
LocManager.SetApplicationLocalization(Settings.Instance.Language);
|
||||
Shell.Initialize();
|
||||
|
||||
var files = 0;
|
||||
if (Directory.Exists(Storage.MAFILE_F))
|
||||
files = Directory.GetFiles(Storage.MafileFolder)
|
||||
.Count(f => Path.GetExtension(f).EqualsIgnoreCase(".mafile"));
|
||||
|
||||
var threads = files > 0 ? files / 100 + 1 : 1;
|
||||
await Storage.Initialize(threads);
|
||||
await Shell.Initialize();
|
||||
var mainWindow = new MainWindow();
|
||||
Current.MainWindow = mainWindow;
|
||||
mainWindow.Show();
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace NebulaAuth.Theme.Controls;
|
||||
namespace NebulaAuth;
|
||||
|
||||
public class CodeProgressBar : ProgressBar
|
||||
{
|
||||
@@ -13,7 +13,7 @@ public class CodeProgressBar : ProgressBar
|
||||
|
||||
public static readonly DependencyProperty MaxTimeProperty =
|
||||
DependencyProperty.Register(nameof(MaxTime), typeof(double), typeof(CodeProgressBar),
|
||||
new PropertyMetadata(30.0)); // По умолчанию 30 сек
|
||||
new PropertyMetadata(30.0));
|
||||
|
||||
public double TimeRemaining
|
||||
{
|
||||
@@ -0,0 +1,52 @@
|
||||
<UserControl x:Class="NebulaAuth.HintBox"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance nebulaAuth:HintBox}"
|
||||
d:DesignHeight="100" d:DesignWidth="400">
|
||||
|
||||
<Border Padding="5"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Visibility="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource NullableToVisibilityConverter}}">
|
||||
<Border.BorderBrush>
|
||||
<SolidColorBrush Color="{DynamicResource BaseShadowColor}" Opacity="0.5" />
|
||||
</Border.BorderBrush>
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.2" />
|
||||
</Border.Background>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<materialDesign:PackIcon Height="22" Width="22"
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,0,0,0"
|
||||
Foreground="{Binding IconBrush, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
Kind="{Binding IconKind, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,10,10,10"
|
||||
Text="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Visibility="{Binding ShowCloseButton, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
Command="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
Style="{StaticResource MaterialDesignToolButton}"
|
||||
Margin="0,0,5,0"
|
||||
Padding="2">
|
||||
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
|
||||
namespace NebulaAuth;
|
||||
|
||||
public partial class HintBox : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
DependencyProperty.Register(nameof(Text), typeof(string), typeof(HintBox));
|
||||
|
||||
public static readonly DependencyProperty SeverityProperty =
|
||||
DependencyProperty.Register(nameof(Severity), typeof(HintBoxSeverity), typeof(HintBox),
|
||||
new PropertyMetadata(HintBoxSeverity.Info, OnSeverityChanged));
|
||||
|
||||
public static readonly DependencyProperty ShowCloseButtonProperty =
|
||||
DependencyProperty.Register(nameof(ShowCloseButton), typeof(bool), typeof(HintBox),
|
||||
new PropertyMetadata(false));
|
||||
|
||||
|
||||
public static readonly DependencyProperty CloseCommandProperty =
|
||||
DependencyProperty.Register(nameof(CloseCommand), typeof(ICommand), typeof(HintBox),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string) GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public HintBoxSeverity Severity
|
||||
{
|
||||
get => (HintBoxSeverity) GetValue(SeverityProperty);
|
||||
set => SetValue(SeverityProperty, value);
|
||||
}
|
||||
|
||||
public ICommand CloseCommand
|
||||
{
|
||||
get => (ICommand) GetValue(CloseCommandProperty) ?? InternalCloseCommand;
|
||||
set => SetValue(CloseCommandProperty, value);
|
||||
}
|
||||
|
||||
public bool ShowCloseButton
|
||||
{
|
||||
get => (bool) GetValue(ShowCloseButtonProperty);
|
||||
set => SetValue(ShowCloseButtonProperty, value);
|
||||
}
|
||||
|
||||
private ICommand InternalCloseCommand { get; }
|
||||
|
||||
public PackIconKind IconKind { get; private set; }
|
||||
public Brush IconBrush { get; private set; }
|
||||
|
||||
public HintBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplySeverityVisuals();
|
||||
|
||||
InternalCloseCommand = new RelayCommand(Close);
|
||||
}
|
||||
|
||||
public event RoutedEventHandler Closed;
|
||||
|
||||
private static void OnSeverityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((HintBox) d).ApplySeverityVisuals();
|
||||
}
|
||||
|
||||
private void ApplySeverityVisuals()
|
||||
{
|
||||
switch (Severity)
|
||||
{
|
||||
case HintBoxSeverity.Error:
|
||||
IconKind = PackIconKind.ErrorOutline;
|
||||
IconBrush = (Brush) Application.Current.FindResource("ErrorBrush")!;
|
||||
break;
|
||||
default:
|
||||
IconKind = PackIconKind.InfoCircleOutline;
|
||||
IconBrush = (Brush) Application.Current.FindResource("InfoBrush")!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
Visibility = Visibility.Collapsed;
|
||||
Closed?.Invoke(this, new RoutedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
public enum HintBoxSeverity
|
||||
{
|
||||
Info,
|
||||
Error
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace NebulaAuth.Converters;
|
||||
|
||||
public class BoolToMafileNamingConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not bool boolValue)
|
||||
return null;
|
||||
return boolValue ? "Login" : "Steam ID";
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace NebulaAuth.Converters;
|
||||
|
||||
public class BoolToValueConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not bool b) return Binding.DoNothing;
|
||||
if (parameter is Array)
|
||||
{
|
||||
var arr = (Array) parameter;
|
||||
if (arr.Length == 0) return Binding.DoNothing;
|
||||
var first = arr.GetValue(0);
|
||||
var second = arr.Length > 1 ? arr.GetValue(1) : null;
|
||||
return b ? first : second;
|
||||
}
|
||||
|
||||
return b ? parameter : null;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,12 @@
|
||||
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" />
|
||||
<converters:ProxyTextConverter x:Key="ProxyTextConverter" />
|
||||
<converters:ProxyDataTextConverter x:Key="ProxyDataTextConverter" />
|
||||
<converters:ProxyDataTextMultiConverter x:Key="ProxyDataTextMultiConverter" />
|
||||
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter" />
|
||||
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
|
||||
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
|
||||
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
|
||||
|
||||
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
|
||||
<converters:BoolToValueConverter x:Key="BoolToValueConverter" />
|
||||
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
|
||||
<!-- Background converters-->
|
||||
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
|
||||
@@ -21,6 +22,7 @@
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
|
||||
|
||||
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
|
||||
|
||||
<system:Boolean x:Key="True">True</system:Boolean>
|
||||
<system:Boolean x:Key="False">False</system:Boolean>
|
||||
|
||||
+4
-10
@@ -1,25 +1,19 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace NebulaAuth.Converters;
|
||||
|
||||
public class PortableMaClientStatusToColorConverter : IValueConverter
|
||||
public class InverseBooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is false)
|
||||
{
|
||||
return new SolidColorBrush(Color.FromRgb(187, 224, 139));
|
||||
}
|
||||
|
||||
|
||||
return new SolidColorBrush(Color.FromRgb(224, 139, 139));
|
||||
return value is bool b && !b ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
return value is Visibility v && v != Visibility.Visible;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Windows.Data;
|
||||
using AchiesUtilities.Web.Proxy;
|
||||
using NebulaAuth.Model.Entities;
|
||||
@@ -33,11 +34,50 @@ public class ProxyDataTextConverter : IValueConverter
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"{p.Address}:{p.Port}";
|
||||
return $"{p.Protocol.ToString().ToLowerInvariant()}://{p.Address}:{p.Port}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class ProxyDataTextMultiConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var value = values[0] as ProxyData;
|
||||
var displayProtocol = values.Length > 1 && values[1] is bool dp && dp;
|
||||
var displayCredentials = values.Length > 2 && values[2] is bool dc && dc;
|
||||
if (value == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
if (displayProtocol)
|
||||
{
|
||||
sb.Append(value.Protocol.ToString().ToLowerInvariant());
|
||||
sb.Append("://");
|
||||
}
|
||||
|
||||
sb.Append(value.Address);
|
||||
sb.Append(':');
|
||||
sb.Append(value.Port);
|
||||
if (displayCredentials && value.AuthEnabled)
|
||||
{
|
||||
sb.Append(':');
|
||||
sb.Append(value.Username);
|
||||
sb.Append(':');
|
||||
sb.Append(value.Password);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using AutoUpdaterDotNET;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Update;
|
||||
using NebulaAuth.View;
|
||||
using NebulaAuth.View.Dialogs;
|
||||
using NebulaAuth.ViewModel.Linker;
|
||||
@@ -13,25 +15,6 @@ namespace NebulaAuth.Core;
|
||||
|
||||
public static class DialogsController
|
||||
{
|
||||
#region CommonDialogs
|
||||
|
||||
public static async Task<bool> ShowConfirmCancelDialog(string? msg = null)
|
||||
{
|
||||
var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
|
||||
|
||||
var result = await DialogHost.Show(content);
|
||||
return result != null && (bool) result;
|
||||
}
|
||||
|
||||
//public static async Task<string?> ShowTextFieldDialog(string? msg = null)
|
||||
//{
|
||||
// var content = msg == null ? new TextFieldDialog() : new TextFieldDialog(msg);
|
||||
// var result = await DialogHost.Show(content);
|
||||
// return result as string;
|
||||
//}
|
||||
|
||||
#endregion
|
||||
|
||||
public static async Task<LoginAgainVM?> ShowLoginAgainDialog(string username, string? currentPassword = null)
|
||||
{
|
||||
var vm = new LoginAgainVM
|
||||
@@ -70,6 +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();
|
||||
@@ -121,4 +124,54 @@ public static class DialogsController
|
||||
vm?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ShowSetAccountsPasswordDialog()
|
||||
{
|
||||
var vm = new SetAccountPasswordsVM();
|
||||
var dialog = new SetAccountPasswordsView
|
||||
{
|
||||
DataContext = vm
|
||||
};
|
||||
await DialogHost.Show(dialog);
|
||||
}
|
||||
|
||||
public static async Task ShowExportDialog()
|
||||
{
|
||||
var vm = new MafileExporterVM();
|
||||
var dialog = new MafileExporterView
|
||||
{
|
||||
DataContext = vm
|
||||
};
|
||||
await DialogHost.Show(dialog);
|
||||
}
|
||||
|
||||
public static async Task ShowUpdateDialog(UpdateInfoEventArgs args, ChangelogEntry? changelog,
|
||||
string? htmlFallbackUrl)
|
||||
{
|
||||
var vm = new UpdateDialogVM(args, changelog, htmlFallbackUrl);
|
||||
var dialog = new UpdateDialog
|
||||
{
|
||||
DataContext = vm
|
||||
};
|
||||
await DialogHost.Show(dialog);
|
||||
}
|
||||
|
||||
#region CommonDialogs
|
||||
|
||||
public static async Task<bool> ShowConfirmCancelDialog(string? msg = null)
|
||||
{
|
||||
var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
|
||||
|
||||
var result = await DialogHost.Show(content);
|
||||
return result != null && (bool) result;
|
||||
}
|
||||
|
||||
public static async Task<string?> ShowTextFieldDialog(string? title = null, string? msg = null)
|
||||
{
|
||||
var content = new TextFieldDialog(title, msg);
|
||||
var result = await DialogHost.Show(content);
|
||||
return result as string;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public static class TrayManager
|
||||
|
||||
var contextMenu = new ContextMenuStrip();
|
||||
|
||||
contextMenu.Items.Add("Выйти", null!, OnExitClick);
|
||||
contextMenu.Items.Add("Exit", null!, OnExitClick);
|
||||
|
||||
_notifyIcon.ContextMenuStrip = contextMenu;
|
||||
|
||||
@@ -69,7 +69,7 @@ public static class TrayManager
|
||||
private static void HideMainWindow()
|
||||
{
|
||||
if (!Init) return;
|
||||
if (IsEnabled == false) return;
|
||||
if (!IsEnabled) return;
|
||||
_notifyIcon.Visible = true;
|
||||
MainWindow.Hide();
|
||||
}
|
||||
|
||||
@@ -1,75 +1,143 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Windows;
|
||||
using AutoUpdaterDotNET;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Update;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Core;
|
||||
|
||||
public static class UpdateManager
|
||||
{
|
||||
private const string UPDATE_URL =
|
||||
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/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;
|
||||
if (Settings.Instance.AllowAutoUpdate)
|
||||
AutoUpdater.UpdateMode = Mode.ForcedDownload;
|
||||
AutoUpdater.RunUpdateAsAdmin = RequiresAdminAccess();
|
||||
AutoUpdater.Start(UPDATE_URL);
|
||||
}
|
||||
|
||||
public static void SkipVersion(string version)
|
||||
{
|
||||
var settings = UpdateSettings.Load();
|
||||
settings.SkipVersion(version);
|
||||
}
|
||||
|
||||
//static UpdateManager()
|
||||
//{
|
||||
// //AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
|
||||
public static void SetRemindAfter(TimeSpan delay)
|
||||
{
|
||||
var settings = UpdateSettings.Load();
|
||||
settings.SetRemindAfter(DateTime.Now + delay);
|
||||
}
|
||||
|
||||
//}
|
||||
private static async void HandleCheckForUpdateEvent(UpdateInfoEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isManual = _isManualCheck;
|
||||
_isManualCheck = false;
|
||||
|
||||
//private static async void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
|
||||
//{
|
||||
// if (args.Error == null)
|
||||
// {
|
||||
// if (args.IsUpdateAvailable)
|
||||
// {
|
||||
// DialogResult dialogResult;
|
||||
// var dialog = new UpdaterView()
|
||||
// {
|
||||
// DataContext = new UpdaterVM(args)
|
||||
// };
|
||||
if (args.Error != null)
|
||||
{
|
||||
if (isManual)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Request error",
|
||||
"RequestError")));
|
||||
}
|
||||
|
||||
// await DialogHost.Show(dialog);
|
||||
// Application.Current.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
if (!args.IsUpdateAvailable)
|
||||
{
|
||||
if (isManual)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
SnackbarController.SendSnackbar(
|
||||
LocManager.GetCodeBehindOrDefault("You are using the latest version", "UpdateService",
|
||||
"LatestVersion")));
|
||||
}
|
||||
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (args.Error is WebException)
|
||||
// {
|
||||
return;
|
||||
}
|
||||
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
var version = args.CurrentVersion;
|
||||
var settings = UpdateSettings.Load();
|
||||
|
||||
// }
|
||||
// }
|
||||
if (!isManual && !settings.ShouldShow(version))
|
||||
{
|
||||
HasPendingUpdate = true;
|
||||
Application.Current.Dispatcher.Invoke(() => PendingUpdateDetected?.Invoke());
|
||||
return;
|
||||
}
|
||||
|
||||
//}
|
||||
ChangelogEntry? changelog = null;
|
||||
try
|
||||
{
|
||||
var jsonUrl = $"{CHANGELOG_BASE_URL}{version}.json";
|
||||
var response = await HttpClient.GetAsync(jsonUrl).ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
changelog = JsonConvert.DeserializeObject<ChangelogEntry>(json);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fallback to HTML changelog URL
|
||||
}
|
||||
|
||||
//private static void RunUpdate(UpdateInfoEventArgs args)
|
||||
//{
|
||||
// Application.Current.Dispatcher.Invoke(() =>
|
||||
// {
|
||||
// if (AutoUpdater.DownloadUpdate(args))
|
||||
// {
|
||||
// Application.Current.Shutdown();
|
||||
// }
|
||||
// }, DispatcherPriority.ContextIdle);
|
||||
//}
|
||||
var htmlFallbackUrl = changelog == null ? args.ChangelogURL : null;
|
||||
|
||||
await Application.Current.Dispatcher.BeginInvoke(async () =>
|
||||
{
|
||||
await DialogsController.ShowUpdateDialog(args, changelog, htmlFallbackUrl);
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Shell.Logger.Error(e, "Error while checking updates");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RequiresAdminAccess()
|
||||
{
|
||||
try
|
||||
{
|
||||
var testFile = Path.Combine(Environment.CurrentDirectory, "test.tmp");
|
||||
using (File.Create(testFile))
|
||||
{
|
||||
}
|
||||
|
||||
File.Delete(testFile);
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
{
|
||||
"CodeBehind": {
|
||||
"ErrorTranslator": {
|
||||
"Login": {
|
||||
"CaptchaRequired": {
|
||||
"en": "Captcha",
|
||||
"ru": "Капча",
|
||||
"zh": "验证码",
|
||||
"uk": "Капча",
|
||||
"fr": "Captcha",
|
||||
"es": "Captcha",
|
||||
"tr": "Captcha",
|
||||
"kk": "Капча"
|
||||
},
|
||||
"InvalidCredentials": {
|
||||
"en": "Invalid password",
|
||||
"ru": "Неверный пароль",
|
||||
"zh": "密码无效",
|
||||
"uk": "Невірний пароль",
|
||||
"fr": "Mot de passe invalide",
|
||||
"es": "Contraseña inválida",
|
||||
"tr": "Geçersiz şifre",
|
||||
"kk": "Қате құпиясөз"
|
||||
},
|
||||
"EmailAuthRequired": {
|
||||
"en": "Email code required",
|
||||
"ru": "Требуется код с почты",
|
||||
"zh": "需要电子邮件验证码",
|
||||
"uk": "Потрібен код з пошти",
|
||||
"fr": "Code email requis",
|
||||
"es": "Se requiere código de correo electrónico",
|
||||
"tr": "E-posta kodu gerekli",
|
||||
"kk": "Электрондық пошта коды қажет"
|
||||
},
|
||||
"InvalidEmailAuthCode": {
|
||||
"en": "Invalid email code",
|
||||
"ru": "Неверный код с почты",
|
||||
"zh": "无效的电子邮件验证码",
|
||||
"uk": "Невірний код з пошти",
|
||||
"fr": "Code email invalide",
|
||||
"es": "Código de correo electrónico inválido",
|
||||
"tr": "Geçersiz e-posta kodu",
|
||||
"kk": "Қате электрондық пошта коды"
|
||||
},
|
||||
"InvalidTwoFactorCode": {
|
||||
"en": "Invalid two factor code",
|
||||
"ru": "Неверный двухфакторный код",
|
||||
"zh": "无效的两步验证码",
|
||||
"uk": "Невірний двофакторний код",
|
||||
"fr": "Code 2FA invalide",
|
||||
"es": "Código de verificación en dos pasos inválido",
|
||||
"tr": "Geçersiz iki faktörlü kod",
|
||||
"kk": "Қате екі факторлы код"
|
||||
},
|
||||
"UndefinedError": {
|
||||
"en": "Undefined error",
|
||||
"ru": "Неизвестная ошибка",
|
||||
"zh": "未定义的错误",
|
||||
"uk": "Невідома помилка",
|
||||
"fr": "Erreur inconnue",
|
||||
"es": "Error desconocido",
|
||||
"tr": "Bilinmeyen hata",
|
||||
"kk": "Белгісіз қате"
|
||||
}
|
||||
},
|
||||
"EResult": {
|
||||
"InvalidPassword": {
|
||||
"ru": "Неверный пароль",
|
||||
"en": "Invalid password",
|
||||
"zh": "密码错误",
|
||||
"uk": "Невірний пароль",
|
||||
"fr": "Mot de passe invalide",
|
||||
"es": "Contraseña inválida",
|
||||
"tr": "Geçersiz şifre",
|
||||
"kk": "Қате құпиясөз"
|
||||
},
|
||||
"InvalidState": {
|
||||
"ru": "Неверное состояние (11)",
|
||||
"en": "Invalid state (11)",
|
||||
"zh": "无效状态 (11)",
|
||||
"uk": "Невірний стан (11)",
|
||||
"fr": "État invalide (11)",
|
||||
"es": "Estado inválido (11)",
|
||||
"tr": "Geçersiz durum (11)",
|
||||
"kk": "Жарамсыз күй (11)"
|
||||
},
|
||||
"Timeout": {
|
||||
"ru": "Таймаут (16)",
|
||||
"en": "Timeout (16)",
|
||||
"zh": "超时 (16)",
|
||||
"uk": "Таймаут (16)",
|
||||
"fr": "Timeout (16)",
|
||||
"es": "Tiempo de espera agotado (16)",
|
||||
"tr": "Zaman aşımı (16)",
|
||||
"kk": "Уақыт аяқталды (16)"
|
||||
},
|
||||
"Banned": {
|
||||
"ru": "Забанен (17)",
|
||||
"en": "Banned (17)",
|
||||
"zh": "被封禁 (17)",
|
||||
"uk": "Забанений (17)",
|
||||
"fr": "Banni (17)",
|
||||
"es": "Prohibido (17)",
|
||||
"tr": "Yasaklandı (17)",
|
||||
"kk": "Бұғатталған (17)"
|
||||
},
|
||||
"AccountNotFound": {
|
||||
"ru": "Аккаунт не найден (18)",
|
||||
"en": "Account not found (18)",
|
||||
"zh": "未找到账户 (18)",
|
||||
"uk": "Акаунт не знайдено (18)",
|
||||
"fr": "Compte non trouvé (18)",
|
||||
"es": "Cuenta no encontrada (18)",
|
||||
"tr": "Hesap bulunamadı (18)",
|
||||
"kk": "Аккаунт табылмады (18)"
|
||||
},
|
||||
"InvalidSteamID": {
|
||||
"ru": "Неправильный SteamID (19)",
|
||||
"en": "Invalid SteamID (19)",
|
||||
"zh": "无效的 SteamID (19)",
|
||||
"uk": "Неправильний SteamID (19)",
|
||||
"fr": "SteamID invalide (19)",
|
||||
"es": "SteamID inválido (19)",
|
||||
"tr": "Geçersiz SteamID (19)",
|
||||
"kk": "Жарамсыз SteamID (19)"
|
||||
},
|
||||
"ServiceUnavailable": {
|
||||
"ru": "Сервер не отвечает (20)",
|
||||
"en": "Service unavailable (20)",
|
||||
"zh": "服务器无响应 (20)",
|
||||
"uk": "Сервер не відповідає (20)",
|
||||
"fr": "Service indisponible (20)",
|
||||
"es": "Servicio no disponible (20)",
|
||||
"tr": "Servis kullanılamıyor (20)",
|
||||
"kk": "Қызмет қолжетімсіз (20)"
|
||||
},
|
||||
"Pending": {
|
||||
"ru": "Ожидание (21)",
|
||||
"en": "Pending (21)",
|
||||
"zh": "等待 (21)",
|
||||
"uk": "Очікування (21)",
|
||||
"fr": "En attente (21)",
|
||||
"es": "Pendiente (21)",
|
||||
"tr": "Beklemede (21)",
|
||||
"kk": "Күту (21)"
|
||||
},
|
||||
"LimitExceeded": {
|
||||
"ru": "Лимит превышен (25)",
|
||||
"en": "Limit exceeded (25)",
|
||||
"zh": "超出限制 (25)",
|
||||
"uk": "Ліміт перевищено (25)",
|
||||
"fr": "Limite dépassée (25)",
|
||||
"es": "Límite excedido (25)",
|
||||
"tr": "Limit aşıldı (25)",
|
||||
"kk": "Лимит асылды (25)"
|
||||
},
|
||||
"Expired": {
|
||||
"ru": "Истекло (27)",
|
||||
"en": "Expired (27)",
|
||||
"zh": "有效期已过 (27)",
|
||||
"uk": "Термін дії закінчився (27)",
|
||||
"fr": "Expiré (27)",
|
||||
"es": "Expirado (27)",
|
||||
"tr": "Süresi doldu (27)",
|
||||
"kk": "Мерзімі аяқталды (27)"
|
||||
},
|
||||
"Blocked": {
|
||||
"ru": "Заблокировано (40)",
|
||||
"en": "Blocked (40)",
|
||||
"zh": "已封锁 (40)",
|
||||
"uk": "Заблоковано (40)",
|
||||
"fr": "Bloqué (40)",
|
||||
"es": "Bloqueado (40)",
|
||||
"tr": "Engellendi (40)",
|
||||
"kk": "Блокталған (40)"
|
||||
},
|
||||
"AccountDisabled": {
|
||||
"ru": "Аккаунт отключен (43)",
|
||||
"en": "Account disabled (43)",
|
||||
"zh": "账户已被禁用 (43)",
|
||||
"uk": "Акаунт відключено (43)",
|
||||
"fr": "Compte désactivé (43)",
|
||||
"es": "Cuenta deshabilitada (43)",
|
||||
"tr": "Hesap devre dışı (43)",
|
||||
"kk": "Аккаунт өшірілген (43)"
|
||||
},
|
||||
"Suspended": {
|
||||
"ru": "Отключено (Suspended 51)",
|
||||
"en": "Suspended (51)",
|
||||
"zh": "已停用(暂停 51)",
|
||||
"uk": "Відключено (Suspended 51)",
|
||||
"fr": "Suspendu (51)",
|
||||
"es": "Suspendido (51)",
|
||||
"tr": "Askıya alındı (51)",
|
||||
"kk": "Тоқтатылған (51)"
|
||||
},
|
||||
"Cancelled": {
|
||||
"ru": "Отмена (52)",
|
||||
"en": "Cancelled (52)",
|
||||
"zh": "已取消 (52)",
|
||||
"uk": "Скасовано (52)",
|
||||
"fr": "Annulé (52)",
|
||||
"es": "Cancelado (52)",
|
||||
"tr": "İptal edildi (52)",
|
||||
"kk": "Бас тартылды (52)"
|
||||
},
|
||||
"InvalidLoginAuthCode": {
|
||||
"ru": "Неверный код авторизации (65)",
|
||||
"en": "Invalid login auth code (65)",
|
||||
"zh": "无效的授权码 (65)",
|
||||
"uk": "Невірний код авторизації (65)",
|
||||
"fr": "Code d'authentification invalide (65)",
|
||||
"es": "Código de autenticación inválido (65)",
|
||||
"tr": "Geçersiz giriş doğrulama kodu (65)",
|
||||
"kk": "Жарамсыз авторизация коды (65)"
|
||||
},
|
||||
"ExpiredLoginAuthCode": {
|
||||
"ru": "Истекло время кода (71)",
|
||||
"en": "Expired login auth code (71)",
|
||||
"zh": "验证码已过期 (71)",
|
||||
"uk": "Термін дії коду закінчився (71)",
|
||||
"fr": "Code d'authentification expiré (71)",
|
||||
"es": "Código de autenticación expirado (71)",
|
||||
"tr": "Giriş doğrulama kodunun süresi doldu (71)",
|
||||
"kk": "Авторизация кодының мерзімі аяқталды (71)"
|
||||
},
|
||||
"AccountLockedDown": {
|
||||
"ru": "Аккаунт заблокирован (КТ 73)",
|
||||
"en": "Account locked down (73)",
|
||||
"zh": "账户被封锁 (КТ 73)",
|
||||
"uk": "Акаунт заблоковано (КТ 73)",
|
||||
"fr": "Compte verrouillé (73)",
|
||||
"es": "Cuenta bloqueada (73)",
|
||||
"tr": "Hesap kilitlendi (73)",
|
||||
"kk": "Аккаунт бұғатталған (73)"
|
||||
},
|
||||
"UnexpectedError": {
|
||||
"ru": "Неожиданная ошибка (79)",
|
||||
"en": "Unexpected error (79)",
|
||||
"zh": "不可预料的错误 (79)",
|
||||
"uk": "Непередбачувана помилка (79)",
|
||||
"fr": "Erreur inattendue (79)",
|
||||
"es": "Error inesperado (79)",
|
||||
"tr": "Beklenmeyen hata (79)",
|
||||
"kk": "Күтпеген қате (79)"
|
||||
},
|
||||
"Disabled": {
|
||||
"ru": "Отключено (80)",
|
||||
"en": "Disabled (80)",
|
||||
"zh": "已关闭 (80)",
|
||||
"uk": "Відключено (80)",
|
||||
"fr": "Désactivé (80)",
|
||||
"es": "Deshabilitado (80)",
|
||||
"tr": "Devre dışı (80)",
|
||||
"kk": "Өшірілген (80)"
|
||||
},
|
||||
"RegionLocked": {
|
||||
"ru": "Регион заблокирован (83)",
|
||||
"en": "Region locked (83)",
|
||||
"zh": "该地区已被封锁 (83)",
|
||||
"uk": "Регіон заблоковано (83)",
|
||||
"fr": "Région bloquée (83)",
|
||||
"es": "Región bloqueada (83)",
|
||||
"tr": "Bölge kilitli (83)",
|
||||
"kk": "Аймақ бұғатталған (83)"
|
||||
},
|
||||
"RateLimitExceeded": {
|
||||
"ru": "Слишком много запросов (84)",
|
||||
"en": "Rate limit exceeded (84)",
|
||||
"zh": "请求过多 (84)",
|
||||
"uk": "Забагато запитів (84)",
|
||||
"fr": "Limite de connexion dépassée (84)",
|
||||
"es": "Límite de solicitudes excedido (84)",
|
||||
"tr": "İstek limiti aşıldı (84)",
|
||||
"kk": "Сұраныс лимиті асты (84)"
|
||||
},
|
||||
"TwoFactorCodeMismatch": {
|
||||
"ru": "2FA код не подошел (88)",
|
||||
"en": "2FA code mismatch (88)",
|
||||
"zh": "双因素代码不正确 (88)",
|
||||
"uk": "2FA код не підійшов (88)",
|
||||
"fr": "Code 2FA invalide (88)",
|
||||
"es": "Código 2FA incorrecto (88)",
|
||||
"tr": "2FA kodu eşleşmedi (88)",
|
||||
"kk": "2FA коды сәйкес келмеді (88)"
|
||||
},
|
||||
"TwoFactorActivationCodeMismatch": {
|
||||
"ru": "2FA код не прошел проверку (89)",
|
||||
"en": "2FA activation code mismatch (89)",
|
||||
"zh": "双因素认证代码未通过验证 (89)",
|
||||
"uk": "2FA код не пройшов перевірку (89)",
|
||||
"fr": "Code d'activation 2FA invalide (89)",
|
||||
"es": "Código de activación 2FA incorrecto (89)",
|
||||
"tr": "2FA aktivasyon kodu eşleşmedi (89)",
|
||||
"kk": "2FA активация коды сәйкес келмеді (89)"
|
||||
},
|
||||
"TimeNotSynced": {
|
||||
"ru": "Время не синхронизировано (93)",
|
||||
"en": "Time not synced (93)",
|
||||
"zh": "时间未同步 (93)",
|
||||
"uk": "Час не синхронізовано (93)",
|
||||
"fr": "Heure non synchronisée (93)",
|
||||
"es": "Hora no sincronizada (93)",
|
||||
"tr": "Saat senkronize değil (93)",
|
||||
"kk": "Уақыт синхрондалмаған (93)"
|
||||
},
|
||||
"SMSCodeFailed": {
|
||||
"ru": "Ошибка SMS-кода (94)",
|
||||
"en": "SMS code failed (94)",
|
||||
"zh": "短信验证码错误 (94)",
|
||||
"uk": "Помилка SMS-коду (94)",
|
||||
"fr": "Code SMS échoué (94)",
|
||||
"es": "Código SMS incorrecto (94)",
|
||||
"tr": "SMS kodu hatalı (94)",
|
||||
"kk": "SMS коды қате (94)"
|
||||
},
|
||||
"AccountLimitExceeded": {
|
||||
"ru": "Лимит аккаунта превышен (95)",
|
||||
"en": "Account limit exceeded (95)",
|
||||
"zh": "账户已超出限制 (95)",
|
||||
"uk": "Ліміт акаунта перевищено (95)",
|
||||
"fr": "Limite de compte dépassée (95)",
|
||||
"es": "Límite de cuenta excedido (95)",
|
||||
"tr": "Hesap limiti aşıldı (95)",
|
||||
"kk": "Аккаунт лимиті асты (95)"
|
||||
},
|
||||
"EmailSendFailure": {
|
||||
"ru": "Не удалось отправить письмо (99)",
|
||||
"en": "Email send failure (99)",
|
||||
"zh": "无法发送邮件 (99)",
|
||||
"uk": "Не вдалося відправити лист (99)",
|
||||
"fr": "Échec de l'envoi de l'email (99)",
|
||||
"es": "Error al enviar el correo (99)",
|
||||
"tr": "E-posta gönderilemedi (99)",
|
||||
"kk": "Электрондық хат жіберілмеді (99)"
|
||||
},
|
||||
"NeedCaptcha": {
|
||||
"ru": "Капча (101)",
|
||||
"en": "Need captcha (101)",
|
||||
"zh": "验证码 (101)",
|
||||
"uk": "Капча (101)",
|
||||
"fr": "Captcha requis (101)",
|
||||
"es": "Captcha requerido (101)",
|
||||
"tr": "Captcha gerekli (101)",
|
||||
"kk": "Капча қажет (101)"
|
||||
},
|
||||
"IPBanned": {
|
||||
"ru": "IP заблокирован (105)",
|
||||
"en": "IP banned (105)",
|
||||
"zh": "IP被阻止 (105)",
|
||||
"uk": "IP заблоковано (105)",
|
||||
"fr": "IP bloquée (105)",
|
||||
"es": "IP bloqueada (105)",
|
||||
"tr": "IP yasaklandı (105)",
|
||||
"kk": "IP бұғатталған (105)"
|
||||
},
|
||||
"LimitedUserAccount": {
|
||||
"ru": "Аккаунт с лимитом (112)",
|
||||
"en": "Limited user account (112)",
|
||||
"zh": "限额账户 (112)",
|
||||
"uk": "Акаунт з лімітом (112)",
|
||||
"fr": "Compte utilisateur limité (112)",
|
||||
"es": "Cuenta limitada (112)",
|
||||
"tr": "Sınırlı kullanıcı hesabı (112)",
|
||||
"kk": "Шектеулі аккаунт (112)"
|
||||
},
|
||||
"AccountDeleted": {
|
||||
"ru": "Аккаунт удален (114)",
|
||||
"en": "Account deleted (114)",
|
||||
"zh": "账户已删除 (114)",
|
||||
"uk": "Акаунт видалено (114)",
|
||||
"fr": "Compte supprimé (114)",
|
||||
"es": "Cuenta eliminada (114)",
|
||||
"tr": "Hesap silindi (114)",
|
||||
"kk": "Аккаунт жойылған (114)"
|
||||
},
|
||||
"PhoneNumberIsVOIP": {
|
||||
"ru": "Номера VOIP запрещены (127)",
|
||||
"en": "Phone number is VOIP (127)",
|
||||
"zh": "VOIP 号码被禁止 (127)",
|
||||
"uk": "Номера VOIP заборонені (127)",
|
||||
"fr": "Numéros VOIP interdits (127)",
|
||||
"es": "Los números VOIP no están permitidos (127)",
|
||||
"tr": "VOIP numaralar yasaklı (127)",
|
||||
"kk": "VOIP нөмірлеріне тыйым салынған (127)"
|
||||
}
|
||||
},
|
||||
"AuthenticatorLinkerError": {
|
||||
"PhoneAlreadyAttached": {
|
||||
"ru": "Телефон привязан, попробуйте через 20 минут",
|
||||
"en": "Phone already attached, try again in 20 minutes",
|
||||
"zh": "手机已绑定,请在20分钟后重试",
|
||||
"uk": "Телефон прив'язано, спробуйте через 20 хвилин",
|
||||
"fr": "Téléphone déjà attaché, réessayer dans 20 minutes",
|
||||
"es": "El teléfono ya está vinculado, inténtalo de nuevo en 20 minutos",
|
||||
"tr": "Telefon zaten bağlı, 20 dakika sonra tekrar deneyin",
|
||||
"kk": "Телефон бұрыннан тіркелген, 20 минуттан кейін қайта көріңіз"
|
||||
},
|
||||
"InvalidPhoneNumber": {
|
||||
"ru": "Неверный номер телефона",
|
||||
"en": "Invalid phone number",
|
||||
"zh": "无效的电话号码",
|
||||
"uk": "Невірний номер телефону",
|
||||
"fr": "Numéro de téléphone invalide",
|
||||
"es": "Número de teléfono inválido",
|
||||
"tr": "Geçersiz telefon numarası",
|
||||
"kk": "Жарамсыз телефон нөмірі"
|
||||
},
|
||||
"CantAttachPhone": {
|
||||
"ru": "Не удается привязать телефон",
|
||||
"en": "Can't attach phone",
|
||||
"zh": "无法绑定手机",
|
||||
"uk": "Не вдається прив'язати телефон",
|
||||
"fr": "Impossible d'attacher le téléphone",
|
||||
"es": "No se puede vincular el teléfono",
|
||||
"tr": "Telefon bağlanamıyor",
|
||||
"kk": "Телефонды тіркеу мүмкін емес"
|
||||
},
|
||||
"CantConfirmAttachingEmail": {
|
||||
"ru": "Не удалось подтвердить письмо",
|
||||
"en": "Can't confirm attaching email",
|
||||
"zh": "无法确认邮件",
|
||||
"uk": "Не вдалося підтвердити лист",
|
||||
"fr": "Impossible de confirmer l'attachement de l'email",
|
||||
"es": "No se pudo confirmar el correo electrónico",
|
||||
"tr": "E-posta doğrulanamadı",
|
||||
"kk": "Электрондық поштаны растау мүмкін болмады"
|
||||
},
|
||||
"CantSendSms": {
|
||||
"ru": "Не удалось отправить SMS",
|
||||
"en": "Can't send SMS",
|
||||
"zh": "无法发送短信",
|
||||
"uk": "Не вдалося відправити SMS",
|
||||
"fr": "Impossible d'envoyer SMS",
|
||||
"es": "No se pudo enviar el SMS",
|
||||
"tr": "SMS gönderilemedi",
|
||||
"kk": "SMS жіберу мүмкін болмады"
|
||||
},
|
||||
"AuthenticatorPresent": {
|
||||
"ru": "Аутентификатор привязан",
|
||||
"en": "Authenticator present",
|
||||
"zh": "身份验证器已绑定",
|
||||
"uk": "Аутентифікатор прив'язано",
|
||||
"fr": "Authenticateur présent",
|
||||
"es": "Autenticador ya vinculado",
|
||||
"tr": "Kimlik doğrulayıcı zaten bağlı",
|
||||
"kk": "Аутентификатор бұрыннан тіркелген"
|
||||
},
|
||||
"BadConfirmationCode": {
|
||||
"ru": "Код не подошел",
|
||||
"en": "Bad confirmation code",
|
||||
"zh": "代码不正确",
|
||||
"uk": "Код не підійшов",
|
||||
"fr": "Code de confirmation invalide",
|
||||
"es": "Código de confirmación incorrecto",
|
||||
"tr": "Doğrulama kodu hatalı",
|
||||
"kk": "Растау коды қате"
|
||||
},
|
||||
"UnableToGenerateCorrectCodes": {
|
||||
"ru": "Не удается сгенерировать коды",
|
||||
"en": "Unable to generate correct codes",
|
||||
"zh": "无法生成代码",
|
||||
"uk": "Не вдається згенерувати коди",
|
||||
"fr": "Impossible de générer des codes corrects",
|
||||
"es": "No se pueden generar códigos correctos",
|
||||
"tr": "Doğru kodlar oluşturulamıyor",
|
||||
"kk": "Дұрыс кодтарды генерациялау мүмкін емес"
|
||||
},
|
||||
"InvalidStateWithStatus2": {
|
||||
"ru": "Неверное состояние (InvalidState) со статусом 2. Откройте ссылку на руководство сверху этого окна.",
|
||||
"en": "Invalid state (InvalidState) with status 2. Open link to the troubleshooting guide at the top of this window.",
|
||||
"zh": "状态无效(InvalidState),状态码为 2. 请打开本窗口顶部的故障排除指南链接。",
|
||||
"uk": "Невірний стан (InvalidState) зі статусом 2. Відкрийте посилання на посібник з усунення несправностей у верхній частині цього вікна.",
|
||||
"fr": "État invalide (InvalidState) avec le statut 2. Ouvrez le lien vers le guide de dépannage en haut de cette fenêtre.",
|
||||
"es": "Estado inválido (InvalidState) con estado 2. Abre el enlace a la guía de solución de problemas en la parte superior de esta ventana.",
|
||||
"tr": "Geçersiz durum (InvalidState) durum 2 ile. Bu pencerenin üst kısmındaki sorun giderme kılavuzu bağlantısını açın.",
|
||||
"kk": "Жарамсыз күй (InvalidState), статус 2. Осы терезенің жоғарғы жағындағы ақауларды жою нұсқаулығының сілтемесін ашыңыз."
|
||||
},
|
||||
"GeneralFailure": {
|
||||
"ru": "Общая ошибка",
|
||||
"en": "General Failure",
|
||||
"zh": "一般故障",
|
||||
"uk": "Загальна помилка",
|
||||
"fr": "Échec général",
|
||||
"es": "Fallo general",
|
||||
"tr": "Genel hata",
|
||||
"kk": "Жалпы қате"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExceptionHandler": {
|
||||
"SessionInvalidException": {
|
||||
"ru": "Сессия истекла. Попробуйте обновить ее или залогиниться заново (Аккаунт → войти)",
|
||||
"en": "Session expired. Try to refresh it or login again (Account → Login)",
|
||||
"zh": "会话已过期。请通过菜单尝试刷新,或重新登录(账户 → 登录)",
|
||||
"uk": "Сесія прострочена. Спробуйте оновити її через меню або залогінитися знову (Акаунт → Увійти)",
|
||||
"fr": "Session expirée. Essayez de la rafraîchir ou de vous reconnecter à nouveau (Compte → Se connecter)",
|
||||
"es": "La sesión ha expirado. Intenta actualizarla o iniciar sesión nuevamente (Cuenta → Iniciar sesión)",
|
||||
"tr": "Oturum süresi doldu. Yenilemeyi deneyin veya tekrar giriş yapın (Hesap → Giriş)",
|
||||
"kk": "Сессия мерзімі аяқталды. Оны жаңартып көріңіз немесе қайта кіріңіз (Аккаунт → Кіру)"
|
||||
},
|
||||
"SessionExpiredException": {
|
||||
"ru": "Сессия полностью истекла. Нужно залогиниться заново (Аккаунт → Войти)",
|
||||
"en": "Session fully expired. Need to login again (Account → Login)",
|
||||
"zh": "会话已完全过期。需要重新登录(账户 → 登录)",
|
||||
"uk": "Сесія повніст'ю прострочена'. Потрібно залогінитися знову (Акаунт → Увійти)",
|
||||
"fr": "Session entièrement expirée. Vous devez vous reconnecter à nouveau (Compte → Se connecter)",
|
||||
"es": "La sesión ha expirado completamente. Debes iniciar sesión nuevamente (Cuenta → Iniciar sesión)",
|
||||
"tr": "Oturum tamamen sona erdi. Tekrar giriş yapmanız gerekiyor (Hesap → Giriş)",
|
||||
"kk": "Сессия толық аяқталды. Қайта кіру қажет (Аккаунт → Кіру)"
|
||||
},
|
||||
"TaskCanceledException": {
|
||||
"ru": "Произошла отмена операции",
|
||||
"en": "Operation canceled",
|
||||
"zh": "操作已取消",
|
||||
"uk": "Операція скасована",
|
||||
"fr": "Opération annulée",
|
||||
"es": "Operación cancelada",
|
||||
"tr": "İşlem iptal edildi",
|
||||
"kk": "Операция тоқтатылды"
|
||||
},
|
||||
"TimeoutException": {
|
||||
"ru": "Таймаут подключения. Проверьте ваше соединение с Proxy или интернетом",
|
||||
"en": "Connection timeout. Check your connection with Proxy or internet",
|
||||
"zh": "连接超时。请检查您的代理或互联网连接。",
|
||||
"uk": "Таймаут підключення. Перевірте ваше з'єднання з Proxy або інтернетом",
|
||||
"fr": "Timeout de connexion. Vérifiez votre connexion avec Proxy ou internet",
|
||||
"es": "Tiempo de espera de conexión agotado. Verifica tu conexión con el proxy o Internet",
|
||||
"tr": "Bağlantı zaman aşımına uğradı. Proxy veya internet bağlantınızı kontrol edin",
|
||||
"kk": "Қосылу уақыты аяқталды. Proxy немесе интернет қосылымыңызды тексеріңіз"
|
||||
},
|
||||
"UnsupportedResponseException": {
|
||||
"ru": "Получен неожиданный ответ от Steam. Результат сохранен в log.log. Свяжитесь с разработчиком",
|
||||
"en": "Unexpected response from Steam. Result saved in log.log. Contact developer",
|
||||
"zh": "从 Steam 收到意外响应。结果已保存到 log.log。请联系开发者",
|
||||
"uk": "Отримано неочікувану відповідь від Steam. Результат збережено в log.log. Зв'яжіться з розробником",
|
||||
"fr": "Réponse inattendue de Steam. Résultat enregistré dans log.log. Contactez le développeur",
|
||||
"es": "Respuesta inesperada de Steam. El resultado se guardó en log.log. Contacta al desarrollador",
|
||||
"tr": "Steam'den beklenmeyen bir yanıt alındı. Sonuç log.log dosyasına kaydedildi. Geliştiriciyle iletişime geçin",
|
||||
"kk": "Steam-нан күтпеген жауап алынды. Нәтиже log.log файлына сақталды. Әзірлеушімен байланысыңыз"
|
||||
},
|
||||
"LoginException": {
|
||||
"ru": "Ошибка входа: ",
|
||||
"en": "Login error: ",
|
||||
"zh": "登录错误:",
|
||||
"uk": "Помилка входу: ",
|
||||
"fr": "Erreur de connexion: ",
|
||||
"es": "Error de inicio de sesión: ",
|
||||
"tr": "Giriş hatası: ",
|
||||
"kk": "Кіру қатесі: "
|
||||
},
|
||||
"UnsupportedAuthTypeException": {
|
||||
"Mail": {
|
||||
"ru": "Ошибка: Был запрошен код с почты. Steam Guard не активирован",
|
||||
"en": "Error: Email code was requested. Steam Guard is not activated",
|
||||
"zh": "错误:已请求电子邮件验证码。令牌未激活",
|
||||
"uk": "Помилка: Був запрошений код з пошти. Steam Guard не активовано",
|
||||
"fr": "Erreur: Code email était demandé. Steam Guard non activé",
|
||||
"es": "Error: Se solicitó un código por correo electrónico. Steam Guard no está activado",
|
||||
"tr": "Hata: E-posta kodu istendi. Steam Guard etkin değil",
|
||||
"kk": "Қате: Электрондық пошта коды сұралды. Steam Guard белсендірілмеген"
|
||||
},
|
||||
"Guard": {
|
||||
"ru": "Ошибка: Был запрошен вход с помощью Steam Guard. На аккаунте уже активирован мобильный аутентификатор",
|
||||
"en": "Error: Login with Steam Guard was requested. Mobile authenticator is already activated on the account",
|
||||
"zh": "错误:请求使用令牌登录。该账户已激活移动验证器",
|
||||
"uk": "Помилка: Був запрошений вхід за допомогою Steam Guard. На акаунті вже активовано мобільний автентифікатор",
|
||||
"fr": "Erreur: Connexion avec Steam Guard demandée. Authenticateur mobile est déjà activé sur le compte",
|
||||
"es": "Error: Se solicitó iniciar sesión con Steam Guard. El autenticador móvil ya está activado en la cuenta",
|
||||
"tr": "Hata: Steam Guard ile giriş istendi. Hesapta mobil doğrulayıcı zaten etkin",
|
||||
"kk": "Қате: Steam Guard арқылы кіру сұралды. Аккаунтта мобильді аутентификатор әлдеқашан қосылған"
|
||||
},
|
||||
"Unknown": {
|
||||
"ru": "Ошибка: Неизвестный тип аутентификации",
|
||||
"en": "Error: Unknown authentication type",
|
||||
"zh": "错误:未知的身份验证类型",
|
||||
"uk": "Помилка: Невідомий тип автентифікації",
|
||||
"fr": "Erreur: Type d'authentification inconnu",
|
||||
"es": "Error: Tipo de autenticación desconocido",
|
||||
"tr": "Hata: Bilinmeyen kimlik doğrulama türü",
|
||||
"kk": "Қате: Белгісіз аутентификация түрі"
|
||||
}
|
||||
},
|
||||
"UnknownException": {
|
||||
"ru": "Неизвестная ошибка: ",
|
||||
"en": "Unknown error: ",
|
||||
"zh": "未知错误:",
|
||||
"uk": "Невідома помилка: ",
|
||||
"fr": "Erreur inconnue: ",
|
||||
"es": "Error desconocido: ",
|
||||
"tr": "Bilinmeyen hata: ",
|
||||
"kk": "Белгісіз қате: "
|
||||
},
|
||||
"CantLoadConfirmationsException": {
|
||||
"Common": {
|
||||
"ru": "Не удалось загрузить потверждения из-за ошибки Steam: ",
|
||||
"en": "Can't load confirmations due to Steam error: ",
|
||||
"zh": "由于 Steam 错误,无法下载确认信息:",
|
||||
"uk": "Не вдалося завантажити підтвердження через помилку Steam: ",
|
||||
"fr": "Impossible de charger les confirmations en raison de l'erreur Steam: ",
|
||||
"es": "No se pueden cargar las confirmaciones debido a un error de Steam: ",
|
||||
"tr": "Steam hatası nedeniyle onaylar yüklenemedi: ",
|
||||
"kk": "Steam қатесіне байланысты растауларды жүктеу мүмкін емес: "
|
||||
},
|
||||
"TryAgainLater": {
|
||||
"ru": "Попробуйте позже",
|
||||
"en": "Try again later",
|
||||
"zh": "稍后再试",
|
||||
"uk": "Спробуйте пізніше",
|
||||
"fr": "Réessayer plus tard",
|
||||
"es": "Inténtalo más tarde",
|
||||
"tr": "Daha sonra tekrar deneyin",
|
||||
"kk": "Кейінірек қайта көріңіз"
|
||||
},
|
||||
"NotSetupToReceiveConfirmations": {
|
||||
"ru": "Аккаунт не настроен на получение подтверждений",
|
||||
"en": "Account is not set up to receive confirmations",
|
||||
"zh": "帐户未设置为接收确认",
|
||||
"uk": "Акаунт не налаштований на отримання підтверджень",
|
||||
"fr": "Le compte n'est pas configuré pour recevoir des confirmations",
|
||||
"es": "La cuenta no está configurada para recibir confirmaciones",
|
||||
"tr": "Hesap onayları almak için ayarlanmamış",
|
||||
"kk": "Аккаунт растауларды алу үшін бапталмаған"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"CodeBehind": {
|
||||
"SessionHandler": {
|
||||
"SessionWasRefreshedAutomatically": {
|
||||
"ru": "Сессия была обновлена автоматически",
|
||||
"en": "Session was refreshed automatically",
|
||||
"zh": "会话已自动更新",
|
||||
"uk": "Сесія була оновлена автоматично",
|
||||
"fr": "La session a été rafraîchie automatiquement",
|
||||
"es": "La sesión se actualizó automáticamente",
|
||||
"tr": "Oturum otomatik olarak yenilendi",
|
||||
"kk": "Сессия автоматты түрде жаңартылды"
|
||||
}
|
||||
},
|
||||
"MAAC": {
|
||||
"TimerConfirmed": {
|
||||
"ru": "Авто: подтверждено ",
|
||||
"en": "Auto: confirmed ",
|
||||
"zh": "自动:已确认 ",
|
||||
"uk": "Авто: підтверджено ",
|
||||
"fr": "Auto: confirmé ",
|
||||
"es": "Auto: confirmado ",
|
||||
"tr": "Otomatik: onaylandı ",
|
||||
"kk": "Авто: расталды "
|
||||
},
|
||||
"TimerNotConfirmed": {
|
||||
"ru": "Авто: подтверждение не сработало",
|
||||
"en": "Auto: confirmation was unsuccessful",
|
||||
"zh": "自动:确认失败",
|
||||
"uk": "Авто: підтвердження не спрацювало",
|
||||
"fr": "Auto: la confirmation n'a pas fonctionné",
|
||||
"es": "Auto: la confirmación falló",
|
||||
"tr": "Otomatik: doğrulama başarısız oldu",
|
||||
"kk": "Авто: растау сәтсіз болды"
|
||||
},
|
||||
"TimerSessionError": {
|
||||
"ru": "необходимо обновить сессию или перелогиниться",
|
||||
"en": "need to refresh session or relogin",
|
||||
"zh": "需要刷新会话或重新登录",
|
||||
"uk": "необхідно оновити сесію або перелогінитися",
|
||||
"fr": "il faut rafraîchir la session ou se reconnecter",
|
||||
"es": "es necesario actualizar la sesión o volver a iniciar sesión",
|
||||
"tr": "oturumu yenilemek veya tekrar giriş yapmak gerekiyor",
|
||||
"kk": "сессияны жаңарту немесе қайта кіру қажет"
|
||||
},
|
||||
"TimerPrefix": {
|
||||
"ru": "Авто ",
|
||||
"en": "Auto ",
|
||||
"zh": "自动 ",
|
||||
"uk": "Авто ",
|
||||
"fr": "Auto ",
|
||||
"es": "Auto ",
|
||||
"tr": "Otomatik ",
|
||||
"kk": "Авто "
|
||||
},
|
||||
"TimerPreventedOverlap": {
|
||||
"ru": "Авто: таймер подтверждений пытался начать новый цикл, когда предыдущий еще не завершился. Советуем увеличить задержку",
|
||||
"en": "Auto: confirmation timer tried to start new cycle when previous one was not finished yet. We recommend to increase delay",
|
||||
"zh": "自动: 确认计时器在前一个周期尚未完成时尝试启动新周期。我们建议增加延迟。",
|
||||
"uk": "Авто: таймер підтверджень намагався запустити новий цикл, коли попередній ще не закінчився. Рекомендуємо збільшити затримку",
|
||||
"fr": "Auto: le timer de confirmation a tenté de démarrer un nouveau cycle alors que le précédent n'était pas encore terminé. Nous recommandons d'augmenter le délai",
|
||||
"es": "Auto: el temporizador de confirmación intentó iniciar un nuevo ciclo cuando el anterior aún no había terminado. Recomendamos aumentar el retraso",
|
||||
"tr": "Otomatik: onay zamanlayıcısı önceki döngü bitmeden yeni bir döngü başlatmaya çalıştı. Gecikmeyi artırmanızı öneririz",
|
||||
"kk": "Авто: растау таймері алдыңғы цикл аяқталмай тұрып жаңасын бастауға тырысты. Кідірісті арттыруды ұсынамыз"
|
||||
},
|
||||
"FailedToLoadStorage": {
|
||||
"en": "Failed to load auto-confirm timers. File seems to be corrupted",
|
||||
"ru": "Не удалось загрузить таймеры авто-подтверждений. Файл, похоже, поврежден",
|
||||
"zh": "无法加载自动确认计时器。文件似乎已损坏",
|
||||
"uk": "Не вдалося завантажити таймери авто-підтверджень. Файл, схоже, пошкоджено",
|
||||
"fr": "Impossible de charger les timers d'auto-confirmation. Le fichier semble être corrompu",
|
||||
"es": "No se pudieron cargar los temporizadores de auto-confirmación. El archivo parece estar dañado",
|
||||
"tr": "Otomatik onay zamanlayıcıları yüklenemedi. Dosya bozulmuş görünüyor",
|
||||
"kk": "Авто-растау таймерлерін жүктеу мүмкін болмады. Файл бүлінген сияқты"
|
||||
},
|
||||
"PortableMaClientStatus": {
|
||||
"SessionError": {
|
||||
"ru": "Ошибка сессии. Нужно обновить сессию или перелогиниться",
|
||||
"en": "Session error. Need to refresh session or relogin",
|
||||
"zh": "会话错误。需要刷新会话或重新登录",
|
||||
"uk": "Помилка сесії. Потрібно оновити сесію або перелогінитися",
|
||||
"fr": "Erreur de session. Il faut rafraîchir la session ou se reconnecter",
|
||||
"es": "Error de sesión. Es necesario actualizar la sesión o volver a iniciar sesión",
|
||||
"tr": "Oturum hatası. Oturumu yenilemek veya tekrar giriş yapmak gerekiyor",
|
||||
"kk": "Сессия қатесі. Сессияны жаңарту немесе қайта кіру қажет"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProxyStorage": {
|
||||
"ErrorWhileLoadingProxies": {
|
||||
"en": "Error while loading proxies",
|
||||
"ru": "Ошибка при загрузке прокси",
|
||||
"zh": "加载代理时出错",
|
||||
"uk": "Помилка при завантаженні проксі",
|
||||
"fr": "Erreur lors du chargement des proxies",
|
||||
"es": "Error al cargar los proxies",
|
||||
"tr": "Proxy yüklenirken hata oluştu",
|
||||
"kk": "Proxy жүктеу кезінде қате"
|
||||
}
|
||||
},
|
||||
"Settings": {
|
||||
"ErrorWhileLoadingSettings": {
|
||||
"en": "Error while loading settings. Settings were reset",
|
||||
"ru": "Ошибка при загрузке настроек. Настройки были сброшены",
|
||||
"zh": "加载设置时出错。设置已重置",
|
||||
"uk": "Помилка при завантаженні налаштувань. Налаштування були скинуті",
|
||||
"fr": "Erreur lors du chargement des paramètres. Les paramètres ont été réinitialisés",
|
||||
"es": "Error al cargar la configuración. Los ajustes fueron restablecidos",
|
||||
"tr": "Ayarlar yüklenirken hata oluştu. Ayarlar sıfırlandı",
|
||||
"kk": "Баптауларды жүктеу кезінде қате. Баптаулар қалпына келтірілді"
|
||||
}
|
||||
},
|
||||
"MafileExporter": {
|
||||
"InvalidPath": {
|
||||
"ru": "Указан некорректный путь для экспорта.",
|
||||
"en": "The specified export path is invalid.",
|
||||
"uk": "Вказано некоректний шлях для експорту.",
|
||||
"fr": "Le chemin d’exportation 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 d’exporter vers un dossier utilisé par l’application. 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 d’accè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 d’exportation",
|
||||
"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 d’identité",
|
||||
"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 d’autres 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 d’accè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 d’accé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 à l’avenir 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 SteamID’leri (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 couldn’t 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 күн"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "Жаңартуларды тексеру"
|
||||
}
|
||||
}
|
||||
}
|
||||
+234
-121
@@ -1,24 +1,25 @@
|
||||
<w:FontScaleWindow x:Class="NebulaAuth.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:w="clr-namespace:NebulaAuth.Theme"
|
||||
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:windowStyle="clr-namespace:NebulaAuth.Theme.WindowStyle"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:NebulaAuth.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
|
||||
xmlns:viewModel="clr-namespace:NebulaAuth.ViewModel"
|
||||
xmlns:controls="clr-namespace:NebulaAuth.Theme.Controls"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
MinHeight="500" MinWidth="500" DefaultFontSize="18" ScaleCoefficient="0.4"
|
||||
Title="NebulaAuth" Height="800" Width="730"
|
||||
Style="{StaticResource MainWindow}"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
FontFamily="{md:MaterialDesignFont}"
|
||||
TextElement.Foreground="{DynamicResource BaseContentBrush}"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance viewModel:MainVM}">
|
||||
<Window x:Class="NebulaAuth.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:windowStyle="clr-namespace:NebulaAuth.Theme.WindowStyle"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:NebulaAuth.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
|
||||
xmlns:viewModel="clr-namespace:NebulaAuth.ViewModel"
|
||||
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
MinHeight="500" MinWidth="500"
|
||||
Title="NebulaAuth" Height="800" Width="730"
|
||||
Style="{StaticResource MainWindow}"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
FontFamily="{md:MaterialDesignFont}"
|
||||
TextElement.Foreground="{DynamicResource BaseContentBrush}"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance viewModel:MainVM}"
|
||||
md:RippleAssist.IsDisabled="{Binding Settings.RippleDisabled}"
|
||||
FontSize="18">
|
||||
<b:Interaction.Behaviors>
|
||||
<windowStyle:WindowChromeRenderedBehavior />
|
||||
</b:Interaction.Behaviors>
|
||||
@@ -27,7 +28,14 @@
|
||||
<KeyBinding Command="{Binding Path=PasteMafilesFromClipboardCommand}"
|
||||
Key="V"
|
||||
Modifiers="Control" />
|
||||
<KeyBinding Command="{x:Static ApplicationCommands.Find}"
|
||||
Key="F"
|
||||
Modifiers="Control" />
|
||||
</Window.InputBindings>
|
||||
<Window.CommandBindings>
|
||||
<CommandBinding Command="Find"
|
||||
Executed="FocusSearchBox" />
|
||||
</Window.CommandBindings>
|
||||
<Border Name="DragNDropBorder" Panel.ZIndex="3" Opacity="1" AllowDrop="True"
|
||||
DragDrop.DragEnter="Rectangle_DragEnter" DragLeave="Rectangle_DragLeave" Drop="Rectangle_Drop">
|
||||
<md:DialogHost DialogOpened="DialogHost_DialogOpened"
|
||||
@@ -57,31 +65,40 @@
|
||||
|
||||
<Rectangle Name="DragNDropOverlay" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="1" Fill="#242123"
|
||||
Opacity="0.9" Visibility="Hidden" />
|
||||
<StackPanel Name="DragNDropPanel" Grid.Row="0" ZIndex="2" w:FontScaleWindow.Scale="1"
|
||||
w:FontScaleWindow.ResizeFont="True" Grid.RowSpan="3" Visibility="Hidden"
|
||||
<StackPanel Name="DragNDropPanel" Grid.Row="0" ZIndex="2" Grid.RowSpan="3" Visibility="Hidden"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock FontWeight="Bold" Foreground="#9a65b8" Text="{Tr MainWindow.Global.DragNDropHint}" />
|
||||
<TextBlock FontSize="24" FontWeight="Bold" Foreground="#9a65b8"
|
||||
Text="{Tr MainWindow.Global.DragNDropHint}" />
|
||||
<md:PackIcon Foreground="#9a65b8" HorizontalAlignment="Center" Width="36" Height="36"
|
||||
Kind="FileReplaceOutline" />
|
||||
|
||||
</StackPanel>
|
||||
<ToolBarTray>
|
||||
<ToolBarTray Grid.Row="0">
|
||||
<ToolBar ClipToBounds="True" HorizontalContentAlignment="Stretch"
|
||||
Style="{StaticResource MaterialDesignToolBar}" FontSize="16">
|
||||
<Menu FontSize="16">
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Caption}">
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Import}"
|
||||
Command="{Binding AddMafileCommand}" />
|
||||
<MenuItem IsEnabled="{Binding IsMafileSelected}"
|
||||
Header="{Tr MainWindow.Menu.File.Remove}"
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Remove}"
|
||||
Command="{Binding RemoveMafileCommand}" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.OpenFolder}"
|
||||
Command="{Binding OpenMafileFolderCommand}" />
|
||||
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.ProxyManager}"
|
||||
Command="{Binding OpenProxyManagerCommand}" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Settings}"
|
||||
Command="{Binding OpenSettingsDialogCommand}" />
|
||||
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.Title}">
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.SetPasswords}"
|
||||
Command="{Binding OpenSetPasswordsDialogCommand}" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.OpenExport}"
|
||||
Command="{Binding OpenExporterDialogCommand}" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Menu w:FontScaleWindow.Scale="0.68" w:FontScaleWindow.ResizeFont="True">
|
||||
<Menu>
|
||||
<MenuItem Header="{Tr MainWindow.Menu.Account.Caption}">
|
||||
<MenuItem Header="{Tr MainWindow.Menu.Account.Link}"
|
||||
Command="{Binding LinkAccountCommand}" />
|
||||
@@ -91,18 +108,18 @@
|
||||
Command="{Binding RemoveAuthenticatorCommand}" />
|
||||
|
||||
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||
<MenuItem IsEnabled="{Binding IsMafileSelected}"
|
||||
Header="{Tr MainWindow.Menu.Account.RefreshSession}"
|
||||
<MenuItem Header="{Tr MainWindow.Menu.Account.RefreshSession}"
|
||||
Command="{Binding RefreshSessionCommand}" />
|
||||
<MenuItem IsEnabled="{Binding IsMafileSelected}"
|
||||
Header="{Tr MainWindow.Menu.Account.LoginAgain}"
|
||||
<MenuItem Header="{Tr MainWindow.Menu.Account.LoginAgain}"
|
||||
Command="{Binding LoginAgainCommand}" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Separator />
|
||||
<ComboBox ToolTip="{Tr MainWindow.AppBar.GroupToolTip}"
|
||||
md:HintAssist.Hint="{Tr MainWindow.AppBar.GroupsHint}"
|
||||
MinWidth="100" Margin="8,0,8,0" VerticalAlignment="Center" IsEditable="True"
|
||||
MinWidth="100"
|
||||
MaxWidth="200"
|
||||
Margin="8,0,8,0" VerticalAlignment="Center" IsEditable="True"
|
||||
ItemsSource="{Binding Groups}"
|
||||
SelectedValue="{Binding SelectedGroup}"
|
||||
md:TextFieldAssist.HasClearButton="True">
|
||||
@@ -123,6 +140,7 @@
|
||||
<ComboBox ToolTip="{Tr MainWindow.AppBar.Proxy.ProxyManipulateToolTip}"
|
||||
|
||||
MinWidth="80"
|
||||
MaxWidth="200"
|
||||
Margin="8,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
md:HintAssist.Hint="{Tr MainWindow.AppBar.Proxy.ProxyHint}"
|
||||
@@ -146,6 +164,7 @@
|
||||
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
|
||||
</UIElement.InputBindings>
|
||||
</ComboBox>
|
||||
|
||||
<md:PackIcon Kind="StopAlert" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource ErrorBrush}" Margin="3"
|
||||
ToolTipService.InitialShowDelay="300">
|
||||
@@ -173,6 +192,7 @@
|
||||
ToolTip="{Tr MainWindow.AppBar.Proxy.ProxyAlert.DefaultInUse}"
|
||||
ToolTipService.InitialShowDelay="300"
|
||||
Visibility="{Binding IsDefaultProxy, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||
<Separator />
|
||||
<ToggleButton ToolTip="{Tr MainWindow.AppBar.MarketTimerHint}"
|
||||
IsEnabled="{Binding IsMafileSelected}" Margin="2"
|
||||
Style="{StaticResource MaterialDesignFlatToggleButton}"
|
||||
@@ -249,9 +269,17 @@
|
||||
TextWrapping="WrapWithOverflow" FontSize="16"
|
||||
Text="{Tr MainWindow.LeftPart.NoMafiles}" />
|
||||
|
||||
<ListBox w:FontScaleWindow.Scale="0.8" w:FontScaleWindow.ResizeFont="True" Margin="2"
|
||||
<ListBox FontSize="17" Margin="2"
|
||||
x:Name="MafileListBox"
|
||||
SelectionMode="Single"
|
||||
ItemsSource="{Binding MaFiles}"
|
||||
SelectedValue="{Binding SelectedMafile}">
|
||||
SelectedValue="{Binding SelectedMafile}"
|
||||
PreviewMouseDown="MafileListBox_OnPreviewMouseDown">
|
||||
<ListBox.Resources>
|
||||
<DiscreteObjectKeyFrame x:Key="groupsProxy" Value="{Binding Groups}" />
|
||||
<DiscreteObjectKeyFrame x:Key="listBoxProxy"
|
||||
Value="{Binding ElementName=MafileListBox}" />
|
||||
</ListBox.Resources>
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem"
|
||||
BasedOn="{StaticResource MaterialDesignListBoxItem}">
|
||||
@@ -261,23 +289,47 @@
|
||||
</ListBox.ItemContainerStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid ToolTip="{Binding Filename}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock HorizontalAlignment="Stretch" Text="{Binding AccountName}">
|
||||
<TextBlock.Resources>
|
||||
<SolidColorBrush x:Key="BaseContentBrushProxy"
|
||||
Color="{DynamicResource BaseContentColor}" />
|
||||
</TextBlock.Resources>
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding AccountName}"
|
||||
ToolTip="{Binding LinkedClient.Status.Message, FallbackValue={x:Null}}">
|
||||
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource BaseContentBrush}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Ok">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource SuccessBrush}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Warning">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource WarningBrush}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Error">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource ErrorBrush}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
|
||||
<TextBlock.Foreground>
|
||||
<Binding Path="LinkedClient.IsError"
|
||||
Converter="{StaticResource PortableMaClientStatusToColorConverter}"
|
||||
FallbackValue="{StaticResource BaseContentBrushProxy}" />
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel
|
||||
Visibility="{Binding LinkedClient, Converter={StaticResource NullableToVisibilityConverter}}"
|
||||
Grid.Column="1" Orientation="Horizontal">
|
||||
@@ -296,13 +348,20 @@
|
||||
<KeyBinding Key="X" Modifiers="Control"
|
||||
Command="{Binding DataContext.CopyMafileCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}" />
|
||||
<KeyBinding Modifiers="Control+Shift" Key="C"
|
||||
Command="{Binding DataContext.CopyPasswordCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}" />
|
||||
</ListBox.InputBindings>
|
||||
<FrameworkElement.ContextMenu>
|
||||
<ContextMenu>
|
||||
<ContextMenu Tag="{Binding Groups}">
|
||||
<MenuItem InputGestureText="Ctrl+C"
|
||||
Header="{Tr MainWindow.ContextMenus.Mafile.CopyLogin}"
|
||||
Command="{Binding CopyLoginCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopyPassword}"
|
||||
Command="{Binding Path=CopyPasswordCommand}"
|
||||
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
|
||||
InputGestureText="CTRL+⇧+C" />
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopySteamId}"
|
||||
Command="{Binding CopySteamIdCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
@@ -310,8 +369,33 @@
|
||||
Header="{Tr MainWindow.ContextMenus.Mafile.CopyMafile}"
|
||||
Command="{Binding CopyMafileCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AddToGroup}"
|
||||
ItemsSource="{Binding Groups}">
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AddToGroup}">
|
||||
<MenuItem.ItemsSource>
|
||||
<CompositeCollection>
|
||||
<MenuItem
|
||||
Command="{Binding CreateGroupCommand}"
|
||||
CommandParameter="{Binding Value.SelectedValue, Source={StaticResource listBoxProxy}}">
|
||||
<MenuItem.Header>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<md:PackIcon Kind="PlusCircle"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Margin="5,0,0,0" Grid.Column="1"
|
||||
Text="{Tr MainWindow.ContextMenus.Mafile.CreateGroup}" />
|
||||
</Grid>
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<CollectionContainer
|
||||
Collection="{Binding Value, Source={StaticResource groupsProxy}}" />
|
||||
|
||||
</CompositeCollection>
|
||||
</MenuItem.ItemsSource>
|
||||
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource MaterialDesignMenuItem}"
|
||||
TargetType="{x:Type MenuItem}">
|
||||
@@ -333,109 +417,138 @@
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}"
|
||||
Command="{Binding Path=RemoveGroupCommand}"
|
||||
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.CopyPassword}"
|
||||
Command="{Binding Path=CopyPasswordCommand}"
|
||||
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
|
||||
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
|
||||
Command="{Binding RemoveProxyCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
</ContextMenu>
|
||||
</FrameworkElement.ContextMenu>
|
||||
|
||||
</ListBox>
|
||||
|
||||
<TextBox Style="{StaticResource MaterialDesignFloatingHintTextBox}"
|
||||
md:TextFieldAssist.HasClearButton="True" w:FontScaleWindow.Scale="0.7"
|
||||
w:FontScaleWindow.ResizeFont="True" Grid.Row="1" Margin="10,5,10,10"
|
||||
FontSize="16"
|
||||
md:TextFieldAssist.HasClearButton="True" Grid.Row="1" Margin="10,5,10,10"
|
||||
md:HintAssist.Hint="{Tr MainWindow.LeftPart.SearchBoxHint}"
|
||||
Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
x:Name="SearchField"
|
||||
KeyDown="SearchField_OnKeyDown" />
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
<md:Card BorderThickness="1" Style="{StaticResource MaterialDesignOutlinedCard}" Grid.Column="1"
|
||||
Margin="10,10,15,10" UniformCornerRadius="12" Opacity="{Binding Settings.RightOpacity}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="115*" />
|
||||
<RowDefinition Height="436*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Row="0">
|
||||
<Grid Grid.Column="1">
|
||||
<Border Background="{DynamicResource MaterialDesign.Brush.Card.Background}"
|
||||
Opacity="{Binding Settings.RightOpacity}"
|
||||
BorderThickness="1"
|
||||
|
||||
Margin="10,10,15,10" CornerRadius="12" VerticalAlignment="Stretch">
|
||||
<Border.BorderBrush>
|
||||
<SolidColorBrush Color="{DynamicResource Base300Color}" Opacity="0.3" />
|
||||
</Border.BorderBrush>
|
||||
</Border>
|
||||
<md:Card Margin="10,10,15,10" Padding="2" UniformCornerRadius="12"
|
||||
Background="Transparent">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="115*" />
|
||||
<RowDefinition Height="436*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox w:FontScaleWindow.Scale="1.1" w:FontScaleWindow.ResizeFont="True"
|
||||
IsReadOnly="True" HorizontalContentAlignment="Center" Background="#00FFFFFF"
|
||||
<Grid Row="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox FontSize="24"
|
||||
IsReadOnly="True" HorizontalContentAlignment="Center"
|
||||
Background="#00FFFFFF"
|
||||
|
||||
Style="{StaticResource MaterialDesignTextBox}"
|
||||
Style="{StaticResource MaterialDesignTextBox}"
|
||||
|
||||
Text="{Binding Code, FallbackValue=Code}">
|
||||
<TextBox.InputBindings>
|
||||
<MouseBinding Gesture="LeftClick" Command="{Binding CopyCodeCommand}" />
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
<controls:CodeProgressBar
|
||||
Visibility="{Binding SelectedMafile, Converter={StaticResource NullableToVisibilityConverter}}"
|
||||
Grid.Row="1" Height="4" Style="{StaticResource MaterialDesignLinearProgressBar}"
|
||||
MaxTime="30" TimeRemaining="{Binding CodeProgress, Mode=OneWay}" />
|
||||
Text="{Binding Code, FallbackValue=Code}">
|
||||
<TextBox.InputBindings>
|
||||
<MouseBinding Gesture="LeftClick" Command="{Binding CopyCodeCommand}" />
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
<nebulaAuth:CodeProgressBar
|
||||
Visibility="{Binding SelectedMafile, Converter={StaticResource NullableToVisibilityConverter}}"
|
||||
Grid.Row="1" Height="4"
|
||||
Style="{StaticResource MaterialDesignLinearProgressBar}"
|
||||
MaxTime="30" TimeRemaining="{Binding CodeProgress, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<Button IsEnabled="{Binding IsMafileSelected}"
|
||||
FontSize="14"
|
||||
Grid.Row="1"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Margin="0,10,0,10"
|
||||
HorizontalAlignment="Center"
|
||||
Content="{Tr MainWindow.RightPart.LoadConfirmations}"
|
||||
Command="{Binding GetConfirmationsCommand}"
|
||||
md:ButtonProgressAssist.IsIndeterminate="True"
|
||||
md:ButtonProgressAssist.IsIndicatorVisible="{Binding GetConfirmationsCommand.IsRunning}"
|
||||
Width="{Binding Path=ActualWidth, Converter={StaticResource CoefficientConverter}, ConverterParameter=0.6, RelativeSource={RelativeSource AncestorType=md:Card}}" />
|
||||
<ItemsControl md:RippleAssist.IsDisabled="True" Focusable="False" Grid.Row="2"
|
||||
FontSize="16"
|
||||
Margin="10,10,10,10"
|
||||
Visibility="{Binding ConfirmationsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding Confirmations}" Grid.RowSpan="2" />
|
||||
<Button FontSize="16"
|
||||
Style="{StaticResource MaterialDesignFlatButton}" Grid.Row="4"
|
||||
Command="{Binding ConfirmLoginCommand}"
|
||||
Content="{Tr MainWindow.RightPart.ConfirmLogin}" />
|
||||
</Grid>
|
||||
|
||||
<Button IsEnabled="{Binding IsMafileSelected}" w:FontScaleWindow.Scale="0.7"
|
||||
w:FontScaleWindow.ResizeFont="True" Grid.Row="1"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,10,0,10"
|
||||
HorizontalAlignment="Center"
|
||||
Content="{Tr MainWindow.RightPart.LoadConfirmations}"
|
||||
Command="{Binding GetConfirmationsCommand}"
|
||||
Width="{Binding Path=ActualWidth, Converter={StaticResource CoefficientConverter}, ConverterParameter=0.6, RelativeSource={RelativeSource AncestorType=md:Card}}" />
|
||||
<ItemsControl md:RippleAssist.IsDisabled="True" Focusable="False" Grid.Row="2"
|
||||
w:FontScaleWindow.Scale="0.72" w:FontScaleWindow.ResizeFont="True"
|
||||
|
||||
Margin="10,10,10,10"
|
||||
Visibility="{Binding ConfirmationsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding Confirmations}" Grid.RowSpan="2" />
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" Grid.Row="4"
|
||||
w:FontScaleWindow.Scale="0.8" w:FontScaleWindow.ResizeFont="True"
|
||||
Command="{Binding ConfirmLoginCommand}"
|
||||
|
||||
Content="{Tr MainWindow.RightPart.ConfirmLogin}" />
|
||||
</Grid>
|
||||
</md:Card>
|
||||
</md:Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Border Grid.Row="2" BorderBrush="#1e1e24">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Opacity="1" Color="{DynamicResource Base300Color}" />
|
||||
</Border.Background>
|
||||
<Grid>
|
||||
|
||||
<ToolBarPanel TextElement.Foreground="{DynamicResource SecondaryContentBrush}"
|
||||
Orientation="Horizontal" Margin="5" w:FontScaleWindow.Scale="0.73"
|
||||
|
||||
w:FontScaleWindow.ResizeFont="True">
|
||||
<TextBlock FontWeight="Bold" Text="{Tr MainWindow.Footer.Account}" />
|
||||
<TextBlock Text="{Binding SelectedMafile.AccountName}" />
|
||||
<TextBlock Text="|" Margin="10,0,10,0" />
|
||||
<TextBlock FontWeight="Bold" Text="{Tr MainWindow.Footer.Group}" />
|
||||
<TextBlock FontWeight="Normal"
|
||||
Text="{Binding SelectedMafile.Group, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
|
||||
Orientation="Horizontal" Margin="5">
|
||||
<TextBlock FontSize="15" Text="{Tr MainWindow.Footer.AccountsCount}" />
|
||||
<TextBlock FontSize="15" Text="{Binding MaFiles.Count}" />
|
||||
<TextBlock FontSize="15" Text="|" Margin="10,0,10,0" />
|
||||
<TextBlock FontSize="15" Text="{Tr MainWindow.Footer.SelectedAccount}" />
|
||||
<TextBlock FontSize="15"
|
||||
Text="{Binding SelectedMafile.AccountName, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
|
||||
<!--<Button Command="{Binding DebugCommand}">Debug</Button>-->
|
||||
</ToolBarPanel>
|
||||
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" w:FontScaleWindow.Scale="0.7"
|
||||
w:FontScaleWindow.ResizeFont="True" Margin="0,0,10,0">
|
||||
<Hyperlink
|
||||
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<md:PackIcon x:Name="UpdateBadgeIcon"
|
||||
Kind="BellAlert" Width="15" Height="15"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,5,0"
|
||||
Visibility="Collapsed"
|
||||
ToolTip="{Tr CodeBehind.UpdateService.UpdateIndicatorHint}">
|
||||
<md:PackIcon.Foreground>
|
||||
<SolidColorBrush x:Name="UpdateBadgeBrush" Color="DodgerBlue" />
|
||||
</md:PackIcon.Foreground>
|
||||
</md:PackIcon>
|
||||
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
|
||||
Margin="0,0,10,0">
|
||||
<Hyperlink
|
||||
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
|
||||
|
||||
Foreground="{DynamicResource AccentBrush}"
|
||||
Command="{Binding OpenLinksViewCommand}">
|
||||
by achies
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
|
||||
Command="{Binding OpenLinksViewCommand}">
|
||||
<Hyperlink.Foreground>
|
||||
<SolidColorBrush x:Name="ByAchiesBrush" Color="DodgerBlue" />
|
||||
</Hyperlink.Foreground>
|
||||
by achies
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<md:Snackbar Grid.Row="1" MessageQueue="{Binding MessageQueue}" />
|
||||
</Grid>
|
||||
</md:DialogHost>
|
||||
</Border>
|
||||
</w:FontScaleWindow>
|
||||
</Window>
|
||||
@@ -2,7 +2,10 @@
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using NebulaAuth.Core;
|
||||
@@ -24,12 +27,50 @@ public partial class MainWindow
|
||||
ThemeManager.InitializeTheme();
|
||||
Title = Title + " " + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3);
|
||||
Loaded += OnApplicationStarted;
|
||||
UpdateManager.PendingUpdateDetected += OnPendingUpdateDetected;
|
||||
}
|
||||
|
||||
private void OnPendingUpdateDetected()
|
||||
{
|
||||
Dispatcher.BeginInvoke(StartUpdateBlink);
|
||||
}
|
||||
|
||||
private void StartUpdateBlink()
|
||||
{
|
||||
UpdateBadgeIcon.Visibility = Visibility.Visible;
|
||||
var baseColor = FindResource("AccentBrush") is SolidColorBrush accent
|
||||
? accent.Color
|
||||
: ByAchiesBrush.Color;
|
||||
var duration = new Duration(TimeSpan.FromSeconds(6));
|
||||
var sb = new Storyboard();
|
||||
sb.Children.Add(BuildBlinkAnimation(duration, "UpdateBadgeBrush"));
|
||||
sb.Children.Add(BuildBlinkAnimation(duration, "ByAchiesBrush"));
|
||||
sb.Completed += (_, _) =>
|
||||
{
|
||||
UpdateBadgeIcon.Visibility = Visibility.Collapsed;
|
||||
ByAchiesBrush.Color = baseColor;
|
||||
};
|
||||
sb.Begin(this);
|
||||
}
|
||||
|
||||
private static ColorAnimationUsingKeyFrames BuildBlinkAnimation(Duration duration, string targetName)
|
||||
{
|
||||
var anim = new ColorAnimationUsingKeyFrames {Duration = duration};
|
||||
for (var i = 0; i <= 6; i++)
|
||||
{
|
||||
var color = i % 2 == 0 ? Colors.DodgerBlue : Colors.OrangeRed;
|
||||
anim.KeyFrames.Add(new DiscreteColorKeyFrame(color, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i))));
|
||||
}
|
||||
|
||||
Storyboard.SetTargetName(anim, targetName);
|
||||
Storyboard.SetTargetProperty(anim, new PropertyPath(SolidColorBrush.ColorProperty));
|
||||
return anim;
|
||||
}
|
||||
|
||||
private async void OnApplicationStarted(object? sender, EventArgs e)
|
||||
{
|
||||
((MainVM) DataContext).CurrentDialogHost = DialogHostInstance;
|
||||
if (Settings.Instance.IsPasswordSet == false) return;
|
||||
if (!Settings.Instance.IsPasswordSet) return;
|
||||
Topmost = false;
|
||||
await Dispatcher.InvokeAsync(ShowSetPasswordDialog, DispatcherPriority.ContextIdle);
|
||||
}
|
||||
@@ -45,22 +86,45 @@ public partial class MainWindow
|
||||
|
||||
var result = await DialogHost.Show(dialog);
|
||||
var pass = vm.Password;
|
||||
if (result is true && string.IsNullOrWhiteSpace(pass) == false)
|
||||
if (result is true && !string.IsNullOrWhiteSpace(pass))
|
||||
{
|
||||
PHandler.SetPassword(pass);
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchField_OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key != Key.Escape || !SearchField.IsFocused) return;
|
||||
Keyboard.ClearFocus();
|
||||
}
|
||||
|
||||
private void MafileListBox_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) return;
|
||||
if (ItemsControl.ContainerFromElement((ListBox) sender, (DependencyObject) e.OriginalSource) is ListBoxItem
|
||||
{
|
||||
IsSelected: true
|
||||
})
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void FocusSearchBox(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
SearchField.Focus();
|
||||
}
|
||||
|
||||
#region Dran'n'Drop
|
||||
|
||||
private void UIElement_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
|
||||
{
|
||||
if (int.TryParse(e.Text, out _) == false) e.Handled = true;
|
||||
if (!int.TryParse(e.Text, out _)) e.Handled = true;
|
||||
}
|
||||
|
||||
private void Rectangle_DragEnter(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
@@ -72,7 +136,7 @@ public partial class MainWindow
|
||||
|
||||
private async void Rectangle_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
||||
var filePaths = (string[]) e.Data.GetData(DataFormats.FileDrop)!;
|
||||
if (filePaths.Length == 0) return;
|
||||
if (DataContext is MainVM mainVm)
|
||||
|
||||
@@ -20,8 +20,15 @@ public partial class Mafile : MobileDataExtended
|
||||
set => SetProperty(ref _linkedClient, value);
|
||||
}
|
||||
|
||||
[JsonIgnore] private PortableMaClient? _linkedClient;
|
||||
[JsonIgnore]
|
||||
public string? Filename
|
||||
{
|
||||
get => _filename;
|
||||
set => SetProperty(ref _filename, value);
|
||||
}
|
||||
|
||||
private string? _filename;
|
||||
[JsonIgnore] private PortableMaClient? _linkedClient;
|
||||
|
||||
public void SetSessionData(MobileSessionData? sessionData)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public static class MAACRequestHandler
|
||||
{
|
||||
private const string LOC_PATH = "MAAC";
|
||||
private static readonly ConcurrentDictionary<Mafile, PortableMaClientErrorData> _errors = new();
|
||||
|
||||
public static void Register(Mafile mafile)
|
||||
{
|
||||
_errors[mafile] = new PortableMaClientErrorData();
|
||||
}
|
||||
|
||||
public static void Unregister(Mafile mafile)
|
||||
{
|
||||
_errors.TryRemove(mafile, out _);
|
||||
}
|
||||
|
||||
public static PortableMaClientStatus ClearErrors(Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
data.Clear();
|
||||
}
|
||||
|
||||
return PortableMaClientStatus.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a MAAC request with progressive error handling strategy: retries and status management based on error
|
||||
/// history.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="func"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<T> HandleRequest<T>(PortableMaClient client, Func<Task<T>> func)
|
||||
{
|
||||
var result = await client.DoRequest(func, client.Mafile.PreviouslyHadNoErrors());
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
InformRequestSuccessful(client.Mafile);
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
if (client.Mafile.PreviouslyHadNoErrors())
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Warning(GetPortableMaClientStatus("SessionError")));
|
||||
}
|
||||
else if (client.Mafile.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval))
|
||||
{
|
||||
Shell.Logger.Info("Retrying MAAC request for {name} MAAC account after previous error.",
|
||||
client.Mafile.AccountName);
|
||||
result = await client.DoRequest(func);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
InformRequestSuccessful(client.Mafile);
|
||||
return result.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if (client.Mafile.ErrorPersistedFor(Settings.Instance.MaacErrorThreshold))
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Error(GetPortableMaClientStatus("SessionError")));
|
||||
}
|
||||
|
||||
AddError(client.Mafile, result.Exception!);
|
||||
throw result.Exception!;
|
||||
}
|
||||
|
||||
|
||||
private static async Task<Result<T>> DoRequest<T>(this PortableMaClient client, Func<Task<T>> req,
|
||||
bool withSessionHandler = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return withSessionHandler
|
||||
? Result<T>.Success(await SessionHandler.Handle(req, client.Mafile, client.Chp(),
|
||||
GetTimerPrefix(client.Mafile)))
|
||||
: Result<T>.Success(await req());
|
||||
}
|
||||
catch (SessionInvalidException ex)
|
||||
{
|
||||
Shell.Logger.Warn("SessionInvalidException caught in MAAC request handler.");
|
||||
return Result<T>.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InformRequestSuccessful(Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data) && data.Clear())
|
||||
{
|
||||
Shell.Logger.Info("MAAC request for {name} MAAC account succeeded, error history cleared",
|
||||
mafile.AccountName);
|
||||
}
|
||||
|
||||
var client = mafile.LinkedClient;
|
||||
if (client != null && client.Status.StatusType != PortableMaClientStatusType.Ok)
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Ok());
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddError(Mafile mafile, Exception ex)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
Shell.Logger.Info("Registering error for {name} MAAC account: {ex}", mafile.AccountName, ex.Message);
|
||||
data.AddEntry(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ErrorPersistedFor(this Mafile mafile, TimeSpan span)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
var oldestError = data.GetOldestErrorTime();
|
||||
if (oldestError.HasValue && DateTime.UtcNow - oldestError.Value > span)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static bool PreviouslyHadNoErrors(this Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
return data.NoErrors;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LastErrorWasAtLeast(this Mafile mafile, TimeSpan timeAgo)
|
||||
{
|
||||
if (!_errors.TryGetValue(mafile, out var data)) return false;
|
||||
var latestError = data.GetTimeFromLastError();
|
||||
if (latestError.HasValue && latestError.Value > timeAgo)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsReady(Mafile maf)
|
||||
{
|
||||
if (maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Ok}) return true;
|
||||
return maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Warning}
|
||||
&& maf.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval);
|
||||
}
|
||||
|
||||
private static string GetTimerPrefix(Mafile mafile)
|
||||
{
|
||||
return GetLocalization("TimerPrefix") + mafile.AccountName + ": ";
|
||||
}
|
||||
|
||||
private static string GetLocalization(string key)
|
||||
{
|
||||
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
|
||||
}
|
||||
|
||||
private static string GetPortableMaClientStatus(string key)
|
||||
{
|
||||
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, "PortableMaClientStatus", key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public static class MAACStorage
|
||||
{
|
||||
private static Dictionary<string, StoredClient> Clients { get; set; } = [];
|
||||
|
||||
static MAACStorage()
|
||||
{
|
||||
}
|
||||
|
||||
private static void ClientsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
Clients.Clear();
|
||||
}
|
||||
else if (e.NewItems != null)
|
||||
{
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
if (item is Mafile {Filename: not null} mafile)
|
||||
{
|
||||
if (mafile.LinkedClient != null)
|
||||
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
|
||||
|
||||
Clients.Add(mafile.Filename, new StoredClient
|
||||
{
|
||||
AutoConfirmMarket = mafile.LinkedClient?.AutoConfirmMarket ?? false,
|
||||
AutoConfirmTrades = mafile.LinkedClient?.AutoConfirmTrades ?? false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
foreach (var item in e.OldItems)
|
||||
{
|
||||
if (item is Mafile {Filename: not null} mafile)
|
||||
{
|
||||
if (mafile.LinkedClient != null)
|
||||
mafile.LinkedClient.PropertyChanged -= LinkedClientOnPropertyChanged;
|
||||
|
||||
Clients.Remove(mafile.Filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private static void LinkedClientOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is not PortableMaClient client) return;
|
||||
if (client.Mafile.Filename == null) return;
|
||||
var anyChanges = false;
|
||||
if (!Clients.TryGetValue(client.Mafile.Filename, out var storedClient))
|
||||
{
|
||||
client.PropertyChanged -= LinkedClientOnPropertyChanged;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.PropertyName == nameof(PortableMaClient.AutoConfirmMarket))
|
||||
{
|
||||
anyChanges = storedClient.AutoConfirmMarket != client.AutoConfirmMarket;
|
||||
storedClient.AutoConfirmMarket = client.AutoConfirmMarket;
|
||||
}
|
||||
else if (e.PropertyName == nameof(PortableMaClient.AutoConfirmTrades))
|
||||
{
|
||||
anyChanges = storedClient.AutoConfirmTrades != client.AutoConfirmTrades;
|
||||
storedClient.AutoConfirmTrades = client.AutoConfirmTrades;
|
||||
}
|
||||
|
||||
if (anyChanges)
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(Clients, Formatting.Indented);
|
||||
File.WriteAllText("maac.json", json);
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
|
||||
if (!File.Exists("maac.json")) return;
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText("maac.json");
|
||||
var clients = JsonConvert.DeserializeObject<Dictionary<string, StoredClient>>(json) ?? [];
|
||||
foreach (var (fileName, storedClient) in clients)
|
||||
{
|
||||
var mafile = Storage.MaFiles.FirstOrDefault(x => x.Filename == fileName);
|
||||
if (mafile == null) continue;
|
||||
if (storedClient is {AutoConfirmMarket: false, AutoConfirmTrades: false}) continue;
|
||||
if (MultiAccountAutoConfirmer.TryAddToConfirm(mafile) && mafile.LinkedClient != null)
|
||||
{
|
||||
mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket;
|
||||
mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades;
|
||||
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
|
||||
Clients[fileName] = storedClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, "Failed to load MAAC storage");
|
||||
SnackbarController.SendSnackbar(
|
||||
LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage"));
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotifyMafilesRenamed(IDictionary<string, string> oldNewNames)
|
||||
{
|
||||
var updatedClients = new Dictionary<string, StoredClient>();
|
||||
foreach (var (oldName, newName) in oldNewNames)
|
||||
{
|
||||
if (Clients.Remove(oldName, out var storedClient))
|
||||
{
|
||||
updatedClients[newName] = storedClient;
|
||||
}
|
||||
}
|
||||
|
||||
Clients = updatedClients;
|
||||
Save();
|
||||
}
|
||||
|
||||
private class StoredClient
|
||||
{
|
||||
public bool AutoConfirmMarket { get; set; }
|
||||
public bool AutoConfirmTrades { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public static class MultiAccountAutoConfirmer
|
||||
Clients = [];
|
||||
Timer = new Timer(TimerConfirm);
|
||||
Settings.Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||
UpdateTimer();
|
||||
UpdateTimer(true);
|
||||
}
|
||||
|
||||
// ReSharper disable once AsyncVoidMethod //Already safe
|
||||
@@ -58,8 +58,7 @@ public static class MultiAccountAutoConfirmer
|
||||
|
||||
private static async Task TimerConfirmInternal()
|
||||
{
|
||||
var clients = Lock.ReadLock(() => Clients.ToArray());
|
||||
var enabledClients = clients.Where(x => x.LinkedClient is {IsError: false}).ToArray();
|
||||
var enabledClients = GetReadyAccounts();
|
||||
enabledClients = DistributeEvenly(enabledClients).ToArray();
|
||||
var confirmed = 0;
|
||||
await Task.Run(async () =>
|
||||
@@ -69,7 +68,8 @@ public static class MultiAccountAutoConfirmer
|
||||
var conf = 0;
|
||||
try
|
||||
{
|
||||
conf = await client.LinkedClient!.Confirm();
|
||||
conf = await MAACRequestHandler.HandleRequest(client.LinkedClient!,
|
||||
() => client.LinkedClient!.Confirm());
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
@@ -114,14 +114,30 @@ public static class MultiAccountAutoConfirmer
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Mafile> GetReadyAccounts()
|
||||
{
|
||||
var clients = Lock.ReadLock(() => Clients.ToArray());
|
||||
var res = new List<Mafile>();
|
||||
foreach (var maf in clients)
|
||||
{
|
||||
if (maf.LinkedClient == null) continue;
|
||||
if (MAACRequestHandler.IsReady(maf))
|
||||
{
|
||||
res.Add(maf);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public static bool TryAddToConfirm(Mafile mafile)
|
||||
{
|
||||
return Lock.WriteLock(() =>
|
||||
{
|
||||
if (Clients.Contains(mafile)) return false;
|
||||
Clients.Add(mafile);
|
||||
mafile.LinkedClient = new PortableMaClient(mafile);
|
||||
Clients.Add(mafile);
|
||||
MAACRequestHandler.Register(mafile);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -131,9 +147,10 @@ public static class MultiAccountAutoConfirmer
|
||||
{
|
||||
Lock.WriteLock(() =>
|
||||
{
|
||||
Clients.Remove(mafile);
|
||||
mafile.LinkedClient?.Dispose();
|
||||
mafile.LinkedClient = null;
|
||||
Clients.Remove(mafile);
|
||||
MAACRequestHandler.Unregister(mafile);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,11 +161,17 @@ public static class MultiAccountAutoConfirmer
|
||||
UpdateTimer();
|
||||
}
|
||||
|
||||
private static void UpdateTimer()
|
||||
private static void UpdateTimer(bool firstStart = false)
|
||||
{
|
||||
var timerInterval = Settings.Instance.TimerSeconds;
|
||||
var intervalTimeSpan = TimeSpan.FromSeconds(timerInterval);
|
||||
Timer.Change(intervalTimeSpan, intervalTimeSpan);
|
||||
var dueTime = intervalTimeSpan;
|
||||
if (firstStart)
|
||||
{
|
||||
dueTime = timerInterval >= 30 ? intervalTimeSpan : TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
Timer.Change(dueTime, intervalTimeSpan);
|
||||
}
|
||||
|
||||
private static string GetLocalization(string key)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,8 @@ namespace NebulaAuth.Model;
|
||||
public static class MaClient
|
||||
{
|
||||
private static HttpClientHandler ClientHandler { get; }
|
||||
|
||||
private static HttpClient Client { get; }
|
||||
|
||||
private static DynamicProxy Proxy { get; }
|
||||
|
||||
public static ProxyData? DefaultProxy { get; set; }
|
||||
|
||||
|
||||
@@ -40,21 +37,9 @@ public static class MaClient
|
||||
public static void SetAccount(Mafile? account)
|
||||
{
|
||||
ClientHandler.CookieContainer.ClearAllCookies();
|
||||
if (account != null)
|
||||
{
|
||||
if (account.SessionData != null)
|
||||
{
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClientHandler.CookieContainer.ClearSteamCookies();
|
||||
ClientHandler.CookieContainer.AddMinimalMobileCookies();
|
||||
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
|
||||
}
|
||||
|
||||
Proxy.SetData(account.Proxy?.Data);
|
||||
}
|
||||
if (account == null) return;
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
|
||||
Proxy.SetData(account.Proxy?.Data);
|
||||
}
|
||||
|
||||
public static Task<IEnumerable<Confirmation>> GetConfirmations(Mafile mafile)
|
||||
@@ -99,8 +84,8 @@ public static class MaClient
|
||||
}
|
||||
|
||||
return await SteamMobileConfirmationsApi.SendConfirmation(Client, confirmation, mafile.SessionData!.SteamId,
|
||||
mafile,
|
||||
confirm);
|
||||
mafile,
|
||||
confirm);
|
||||
}
|
||||
|
||||
public static async Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations,
|
||||
@@ -158,7 +143,7 @@ public static class MaClient
|
||||
if (mafile.SessionData.RefreshToken.IsExpired)
|
||||
throw new SessionPermanentlyExpiredException();
|
||||
|
||||
if (ignoreAccessToken == false)
|
||||
if (!ignoreAccessToken)
|
||||
{
|
||||
var access = mafile.SessionData.GetMobileToken();
|
||||
if (access == null || access.Value.IsExpired)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NebulaAuth.Model.Entities;
|
||||
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public class ExportResult
|
||||
{
|
||||
public string? Error { get; }
|
||||
public Dictionary<Mafile, string>? Exported { get; }
|
||||
public List<string>? NotFound { get; }
|
||||
public List<string>? Conflict { get; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Exported))]
|
||||
[MemberNotNullWhen(true, nameof(NotFound))]
|
||||
[MemberNotNullWhen(true, nameof(Conflict))]
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
public bool Success => Error == null;
|
||||
|
||||
private ExportResult(string? error, Dictionary<Mafile, string>? exported, List<string>? notFound,
|
||||
List<string>? conflict)
|
||||
{
|
||||
Error = error;
|
||||
Exported = exported;
|
||||
NotFound = notFound;
|
||||
Conflict = conflict;
|
||||
}
|
||||
|
||||
public ExportResult(Dictionary<Mafile, string> exported, List<string> notFound, List<string> conflict)
|
||||
: this(null, exported, notFound, conflict)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public ExportResult(string? error)
|
||||
: this(error, null, null, null)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public class MafileExportTemplate
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool UseLoginAsMafileName { get; set; }
|
||||
public bool IncludeSharedSecret { get; set; }
|
||||
public bool IncludeIdentitySecret { get; set; }
|
||||
public bool IncludeRCode { get; set; }
|
||||
public bool IncludeSessionData { get; set; }
|
||||
public bool IncludeOtherInfo { get; set; }
|
||||
public bool IncludeNebulaProxy { get; set; }
|
||||
public bool IncludeNebulaPassword { get; set; }
|
||||
public bool IncludeNebulaGroup { get; set; }
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using SteamLibForked.Models.Session;
|
||||
using SteamLibForked.Models.SteamIds;
|
||||
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public static class MafileExporter
|
||||
{
|
||||
public static async Task<ExportResult> ExportMafiles(IEnumerable<string> keys, MafileExportTemplate template)
|
||||
{
|
||||
if (template.Path == null || !PathIsValid(template.Path))
|
||||
{
|
||||
return new ExportResult(LocManager.GetCodeBehindOrDefault("InvalidPath", "MafileExporter.InvalidPath"));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(template.Path))
|
||||
{
|
||||
Directory.CreateDirectory(template.Path);
|
||||
}
|
||||
|
||||
if (IsNebulaUtilizedDirectory(template.Path))
|
||||
{
|
||||
return new ExportResult(LocManager.GetCodeBehindOrDefault("NebulaUtilizedDirectory",
|
||||
"MafileExporter.NebulaUtilizedDirectory"));
|
||||
}
|
||||
|
||||
if (!IsAllowedToWrite(template.Path))
|
||||
{
|
||||
return new ExportResult(LocManager.GetCodeBehindOrDefault("AccessDenied", "MafileExporter.AccessDenied"));
|
||||
}
|
||||
|
||||
|
||||
var exported = new Dictionary<Mafile, string>();
|
||||
var notFound = new List<string>();
|
||||
var conflict = new List<string>();
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
SteamId? steamId = null;
|
||||
if (SteamId64.TryParse(key, out var id64))
|
||||
{
|
||||
steamId = id64;
|
||||
}
|
||||
|
||||
var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId);
|
||||
if (maf != null)
|
||||
{
|
||||
var fileName = await ExportMafile(template.Path, template, maf);
|
||||
if (fileName == null)
|
||||
{
|
||||
conflict.Add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
exported[maf] = fileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
notFound.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return new ExportResult(exported, notFound, conflict);
|
||||
}
|
||||
|
||||
private static async Task<string?> ExportMafile(string path, MafileExportTemplate template, Mafile mafile)
|
||||
{
|
||||
// We preserve SteamId even if other info is not included
|
||||
var session = template.IncludeSessionData
|
||||
? mafile.SessionData
|
||||
: new MobileSessionData(null!, mafile.SteamId, default, null, tokens: null);
|
||||
|
||||
|
||||
var serializeMaf = new Mafile
|
||||
{
|
||||
SharedSecret = template.IncludeSharedSecret ? mafile.SharedSecret : null!,
|
||||
IdentitySecret = template.IncludeIdentitySecret ? mafile.IdentitySecret : null!,
|
||||
DeviceId = template.IncludeIdentitySecret ? mafile.DeviceId : null!,
|
||||
RevocationCode = template.IncludeRCode ? mafile.RevocationCode : null!,
|
||||
AccountName = mafile.AccountName,
|
||||
SessionData = session,
|
||||
SteamId = mafile.SteamId,
|
||||
ServerTime = template.IncludeOtherInfo ? mafile.ServerTime : 0,
|
||||
SerialNumber = template.IncludeOtherInfo ? mafile.SerialNumber : 0,
|
||||
Uri = template.IncludeOtherInfo ? mafile.Uri : null!,
|
||||
TokenGid = template.IncludeOtherInfo ? mafile.TokenGid : null!,
|
||||
Secret1 = template.IncludeOtherInfo ? mafile.Secret1 : null!,
|
||||
Proxy = template.IncludeNebulaProxy ? mafile.Proxy : null!,
|
||||
Group = template.IncludeNebulaGroup ? mafile.Group : null!,
|
||||
Password = template.IncludeNebulaPassword ? mafile.Password : null!,
|
||||
LinkedClient = null,
|
||||
Filename = null
|
||||
};
|
||||
|
||||
var serialized = NebulaSerializer.SerializeMafile(serializeMaf);
|
||||
var strategy = template.UseLoginAsMafileName ? MafileNamingStrategy.Login : MafileNamingStrategy.SteamId;
|
||||
var fileName = strategy.GetMafileName(serializeMaf);
|
||||
var fullPath = Path.Combine(path, fileName);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(fullPath, serialized);
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private static bool PathIsValid(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var full = Path.GetFullPath(path);
|
||||
|
||||
var invalidPathChars = Path.GetInvalidPathChars();
|
||||
if (full.IndexOfAny(invalidPathChars) >= 0) return false;
|
||||
|
||||
var invalidFileChars = Path.GetInvalidFileNameChars();
|
||||
|
||||
foreach (var part in full.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
|
||||
{
|
||||
if (string.IsNullOrEmpty(part)) continue;
|
||||
if (part is [_, ':']) continue;
|
||||
|
||||
if (part.IndexOfAny(invalidFileChars) >= 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAllowedToWrite(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var testFilePath = Path.Combine(path, "test.tmp");
|
||||
using (var fs = File.Create(testFilePath))
|
||||
{
|
||||
}
|
||||
|
||||
File.Delete(testFilePath);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNebulaUtilizedDirectory(string path)
|
||||
{
|
||||
return Storage.MafilesDirectories.Any(x => Path.GetFullPath(path).EqualsIgnoreCase(x));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public static class MafileExporterStorage
|
||||
{
|
||||
private const string FILE_NAME = "export_templates.json";
|
||||
public static ObservableCollection<MafileExportTemplate> Templates { get; } = new();
|
||||
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
LoadOrCreateTemplate();
|
||||
}
|
||||
|
||||
public static MafileExportTemplate? GetTemplate(string name)
|
||||
{
|
||||
return Templates.FirstOrDefault(x => x.Name == name);
|
||||
}
|
||||
|
||||
public static void AddTemplate(MafileExportTemplate template)
|
||||
{
|
||||
Templates.Add(template);
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void DeleteTemplate(MafileExportTemplate template)
|
||||
{
|
||||
if (Templates.Remove(template))
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadOrCreateTemplate()
|
||||
{
|
||||
if (!File.Exists(FILE_NAME))
|
||||
{
|
||||
Templates.Clear();
|
||||
foreach (var mafileExportTemplate in CreateDefaultTemplate())
|
||||
{
|
||||
Templates.Add(mafileExportTemplate);
|
||||
}
|
||||
|
||||
Save();
|
||||
return;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(FILE_NAME);
|
||||
try
|
||||
{
|
||||
var loadedTemplates = JsonConvert.DeserializeObject<List<MafileExportTemplate>>(json);
|
||||
if (loadedTemplates != null)
|
||||
{
|
||||
Templates.Clear();
|
||||
foreach (var tmpl in loadedTemplates)
|
||||
{
|
||||
Templates.Add(tmpl);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, $"Failed to deserialize {FILE_NAME}");
|
||||
Templates.Clear();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(Templates, Formatting.Indented);
|
||||
File.WriteAllText(FILE_NAME, json);
|
||||
}
|
||||
|
||||
private static IEnumerable<MafileExportTemplate> CreateDefaultTemplate()
|
||||
{
|
||||
return
|
||||
[
|
||||
new MafileExportTemplate
|
||||
{
|
||||
Name = "Default",
|
||||
UseLoginAsMafileName = false,
|
||||
IncludeSharedSecret = true,
|
||||
IncludeIdentitySecret = true,
|
||||
IncludeRCode = true,
|
||||
IncludeSessionData = true,
|
||||
IncludeOtherInfo = true,
|
||||
IncludeNebulaProxy = true,
|
||||
IncludeNebulaPassword = true,
|
||||
IncludeNebulaGroup = true,
|
||||
Path = null
|
||||
},
|
||||
new MafileExportTemplate
|
||||
{
|
||||
Name = "Auth only",
|
||||
UseLoginAsMafileName = true,
|
||||
IncludeSharedSecret = true,
|
||||
IncludeIdentitySecret = false,
|
||||
IncludeRCode = false,
|
||||
IncludeSessionData = false,
|
||||
IncludeOtherInfo = false,
|
||||
IncludeNebulaProxy = false,
|
||||
IncludeNebulaPassword = false,
|
||||
IncludeNebulaGroup = false,
|
||||
Path = null
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public enum AddMafileResult
|
||||
{
|
||||
Added,
|
||||
AlreadyExist,
|
||||
Error
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public class MafileImportSummary
|
||||
{
|
||||
public int Added { get; private set; }
|
||||
public int NotAdded { get; private set; }
|
||||
public int Errors { get; private set; }
|
||||
|
||||
public void AddedOne()
|
||||
{
|
||||
Added++;
|
||||
}
|
||||
|
||||
public void SkippedOne()
|
||||
{
|
||||
NotAdded++;
|
||||
}
|
||||
|
||||
public void ErrorOne()
|
||||
{
|
||||
Errors++;
|
||||
}
|
||||
|
||||
public void Add(int added = 0, int notAdded = 0, int errors = 0)
|
||||
{
|
||||
Added += added;
|
||||
NotAdded += notAdded;
|
||||
Errors += errors;
|
||||
}
|
||||
|
||||
public void Apply(AddMafileResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case AddMafileResult.Added:
|
||||
AddedOne();
|
||||
break;
|
||||
case AddMafileResult.AlreadyExist:
|
||||
SkippedOne();
|
||||
break;
|
||||
case AddMafileResult.Error:
|
||||
ErrorOne();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Utility;
|
||||
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public interface IMafileNamingStrategy
|
||||
{
|
||||
bool CanNameMafile(Mafile mafile);
|
||||
string GetMafileName(Mafile mafile);
|
||||
}
|
||||
|
||||
public class MafileNamingStrategy : IMafileNamingStrategy
|
||||
{
|
||||
public const string DEF_EXTENSION = ".mafile";
|
||||
|
||||
public static readonly IDictionary<string, Func<Mafile, string?>> Placeholders =
|
||||
new Dictionary<string, Func<Mafile, string?>>
|
||||
{
|
||||
{"{steamid}", GetSteamId},
|
||||
{"{login}", GetLogin},
|
||||
{"{group}", GetGroup},
|
||||
{"{servertime}", GetServerTime}
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public static MafileNamingStrategy SteamId { get; } = new("{steamid}");
|
||||
public static MafileNamingStrategy Login { get; } = new("{login}");
|
||||
|
||||
|
||||
public string Pattern { get; }
|
||||
public bool IncludeExtension { get; }
|
||||
|
||||
public MafileNamingStrategy(string pattern, bool includeExtension = true)
|
||||
{
|
||||
Pattern = pattern;
|
||||
IncludeExtension = includeExtension;
|
||||
}
|
||||
|
||||
public bool CanNameMafile(Mafile mafile)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Pattern)) return false;
|
||||
var fileName = ApplyPatternSubstitution(mafile, Pattern, IncludeExtension);
|
||||
return FileNameValidator.IsValidFileName(fileName);
|
||||
}
|
||||
|
||||
public string GetMafileName(Mafile mafile)
|
||||
{
|
||||
if (!CanNameMafile(mafile))
|
||||
throw new InvalidOperationException($"Cannot name mafile with the current pattern {Pattern}");
|
||||
return ApplyPatternSubstitution(mafile, Pattern, IncludeExtension);
|
||||
}
|
||||
|
||||
private static string ApplyPatternSubstitution(Mafile mafile, string pattern, bool includeExtension)
|
||||
{
|
||||
var result = pattern;
|
||||
foreach (var placeholder in Placeholders)
|
||||
{
|
||||
if (result.Contains(placeholder.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = placeholder.Value(mafile);
|
||||
result = result.Replace(placeholder.Key, value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
return includeExtension ? result + DEF_EXTENSION : result;
|
||||
}
|
||||
|
||||
private static string GetSteamId(Mafile mafile)
|
||||
{
|
||||
return mafile.SteamId.Steam64.ToString();
|
||||
}
|
||||
|
||||
private static string GetLogin(Mafile mafile)
|
||||
{
|
||||
return mafile.AccountName;
|
||||
}
|
||||
|
||||
private static string? GetGroup(Mafile mafile)
|
||||
{
|
||||
return mafile.Group;
|
||||
}
|
||||
|
||||
private static string GetServerTime(Mafile mafile)
|
||||
{
|
||||
return mafile.ServerTime.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public class MafilesBulkRenameResult
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int Renamed { get; set; }
|
||||
public int NotRenamed => Errors + Conflict;
|
||||
public int Errors { get; set; }
|
||||
public int Conflict { get; set; }
|
||||
public string BackupFileName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.IO;
|
||||
using NebulaAuth.Model.Entities;
|
||||
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
internal static class MafilesStorageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns <see cref="Mafile.Filename" /> or creates a new one and updates the property.
|
||||
/// <see cref="Mafile.Filename" /> is always not <see langword="null" /> after this method call.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetOrUpdateMafilePath(Mafile data)
|
||||
{
|
||||
if (data.Filename != null)
|
||||
{
|
||||
return Path.Combine(Storage.MafilesDirectory, data.Filename);
|
||||
}
|
||||
|
||||
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
|
||||
data.Filename = fileName;
|
||||
return Path.Combine(Storage.MafilesDirectory, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates mafile file name according to the current settings.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="useAccountName"></param>
|
||||
/// <returns></returns>
|
||||
public static string CreateMafileFileName(Mafile data, bool useAccountName)
|
||||
{
|
||||
return useAccountName
|
||||
? MafileNamingStrategy.Login.GetMafileName(data)
|
||||
: MafileNamingStrategy.SteamId.GetMafileName(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using NebulaAuth.Model.MAAC;
|
||||
using SteamLib;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
using SteamLib.SteamMobile;
|
||||
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public static class Storage
|
||||
{
|
||||
public const string DIR_MAFILES = "maFiles";
|
||||
public const string DIR_REMOVED_MAFILES = "maFiles_removed";
|
||||
public const string DIR_BACKUP_MAFILES = "maFiles_backup";
|
||||
|
||||
public static readonly string[] MafilesDirectories;
|
||||
public static string MafilesDirectory { get; } = Path.GetFullPath(DIR_MAFILES);
|
||||
public static string RemovedMafilesDirectory { get; } = Path.GetFullPath(DIR_REMOVED_MAFILES);
|
||||
public static string BackupMafilesDirectory { get; } = Path.GetFullPath(DIR_BACKUP_MAFILES);
|
||||
|
||||
public static ObservableCollection<Mafile> MaFiles { get; private set; } = [];
|
||||
|
||||
static Storage()
|
||||
{
|
||||
MafilesDirectories = [MafilesDirectory, RemovedMafilesDirectory, BackupMafilesDirectory];
|
||||
}
|
||||
|
||||
public static async Task Initialize(int threadCount, CancellationToken token = default)
|
||||
{
|
||||
Directory.CreateDirectory(MafilesDirectory);
|
||||
Directory.CreateDirectory(RemovedMafilesDirectory);
|
||||
Directory.CreateDirectory(BackupMafilesDirectory);
|
||||
|
||||
var files = Directory
|
||||
.GetFiles(MafilesDirectory)
|
||||
.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile"))
|
||||
.ToList();
|
||||
|
||||
|
||||
var localList = new ConcurrentBag<Mafile>();
|
||||
await Task.Run(() =>
|
||||
{
|
||||
return Parallel.ForEachAsync(files,
|
||||
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
|
||||
async (file, ct) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await ReadMafileAsync(file);
|
||||
localList.Add(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, "Can't load mafile {file}", Path.GetFileName(file));
|
||||
}
|
||||
});
|
||||
}, token);
|
||||
|
||||
MaFiles = new ObservableCollection<Mafile>(localList.OrderBy(m => m.AccountName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="overwrite"></param>
|
||||
/// <returns> Result of adding mafile</returns>
|
||||
/// <exception cref="SessionInvalidException"></exception>
|
||||
public static async Task<AddMafileResult> AddNewMafile(string path, bool overwrite)
|
||||
{
|
||||
Mafile data;
|
||||
|
||||
try
|
||||
{
|
||||
data = await ReadMafileAsync(path);
|
||||
}
|
||||
catch (Exception ex) when (ex is not MafileNeedReloginException)
|
||||
{
|
||||
Shell.Logger.Warn(ex, "Can't load mafile");
|
||||
throw new FormatException("File data is not valid", ex);
|
||||
}
|
||||
|
||||
await AddNewMafileInternal(data, overwrite);
|
||||
return AddMafileResult.Added;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="overwrite"></param>
|
||||
/// <returns> Result of adding mafile</returns>
|
||||
/// <exception cref="SessionInvalidException"></exception>
|
||||
public static Task<AddMafileResult> AddNewMafileFromData(Mafile data, bool overwrite)
|
||||
{
|
||||
return AddNewMafileInternal(data, overwrite);
|
||||
}
|
||||
|
||||
private static async Task<AddMafileResult> AddNewMafileInternal(Mafile data, bool overwrite)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(data.AccountName))
|
||||
{
|
||||
Shell.Logger.Warn("Mafile account name is empty");
|
||||
return AddMafileResult.Error;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, "Can't generate code for mafile {accountName}", data.AccountName);
|
||||
return AddMafileResult.Error;
|
||||
}
|
||||
|
||||
data.Filename = null;
|
||||
|
||||
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
|
||||
{
|
||||
return AddMafileResult.AlreadyExist;
|
||||
}
|
||||
|
||||
await SaveMafileAsync(data);
|
||||
return AddMafileResult.Added;
|
||||
}
|
||||
|
||||
public static async Task<Mafile> ReadMafileAsync(string path)
|
||||
{
|
||||
var str = await File.ReadAllTextAsync(path);
|
||||
return NebulaSerializer.Deserialize(str, path);
|
||||
}
|
||||
|
||||
public static async Task SaveMafileAsync(Mafile data)
|
||||
{
|
||||
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
|
||||
|
||||
// Replace if exist
|
||||
for (var i = 0; i < MaFiles.Count; i++)
|
||||
{
|
||||
var existed = MaFiles[i];
|
||||
if (ReferenceEquals(existed, data) || (existed.Filename != null && existed.Filename == data.Filename))
|
||||
{
|
||||
MaFiles[i] = data;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MaFiles.Add(data);
|
||||
}
|
||||
|
||||
public static void UpdateMafile(Mafile data)
|
||||
{
|
||||
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
File.WriteAllText(Path.GetFullPath(path), str);
|
||||
}
|
||||
|
||||
public static async Task UpdateMafileAsync(Mafile data)
|
||||
{
|
||||
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
|
||||
}
|
||||
|
||||
public static void MoveToRemoved(Mafile data)
|
||||
{
|
||||
var sourcePath = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var fileName = Path.GetFileNameWithoutExtension(data.Filename!);
|
||||
var destinationPathBase = Path.Combine(DIR_REMOVED_MAFILES, fileName);
|
||||
var destinationPathFinal = Path.ChangeExtension(destinationPathBase, MafileNamingStrategy.DEF_EXTENSION);
|
||||
var i = 0;
|
||||
while (File.Exists(destinationPathFinal))
|
||||
{
|
||||
i++;
|
||||
destinationPathFinal = destinationPathBase + $" ({i})";
|
||||
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
|
||||
}
|
||||
|
||||
File.Copy(sourcePath, destinationPathFinal, false);
|
||||
File.Delete(sourcePath);
|
||||
MaFiles.Remove(data);
|
||||
}
|
||||
|
||||
|
||||
public static string? TryGetMafilePath(Mafile data)
|
||||
{
|
||||
if (data.Filename == null) return null;
|
||||
return Path.Combine(MafilesDirectory, data.Filename);
|
||||
}
|
||||
|
||||
public static void WriteBackup(MobileDataExtended data)
|
||||
{
|
||||
var json = NebulaSerializer.SerializeMafile(data, null);
|
||||
WriteBackup(data.AccountName, json);
|
||||
}
|
||||
|
||||
public static void WriteBackup(string accountName, string data)
|
||||
{
|
||||
Directory.CreateDirectory(DIR_BACKUP_MAFILES);
|
||||
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
|
||||
}
|
||||
|
||||
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
|
||||
IProgress<double>? progress = null)
|
||||
{
|
||||
if (MaFiles.Count == 0) return new MafilesBulkRenameResult();
|
||||
var now = DateTime.Now;
|
||||
var backupFileName = $"rename_backup_{now:yyyy-MM-dd_HH-mm-ss}.zip";
|
||||
var zipPath = Path.Combine(BackupMafilesDirectory, backupFileName);
|
||||
await using (var zipStream = new FileStream(zipPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, false);
|
||||
var files = Directory
|
||||
.EnumerateFiles(MafilesDirectory, "*.*", SearchOption.TopDirectoryOnly)
|
||||
.Where(f => Path.GetExtension(f).Equals(".mafile", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
var counter = 0;
|
||||
foreach (var file in files)
|
||||
{
|
||||
counter++;
|
||||
var entry = archive.CreateEntry(Path.GetFileName(file), CompressionLevel.Optimal);
|
||||
|
||||
await using var entryStream = entry.Open();
|
||||
await using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096,
|
||||
true);
|
||||
await fileStream.CopyToAsync(entryStream);
|
||||
|
||||
if (counter % 5 == 0)
|
||||
{
|
||||
progress?.Report(counter / (double) files.Count);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mafiles = MaFiles.ToList();
|
||||
var res = new MafilesBulkRenameResult
|
||||
{
|
||||
Total = mafiles.Count,
|
||||
BackupFileName = backupFileName
|
||||
};
|
||||
|
||||
var dic = new Dictionary<string, string>();
|
||||
foreach (var mafile in mafiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
|
||||
if (mafile.Filename == targetFileName || mafile.Filename == null)
|
||||
{
|
||||
res.Renamed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceFileName = mafile.Filename;
|
||||
var fullSourcePath = Path.Combine(MafilesDirectory, sourceFileName);
|
||||
var fullTargetPath = Path.Combine(MafilesDirectory, targetFileName);
|
||||
|
||||
if (!File.Exists(fullSourcePath))
|
||||
{
|
||||
Shell.Logger.Warn("Can't rename mafile {old} to {new} because source file not found",
|
||||
mafile.Filename, targetFileName);
|
||||
IncErrors();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.Exists(fullTargetPath))
|
||||
{
|
||||
Shell.Logger.Warn("Can't rename mafile {old} to {new} because target file already exist",
|
||||
mafile.Filename, targetFileName);
|
||||
res.Conflict += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
File.Move(fullSourcePath, fullTargetPath);
|
||||
dic[sourceFileName] = targetFileName;
|
||||
res.Renamed += 1;
|
||||
mafile.Filename = targetFileName;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, "Error renaming mafile {file} {accountName}", mafile.Filename,
|
||||
mafile.AccountName);
|
||||
IncErrors();
|
||||
}
|
||||
}
|
||||
|
||||
MAACStorage.NotifyMafilesRenamed(dic);
|
||||
return res;
|
||||
|
||||
void IncErrors()
|
||||
{
|
||||
res.Errors += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.MafilesLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for detecting and handling SDA encrypted mafiles.
|
||||
/// This is used in the legacy mafile handling code to support importing SDA encrypted mafiles without requiring the
|
||||
/// user to manually decrypt them first.
|
||||
/// </summary>
|
||||
public static class SDAEncryptionHelper
|
||||
{
|
||||
private const string ManifestFileName = "manifest.json";
|
||||
|
||||
public static bool LooksLikeSdaEncryptedBlob(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return false;
|
||||
|
||||
var trimmed = content.Trim();
|
||||
|
||||
if (trimmed.StartsWith('{') || trimmed.Length < 64)
|
||||
return false;
|
||||
|
||||
Span<byte> buffer = stackalloc byte[trimmed.Length];
|
||||
return Convert.TryFromBase64String(trimmed, buffer, out _);
|
||||
}
|
||||
|
||||
public static Context? TryDetect(string mafilePath, SDAManifest? sdaManifest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mafilePath))
|
||||
return null;
|
||||
|
||||
if (sdaManifest == null)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(mafilePath);
|
||||
if (string.IsNullOrWhiteSpace(dir))
|
||||
return null;
|
||||
|
||||
var manifestPath = FindManifestPath(dir);
|
||||
if (manifestPath == null)
|
||||
return null;
|
||||
|
||||
sdaManifest = TryReadEncryptedManifest(manifestPath);
|
||||
}
|
||||
|
||||
if (sdaManifest is not {Encrypted: true} || sdaManifest.Entries.Length == 0)
|
||||
return null;
|
||||
|
||||
var fileName = Path.GetFileName(mafilePath);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return null;
|
||||
|
||||
var entries =
|
||||
sdaManifest.Entries.ToDictionary(x => Path.GetFileName(x.Filename), StringComparer.OrdinalIgnoreCase);
|
||||
return new Context(sdaManifest, entries, null);
|
||||
}
|
||||
|
||||
private static string? FindManifestPath(string startDirectory)
|
||||
{
|
||||
var current = startDirectory;
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var candidate = Path.Combine(current, ManifestFileName);
|
||||
if (File.Exists(candidate))
|
||||
return candidate;
|
||||
|
||||
var parent = Directory.GetParent(current);
|
||||
if (parent?.FullName == null)
|
||||
break;
|
||||
current = parent.FullName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static SDAManifest? TryReadEncryptedManifest(string manifestPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manifestText = File.ReadAllText(manifestPath);
|
||||
var manifest = JsonConvert.DeserializeObject<SDAManifest>(manifestText);
|
||||
if (manifest is not {Encrypted: true} || manifest.Entries.Length == 0)
|
||||
return null;
|
||||
return manifest;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? TryDecrypt(string content, string path, Context context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(context.Password))
|
||||
return null;
|
||||
|
||||
// If we have password, check if we have entry
|
||||
var entry = context.GetEntry(path);
|
||||
if (entry == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// We have entry, try to decrypt
|
||||
return SDAEncryptor.TryDecryptData(context.Password, entry.EncryptionSalt, entry.EncryptionIv, content,
|
||||
out var decrypted)
|
||||
? decrypted
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of SDA encrypted mafile detection. When not null, the caller can decrypt
|
||||
/// using SDAEncryptor.DecryptData(password, Entry.EncryptionSalt, Entry.EncryptionIv, fileContent).
|
||||
/// </summary>
|
||||
public sealed class Context
|
||||
{
|
||||
public SDAManifest SdaManifest { get; }
|
||||
public Dictionary<string, SDAManifestEntry> Entries { get; }
|
||||
public string? Password { get; }
|
||||
|
||||
public Context(SDAManifest sdaManifest, Dictionary<string, SDAManifestEntry> entries, string? password)
|
||||
{
|
||||
SdaManifest = sdaManifest;
|
||||
Entries = entries;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
public Context WithPassword(string? password)
|
||||
{
|
||||
return new Context(SdaManifest, Entries, password);
|
||||
}
|
||||
|
||||
public SDAManifestEntry? GetEntry(string path)
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
return Entries.GetValueOrDefault(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-2
@@ -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
-4
@@ -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; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using Newtonsoft.Json;
|
||||
@@ -32,21 +33,22 @@ public static class NebulaSerializer
|
||||
}
|
||||
|
||||
|
||||
public static Mafile Deserialize(string cont)
|
||||
public static Mafile Deserialize(string cont, string path)
|
||||
{
|
||||
var data = Serializer.Deserialize(cont);
|
||||
var mobileData = data.Data;
|
||||
var info = data.Info;
|
||||
if (info.IsExtended == false)
|
||||
if (!info.IsExtended)
|
||||
throw new FormatException("Mafile is not extended data");
|
||||
|
||||
|
||||
var props = info.UnusedProperties ?? new Dictionary<string, JProperty>();
|
||||
var props = info.UnusedProperties ?? [];
|
||||
var proxy = GetPropertyValue<MaProxy>("Proxy", props);
|
||||
var group = GetPropertyValue<string>("Group", props);
|
||||
var password = GetPropertyValue<string>("Password", props);
|
||||
var mafile = Mafile.FromMobileDataExtended((MobileDataExtended) mobileData, proxy, group, password);
|
||||
|
||||
mafile.Filename = Path.GetFileName(path);
|
||||
|
||||
if (!info.SteamIdValid)
|
||||
throw new MafileNeedReloginException(mafile);
|
||||
@@ -56,7 +58,7 @@ public static class NebulaSerializer
|
||||
|
||||
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
|
||||
{
|
||||
if (dictionary.TryGetValue(name, out var prop) == false) return default;
|
||||
if (!dictionary.TryGetValue(name, out var prop)) return default;
|
||||
var value = prop.Value;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ public static class ProxyStorage
|
||||
|
||||
static ProxyStorage()
|
||||
{
|
||||
if (File.Exists("proxies.json") == false)
|
||||
if (!File.Exists("proxies.json"))
|
||||
return;
|
||||
try
|
||||
{
|
||||
@@ -46,7 +46,9 @@ public static class ProxyStorage
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SnackbarController.SendSnackbar("Ошибка при загрузке прокси");
|
||||
Shell.Logger.Error(ex, "Error while loading proxies");
|
||||
SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("Error when loading proxies",
|
||||
"ProxyStorage", "ErrorWhileLoadingProxies"));
|
||||
SnackbarController.SendSnackbar(ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +95,7 @@ public static class ProxyStorage
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void OrderCollection() //RETHINK: maybe there is a better way to handle it
|
||||
public static void SortCollection() //RETHINK: maybe there is a better way to handle it
|
||||
{
|
||||
var proxies = Proxies.OrderBy(p => p.Key)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
+15
-60
@@ -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
|
||||
+3
-4
@@ -1,6 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using AchiesUtilities.Web.Models;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using SteamLib.Api.Mobile;
|
||||
using SteamLib.Authentication;
|
||||
using SteamLib.Authentication.LoginV2;
|
||||
@@ -27,9 +28,8 @@ public partial class SessionHandler //API
|
||||
mafile.SessionData.SetMobileToken(newToken);
|
||||
|
||||
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
|
||||
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
|
||||
mafile.SetSessionData(mafile.SessionData);
|
||||
Storage.UpdateMafile(mafile);
|
||||
await Storage.UpdateMafileAsync(mafile);
|
||||
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
|
||||
}
|
||||
|
||||
@@ -49,10 +49,9 @@ public partial class SessionHandler //API
|
||||
AdmissionHelper.TransferCommunityCookies(chp.Handler.CookieContainer);
|
||||
|
||||
//Triggers PropertyChanged event for PortableMaClient handling session updated from MaClient
|
||||
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
|
||||
mafile.SetSessionData((MobileSessionData) result);
|
||||
if (PHandler.IsPasswordSet)
|
||||
mafile.Password = savePassword ? PHandler.Encrypt(password) : null;
|
||||
Storage.UpdateMafile(mafile);
|
||||
await Storage.UpdateMafileAsync(mafile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NebulaAuth.Model.Entities;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
|
||||
public partial class SessionHandler //Helpers
|
||||
{
|
||||
private static bool MobileTokenExpired(this Mafile mafile)
|
||||
{
|
||||
var mobileToken = mafile.SessionData?.GetMobileToken();
|
||||
return mobileToken == null || mobileToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static bool RefreshTokenExpired(this Mafile mafile)
|
||||
{
|
||||
var refreshToken = mafile.SessionData?.RefreshToken;
|
||||
return refreshToken == null || refreshToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static bool HasPassword(this Mafile mafile, [NotNullWhen(true)] out string? plainPassword)
|
||||
{
|
||||
plainPassword = GetPassword(mafile);
|
||||
return !string.IsNullOrWhiteSpace(plainPassword);
|
||||
}
|
||||
|
||||
private static string? GetPassword(Mafile mafile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
|
||||
{
|
||||
return PHandler.Decrypt(mafile.Password);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Windows.Media;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Utility;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
@@ -24,10 +25,11 @@ public partial class Settings : ObservableObject
|
||||
|
||||
static Settings()
|
||||
{
|
||||
if (File.Exists("settings.json") == false)
|
||||
if (!File.Exists("settings.json"))
|
||||
{
|
||||
Instance = new Settings();
|
||||
Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||
Instance.Language = LanguageUtility.DetectPreferredLanguage();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,7 +41,8 @@ public partial class Settings : ObservableObject
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SnackbarController.SendSnackbar("Ошибка при загрузке настроек. Настройки были сброшены");
|
||||
SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("Error when loading settings", "Settings",
|
||||
"ErrorWhileLoadingSettings"));
|
||||
SnackbarController.SendSnackbar(ex.Message);
|
||||
Instance = new Settings();
|
||||
}
|
||||
@@ -47,6 +50,7 @@ public partial class Settings : ObservableObject
|
||||
Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||
}
|
||||
|
||||
|
||||
private static void SettingsOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
Save();
|
||||
@@ -68,8 +72,9 @@ public partial class Settings : ObservableObject
|
||||
Instance.BackgroundOpacity = 1.0;
|
||||
Instance.BackgroundGamma = 0.0;
|
||||
Instance.LeftOpacity = 0.4;
|
||||
Instance.RightOpacity = 0.8;
|
||||
Instance.RightOpacity = 0.4;
|
||||
Instance.ApplyBlurBackground = true;
|
||||
Instance.RippleDisabled = false;
|
||||
Save();
|
||||
}
|
||||
|
||||
@@ -89,16 +94,21 @@ public partial class Settings : ObservableObject
|
||||
[ObservableProperty] private bool _legacyMode = true;
|
||||
[ObservableProperty] private bool _allowAutoUpdate;
|
||||
[ObservableProperty] private bool _useAccountNameAsMafileName;
|
||||
[ObservableProperty] private bool _ignorePatchTuesdayErrors;
|
||||
|
||||
[ObservableProperty] private BackgroundMode _backgroundMode = BackgroundMode.Default;
|
||||
[ObservableProperty] private double _leftOpacity = 0.4;
|
||||
[ObservableProperty] private double _rightOpacity = 1.0;
|
||||
[ObservableProperty] private double _rightOpacity = 0.4;
|
||||
[ObservableProperty] private double _backgroundBlur;
|
||||
[ObservableProperty] private double _backgroundOpacity = 1;
|
||||
[ObservableProperty] private double _backgroundGamma;
|
||||
[ObservableProperty] private bool _applyBlurBackground = true;
|
||||
[ObservableProperty] private ThemeType _themeType = ThemeType.Default;
|
||||
[ObservableProperty] private bool _rippleDisabled;
|
||||
[ObservableProperty] private bool _proxyManagerDisplayProtocol;
|
||||
[ObservableProperty] private bool _proxyManagerDisplayCredentials;
|
||||
|
||||
[ObservableProperty] private TimeSpan _maacErrorThreshold = TimeSpan.FromHours(3);
|
||||
[ObservableProperty] private TimeSpan _maacRetryInterval = TimeSpan.FromMinutes(15);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using SteamLib;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
using SteamLib.SteamMobile;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
|
||||
public static class Storage
|
||||
{
|
||||
public const string MAFILE_F = "maFiles";
|
||||
public const string REMOVED_F = "maFiles_removed";
|
||||
private static int _duplicateFound;
|
||||
|
||||
public static int DuplicateFound => _duplicateFound;
|
||||
public static string MafileFolder { get; } = Path.GetFullPath(MAFILE_F);
|
||||
public static string RemovedMafileFolder { get; } = Path.GetFullPath(REMOVED_F);
|
||||
|
||||
public static ObservableCollection<Mafile> MaFiles { get; private set; } = new();
|
||||
|
||||
static Storage()
|
||||
{
|
||||
}
|
||||
|
||||
public static async Task Initialize(int threadCount, CancellationToken token = default)
|
||||
{
|
||||
if (!Directory.Exists(MafileFolder))
|
||||
Directory.CreateDirectory(MafileFolder);
|
||||
|
||||
if (!Directory.Exists(RemovedMafileFolder))
|
||||
Directory.CreateDirectory(RemovedMafileFolder);
|
||||
var comparer = new MafileNameComparer(Settings.Instance.UseAccountNameAsMafileName);
|
||||
var files = Directory
|
||||
.GetFiles(MafileFolder)
|
||||
.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile"))
|
||||
.Order(comparer)
|
||||
.ToList();
|
||||
|
||||
|
||||
var hashNames = new ConcurrentDictionary<string, byte>();
|
||||
var hashIds = new ConcurrentDictionary<SteamId, byte>();
|
||||
var localList = new ConcurrentBag<Mafile>();
|
||||
|
||||
var processed = 0;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
return Parallel.ForEachAsync(files,
|
||||
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
|
||||
async (file, ct) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await ReadMafileAsync(file);
|
||||
|
||||
if (!hashNames.TryAdd(data.AccountName, 0) ||
|
||||
(data.SessionData != null && !hashIds.TryAdd(data.SteamId, 0)))
|
||||
{
|
||||
Interlocked.Increment(ref _duplicateFound);
|
||||
Shell.Logger.Error("Duplicate mafile {file}", Path.GetFileName(file));
|
||||
}
|
||||
|
||||
localList.Add(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, "Can't load mafile {file}", Path.GetFileName(file));
|
||||
}
|
||||
});
|
||||
}, token);
|
||||
|
||||
MaFiles = new ObservableCollection<Mafile>(localList.OrderBy(m => m.AccountName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="overwrite"></param>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
/// <exception cref="SessionInvalidException"></exception>
|
||||
/// <exception cref="IOException"></exception>
|
||||
public static void AddNewMafile(string path, bool overwrite)
|
||||
{
|
||||
Mafile data;
|
||||
try
|
||||
{
|
||||
data = ReadMafile(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is not MafileNeedReloginException)
|
||||
{
|
||||
Shell.Logger.Warn(ex, "Can't load mafile");
|
||||
throw new FormatException("File data is not valid", ex);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(data.AccountName))
|
||||
throw new FormatException("File data is not valid. Missing AccountName");
|
||||
|
||||
try
|
||||
{
|
||||
_ = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException("Can't generate code on this mafile", ex);
|
||||
}
|
||||
|
||||
if (overwrite == false && File.Exists(CreatePathForMafile(data)))
|
||||
{
|
||||
throw new IOException("File already exist and overwrite is False");
|
||||
}
|
||||
|
||||
|
||||
SaveMafile(data);
|
||||
}
|
||||
|
||||
|
||||
public static Mafile ReadMafile(string path)
|
||||
{
|
||||
var str = File.ReadAllText(path);
|
||||
return NebulaSerializer.Deserialize(str);
|
||||
}
|
||||
|
||||
public static async Task<Mafile> ReadMafileAsync(string path)
|
||||
{
|
||||
var str = await File.ReadAllTextAsync(path);
|
||||
return NebulaSerializer.Deserialize(str);
|
||||
}
|
||||
|
||||
public static void SaveMafile(Mafile data)
|
||||
{
|
||||
var path = CreatePathForMafile(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
File.WriteAllText(Path.GetFullPath(path), str);
|
||||
|
||||
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
|
||||
if (existed != null)
|
||||
{
|
||||
var index = MaFiles.IndexOf(existed);
|
||||
MaFiles[index] = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaFiles.Add(data);
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateMafile(Mafile data)
|
||||
{
|
||||
var path = CreatePathForMafile(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
File.WriteAllText(Path.GetFullPath(path), str);
|
||||
}
|
||||
|
||||
public static void MoveToRemoved(Mafile data)
|
||||
{
|
||||
var path = CreatePathForMafile(data);
|
||||
var copyPath = Path.Combine(REMOVED_F, data.SteamId + ".mafile");
|
||||
var copyPathCompleted = copyPath;
|
||||
var i = 0;
|
||||
while (File.Exists(copyPathCompleted))
|
||||
{
|
||||
i++;
|
||||
copyPathCompleted = copyPath + $" ({i})";
|
||||
}
|
||||
|
||||
File.Copy(path, copyPathCompleted, false);
|
||||
File.Delete(path);
|
||||
MaFiles.Remove(data);
|
||||
}
|
||||
|
||||
private static string CreatePathForMafile(Mafile data)
|
||||
{
|
||||
var fileName = Settings.Instance.UseAccountNameAsMafileName
|
||||
? CreateFileNameWithAccountName(data.AccountName)
|
||||
: CreateFileNameWithSteamId(data.SteamId);
|
||||
|
||||
return Path.Combine(MafileFolder, fileName);
|
||||
}
|
||||
|
||||
private static string CreateFileNameWithAccountName(string accountName)
|
||||
{
|
||||
return accountName + ".mafile";
|
||||
}
|
||||
|
||||
private static string CreateFileNameWithSteamId(SteamId steamId)
|
||||
{
|
||||
return steamId.Steam64.Id + ".mafile";
|
||||
}
|
||||
|
||||
public static string? TryFindMafilePath(Mafile data)
|
||||
{
|
||||
var pathFileName = Path.Combine(MafileFolder, CreateFileNameWithAccountName(data.AccountName));
|
||||
string? pathSteamId = null;
|
||||
if (data.SessionData != null)
|
||||
{
|
||||
pathSteamId = Path.Combine(MafileFolder, CreateFileNameWithSteamId(data.SteamId));
|
||||
}
|
||||
|
||||
var steamIdExist = pathSteamId != null && File.Exists(pathSteamId);
|
||||
var accountNameExist = File.Exists(pathFileName);
|
||||
|
||||
if (steamIdExist && accountNameExist)
|
||||
{
|
||||
return Settings.Instance.UseAccountNameAsMafileName ? pathFileName : pathSteamId;
|
||||
}
|
||||
|
||||
if (steamIdExist ^ accountNameExist)
|
||||
{
|
||||
return steamIdExist ? pathSteamId : pathFileName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void BackupHandler(MobileDataExtended data)
|
||||
{
|
||||
if (Directory.Exists("mafiles_backup") == false)
|
||||
{
|
||||
Directory.CreateDirectory("mafiles_backup");
|
||||
}
|
||||
|
||||
|
||||
var json = NebulaSerializer.SerializeMafile(data, null);
|
||||
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"),
|
||||
json);
|
||||
}
|
||||
|
||||
public static void BackupHandlerStr(string accountName, string data)
|
||||
{
|
||||
if (Directory.Exists("mafiles_backup") == false)
|
||||
{
|
||||
Directory.CreateDirectory("mafiles_backup");
|
||||
}
|
||||
|
||||
|
||||
File.WriteAllText(Path.Combine("mafiles_backup", accountName + ".mafile"),
|
||||
data);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Refactor
|
||||
//TODO: use numeric orderer when .net 10 released
|
||||
internal class MafileNameComparer : IComparer<string>
|
||||
{
|
||||
private const string MAF_64_START = "765";
|
||||
private static readonly IComparer<string> DefaultComparer = Comparer<string>.Default;
|
||||
public bool MafileNameMode { get; }
|
||||
|
||||
public MafileNameComparer(bool mafileNameMode)
|
||||
{
|
||||
MafileNameMode = mafileNameMode;
|
||||
}
|
||||
|
||||
|
||||
public int Compare(string? x, string? y)
|
||||
{
|
||||
if (x == null && y == null) return 0;
|
||||
if (x == null) return -1;
|
||||
if (y == null) return 1;
|
||||
|
||||
|
||||
var xisSteamId = Path.GetFileName(x).StartsWith(MAF_64_START);
|
||||
var yisSteamId = Path.GetFileName(y).StartsWith(MAF_64_START);
|
||||
|
||||
if (xisSteamId ^ yisSteamId)
|
||||
{
|
||||
if (MafileNameMode)
|
||||
{
|
||||
return xisSteamId ? 1 : -1;
|
||||
}
|
||||
|
||||
return yisSteamId ? 1 : -1;
|
||||
}
|
||||
|
||||
return DefaultComparer.Compare(x, y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.Update;
|
||||
|
||||
public class ChangelogEntry
|
||||
{
|
||||
[JsonProperty("version")] public string Version { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("date")] public string Date { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("changes")] public List<ChangeItem> Changes { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ChangeItem
|
||||
{
|
||||
[JsonProperty("type")] public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("text")] public string Text { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("link")] public string? Link { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.Update;
|
||||
|
||||
public class UpdateSettings
|
||||
{
|
||||
private static readonly string FilePath = Path.Combine("settings", "update.json");
|
||||
|
||||
[JsonProperty("skippedVersion")] public string? SkippedVersion { get; set; }
|
||||
|
||||
[JsonProperty("remindAfter")] public DateTime? RemindAfter { get; set; }
|
||||
|
||||
public static UpdateSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(FilePath)) return new UpdateSettings();
|
||||
var json = File.ReadAllText(FilePath);
|
||||
return JsonConvert.DeserializeObject<UpdateSettings>(json) ?? new UpdateSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new UpdateSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var dir = Path.GetDirectoryName(FilePath)!;
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||
File.WriteAllText(FilePath, json);
|
||||
}
|
||||
|
||||
public void SkipVersion(string version)
|
||||
{
|
||||
SkippedVersion = version;
|
||||
RemindAfter = null;
|
||||
Save();
|
||||
}
|
||||
|
||||
public void SetRemindAfter(DateTime remindAfter)
|
||||
{
|
||||
RemindAfter = remindAfter;
|
||||
Save();
|
||||
}
|
||||
|
||||
public bool ShouldShow(string version)
|
||||
{
|
||||
if (SkippedVersion == version) return false;
|
||||
if (RemindAfter.HasValue && DateTime.Now < RemindAfter.Value) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,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}" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
@@ -10,7 +10,7 @@
|
||||
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
|
||||
<ApplicationIcon>Theme\lock.ico</ApplicationIcon>
|
||||
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
|
||||
<AssemblyVersion>1.7.4</AssemblyVersion>
|
||||
<AssemblyVersion>1.8.3</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,16 +53,69 @@
|
||||
<Compile Update="View\Dialogs\LoginAgainOnImportDialog.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="View\Dialogs\SdaPasswordDialog.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="NLog.config">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<None Update="Localization\codebehind.errors.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="localization.loc.json">
|
||||
<None Update="Localization\codebehind.other.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.setpasswords.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.mafilemover.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.export.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.update.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.sdapassword.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.encryptionpassword.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.linker.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.proxymanager.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.loginagain.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.settings.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\dialogs.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\global.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\mainwindow.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\views.loc.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="NLog.config">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Theme\Controls\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -2,5 +2,6 @@
|
||||
xmlns:s="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml"
|
||||
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=components/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=viewmodel_005Clinker_005Csteps/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -1,11 +0,0 @@
|
||||
• Сейчас, при наличии новой версии, отображается стандартное окно обновлений из библиотеки. Планируется сделать кастомный дизайн, для соответствия общему стилю.
|
||||
• В приложении реализованы "совмещённые" подтверждения для торговой площадки. Т.е. при наличии более одного предмета, ожидающего подтверждение, можно либо отменить, либо подтвердить все. В планах добавить расширенный функционал, с возможностью точечного управления.
|
||||
• Добавить кнопку "открыть мафайл" в меню "Файл" по аналогии с открытием папки
|
||||
• Добавить полное шифрование мафайлов по аналогии с SDA
|
||||
• Сделать автоматический билд и загрузку обновления на Github при изменении в мастер ветке
|
||||
• Антик-окно как в MarketApp, возможность сразу открыть браузер напр. на CefSharp с залогиненым аккаунтом
|
||||
• Стабильность авто-подтверждений, возможность задать пользователю порог времени когда льются ошибки и только тогда выключать авто-подтверждение
|
||||
• Безопасное сохранение мафайлов через .tmp / .bak
|
||||
• Создание механизма накопления ошибок, чтобы исправить Patch Tuesday, условно аккаунт может игнорировать ошибки 1-2 часа (настриавемо)
|
||||
|
||||
• Исправить "Перенос гуарда". Иногда не хочет принимать код / подтверждение
|
||||
@@ -1,220 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace NebulaAuth.Theme;
|
||||
|
||||
public class FontScaleWindow : Window
|
||||
{
|
||||
// Using a DependencyProperty as the backing store for ScaleCoefficient. This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty ScaleCoefficientProperty =
|
||||
DependencyProperty.Register("ScaleCoefficient", typeof(double), typeof(FontScaleWindow),
|
||||
new PropertyMetadata(1d));
|
||||
|
||||
|
||||
public static readonly DependencyProperty DefaultFontSizeProperty =
|
||||
DependencyProperty.Register("DefaultFontSize", typeof(double), typeof(FontScaleWindow),
|
||||
new PropertyMetadata(20d));
|
||||
|
||||
|
||||
private static readonly DependencyPropertyKey ParentScaleWindowKey
|
||||
= DependencyProperty.RegisterAttachedReadOnly(
|
||||
"ParentScaleWindow",
|
||||
typeof(FontScaleWindow), typeof(FontScaleWindow),
|
||||
new FrameworkPropertyMetadata(default(FontScaleWindow),
|
||||
FrameworkPropertyMetadataOptions.None));
|
||||
|
||||
public static readonly DependencyProperty ParentScaleWindowProperty
|
||||
= ParentScaleWindowKey.DependencyProperty;
|
||||
|
||||
|
||||
public static readonly DependencyProperty ResizeFontProperty = DependencyProperty.RegisterAttached(
|
||||
"ResizeFont", typeof(bool), typeof(FontScaleWindow), new FrameworkPropertyMetadata(false, ResizeCallBack));
|
||||
//<-------------------Window-------------------->
|
||||
|
||||
|
||||
//<-------------------Scaling-------------------->
|
||||
|
||||
public static readonly DependencyProperty ScaleProperty = DependencyProperty.RegisterAttached(
|
||||
"Scale", typeof(double), typeof(FontScaleWindow),
|
||||
new FrameworkPropertyMetadata(0.9999d, FrameworkPropertyMetadataOptions.Inherits, ScalePropertyCallback));
|
||||
|
||||
|
||||
//<-------------------Window-------------------->
|
||||
public double DefaultFontSize
|
||||
{
|
||||
get => (double) GetValue(DefaultFontSizeProperty);
|
||||
set => SetValue(DefaultFontSizeProperty, value);
|
||||
}
|
||||
|
||||
public double ScaleCoefficient
|
||||
{
|
||||
get => (double) GetValue(ScaleCoefficientProperty);
|
||||
set => SetValue(ScaleCoefficientProperty, value);
|
||||
}
|
||||
|
||||
private readonly HashSet<FrameworkElement> _cachedObjects = new();
|
||||
|
||||
private readonly Func<double, double, double> _diagonal = (h, w) => Math.Sqrt(h * h + w * w);
|
||||
private double _currentDiagonal;
|
||||
private double _defaultDiagonal = 1;
|
||||
private bool _loaded;
|
||||
|
||||
public FontScaleWindow()
|
||||
{
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var w = (FontScaleWindow) sender;
|
||||
w._defaultDiagonal = _diagonal(MinHeight, MinWidth);
|
||||
w.SizeChanged += OnSizeChanged;
|
||||
w.Loaded -= OnLoaded;
|
||||
_loaded = true;
|
||||
w.Width += 1;
|
||||
w.Width -= 1;
|
||||
}
|
||||
|
||||
private static FontScaleWindow? GetParentScaleWindow(DependencyObject element)
|
||||
{
|
||||
return (FontScaleWindow) element.GetValue(ParentScaleWindowProperty);
|
||||
}
|
||||
|
||||
private static void SetParentScaleWindow(DependencyObject element, FontScaleWindow value)
|
||||
{
|
||||
element.SetValue(ParentScaleWindowKey, value);
|
||||
}
|
||||
|
||||
public static void SetResizeFont(DependencyObject element, bool value)
|
||||
{
|
||||
element.SetValue(ResizeFontProperty, value);
|
||||
}
|
||||
|
||||
public static bool GetResizeFont(DependencyObject element)
|
||||
{
|
||||
return (bool) element.GetValue(ResizeFontProperty);
|
||||
}
|
||||
|
||||
private static bool SetWindow(FrameworkElement el)
|
||||
{
|
||||
var w = GetWindow(el);
|
||||
if (w is not FontScaleWindow fsWindow) return false;
|
||||
SetParentScaleWindow(el, fsWindow);
|
||||
if (fsWindow._cachedObjects.Contains(el)) return true;
|
||||
fsWindow._cachedObjects.Add(el);
|
||||
el.Unloaded += ObjOnUnloaded;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ObjOnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var el = (FrameworkElement) sender;
|
||||
if (GetParentScaleWindow(el) == null)
|
||||
{
|
||||
if (!SetWindow(el))
|
||||
{
|
||||
el.Loaded -= ObjOnLoaded;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ResizeCallBack(sender, new DependencyPropertyChangedEventArgs());
|
||||
}
|
||||
|
||||
private static void ResizeCallBack(object? sender, DependencyPropertyChangedEventArgs eventArgs)
|
||||
{
|
||||
if (sender is not FrameworkElement obj) return;
|
||||
var window = GetParentScaleWindow(obj);
|
||||
if (window != null && !window._cachedObjects.Contains(obj))
|
||||
{
|
||||
if (!SetWindow(obj)) return;
|
||||
}
|
||||
else if (window == null)
|
||||
{
|
||||
obj.Loaded += ObjOnLoaded;
|
||||
}
|
||||
|
||||
if (window is not {_loaded: true}) return;
|
||||
CalculateFontSizeResized(obj);
|
||||
}
|
||||
|
||||
private static void ObjOnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var obj = (FrameworkElement) sender;
|
||||
var w = GetParentScaleWindow(obj)!;
|
||||
w._cachedObjects.Remove(obj);
|
||||
obj.Unloaded -= ObjOnUnloaded;
|
||||
obj.Loaded -= ObjOnLoaded;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
_currentDiagonal = _diagonal(e.NewSize.Width, e.NewSize.Height);
|
||||
foreach (var cached in _cachedObjects)
|
||||
{
|
||||
CalculateFontSizeResized(cached);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetScale(DependencyObject element, double value)
|
||||
{
|
||||
element.SetValue(ScaleProperty, value);
|
||||
}
|
||||
|
||||
public static double GetScale(DependencyObject element)
|
||||
{
|
||||
return (double) element.GetValue(ScaleProperty);
|
||||
}
|
||||
|
||||
|
||||
private static void ScalePropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var window = GetParentScaleWindow(d);
|
||||
if (window == null)
|
||||
{
|
||||
var w = GetWindow(d);
|
||||
if (w is FontScaleWindow fsWindow)
|
||||
{
|
||||
SetParentScaleWindow(d, fsWindow);
|
||||
}
|
||||
}
|
||||
|
||||
ResizeElement(d);
|
||||
}
|
||||
|
||||
public static void ResizeElement(DependencyObject d)
|
||||
{
|
||||
var set = d.ReadLocalValue(ScaleProperty) != DependencyProperty.UnsetValue;
|
||||
if (set)
|
||||
{
|
||||
if (GetResizeFont(d))
|
||||
{
|
||||
CalculateFontSizeResized(d);
|
||||
}
|
||||
else
|
||||
{
|
||||
CalculateFontSize(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CalculateFontSize(DependencyObject d)
|
||||
{
|
||||
var window = GetParentScaleWindow(d);
|
||||
if (window == null) return;
|
||||
var fontSize = GetScale(d) * window._defaultDiagonal;
|
||||
d.SetValue(FontSizeProperty, fontSize);
|
||||
}
|
||||
|
||||
public static void CalculateFontSizeResized(DependencyObject d)
|
||||
{
|
||||
var window = GetParentScaleWindow(d);
|
||||
if (window == null) return;
|
||||
var windowsScale = Math.Pow(window._currentDiagonal / window._defaultDiagonal, window.ScaleCoefficient);
|
||||
var elScale = GetScale(d);
|
||||
var fontSize = window.DefaultFontSize * elScale * windowsScale;
|
||||
d.SetValue(FontSizeProperty, fontSize);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
namespace NebulaAuth.Theme.WindowStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Логика взаимодействия для WindowStyle.xaml
|
||||
/// </summary>
|
||||
partial class WindowStyle
|
||||
{
|
||||
public WindowStyle()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
|
||||
@@ -10,24 +10,15 @@ public class ClipboardHelper
|
||||
{
|
||||
public static bool Set(string text)
|
||||
{
|
||||
var i = 0;
|
||||
while (i < 20)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(text);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (i == 19)
|
||||
{
|
||||
Shell.Logger.Error(ex);
|
||||
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
Clipboard.SetText(text);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex);
|
||||
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -35,24 +26,15 @@ public class ClipboardHelper
|
||||
|
||||
public static bool SetFiles(StringCollection files)
|
||||
{
|
||||
var i = 0;
|
||||
while (i < 20)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetFileDropList(files);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (i == 19)
|
||||
{
|
||||
Shell.Logger.Error(ex);
|
||||
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
Clipboard.SetFileDropList(files);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex);
|
||||
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error"));
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NebulaAuth.Core;
|
||||
@@ -6,6 +7,7 @@ using NebulaAuth.Model;
|
||||
using SteamLib.Exceptions;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
using SteamLib.Exceptions.General;
|
||||
using SteamLib.ProtoCore.Enums;
|
||||
using SteamLibForked.Exceptions.Authorization;
|
||||
|
||||
namespace NebulaAuth.Utility;
|
||||
@@ -94,6 +96,18 @@ public static class ExceptionHandler
|
||||
return "LoginException".GetCodeBehindLocalization() + ": " +
|
||||
ErrorTranslatorHelper.TranslateLoginError(e.Error);
|
||||
}
|
||||
case UnsupportedAuthTypeException e:
|
||||
{
|
||||
var requestedType = e.AllowedGuardTypes.First();
|
||||
var msgType = requestedType switch
|
||||
{
|
||||
EAuthSessionGuardType.DeviceCode or EAuthSessionGuardType.DeviceConfirmation => "Guard",
|
||||
EAuthSessionGuardType.EmailCode => "Mail",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
return GetCodeBehindLocalization("UnsupportedAuthTypeException", msgType);
|
||||
}
|
||||
case not null when handleAllExceptions:
|
||||
{
|
||||
return "UnknownException".GetCodeBehindLocalization() + exception.Message;
|
||||
@@ -112,4 +126,11 @@ public static class ExceptionHandler
|
||||
{
|
||||
return LocManager.GetCodeBehindOrDefault(key, EXCEPTION_HANDLER_LOC_PATH, key);
|
||||
}
|
||||
|
||||
private static string GetCodeBehindLocalization(params string[] path)
|
||||
{
|
||||
var def = path.Length == 0 ? "" : path.Last();
|
||||
var newArr = path.Prepend(EXCEPTION_HANDLER_LOC_PATH).ToArray();
|
||||
return LocManager.GetCodeBehindOrDefault(def, newArr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace NebulaAuth.Utility;
|
||||
|
||||
public static class FileNameValidator
|
||||
{
|
||||
public static bool IsValidFileName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return false;
|
||||
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
return !name.Any(c => invalidChars.Contains(c));
|
||||
}
|
||||
|
||||
public static string GetInvalidChars(string name)
|
||||
{
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
return new string(name.Where(c => invalidChars.Contains(c)).Distinct().ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NebulaAuth.Core;
|
||||
|
||||
namespace NebulaAuth.Utility;
|
||||
|
||||
public static class LanguageUtility
|
||||
{
|
||||
public static LocalizationLanguage DetectPreferredLanguage()
|
||||
{
|
||||
var userCulture = CultureInfo.CurrentUICulture;
|
||||
var userLang = userCulture.TwoLetterISOLanguageName;
|
||||
var userRegion = new RegionInfo(userCulture.Name).TwoLetterISORegionName;
|
||||
|
||||
|
||||
switch (userLang)
|
||||
{
|
||||
case "ru": return LocalizationLanguage.Russian;
|
||||
case "uk": return LocalizationLanguage.Ukrainian;
|
||||
case "en": return LocalizationLanguage.English;
|
||||
case "zh": return LocalizationLanguage.ChineseSimplified;
|
||||
case "fr": return LocalizationLanguage.French;
|
||||
}
|
||||
|
||||
if (userRegion.EndsWith("UA", StringComparison.OrdinalIgnoreCase))
|
||||
return LocalizationLanguage.Ukrainian;
|
||||
|
||||
string[] cisRegions =
|
||||
[
|
||||
"RU", "BY", "KZ", "KG", "TJ", "TM", "UZ", "AM", "AZ", "GE", "MD"
|
||||
];
|
||||
|
||||
return cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase))
|
||||
? LocalizationLanguage.Russian
|
||||
: LocalizationLanguage.English;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||
xmlns:converters="clr-namespace:NebulaAuth.Converters"
|
||||
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
|
||||
xmlns:vm="clr-namespace:NebulaAuth.ViewModel">
|
||||
|
||||
|
||||
<DataTemplate DataType="{x:Type confirmations:Confirmation}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -110,7 +108,7 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="ShoppingCart"
|
||||
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
|
||||
Margin="0,0,10,0" />
|
||||
<Border Grid.Column="1" Background="{DynamicResource MaterialDesignPaper}" MaxHeight="36"
|
||||
HorizontalAlignment="Center" BorderBrush="{DynamicResource PrimaryHueMidBrush}"
|
||||
@@ -119,7 +117,7 @@
|
||||
Source="{Binding ItemImageUri}" />
|
||||
</Border>
|
||||
<Grid Grid.Column="2">
|
||||
<TextBlock VerticalAlignment="Center" x:Name="ItemName" theme:FontScaleWindow.ResizeFont="True"
|
||||
<TextBlock VerticalAlignment="Center" x:Name="ItemName"
|
||||
TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
|
||||
|
||||
<TextBlock Foreground="LightGray" Text="{Binding PriceString}">
|
||||
@@ -176,36 +174,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="ShoppingCartPlus"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="1">
|
||||
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
|
||||
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
|
||||
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
|
||||
</TextBlock>
|
||||
<Expander Padding="0" Background="Transparent" materialDesign:ExpanderAssist.ExpanderButtonPosition="Start"
|
||||
materialDesign:ExpanderAssist.HorizontalHeaderPadding="0" HorizontalAlignment="Stretch">
|
||||
<Expander.Header>
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="1">
|
||||
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
|
||||
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
|
||||
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left" Margin="5,0,0,0"
|
||||
Text="{Binding Time, StringFormat=t}" />
|
||||
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="Check" />
|
||||
</Button>
|
||||
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Center" Margin="5,0,0,0"
|
||||
Text="{Binding Time, StringFormat=t}" />
|
||||
|
||||
<!-- Confirm all -->
|
||||
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="4"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmAll}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="CheckAll" />
|
||||
</Button>
|
||||
<!-- Cancel all -->
|
||||
<Button Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelAll}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Expander.Header>
|
||||
|
||||
<Border Margin="30,6,0,0" Background="{DynamicResource MaterialDesignPaper}" CornerRadius="4">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
MaxHeight="320">
|
||||
<ItemsControl ItemsSource="{Binding Confirmations}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type confirmations:MarketConfirmation}">
|
||||
<Grid Margin="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="32"
|
||||
MaxHeight="32" Margin="0,0,10,0"
|
||||
Source="{Binding ItemImageUri}" />
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left"
|
||||
Margin="8,0,0,0"
|
||||
Text="{Binding Time, StringFormat=t}" />
|
||||
|
||||
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmOne}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
|
||||
CommandParameter="{Binding}">
|
||||
|
||||
<materialDesign:PackIcon Kind="Check" />
|
||||
</Button>
|
||||
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelOne}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
|
||||
CommandParameter="{Binding}">
|
||||
<materialDesign:PackIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Expander>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type confirmations:PurchaseConfirmation}">
|
||||
<Grid>
|
||||
|
||||
@@ -4,42 +4,69 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
|
||||
mc:Ignorable="d"
|
||||
Height="Auto" Width="Auto" MaxWidth="500"
|
||||
Background="{DynamicResource WindowBackground}">
|
||||
<Grid Margin="15">
|
||||
<Grid MinHeight="140" MinWidth="350">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock theme:FontScaleWindow.Scale="0.8"
|
||||
theme:FontScaleWindow.ResizeFont="True"
|
||||
HorizontalAlignment="Center" Margin="10,10,10,15"
|
||||
Text="Подтвердите действие"
|
||||
x:Name="ConfirmTextBlock"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<Grid Grid.Row="1" Margin="10,0,10,5">
|
||||
<Grid Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button DockPanel.Dock="Left"
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!--<materialDesign:PackIcon Kind="WarningCircleOutline" Width="20" Height="20" Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />-->
|
||||
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" FontSize="18" Text="{Tr ConfirmCancelDialog.Title}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
|
||||
|
||||
</Button>
|
||||
</Grid>
|
||||
<Separator Grid.Row="1" Opacity="0.7" />
|
||||
<Grid Grid.Row="2" Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<nebulaAuth:HintBox FontSize="14" x:Name="ConfirmHint" Text="Confirm?" Margin="10" />
|
||||
<Grid Grid.Row="1" Margin="10,0,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Width="130"
|
||||
HorizontalAlignment="Left"
|
||||
|
||||
Margin="2,0,4,0"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource True}"
|
||||
FontSize="{Binding ElementName=ConfirmTextBlock, Path=FontSize,
|
||||
Converter={StaticResource CoefficientConverter},
|
||||
ConverterParameter=0.85}">
|
||||
ОК
|
||||
</Button>
|
||||
<Button Grid.Column="1" DockPanel.Dock="Right"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource False}"
|
||||
FontSize="{Binding ElementName=ConfirmTextBlock, Path=FontSize,
|
||||
Converter={StaticResource CoefficientConverter},
|
||||
ConverterParameter=0.85}">
|
||||
Отмена
|
||||
</Button>
|
||||
Content="{Tr Common.OK}"
|
||||
IsDefault="True" />
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Width="130"
|
||||
Margin="0,0,2,0"
|
||||
IsCancel="True"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource False}"
|
||||
Content="{Tr Common.Cancel}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,8 +1,5 @@
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Логика взаимодействия для ConfirmCancelDialog.xaml
|
||||
/// </summary>
|
||||
public partial class ConfirmCancelDialog
|
||||
{
|
||||
public ConfirmCancelDialog()
|
||||
@@ -13,6 +10,6 @@ public partial class ConfirmCancelDialog
|
||||
public ConfirmCancelDialog(string msg)
|
||||
{
|
||||
InitializeComponent();
|
||||
ConfirmTextBlock.Text = msg;
|
||||
ConfirmHint.Text = msg;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
<TextBox TabIndex="0" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
|
||||
materialDesign:TextFieldAssist.HasClearButton="True"
|
||||
Padding="10"
|
||||
@@ -58,7 +58,7 @@
|
||||
materialDesign:HintAssist.IsFloating="False"
|
||||
materialDesign:TextFieldAssist.LeadingIcon="Key"
|
||||
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
|
||||
<CheckBox FontSize="15" Grid.Row="1" Margin="10,10,10,0"
|
||||
<CheckBox FontSize="15" Grid.Row="1" Margin="10,5,10,0"
|
||||
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
|
||||
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
|
||||
<Grid Grid.Row="2" Margin="10,10,10,0">
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Логика взаимодействия для LoginAgainDialog.xaml
|
||||
/// </summary>
|
||||
public partial class LoginAgainDialog
|
||||
{
|
||||
public LoginAgainDialog()
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
<TextBox TabIndex="0" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
|
||||
materialDesign:TextFieldAssist.HasClearButton="True"
|
||||
Padding="10"
|
||||
@@ -74,10 +74,10 @@
|
||||
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
|
||||
</UIElement.InputBindings>
|
||||
</ComboBox>
|
||||
<CheckBox FontSize="15" Grid.Row="2" Margin="10,10,10,0"
|
||||
<CheckBox FontSize="15" Grid.Row="2" Margin="10,5,10,0"
|
||||
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
|
||||
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
|
||||
<CheckBox FontSize="15" Grid.Row="3" Margin="10,10,10,0" IsEnabled="{Binding MafileHasProxy}"
|
||||
<CheckBox FontSize="15" Grid.Row="3" Margin="10,5,10,0" IsEnabled="{Binding MafileHasProxy}"
|
||||
Content="{Tr LoginAgainDialog.UseMafileProxy}" IsChecked="{Binding UseMafileProxy}" />
|
||||
<Grid Grid.Row="4" Margin="10,10,10,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Логика взаимодействия для LoginAgainDialog.xaml
|
||||
/// </summary>
|
||||
public partial class LoginAgainOnImportDialog
|
||||
{
|
||||
public LoginAgainOnImportDialog()
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<UserControl x:Class="NebulaAuth.View.Dialogs.SdaPasswordDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||
mc:Ignorable="d"
|
||||
TextElement.Foreground="{DynamicResource BaseContentBrush}"
|
||||
d:DataContext="{d:DesignInstance other:SdaPasswordDialogVM}"
|
||||
Background="{DynamicResource WindowBackground}">
|
||||
|
||||
<Grid MinHeight="200" MinWidth="400" MaxWidth="500">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Lock" Width="20" Height="20" Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />
|
||||
<TextBlock FontSize="16" VerticalAlignment="Center" Foreground="{DynamicResource BaseContentBrush}"
|
||||
Margin="7,0,0,0" HorizontalAlignment="Left"
|
||||
Text="{Tr SdaPasswordDialog.Title}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Separator Grid.Row="1" Opacity="0.7" />
|
||||
<Grid Grid.Row="2" Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="{Tr SdaPasswordDialog.Message}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource BaseContentBrush}" Margin="10,10,10,8" />
|
||||
<TextBox Grid.Row="1" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
materialDesign:TextFieldAssist.HasClearButton="True"
|
||||
Padding="10" FontSize="15" Margin="10,0,10,0"
|
||||
Style="{StaticResource MaterialDesignOutlinedTextBox}"
|
||||
materialDesign:HintAssist.Hint="{Tr SdaPasswordDialog.PasswordHint}"
|
||||
materialDesign:HintAssist.IsFloating="False"
|
||||
materialDesign:TextFieldAssist.LeadingIcon="Key"
|
||||
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
|
||||
<Grid Grid.Row="2" Margin="10,16,10,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button IsEnabled="{Binding IsFormValid}" IsDefault="True" Margin="0,5,5,5"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource True}" Content="{Tr Common.OK}" />
|
||||
<Button IsCancel="True" Grid.Column="1" Margin="5,5,0,5"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource False}" Content="{Tr Common.Cancel}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
public partial class SdaPasswordDialog
|
||||
{
|
||||
public SdaPasswordDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||
xmlns:theme="clr-namespace:NebulaAuth.Theme"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance other:LoginAgainVM}">
|
||||
@@ -29,7 +28,7 @@
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />
|
||||
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" FontSize="18" Text="{Tr SetEncryptedPasswordDialog.Title}" />
|
||||
VerticalAlignment="Center" FontSize="18" Text="{Tr SetEncryptionPasswordDialog.Title}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
|
||||
@@ -40,12 +39,13 @@
|
||||
</Grid>
|
||||
<Separator Grid.Row="1" Opacity="0.7" />
|
||||
<TextBlock Grid.Row="2" IsHitTestVisible="False" TextWrapping="Wrap" FontWeight="Normal"
|
||||
Text="{Tr SetEncryptedPasswordDialog.DialogText}" theme:FontScaleWindow.Scale="0.8"
|
||||
theme:FontScaleWindow.ResizeFont="True" Margin="10" HorizontalAlignment="Left" />
|
||||
<PasswordBox FontSize="16"
|
||||
FontSize="16"
|
||||
Text="{Tr SetEncryptionPasswordDialog.DialogText}"
|
||||
Margin="10" HorizontalAlignment="Left" />
|
||||
<PasswordBox TabIndex="0" FontSize="16" x:Name="PasswordBox"
|
||||
materialDesign:PasswordBoxAssist.Password="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="10" Grid.Row="3" Style="{StaticResource MaterialDesignFloatingHintRevealPasswordBox}"
|
||||
materialDesign:HintAssist.Hint="{Tr SetEncryptedPasswordDialog.Password}" />
|
||||
materialDesign:HintAssist.Hint="{Tr SetEncryptionPasswordDialog.Password}" />
|
||||
<Grid Grid.Row="4" Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
@@ -53,11 +53,11 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button IsDefault="True" Margin="10,5,5,5" Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource True}" Content="{Tr SetEncryptedPasswordDialog.Ok}" />
|
||||
CommandParameter="{StaticResource True}" Content="{Tr SetEncryptionPasswordDialog.Ok}" />
|
||||
<Button IsCancel="True" Grid.Column="1" Margin="5,5,10,5"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource False}" Content="{Tr SetEncryptedPasswordDialog.Cancel}" />
|
||||
CommandParameter="{StaticResource False}" Content="{Tr SetEncryptionPasswordDialog.Cancel}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,8 +1,5 @@
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Логика взаимодействия для SetCryptPasswordDialog.xaml
|
||||
/// </summary>
|
||||
public partial class SetCryptPasswordDialog
|
||||
{
|
||||
public SetCryptPasswordDialog()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<UserControl x:Class="NebulaAuth.View.Dialogs.TextFieldDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
Height="Auto" Width="Auto" MaxWidth="500"
|
||||
Background="{DynamicResource WindowBackground}">
|
||||
<Grid MinHeight="140" MinWidth="350">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!--<materialDesign:PackIcon Kind="WarningCircleOutline" Width="20" Height="20" Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />-->
|
||||
<TextBlock x:Name="TitleTextBlock" FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" FontSize="18" Text="{Tr TextFieldDialog.Title}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right" CommandParameter="{x:Null}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
|
||||
|
||||
</Button>
|
||||
</Grid>
|
||||
<Separator Grid.Row="1" Opacity="0.7" />
|
||||
<Grid Grid.Row="2" Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox x:Name="TextField" Margin="10" materialDesign:HintAssist.Hint="" />
|
||||
<Grid Grid.Row="1" Margin="10,0,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Width="130"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="2,0,4,0"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{Binding Text, ElementName=TextField}"
|
||||
Content="{Tr Common.OK}"
|
||||
IsDefault="True" />
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Width="130"
|
||||
Margin="0,0,2,0"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
IsCancel="True"
|
||||
CommandParameter="{x:Null}"
|
||||
Content="{Tr Common.Cancel}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,22 @@
|
||||
using MaterialDesignThemes.Wpf;
|
||||
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
public partial class TextFieldDialog
|
||||
{
|
||||
public TextFieldDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public TextFieldDialog(string? title, string? msg)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
TitleTextBlock.Text = title;
|
||||
|
||||
if (!string.IsNullOrEmpty(msg))
|
||||
HintAssist.SetHint(TextField, msg);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user