mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-26 14:51:42 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ac4ffdc34 | |||
| 17635ccba0 | |||
| 69589075e5 | |||
| 4b547e19c4 | |||
| 053faec343 | |||
| b8ac76d2a9 | |||
| b81d45765a | |||
| 372b8c6463 | |||
| 5de26381bb | |||
| 16fd4992f8 | |||
| 7835a53717 | |||
| f15632f2d0 | |||
| b56c0e578d | |||
| 969590d9f2 | |||
| 3e87a0d50e | |||
| 690c98527f | |||
| 5242b0009b | |||
| 1f14bd77e7 | |||
| f2b1cdfcc9 |
@@ -1,78 +0,0 @@
|
||||
name: Publish release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version
|
||||
id: ver
|
||||
run: echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Build NebulaAuth
|
||||
run: dotnet publish src/NebulaAuth/NebulaAuth.csproj -c Release -o build
|
||||
|
||||
- name: Package release
|
||||
run: |
|
||||
cd build
|
||||
powershell Compress-Archive * "../NebulaAuth.${env:GITHUB_REF_NAME}.zip"
|
||||
shell: pwsh
|
||||
|
||||
- name: Install pandoc
|
||||
run: |
|
||||
choco install pandoc -y
|
||||
|
||||
- name: Convert changelog HTML to Markdown (ul-only)
|
||||
id: changelog
|
||||
shell: bash
|
||||
run: |
|
||||
version="${{ steps.ver.outputs.version }}"
|
||||
file="changelog/${version}.html"
|
||||
echo "Looking for file: $file"
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
echo "✅ File found, extracting <ul> section"
|
||||
sed -n '/<ul>/,/<\/ul>/p' "$file" > body.html
|
||||
|
||||
echo "Converting with pandoc..."
|
||||
pandoc body.html -f html -t markdown --wrap=none -o changelog.md
|
||||
|
||||
echo "-------- Converted changelog.md --------"
|
||||
cat changelog.md
|
||||
echo "----------------------------------------"
|
||||
|
||||
echo "body<<EOF" >> $GITHUB_OUTPUT
|
||||
cat changelog.md >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ Changelog not found for version $version"
|
||||
echo "body=No changelog found for $version" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.ver.outputs.version }}
|
||||
name: Release ${{ env.RELEASE_NAME }}
|
||||
body: ${{ steps.changelog.outputs.body }}
|
||||
files: |
|
||||
NebulaAuth.${{ steps.ver.outputs.version }}.zip
|
||||
env:
|
||||
RELEASE_NAME: ${{ steps.ver.outputs.version }}
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Prepare release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'src/NebulaAuth/NebulaAuth.csproj'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: Read version from csproj
|
||||
id: ver
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(grep -oPm1 "(?<=<AssemblyVersion>)[^<]+" src/NebulaAuth/NebulaAuth.csproj)
|
||||
echo "Detected version $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate update.xml
|
||||
run: |
|
||||
cat > NebulaAuth/update.xml <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>${{ steps.ver.outputs.version }}</version>
|
||||
<url>https://github.com/${{ github.repository }}/releases/download/${{ steps.ver.outputs.version }}/NebulaAuth.${{ steps.ver.outputs.version }}.zip</url>
|
||||
<changelog>https://achiez.github.io/${{ github.repository }}/changelog/${{ steps.ver.outputs.version }}.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
||||
EOF
|
||||
echo "Generated NebulaAuth/update.xml:"
|
||||
cat NebulaAuth/update.xml
|
||||
|
||||
- name: Check if update.xml changed
|
||||
id: diff
|
||||
run: |
|
||||
if git diff --exit-code NebulaAuth/update.xml >/dev/null 2>&1; then
|
||||
echo "no_changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "no_changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit and tag
|
||||
if: steps.diff.outputs.no_changes == 'false'
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "actions@github.com"
|
||||
git add NebulaAuth/update.xml
|
||||
git commit -m "chore(release): ${{ steps.ver.outputs.version }}"
|
||||
git tag -a "${{ steps.ver.outputs.version }}" -m "Release ${{ steps.ver.outputs.version }}"
|
||||
git push origin HEAD:master --tags
|
||||
|
||||
- name: Skip message
|
||||
if: steps.diff.outputs.no_changes == 'true'
|
||||
run: echo "No changes in version or update.xml — skipping release preparation."
|
||||
@@ -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
|
||||
+4
-8
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.4.33205.214
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.3.11520.95 d18.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{161BFADE-FCF3-45D1-82EA-8A1B187529F7}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
@@ -24,12 +24,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{
|
||||
changelog\1.7.3.html = changelog\1.7.3.html
|
||||
changelog\1.7.4.html = changelog\1.7.4.html
|
||||
changelog\1.8.0.html = changelog\1.8.0.html
|
||||
changelog\1.8.1.html = changelog\1.8.1.html
|
||||
changelog\1.8.2.html = changelog\1.8.2.html
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamLibForked", "src\SteamLibForked\SteamLibForked.csproj", "{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaAuth.LegacyConverter", "src\NebulaAuth.LegacyConverter\NebulaAuth.LegacyConverter.csproj", "{C8235305-E5C4-155B-C718-C0F239CA3AB7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaAuth", "src\NebulaAuth\NebulaAuth.csproj", "{C42F63B6-32F7-ED08-5B86-CD51989761AD}"
|
||||
EndProject
|
||||
Global
|
||||
@@ -42,10 +42,6 @@ Global
|
||||
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>1.8.0</version>
|
||||
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.0/NebulaAuth.1.8.0.zip</url>
|
||||
<changelog>https://achiez.github.io/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.0.html</changelog>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SteamLibForked\SteamLibForked.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,176 +0,0 @@
|
||||
using System.Reflection;
|
||||
using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.LegacyConverter;
|
||||
using Newtonsoft.Json;
|
||||
using SteamLib.Utility.MafileSerialization;
|
||||
|
||||
try
|
||||
{
|
||||
var mafileSerializer = new MafileSerializer(new MafileSerializerSettings
|
||||
{
|
||||
DeserializationOptions =
|
||||
{
|
||||
AllowDeviceIdGeneration = true,
|
||||
AllowSessionIdGeneration = true
|
||||
}
|
||||
});
|
||||
var currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
if (currentPath != null)
|
||||
Environment.CurrentDirectory = currentPath;
|
||||
const string toStoreFolder = "ConvertedMafiles";
|
||||
|
||||
if (Directory.Exists(toStoreFolder) == false)
|
||||
{
|
||||
Directory.CreateDirectory(toStoreFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
var isEmpty = Directory.GetFiles(toStoreFolder).Length == 0;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
if (!isEmpty)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"WARNING! 'ConverterdMafiles' folder is not empty. Please backup data from it and then continue.");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine("Press Y to continue");
|
||||
while (Console.ReadKey(true).Key != ConsoleKey.Y)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decryptMode = false;
|
||||
while (true)
|
||||
{
|
||||
Console.WriteLine("Press 'D' to select decrypt mode. Press 'C' to convert mode. Press ESC to exit");
|
||||
var key = Console.ReadKey(true);
|
||||
switch (key.Key)
|
||||
{
|
||||
case ConsoleKey.D:
|
||||
decryptMode = true;
|
||||
break;
|
||||
case ConsoleKey.C:
|
||||
break;
|
||||
case ConsoleKey.Escape:
|
||||
return;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Manifest? manifest = null;
|
||||
string? password = null;
|
||||
if (decryptMode)
|
||||
{
|
||||
var files = Directory.GetFiles(currentPath, "manifest.json");
|
||||
var manifestPath = files.FirstOrDefault();
|
||||
if (manifestPath == null)
|
||||
{
|
||||
Console.WriteLine("No manifest.json found in current directory");
|
||||
return;
|
||||
}
|
||||
|
||||
var manifestText = File.ReadAllText(manifestPath);
|
||||
try
|
||||
{
|
||||
manifest = JsonConvert.DeserializeObject<Manifest>(manifestText)!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Can't read manifest: " + ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (manifest.Encrypted == false)
|
||||
{
|
||||
Console.WriteLine("Manifest is not encrypted");
|
||||
return;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.WriteLine("Please enter your encryption password: ");
|
||||
password = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
continue;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Console.WriteLine(currentPath);
|
||||
|
||||
|
||||
foreach (var path in args)
|
||||
{
|
||||
if (Path.Exists(path) == false)
|
||||
{
|
||||
Console.WriteLine($"NOT VALID PATH: '{path}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine("Reading: " + path);
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(path);
|
||||
if (decryptMode)
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
text = DecryptMafile(fileName, text);
|
||||
if (text == null)
|
||||
{
|
||||
Console.WriteLine(path + " not found in manifest. Skipped");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var data = mafileSerializer.Deserialize(text);
|
||||
var maf = data.Data;
|
||||
var legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented);
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
|
||||
Write(legacy, fileNameWithoutExtension);
|
||||
|
||||
Console.WriteLine("DONE: " + fileNameWithoutExtension);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"ERROR: {ex.Message}");
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.WriteLine("-----------------------------------------");
|
||||
}
|
||||
}
|
||||
|
||||
//Local Functions
|
||||
|
||||
void Write(string maf, string name)
|
||||
{
|
||||
var path = Path.Combine(toStoreFolder, name + "_legacy.mafile");
|
||||
File.WriteAllText(path, maf);
|
||||
}
|
||||
|
||||
string? DecryptMafile(string fileName, string cipherText)
|
||||
{
|
||||
if (password == null) return null;
|
||||
var entry = manifest?.Entries.FirstOrDefault(x => x.Filename.EqualsIgnoreCase(fileName));
|
||||
if (entry == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var iv = entry.EncryptionIv;
|
||||
var salt = entry.EncryptionSalt;
|
||||
return SDAEncryptor.DecryptData(password, salt, iv, cipherText);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.WriteLine("Press any key to exit...");
|
||||
Console.ReadKey();
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using System.Windows;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using NebulaAuth.Model.MAAC;
|
||||
|
||||
namespace NebulaAuth;
|
||||
|
||||
@@ -16,12 +15,8 @@ public partial class App
|
||||
var splashScreen = new SplashScreen("Theme\\SplashScreen.png");
|
||||
splashScreen.Show(false, true);
|
||||
base.OnStartup(e);
|
||||
LocManager.Init();
|
||||
LocManager.SetApplicationLocalization(Settings.Instance.Language);
|
||||
Shell.Initialize();
|
||||
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
|
||||
await Storage.Initialize(threads);
|
||||
MAACStorage.Initialize();
|
||||
|
||||
await Shell.Initialize();
|
||||
var mainWindow = new MainWindow();
|
||||
Current.MainWindow = mainWindow;
|
||||
mainWindow.Show();
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter" />
|
||||
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
|
||||
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
|
||||
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
|
||||
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
|
||||
<converters:BoolToValueConverter x:Key="BoolToValueConverter" />
|
||||
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
|
||||
@@ -23,6 +22,7 @@
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
|
||||
|
||||
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
|
||||
|
||||
<system:Boolean x:Key="True">True</system:Boolean>
|
||||
<system:Boolean x:Key="False">False</system:Boolean>
|
||||
|
||||
+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,8 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using AutoUpdaterDotNET;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Microsoft.Win32;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Update;
|
||||
using NebulaAuth.View;
|
||||
using NebulaAuth.View.Dialogs;
|
||||
using NebulaAuth.ViewModel.Linker;
|
||||
@@ -51,6 +54,25 @@ public static class DialogsController
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the SDA encryption password dialog for unlocking SDA-encrypted mafiles.
|
||||
/// </summary>
|
||||
/// <returns>The entered password if user clicked OK, otherwise null.</returns>
|
||||
public static async Task<string?> ShowSdaPasswordDialog()
|
||||
{
|
||||
var vm = new SdaPasswordDialogVM();
|
||||
var content = new SdaPasswordDialog
|
||||
{
|
||||
DataContext = vm
|
||||
};
|
||||
var result = await DialogHost.Show(content);
|
||||
if (result is true)
|
||||
{
|
||||
return vm.Password;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
public static async Task<bool> ShowProxyManager()
|
||||
{
|
||||
var vm = new ProxyManagerVM();
|
||||
@@ -113,6 +135,27 @@ public static class DialogsController
|
||||
await DialogHost.Show(dialog);
|
||||
}
|
||||
|
||||
public static async Task ShowExportDialog()
|
||||
{
|
||||
var vm = new MafileExporterVM();
|
||||
var dialog = new MafileExporterView
|
||||
{
|
||||
DataContext = vm
|
||||
};
|
||||
await DialogHost.Show(dialog);
|
||||
}
|
||||
|
||||
public static async Task ShowUpdateDialog(UpdateInfoEventArgs args, ChangelogEntry? changelog,
|
||||
string? htmlFallbackUrl)
|
||||
{
|
||||
var vm = new UpdateDialogVM(args, changelog, htmlFallbackUrl);
|
||||
var dialog = new UpdateDialog
|
||||
{
|
||||
DataContext = vm
|
||||
};
|
||||
await DialogHost.Show(dialog);
|
||||
}
|
||||
|
||||
#region CommonDialogs
|
||||
|
||||
public static async Task<bool> ShowConfirmCancelDialog(string? msg = null)
|
||||
@@ -120,7 +163,7 @@ public static class DialogsController
|
||||
var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
|
||||
|
||||
var result = await DialogHost.Show(content);
|
||||
return result != null && (bool) result;
|
||||
return result != null && (bool)result;
|
||||
}
|
||||
|
||||
public static async Task<string?> ShowTextFieldDialog(string? title = null, string? msg = null)
|
||||
|
||||
@@ -31,6 +31,8 @@ public static class LocManager
|
||||
LocalizationLanguage.English => "en",
|
||||
LocalizationLanguage.Russian => "ru",
|
||||
LocalizationLanguage.Ukrainian => "ua",
|
||||
LocalizationLanguage.ChineseSimplified => "zh",
|
||||
LocalizationLanguage.French => "fr",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
|
||||
};
|
||||
}
|
||||
@@ -107,5 +109,7 @@ public enum LocalizationLanguage
|
||||
{
|
||||
English,
|
||||
Russian,
|
||||
Ukrainian
|
||||
Ukrainian,
|
||||
ChineseSimplified,
|
||||
French
|
||||
}
|
||||
@@ -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,16 +1,36 @@
|
||||
using System;
|
||||
using AutoUpdaterDotNET;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Update;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using AutoUpdaterDotNET;
|
||||
using System.Net.Http;
|
||||
using System.Windows;
|
||||
|
||||
namespace NebulaAuth.Core;
|
||||
|
||||
public static class UpdateManager
|
||||
{
|
||||
private const string UPDATE_URL =
|
||||
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/NebulaAuth/update.xml";
|
||||
private const string BASE_URL =
|
||||
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/";
|
||||
private const string UPDATE_URL = BASE_URL + "NebulaAuth/update.xml";
|
||||
|
||||
public static void CheckForUpdates()
|
||||
private const string CHANGELOG_BASE_URL = BASE_URL + "changelog/";
|
||||
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
private static bool _isManualCheck;
|
||||
|
||||
public static bool HasPendingUpdate { get; private set; }
|
||||
public static event Action? PendingUpdateDetected;
|
||||
|
||||
static UpdateManager()
|
||||
{
|
||||
AutoUpdater.CheckForUpdateEvent += HandleCheckForUpdateEvent;
|
||||
}
|
||||
|
||||
public static void CheckForUpdates(bool manual = false)
|
||||
{
|
||||
_isManualCheck = manual;
|
||||
var jsonPath = Path.Combine(Environment.CurrentDirectory, "update-settings.json");
|
||||
AutoUpdater.PersistenceProvider = new JsonFilePersistenceProvider(jsonPath);
|
||||
AutoUpdater.ShowSkipButton = false;
|
||||
@@ -18,6 +38,88 @@ public static class UpdateManager
|
||||
AutoUpdater.Start(UPDATE_URL);
|
||||
}
|
||||
|
||||
public static void SkipVersion(string version)
|
||||
{
|
||||
var settings = UpdateSettings.Load();
|
||||
settings.SkipVersion(version);
|
||||
}
|
||||
|
||||
public static void SetRemindAfter(TimeSpan delay)
|
||||
{
|
||||
var settings = UpdateSettings.Load();
|
||||
settings.SetRemindAfter(DateTime.Now + delay);
|
||||
}
|
||||
|
||||
private static async void HandleCheckForUpdateEvent(UpdateInfoEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isManual = _isManualCheck;
|
||||
_isManualCheck = false;
|
||||
|
||||
if (args.Error != null)
|
||||
{
|
||||
if (isManual)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Request error", "RequestError")));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.IsUpdateAvailable)
|
||||
{
|
||||
if (isManual)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
SnackbarController.SendSnackbar(
|
||||
LocManager.GetCodeBehindOrDefault("You are using the latest version", "UpdateService",
|
||||
"LatestVersion")));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var version = args.CurrentVersion.ToString();
|
||||
var settings = UpdateSettings.Load();
|
||||
|
||||
if (!isManual && !settings.ShouldShow(version))
|
||||
{
|
||||
HasPendingUpdate = true;
|
||||
Application.Current.Dispatcher.Invoke(() => PendingUpdateDetected?.Invoke());
|
||||
return;
|
||||
}
|
||||
|
||||
ChangelogEntry? changelog = null;
|
||||
try
|
||||
{
|
||||
var jsonUrl = $"{CHANGELOG_BASE_URL}{version}.json";
|
||||
var response = await HttpClient.GetAsync(jsonUrl).ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
changelog = JsonConvert.DeserializeObject<ChangelogEntry>(json);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fallback to HTML changelog URL
|
||||
}
|
||||
|
||||
var htmlFallbackUrl = changelog == null ? args.ChangelogURL : null;
|
||||
|
||||
await Application.Current.Dispatcher.BeginInvoke(async () =>
|
||||
{
|
||||
await DialogsController.ShowUpdateDialog(args, changelog, htmlFallbackUrl);
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Shell.Logger.Error(e, "Error while checking updates");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RequiresAdminAccess()
|
||||
{
|
||||
try
|
||||
@@ -35,57 +137,4 @@ public static class UpdateManager
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//static UpdateManager()
|
||||
//{
|
||||
// //AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
|
||||
|
||||
//}
|
||||
|
||||
//private static async void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
|
||||
//{
|
||||
// if (args.Error == null)
|
||||
// {
|
||||
// if (args.IsUpdateAvailable)
|
||||
// {
|
||||
// DialogResult dialogResult;
|
||||
// var dialog = new UpdaterView()
|
||||
// {
|
||||
// DataContext = new UpdaterVM(args)
|
||||
// };
|
||||
|
||||
// await DialogHost.Show(dialog);
|
||||
// Application.Current.Shutdown();
|
||||
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (args.Error is WebException)
|
||||
// {
|
||||
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
//}
|
||||
|
||||
//private static void RunUpdate(UpdateInfoEventArgs args)
|
||||
//{
|
||||
// Application.Current.Dispatcher.Invoke(() =>
|
||||
// {
|
||||
// if (AutoUpdater.DownloadUpdate(args))
|
||||
// {
|
||||
// Application.Current.Shutdown();
|
||||
// }
|
||||
// }, DispatcherPriority.ContextIdle);
|
||||
//}
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
Kind="FileReplaceOutline" />
|
||||
|
||||
</StackPanel>
|
||||
<ToolBarTray>
|
||||
<ToolBarTray Grid.Row="0">
|
||||
<ToolBar ClipToBounds="True" HorizontalContentAlignment="Stretch"
|
||||
Style="{StaticResource MaterialDesignToolBar}" FontSize="16">
|
||||
<Menu FontSize="16">
|
||||
@@ -92,7 +92,9 @@
|
||||
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.Title}">
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.SetPasswords}"
|
||||
Command="{Binding DebugCommand}" />
|
||||
Command="{Binding OpenSetPasswordsDialogCommand}" />
|
||||
<MenuItem Header="{Tr MainWindow.Menu.File.Other.OpenExport}"
|
||||
Command="{Binding OpenExporterDialogCommand}" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
@@ -292,18 +294,42 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock HorizontalAlignment="Stretch" Text="{Binding AccountName}">
|
||||
<TextBlock.Resources>
|
||||
<SolidColorBrush x:Key="BaseContentBrushProxy"
|
||||
Color="{DynamicResource BaseContentColor}" />
|
||||
</TextBlock.Resources>
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding AccountName}"
|
||||
ToolTip="{Binding LinkedClient.Status.Message, FallbackValue={x:Null}}">
|
||||
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource BaseContentBrush}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Ok">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource SuccessBrush}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Warning">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource WarningBrush}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Error">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource ErrorBrush}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
|
||||
<TextBlock.Foreground>
|
||||
<Binding Path="LinkedClient.IsError"
|
||||
Converter="{StaticResource PortableMaClientStatusToColorConverter}"
|
||||
FallbackValue="{StaticResource BaseContentBrushProxy}" />
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel
|
||||
Visibility="{Binding LinkedClient, Converter={StaticResource NullableToVisibilityConverter}}"
|
||||
Grid.Column="1" Orientation="Horizontal">
|
||||
@@ -356,7 +382,7 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<md:PackIcon Kind="PlusCircle"
|
||||
VerticalAlignment="Center" />
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Margin="5,0,0,0" Grid.Column="1"
|
||||
Text="{Tr MainWindow.ContextMenus.Mafile.CreateGroup}" />
|
||||
@@ -391,7 +417,9 @@
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}"
|
||||
Command="{Binding Path=RemoveGroupCommand}"
|
||||
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
|
||||
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
|
||||
Command="{Binding RemoveProxyCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
</ContextMenu>
|
||||
</FrameworkElement.ContextMenu>
|
||||
|
||||
@@ -491,16 +519,32 @@
|
||||
Text="{Binding SelectedMafile.AccountName, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
|
||||
<!--<Button Command="{Binding DebugCommand}">Debug</Button>-->
|
||||
</ToolBarPanel>
|
||||
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
|
||||
Margin="0,0,10,0">
|
||||
<Hyperlink
|
||||
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<md:PackIcon x:Name="UpdateBadgeIcon"
|
||||
Kind="BellAlert" Width="15" Height="15"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,5,0"
|
||||
Visibility="Collapsed"
|
||||
ToolTip="{Tr CodeBehind.UpdateService.UpdateIndicatorHint}">
|
||||
<md:PackIcon.Foreground>
|
||||
<SolidColorBrush x:Name="UpdateBadgeBrush" Color="DodgerBlue" />
|
||||
</md:PackIcon.Foreground>
|
||||
</md:PackIcon>
|
||||
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
|
||||
Margin="0,0,10,0">
|
||||
<Hyperlink
|
||||
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
|
||||
|
||||
Foreground="{DynamicResource AccentBrush}"
|
||||
Command="{Binding OpenLinksViewCommand}">
|
||||
by achies
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
|
||||
Command="{Binding OpenLinksViewCommand}">
|
||||
<Hyperlink.Foreground>
|
||||
<SolidColorBrush x:Name="ByAchiesBrush" Color="DodgerBlue" />
|
||||
</Hyperlink.Foreground>
|
||||
by achies
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<md:Snackbar Grid.Row="1" MessageQueue="{Binding MessageQueue}" />
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.View.Dialogs;
|
||||
using NebulaAuth.ViewModel;
|
||||
using NebulaAuth.ViewModel.Other;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace NebulaAuth;
|
||||
|
||||
@@ -25,12 +27,49 @@ public partial class MainWindow
|
||||
ThemeManager.InitializeTheme();
|
||||
Title = Title + " " + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3);
|
||||
Loaded += OnApplicationStarted;
|
||||
UpdateManager.PendingUpdateDetected += OnPendingUpdateDetected;
|
||||
}
|
||||
|
||||
private void OnPendingUpdateDetected()
|
||||
{
|
||||
Dispatcher.BeginInvoke(StartUpdateBlink);
|
||||
}
|
||||
|
||||
private void StartUpdateBlink()
|
||||
{
|
||||
UpdateBadgeIcon.Visibility = Visibility.Visible;
|
||||
var baseColor = FindResource("AccentBrush") is SolidColorBrush accent
|
||||
? accent.Color
|
||||
: ByAchiesBrush.Color;
|
||||
var duration = new Duration(TimeSpan.FromSeconds(6));
|
||||
var sb = new Storyboard();
|
||||
sb.Children.Add(BuildBlinkAnimation(duration, "UpdateBadgeBrush"));
|
||||
sb.Children.Add(BuildBlinkAnimation(duration, "ByAchiesBrush"));
|
||||
sb.Completed += (_, _) =>
|
||||
{
|
||||
UpdateBadgeIcon.Visibility = Visibility.Collapsed;
|
||||
ByAchiesBrush.Color = baseColor;
|
||||
};
|
||||
sb.Begin(this);
|
||||
}
|
||||
|
||||
private static ColorAnimationUsingKeyFrames BuildBlinkAnimation(Duration duration, string targetName)
|
||||
{
|
||||
var anim = new ColorAnimationUsingKeyFrames { Duration = duration };
|
||||
for (var i = 0; i <= 6; i++)
|
||||
{
|
||||
var color = i % 2 == 0 ? Colors.DodgerBlue : Colors.OrangeRed;
|
||||
anim.KeyFrames.Add(new DiscreteColorKeyFrame(color, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i))));
|
||||
}
|
||||
Storyboard.SetTargetName(anim, targetName);
|
||||
Storyboard.SetTargetProperty(anim, new PropertyPath(SolidColorBrush.ColorProperty));
|
||||
return anim;
|
||||
}
|
||||
|
||||
private async void OnApplicationStarted(object? sender, EventArgs e)
|
||||
{
|
||||
((MainVM) DataContext).CurrentDialogHost = DialogHostInstance;
|
||||
if (Settings.Instance.IsPasswordSet == false) return;
|
||||
if (!Settings.Instance.IsPasswordSet) return;
|
||||
Topmost = false;
|
||||
await Dispatcher.InvokeAsync(ShowSetPasswordDialog, DispatcherPriority.ContextIdle);
|
||||
}
|
||||
@@ -46,7 +85,7 @@ public partial class MainWindow
|
||||
|
||||
var result = await DialogHost.Show(dialog);
|
||||
var pass = vm.Password;
|
||||
if (result is true && string.IsNullOrWhiteSpace(pass) == false)
|
||||
if (result is true && !string.IsNullOrWhiteSpace(pass))
|
||||
{
|
||||
PHandler.SetPassword(pass);
|
||||
}
|
||||
@@ -79,12 +118,12 @@ public partial class MainWindow
|
||||
|
||||
private void UIElement_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
|
||||
{
|
||||
if (int.TryParse(e.Text, out _) == false) e.Handled = true;
|
||||
if (!int.TryParse(e.Text, out _)) e.Handled = true;
|
||||
}
|
||||
|
||||
private void Rectangle_DragEnter(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
@@ -96,7 +135,7 @@ public partial class MainWindow
|
||||
|
||||
private async void Rectangle_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
||||
var filePaths = (string[]) e.Data.GetData(DataFormats.FileDrop)!;
|
||||
if (filePaths.Length == 0) return;
|
||||
if (DataContext is MainVM mainVm)
|
||||
|
||||
@@ -28,7 +28,6 @@ public partial class Mafile : MobileDataExtended
|
||||
}
|
||||
|
||||
private string? _filename;
|
||||
|
||||
[JsonIgnore] private PortableMaClient? _linkedClient;
|
||||
|
||||
public void SetSessionData(MobileSessionData? sessionData)
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public static class MAACRequestHandler
|
||||
{
|
||||
private const string LOC_PATH = "MAAC";
|
||||
private static readonly ConcurrentDictionary<Mafile, PortableMaClientErrorData> _errors = new();
|
||||
|
||||
public static void Register(Mafile mafile)
|
||||
{
|
||||
_errors[mafile] = new PortableMaClientErrorData();
|
||||
}
|
||||
|
||||
public static void Unregister(Mafile mafile)
|
||||
{
|
||||
_errors.TryRemove(mafile, out _);
|
||||
}
|
||||
|
||||
public static PortableMaClientStatus ClearErrors(Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
data.Clear();
|
||||
}
|
||||
|
||||
return PortableMaClientStatus.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a MAAC request with progressive error handling strategy: retries and status management based on error
|
||||
/// history.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="func"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<T> HandleRequest<T>(PortableMaClient client, Func<Task<T>> func)
|
||||
{
|
||||
var result = await client.DoRequest(func, client.Mafile.PreviouslyHadNoErrors());
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
InformRequestSuccessful(client.Mafile);
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
if (client.Mafile.PreviouslyHadNoErrors())
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Warning(GetPortableMaClientStatus("SessionError")));
|
||||
}
|
||||
else if (client.Mafile.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval))
|
||||
{
|
||||
Shell.Logger.Info("Retrying MAAC request for {name} MAAC account after previous error.",
|
||||
client.Mafile.AccountName);
|
||||
result = await client.DoRequest(func);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
InformRequestSuccessful(client.Mafile);
|
||||
return result.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if (client.Mafile.ErrorPersistedFor(Settings.Instance.MaacErrorThreshold))
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Error(GetPortableMaClientStatus("SessionError")));
|
||||
}
|
||||
|
||||
AddError(client.Mafile, result.Exception!);
|
||||
throw result.Exception!;
|
||||
}
|
||||
|
||||
|
||||
private static async Task<Result<T>> DoRequest<T>(this PortableMaClient client, Func<Task<T>> req,
|
||||
bool withSessionHandler = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return withSessionHandler
|
||||
? Result<T>.Success(await SessionHandler.Handle(req, client.Mafile, client.Chp(),
|
||||
GetTimerPrefix(client.Mafile)))
|
||||
: Result<T>.Success(await req());
|
||||
}
|
||||
catch (SessionInvalidException ex)
|
||||
{
|
||||
Shell.Logger.Warn("SessionInvalidException caught in MAAC request handler.");
|
||||
return Result<T>.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InformRequestSuccessful(Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data) && data.Clear())
|
||||
{
|
||||
Shell.Logger.Info("MAAC request for {name} MAAC account succeeded, error history cleared",
|
||||
mafile.AccountName);
|
||||
}
|
||||
|
||||
var client = mafile.LinkedClient;
|
||||
if (client != null && client.Status.StatusType != PortableMaClientStatusType.Ok)
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Ok());
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddError(Mafile mafile, Exception ex)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
Shell.Logger.Info("Registering error for {name} MAAC account: {ex}", mafile.AccountName, ex.Message);
|
||||
data.AddEntry(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ErrorPersistedFor(this Mafile mafile, TimeSpan span)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
var oldestError = data.GetOldestErrorTime();
|
||||
if (oldestError.HasValue && DateTime.UtcNow - oldestError.Value > span)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static bool PreviouslyHadNoErrors(this Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
return data.NoErrors;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LastErrorWasAtLeast(this Mafile mafile, TimeSpan timeAgo)
|
||||
{
|
||||
if (!_errors.TryGetValue(mafile, out var data)) return false;
|
||||
var latestError = data.GetTimeFromLastError();
|
||||
if (latestError.HasValue && latestError.Value > timeAgo)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsReady(Mafile maf)
|
||||
{
|
||||
if (maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Ok}) return true;
|
||||
return maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Warning}
|
||||
&& maf.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval);
|
||||
}
|
||||
|
||||
private static string GetTimerPrefix(Mafile mafile)
|
||||
{
|
||||
return GetLocalization("TimerPrefix") + mafile.AccountName + ": ";
|
||||
}
|
||||
|
||||
private static string GetLocalization(string key)
|
||||
{
|
||||
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
|
||||
}
|
||||
|
||||
private static string GetPortableMaClientStatus(string key)
|
||||
{
|
||||
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, "PortableMaClientStatus", key);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
@@ -93,6 +94,7 @@ public static class MAACStorage
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
|
||||
if (!File.Exists("maac.json")) return;
|
||||
try
|
||||
{
|
||||
@@ -107,6 +109,7 @@ public static class MAACStorage
|
||||
{
|
||||
mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket;
|
||||
mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades;
|
||||
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
|
||||
Clients[fileName] = storedClient;
|
||||
}
|
||||
}
|
||||
@@ -117,8 +120,6 @@ public static class MAACStorage
|
||||
SnackbarController.SendSnackbar(
|
||||
LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage"));
|
||||
}
|
||||
|
||||
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
|
||||
}
|
||||
|
||||
public static void NotifyMafilesRenamed(IDictionary<string, string> oldNewNames)
|
||||
|
||||
@@ -58,8 +58,7 @@ public static class MultiAccountAutoConfirmer
|
||||
|
||||
private static async Task TimerConfirmInternal()
|
||||
{
|
||||
var clients = Lock.ReadLock(() => Clients.ToArray());
|
||||
var enabledClients = clients.Where(x => x.LinkedClient is {IsError: false}).ToArray();
|
||||
var enabledClients = GetReadyAccounts();
|
||||
enabledClients = DistributeEvenly(enabledClients).ToArray();
|
||||
var confirmed = 0;
|
||||
await Task.Run(async () =>
|
||||
@@ -69,7 +68,8 @@ public static class MultiAccountAutoConfirmer
|
||||
var conf = 0;
|
||||
try
|
||||
{
|
||||
conf = await client.LinkedClient!.Confirm();
|
||||
conf = await MAACRequestHandler.HandleRequest(client.LinkedClient!,
|
||||
() => client.LinkedClient!.Confirm());
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
@@ -114,6 +114,21 @@ public static class MultiAccountAutoConfirmer
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Mafile> GetReadyAccounts()
|
||||
{
|
||||
var clients = Lock.ReadLock(() => Clients.ToArray());
|
||||
var res = new List<Mafile>();
|
||||
foreach (var maf in clients)
|
||||
{
|
||||
if (maf.LinkedClient == null) continue;
|
||||
if (MAACRequestHandler.IsReady(maf))
|
||||
{
|
||||
res.Add(maf);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public static bool TryAddToConfirm(Mafile mafile)
|
||||
{
|
||||
@@ -122,6 +137,7 @@ public static class MultiAccountAutoConfirmer
|
||||
if (Clients.Contains(mafile)) return false;
|
||||
mafile.LinkedClient = new PortableMaClient(mafile);
|
||||
Clients.Add(mafile);
|
||||
MAACRequestHandler.Register(mafile);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -134,6 +150,7 @@ public static class MultiAccountAutoConfirmer
|
||||
Clients.Remove(mafile);
|
||||
mafile.LinkedClient?.Dispose();
|
||||
mafile.LinkedClient = null;
|
||||
MAACRequestHandler.Unregister(mafile);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -38,17 +38,7 @@ public static class MaClient
|
||||
{
|
||||
ClientHandler.CookieContainer.ClearAllCookies();
|
||||
if (account == null) return;
|
||||
if (account.SessionData != null)
|
||||
{
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClientHandler.CookieContainer.ClearSteamCookies();
|
||||
ClientHandler.CookieContainer.AddMinimalMobileCookies();
|
||||
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
|
||||
}
|
||||
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
|
||||
Proxy.SetData(account.Proxy?.Data);
|
||||
}
|
||||
|
||||
@@ -153,7 +143,7 @@ public static class MaClient
|
||||
if (mafile.SessionData.RefreshToken.IsExpired)
|
||||
throw new SessionPermanentlyExpiredException();
|
||||
|
||||
if (ignoreAccessToken == false)
|
||||
if (!ignoreAccessToken)
|
||||
{
|
||||
var access = mafile.SessionData.GetMobileToken();
|
||||
if (access == null || access.Value.IsExpired)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NebulaAuth.Model.Entities;
|
||||
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public class ExportResult
|
||||
{
|
||||
public string? Error { get; }
|
||||
public Dictionary<Mafile, string>? Exported { get; }
|
||||
public List<string>? NotFound { get; }
|
||||
public List<string>? Conflict { get; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Exported))]
|
||||
[MemberNotNullWhen(true, nameof(NotFound))]
|
||||
[MemberNotNullWhen(true, nameof(Conflict))]
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
public bool Success => Error == null;
|
||||
|
||||
private ExportResult(string? error, Dictionary<Mafile, string>? exported, List<string>? notFound,
|
||||
List<string>? conflict)
|
||||
{
|
||||
Error = error;
|
||||
Exported = exported;
|
||||
NotFound = notFound;
|
||||
Conflict = conflict;
|
||||
}
|
||||
|
||||
public ExportResult(Dictionary<Mafile, string> exported, List<string> notFound, List<string> conflict)
|
||||
: this(null, exported, notFound, conflict)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public ExportResult(string? error)
|
||||
: this(error, null, null, null)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public class MafileExportTemplate
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool UseLoginAsMafileName { get; set; }
|
||||
public bool IncludeSharedSecret { get; set; }
|
||||
public bool IncludeIdentitySecret { get; set; }
|
||||
public bool IncludeRCode { get; set; }
|
||||
public bool IncludeSessionData { get; set; }
|
||||
public bool IncludeOtherInfo { get; set; }
|
||||
public bool IncludeNebulaProxy { get; set; }
|
||||
public bool IncludeNebulaPassword { get; set; }
|
||||
public bool IncludeNebulaGroup { get; set; }
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using SteamLibForked.Models.Session;
|
||||
using SteamLibForked.Models.SteamIds;
|
||||
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public static class MafileExporter
|
||||
{
|
||||
public static async Task<ExportResult> ExportMafiles(IEnumerable<string> keys, MafileExportTemplate template)
|
||||
{
|
||||
if (template.Path == null || !PathIsValid(template.Path))
|
||||
{
|
||||
return new ExportResult(LocManager.GetCodeBehindOrDefault("InvalidPath", "MafileExporter.InvalidPath"));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(template.Path))
|
||||
{
|
||||
Directory.CreateDirectory(template.Path);
|
||||
}
|
||||
|
||||
if (IsNebulaUtilizedDirectory(template.Path))
|
||||
{
|
||||
return new ExportResult(LocManager.GetCodeBehindOrDefault("NebulaUtilizedDirectory",
|
||||
"MafileExporter.NebulaUtilizedDirectory"));
|
||||
}
|
||||
|
||||
if (!IsAllowedToWrite(template.Path))
|
||||
{
|
||||
return new ExportResult(LocManager.GetCodeBehindOrDefault("AccessDenied", "MafileExporter.AccessDenied"));
|
||||
}
|
||||
|
||||
|
||||
var exported = new Dictionary<Mafile, string>();
|
||||
var notFound = new List<string>();
|
||||
var conflict = new List<string>();
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
SteamId? steamId = null;
|
||||
if (SteamId64.TryParse(key, out var id64))
|
||||
{
|
||||
steamId = id64;
|
||||
}
|
||||
|
||||
var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId);
|
||||
if (maf != null)
|
||||
{
|
||||
var fileName = await ExportMafile(template.Path, template, maf);
|
||||
if (fileName == null)
|
||||
{
|
||||
conflict.Add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
exported[maf] = fileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
notFound.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return new ExportResult(exported, notFound, conflict);
|
||||
}
|
||||
|
||||
private static async Task<string?> ExportMafile(string path, MafileExportTemplate template, Mafile mafile)
|
||||
{
|
||||
// We preserve SteamId even if other info is not included
|
||||
var session = template.IncludeSessionData
|
||||
? mafile.SessionData
|
||||
: new MobileSessionData(null!, mafile.SteamId, default, null, tokens: null);
|
||||
|
||||
|
||||
var serializeMaf = new Mafile
|
||||
{
|
||||
SharedSecret = template.IncludeSharedSecret ? mafile.SharedSecret : null!,
|
||||
IdentitySecret = template.IncludeIdentitySecret ? mafile.IdentitySecret : null!,
|
||||
DeviceId = template.IncludeIdentitySecret ? mafile.DeviceId : null!,
|
||||
RevocationCode = template.IncludeRCode ? mafile.RevocationCode : null!,
|
||||
AccountName = mafile.AccountName,
|
||||
SessionData = session,
|
||||
SteamId = mafile.SteamId,
|
||||
ServerTime = template.IncludeOtherInfo ? mafile.ServerTime : 0,
|
||||
SerialNumber = template.IncludeOtherInfo ? mafile.SerialNumber : 0,
|
||||
Uri = template.IncludeOtherInfo ? mafile.Uri : null!,
|
||||
TokenGid = template.IncludeOtherInfo ? mafile.TokenGid : null!,
|
||||
Secret1 = template.IncludeOtherInfo ? mafile.Secret1 : null!,
|
||||
Proxy = template.IncludeNebulaProxy ? mafile.Proxy : null!,
|
||||
Group = template.IncludeNebulaGroup ? mafile.Group : null!,
|
||||
Password = template.IncludeNebulaPassword ? mafile.Password : null!,
|
||||
LinkedClient = null,
|
||||
Filename = null
|
||||
};
|
||||
|
||||
var serialized = NebulaSerializer.SerializeMafile(serializeMaf);
|
||||
var strategy = template.UseLoginAsMafileName ? MafileNamingStrategy.Login : MafileNamingStrategy.SteamId;
|
||||
var fileName = strategy.GetMafileName(serializeMaf);
|
||||
var fullPath = Path.Combine(path, fileName);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(fullPath, serialized);
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private static bool PathIsValid(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var full = Path.GetFullPath(path);
|
||||
|
||||
var invalidPathChars = Path.GetInvalidPathChars();
|
||||
if (full.IndexOfAny(invalidPathChars) >= 0) return false;
|
||||
|
||||
var invalidFileChars = Path.GetInvalidFileNameChars();
|
||||
|
||||
foreach (var part in full.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
|
||||
{
|
||||
if (string.IsNullOrEmpty(part)) continue;
|
||||
if (part is [_, ':']) continue;
|
||||
|
||||
if (part.IndexOfAny(invalidFileChars) >= 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAllowedToWrite(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var testFilePath = Path.Combine(path, "test.tmp");
|
||||
using (var fs = File.Create(testFilePath))
|
||||
{
|
||||
}
|
||||
|
||||
File.Delete(testFilePath);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNebulaUtilizedDirectory(string path)
|
||||
{
|
||||
return Storage.MafilesDirectories.Any(x => Path.GetFullPath(path).EqualsIgnoreCase(x));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
|
||||
public static class MafileExporterStorage
|
||||
{
|
||||
private const string FILE_NAME = "export_templates.json";
|
||||
public static ObservableCollection<MafileExportTemplate> Templates { get; } = new();
|
||||
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
LoadOrCreateTemplate();
|
||||
}
|
||||
|
||||
public static MafileExportTemplate? GetTemplate(string name)
|
||||
{
|
||||
return Templates.FirstOrDefault(x => x.Name == name);
|
||||
}
|
||||
|
||||
public static void AddTemplate(MafileExportTemplate template)
|
||||
{
|
||||
Templates.Add(template);
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void DeleteTemplate(MafileExportTemplate template)
|
||||
{
|
||||
if (Templates.Remove(template))
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadOrCreateTemplate()
|
||||
{
|
||||
if (!File.Exists(FILE_NAME))
|
||||
{
|
||||
Templates.Clear();
|
||||
foreach (var mafileExportTemplate in CreateDefaultTemplate())
|
||||
{
|
||||
Templates.Add(mafileExportTemplate);
|
||||
}
|
||||
|
||||
Save();
|
||||
return;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(FILE_NAME);
|
||||
try
|
||||
{
|
||||
var loadedTemplates = JsonConvert.DeserializeObject<List<MafileExportTemplate>>(json);
|
||||
if (loadedTemplates != null)
|
||||
{
|
||||
Templates.Clear();
|
||||
foreach (var tmpl in loadedTemplates)
|
||||
{
|
||||
Templates.Add(tmpl);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Shell.Logger.Error(ex, $"Failed to deserialize {FILE_NAME}");
|
||||
Templates.Clear();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(Templates, Formatting.Indented);
|
||||
File.WriteAllText(FILE_NAME, json);
|
||||
}
|
||||
|
||||
private static IEnumerable<MafileExportTemplate> CreateDefaultTemplate()
|
||||
{
|
||||
return
|
||||
[
|
||||
new MafileExportTemplate
|
||||
{
|
||||
Name = "Default",
|
||||
UseLoginAsMafileName = false,
|
||||
IncludeSharedSecret = true,
|
||||
IncludeIdentitySecret = true,
|
||||
IncludeRCode = true,
|
||||
IncludeSessionData = true,
|
||||
IncludeOtherInfo = true,
|
||||
IncludeNebulaProxy = true,
|
||||
IncludeNebulaPassword = true,
|
||||
IncludeNebulaGroup = true,
|
||||
Path = null
|
||||
},
|
||||
new MafileExportTemplate
|
||||
{
|
||||
Name = "Auth only",
|
||||
UseLoginAsMafileName = true,
|
||||
IncludeSharedSecret = true,
|
||||
IncludeIdentitySecret = false,
|
||||
IncludeRCode = false,
|
||||
IncludeSessionData = false,
|
||||
IncludeOtherInfo = false,
|
||||
IncludeNebulaProxy = false,
|
||||
IncludeNebulaPassword = false,
|
||||
IncludeNebulaGroup = false,
|
||||
Path = null
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public enum AddMafileResult
|
||||
{
|
||||
Added,
|
||||
AlreadyExist,
|
||||
Error
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public class MafileImportSummary
|
||||
{
|
||||
public int Added { get; private set; }
|
||||
public int NotAdded { get; private set; }
|
||||
public int Errors { get; private set; }
|
||||
|
||||
public void AddedOne()
|
||||
{
|
||||
Added++;
|
||||
}
|
||||
|
||||
public void SkippedOne()
|
||||
{
|
||||
NotAdded++;
|
||||
}
|
||||
|
||||
public void ErrorOne()
|
||||
{
|
||||
Errors++;
|
||||
}
|
||||
|
||||
public void Add(int added = 0, int notAdded = 0, int errors = 0)
|
||||
{
|
||||
Added += added;
|
||||
NotAdded += notAdded;
|
||||
Errors += errors;
|
||||
}
|
||||
|
||||
public void Apply(AddMafileResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case AddMafileResult.Added:
|
||||
AddedOne();
|
||||
break;
|
||||
case AddMafileResult.AlreadyExist:
|
||||
SkippedOne();
|
||||
break;
|
||||
case AddMafileResult.Error:
|
||||
ErrorOne();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public class MafilesBulkRenameResult
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int Renamed { get; set; }
|
||||
public int NotRenamed => Errors + Conflict;
|
||||
public int Errors { get; set; }
|
||||
public int Conflict { get; set; }
|
||||
public string BackupFileName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.IO;
|
||||
using NebulaAuth.Model.Entities;
|
||||
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
internal static class MafilesStorageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns <see cref="Mafile.Filename" /> or creates a new one and updates the property.
|
||||
/// <see cref="Mafile.Filename" /> is always not <see langword="null" /> after this method call.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetOrUpdateMafilePath(Mafile data)
|
||||
{
|
||||
if (data.Filename != null)
|
||||
{
|
||||
return Path.Combine(Storage.MafilesDirectory, data.Filename);
|
||||
}
|
||||
|
||||
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
|
||||
data.Filename = fileName;
|
||||
return Path.Combine(Storage.MafilesDirectory, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates mafile file name according to the current settings.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="useAccountName"></param>
|
||||
/// <returns></returns>
|
||||
public static string CreateMafileFileName(Mafile data, bool useAccountName)
|
||||
{
|
||||
return useAccountName
|
||||
? MafileNamingStrategy.Login.GetMafileName(data)
|
||||
: MafileNamingStrategy.SteamId.GetMafileName(data);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -11,24 +11,30 @@ using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using NebulaAuth.Model.MAAC;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using SteamLib;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
using SteamLib.SteamMobile;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
namespace NebulaAuth.Model.Mafiles;
|
||||
|
||||
public static class Storage
|
||||
{
|
||||
public const string DIR_MAFILES = "maFiles";
|
||||
public const string DIR_REMOVED_MAFILES = "maFiles_removed";
|
||||
public const string DIR_BACKUP_MAFILES = "maFiles_backup";
|
||||
|
||||
public static readonly string[] MafilesDirectories;
|
||||
public static string MafilesDirectory { get; } = Path.GetFullPath(DIR_MAFILES);
|
||||
public static string RemovedMafilesDirectory { get; } = Path.GetFullPath(DIR_REMOVED_MAFILES);
|
||||
public static string BackupMafilesDirectory { get; } = Path.GetFullPath(DIR_BACKUP_MAFILES);
|
||||
|
||||
public static ObservableCollection<Mafile> MaFiles { get; private set; } = [];
|
||||
|
||||
static Storage()
|
||||
{
|
||||
MafilesDirectories = [MafilesDirectory, RemovedMafilesDirectory, BackupMafilesDirectory];
|
||||
}
|
||||
|
||||
public static async Task Initialize(int threadCount, CancellationToken token = default)
|
||||
{
|
||||
Directory.CreateDirectory(MafilesDirectory);
|
||||
@@ -45,7 +51,7 @@ public static class Storage
|
||||
await Task.Run(() =>
|
||||
{
|
||||
return Parallel.ForEachAsync(files,
|
||||
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
|
||||
new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = threadCount },
|
||||
async (file, ct) =>
|
||||
{
|
||||
try
|
||||
@@ -67,25 +73,46 @@ public static class Storage
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="overwrite"></param>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
/// <returns> Result of adding mafile</returns>
|
||||
/// <exception cref="SessionInvalidException"></exception>
|
||||
/// <exception cref="IOException"></exception>
|
||||
public static void AddNewMafile(string path, bool overwrite)
|
||||
public static async Task<AddMafileResult> AddNewMafile(string path, bool overwrite)
|
||||
{
|
||||
Mafile data;
|
||||
|
||||
try
|
||||
{
|
||||
data = ReadMafile(path);
|
||||
data = await ReadMafileAsync(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is not MafileNeedReloginException)
|
||||
catch (Exception ex) when (ex is not MafileNeedReloginException)
|
||||
{
|
||||
Shell.Logger.Warn(ex, "Can't load mafile");
|
||||
throw new FormatException("File data is not valid", ex);
|
||||
}
|
||||
|
||||
await AddNewMafileInternal(data, overwrite);
|
||||
return AddMafileResult.Added;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="overwrite"></param>
|
||||
/// <returns> Result of adding mafile</returns>
|
||||
/// <exception cref="SessionInvalidException"></exception>
|
||||
public static Task<AddMafileResult> AddNewMafileFromData(Mafile data, bool overwrite)
|
||||
{
|
||||
return AddNewMafileInternal(data, overwrite);
|
||||
}
|
||||
|
||||
private static async Task<AddMafileResult> AddNewMafileInternal(Mafile data, bool overwrite)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(data.AccountName))
|
||||
throw new FormatException("File data is not valid. Missing AccountName");
|
||||
{
|
||||
Shell.Logger.Warn("Mafile account name is empty");
|
||||
return AddMafileResult.Error;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -93,22 +120,19 @@ public static class Storage
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException("Can't generate code on this mafile", ex);
|
||||
Shell.Logger.Error(ex, "Can't generate code for mafile {accountName}", data.AccountName);
|
||||
return AddMafileResult.Error;
|
||||
}
|
||||
|
||||
if (overwrite == false && File.Exists(GetOrCreateMafilePath(data)))
|
||||
data.Filename = null;
|
||||
|
||||
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
|
||||
{
|
||||
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
|
||||
return AddMafileResult.AlreadyExist;
|
||||
}
|
||||
|
||||
|
||||
SaveMafile(data);
|
||||
}
|
||||
|
||||
public static Mafile ReadMafile(string path)
|
||||
{
|
||||
var str = File.ReadAllText(path);
|
||||
return NebulaSerializer.Deserialize(str, path);
|
||||
await SaveMafileAsync(data);
|
||||
return AddMafileResult.Added;
|
||||
}
|
||||
|
||||
public static async Task<Mafile> ReadMafileAsync(string path)
|
||||
@@ -117,66 +141,52 @@ public static class Storage
|
||||
return NebulaSerializer.Deserialize(str, path);
|
||||
}
|
||||
|
||||
public static void SaveMafile(Mafile data)
|
||||
{
|
||||
var path = GetOrCreateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
File.WriteAllText(Path.GetFullPath(path), str);
|
||||
|
||||
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
|
||||
if (existed != null)
|
||||
{
|
||||
var index = MaFiles.IndexOf(existed);
|
||||
MaFiles[index] = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaFiles.Add(data);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SaveMafileAsync(Mafile data)
|
||||
{
|
||||
var path = GetOrCreateMafilePath(data);
|
||||
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
|
||||
|
||||
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
|
||||
if (existed != null)
|
||||
// Replace if exist
|
||||
for (var i = 0; i < MaFiles.Count; i++)
|
||||
{
|
||||
var index = MaFiles.IndexOf(existed);
|
||||
MaFiles[index] = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaFiles.Add(data);
|
||||
var existed = MaFiles[i];
|
||||
if (ReferenceEquals(existed, data) || (existed.Filename != null && existed.Filename == data.Filename))
|
||||
{
|
||||
MaFiles[i] = data;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MaFiles.Add(data);
|
||||
}
|
||||
|
||||
public static void UpdateMafile(Mafile data)
|
||||
{
|
||||
var path = GetOrCreateMafilePath(data);
|
||||
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
File.WriteAllText(Path.GetFullPath(path), str);
|
||||
}
|
||||
|
||||
public static async Task UpdateMafileAsync(Mafile data)
|
||||
{
|
||||
var path = GetOrCreateMafilePath(data);
|
||||
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var str = NebulaSerializer.SerializeMafile(data);
|
||||
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
|
||||
}
|
||||
|
||||
public static void MoveToRemoved(Mafile data)
|
||||
{
|
||||
var sourcePath = GetOrCreateMafilePath(data);
|
||||
var destinationPath = Path.Combine(DIR_REMOVED_MAFILES, data.Filename + ".mafile");
|
||||
var destinationPathFinal = destinationPath;
|
||||
var sourcePath = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
||||
var fileName = Path.GetFileNameWithoutExtension(data.Filename!);
|
||||
var destinationPathBase = Path.Combine(DIR_REMOVED_MAFILES, fileName);
|
||||
var destinationPathFinal = Path.ChangeExtension(destinationPathBase, MafileNamingStrategy.DEF_EXTENSION);
|
||||
var i = 0;
|
||||
while (File.Exists(destinationPathFinal))
|
||||
{
|
||||
i++;
|
||||
destinationPathFinal = destinationPath + $" ({i})";
|
||||
destinationPathFinal = destinationPathBase + $" ({i})";
|
||||
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
|
||||
}
|
||||
|
||||
File.Copy(sourcePath, destinationPathFinal, false);
|
||||
@@ -184,24 +194,6 @@ public static class Storage
|
||||
MaFiles.Remove(data);
|
||||
}
|
||||
|
||||
private static string GetOrCreateMafilePath(Mafile data)
|
||||
{
|
||||
if (data.Filename != null)
|
||||
{
|
||||
return Path.Combine(MafilesDirectory, data.Filename);
|
||||
}
|
||||
|
||||
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
|
||||
data.Filename = fileName;
|
||||
return Path.Combine(MafilesDirectory, fileName);
|
||||
}
|
||||
|
||||
private static string CreateMafileFileName(Mafile data, bool useAccountName)
|
||||
{
|
||||
return useAccountName
|
||||
? MafileNamingStrategy.Login.GetMafileName(data)
|
||||
: MafileNamingStrategy.SteamId.GetMafileName(data);
|
||||
}
|
||||
|
||||
public static string? TryGetMafilePath(Mafile data)
|
||||
{
|
||||
@@ -221,9 +213,10 @@ public static class Storage
|
||||
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
|
||||
}
|
||||
|
||||
public static async Task<MafileRenameResult> RenameMafiles(bool loginAsFileName, IProgress<double>? progress = null)
|
||||
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
|
||||
IProgress<double>? progress = null)
|
||||
{
|
||||
if (MaFiles.Count == 0) return new MafileRenameResult();
|
||||
if (MaFiles.Count == 0) return new MafilesBulkRenameResult();
|
||||
var now = DateTime.Now;
|
||||
var backupFileName = $"rename_backup_{now:yyyy-MM-dd_HH-mm-ss}.zip";
|
||||
var zipPath = Path.Combine(BackupMafilesDirectory, backupFileName);
|
||||
@@ -248,14 +241,14 @@ public static class Storage
|
||||
|
||||
if (counter % 5 == 0)
|
||||
{
|
||||
progress?.Report(counter / (double) files.Count);
|
||||
progress?.Report(counter / (double)files.Count);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mafiles = MaFiles.ToList();
|
||||
var res = new MafileRenameResult
|
||||
var res = new MafilesBulkRenameResult
|
||||
{
|
||||
Total = mafiles.Count,
|
||||
BackupFileName = backupFileName
|
||||
@@ -266,7 +259,7 @@ public static class Storage
|
||||
{
|
||||
try
|
||||
{
|
||||
var targetFileName = CreateMafileFileName(mafile, loginAsFileName);
|
||||
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
|
||||
if (mafile.Filename == targetFileName || mafile.Filename == null)
|
||||
{
|
||||
res.Renamed += 1;
|
||||
@@ -314,14 +307,4 @@ public static class Storage
|
||||
res.Errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
public class MafileRenameResult
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int Renamed { get; set; }
|
||||
public int NotRenamed => Errors + Conflict;
|
||||
public int Errors { get; set; }
|
||||
public int Conflict { get; set; }
|
||||
public string BackupFileName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.MafilesLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for detecting and handling SDA encrypted mafiles.
|
||||
/// This is used in the legacy mafile handling code to support importing SDA encrypted mafiles without requiring the
|
||||
/// user to manually decrypt them first.
|
||||
/// </summary>
|
||||
public static class SDAEncryptionHelper
|
||||
{
|
||||
private const string ManifestFileName = "manifest.json";
|
||||
|
||||
public static bool LooksLikeSdaEncryptedBlob(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return false;
|
||||
|
||||
var trimmed = content.Trim();
|
||||
|
||||
if (trimmed.StartsWith('{') || trimmed.Length < 64)
|
||||
return false;
|
||||
|
||||
Span<byte> buffer = stackalloc byte[trimmed.Length];
|
||||
return Convert.TryFromBase64String(trimmed, buffer, out _);
|
||||
}
|
||||
|
||||
public static Context? TryDetect(string mafilePath, SDAManifest? sdaManifest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mafilePath))
|
||||
return null;
|
||||
|
||||
if (sdaManifest == null)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(mafilePath);
|
||||
if (string.IsNullOrWhiteSpace(dir))
|
||||
return null;
|
||||
|
||||
var manifestPath = FindManifestPath(dir);
|
||||
if (manifestPath == null)
|
||||
return null;
|
||||
|
||||
sdaManifest = TryReadEncryptedManifest(manifestPath);
|
||||
}
|
||||
|
||||
if (sdaManifest is not {Encrypted: true} || sdaManifest.Entries.Length == 0)
|
||||
return null;
|
||||
|
||||
var fileName = Path.GetFileName(mafilePath);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return null;
|
||||
|
||||
var entries = sdaManifest.Entries.ToDictionary(x => Path.GetFileName(x.Filename), StringComparer.OrdinalIgnoreCase);
|
||||
return new Context(sdaManifest, entries, null);
|
||||
}
|
||||
|
||||
private static string? FindManifestPath(string startDirectory)
|
||||
{
|
||||
var current = startDirectory;
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var candidate = Path.Combine(current, ManifestFileName);
|
||||
if (File.Exists(candidate))
|
||||
return candidate;
|
||||
|
||||
var parent = Directory.GetParent(current);
|
||||
if (parent?.FullName == null)
|
||||
break;
|
||||
current = parent.FullName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static SDAManifest? TryReadEncryptedManifest(string manifestPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manifestText = File.ReadAllText(manifestPath);
|
||||
var manifest = JsonConvert.DeserializeObject<SDAManifest>(manifestText);
|
||||
if (manifest is not {Encrypted: true} || manifest.Entries.Length == 0)
|
||||
return null;
|
||||
return manifest;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? TryDecrypt(string content, string path, Context context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(context.Password))
|
||||
return null;
|
||||
|
||||
// If we have password, check if we have entry
|
||||
var entry = context.GetEntry(path);
|
||||
if (entry == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// We have entry, try to decrypt
|
||||
return SDAEncryptor.TryDecryptData(context.Password, entry.EncryptionSalt, entry.EncryptionIv, content,
|
||||
out var decrypted)
|
||||
? decrypted
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of SDA encrypted mafile detection. When not null, the caller can decrypt
|
||||
/// using SDAEncryptor.DecryptData(password, Entry.EncryptionSalt, Entry.EncryptionIv, fileContent).
|
||||
/// </summary>
|
||||
public sealed class Context
|
||||
{
|
||||
public SDAManifest SdaManifest { get; }
|
||||
public Dictionary<string, SDAManifestEntry> Entries { get; }
|
||||
public string? Password { get; }
|
||||
|
||||
public Context(SDAManifest sdaManifest, Dictionary<string, SDAManifestEntry> entries, string? password)
|
||||
{
|
||||
SdaManifest = sdaManifest;
|
||||
Entries = entries;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
public Context WithPassword(string? password)
|
||||
{
|
||||
return new Context(SdaManifest, Entries, password);
|
||||
}
|
||||
|
||||
public SDAManifestEntry? GetEntry(string path)
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
return Entries.GetValueOrDefault(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-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,20 @@ public static class SDAEncryptor
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryDecryptData(string password, string passwordSalt, string IV, string encryptedData, [NotNullWhen(true)] out string? plaintext)
|
||||
{
|
||||
plaintext = null;
|
||||
try
|
||||
{
|
||||
plaintext = DecryptData(password, passwordSalt, IV, encryptedData);
|
||||
return plaintext != null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to decrypt and return data given an encrypted base64 encoded string. Must use the same
|
||||
/// password, salt, IV, and ciphertext that was used during the original encryption of the data.
|
||||
+4
-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; }
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class NebulaSerializer
|
||||
var data = Serializer.Deserialize(cont);
|
||||
var mobileData = data.Data;
|
||||
var info = data.Info;
|
||||
if (info.IsExtended == false)
|
||||
if (!info.IsExtended)
|
||||
throw new FormatException("Mafile is not extended data");
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public static class NebulaSerializer
|
||||
|
||||
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
|
||||
{
|
||||
if (dictionary.TryGetValue(name, out var prop) == false) return default;
|
||||
if (!dictionary.TryGetValue(name, out var prop)) return default;
|
||||
var value = prop.Value;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ public static class ProxyStorage
|
||||
|
||||
static ProxyStorage()
|
||||
{
|
||||
if (File.Exists("proxies.json") == false)
|
||||
if (!File.Exists("proxies.json"))
|
||||
return;
|
||||
try
|
||||
{
|
||||
|
||||
+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
|
||||
+1
-2
@@ -1,6 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using AchiesUtilities.Web.Models;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using SteamLib.Api.Mobile;
|
||||
using SteamLib.Authentication;
|
||||
using SteamLib.Authentication.LoginV2;
|
||||
@@ -27,7 +28,6 @@ public partial class SessionHandler //API
|
||||
mafile.SessionData.SetMobileToken(newToken);
|
||||
|
||||
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
|
||||
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
|
||||
mafile.SetSessionData(mafile.SessionData);
|
||||
await Storage.UpdateMafileAsync(mafile);
|
||||
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
|
||||
@@ -49,7 +49,6 @@ public partial class SessionHandler //API
|
||||
AdmissionHelper.TransferCommunityCookies(chp.Handler.CookieContainer);
|
||||
|
||||
//Triggers PropertyChanged event for PortableMaClient handling session updated from MaClient
|
||||
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
|
||||
mafile.SetSessionData((MobileSessionData) result);
|
||||
if (PHandler.IsPasswordSet)
|
||||
mafile.Password = savePassword ? PHandler.Encrypt(password) : null;
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NebulaAuth.Model.Entities;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
|
||||
public partial class SessionHandler //Helpers
|
||||
{
|
||||
private static bool MobileTokenExpired(this Mafile mafile)
|
||||
{
|
||||
var mobileToken = mafile.SessionData?.GetMobileToken();
|
||||
return mobileToken == null || mobileToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static bool RefreshTokenExpired(this Mafile mafile)
|
||||
{
|
||||
var refreshToken = mafile.SessionData?.RefreshToken;
|
||||
return refreshToken == null || refreshToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static bool HasPassword(this Mafile mafile, [NotNullWhen(true)] out string? plainPassword)
|
||||
{
|
||||
plainPassword = GetPassword(mafile);
|
||||
return !string.IsNullOrWhiteSpace(plainPassword);
|
||||
}
|
||||
|
||||
private static string? GetPassword(Mafile mafile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
|
||||
{
|
||||
return PHandler.Decrypt(mafile.Password);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public partial class Settings : ObservableObject
|
||||
|
||||
static Settings()
|
||||
{
|
||||
if (File.Exists("settings.json") == false)
|
||||
if (!File.Exists("settings.json"))
|
||||
{
|
||||
Instance = new Settings();
|
||||
Instance.PropertyChanged += SettingsOnPropertyChanged;
|
||||
@@ -94,7 +94,6 @@ public partial class Settings : ObservableObject
|
||||
[ObservableProperty] private bool _legacyMode = true;
|
||||
[ObservableProperty] private bool _allowAutoUpdate;
|
||||
[ObservableProperty] private bool _useAccountNameAsMafileName;
|
||||
[ObservableProperty] private bool _ignorePatchTuesdayErrors;
|
||||
|
||||
[ObservableProperty] private BackgroundMode _backgroundMode = BackgroundMode.Default;
|
||||
[ObservableProperty] private double _leftOpacity = 0.4;
|
||||
@@ -108,6 +107,9 @@ public partial class Settings : ObservableObject
|
||||
[ObservableProperty] private bool _proxyManagerDisplayProtocol;
|
||||
[ObservableProperty] private bool _proxyManagerDisplayCredentials;
|
||||
|
||||
[ObservableProperty] private TimeSpan _maacErrorThreshold = TimeSpan.FromHours(3);
|
||||
[ObservableProperty] private TimeSpan _maacRetryInterval = TimeSpan.FromMinutes(15);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using NebulaAuth.Model.MAAC;
|
||||
using NebulaAuth.Model.MafileExport;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using NLog;
|
||||
using NLog.Extensions.Logging;
|
||||
using SteamLib.Core;
|
||||
@@ -14,8 +20,11 @@ public static class Shell
|
||||
public static Logger Logger { get; private set; } = null!;
|
||||
public static ILogger ExtensionsLogger { get; private set; } = null!;
|
||||
|
||||
public static void Initialize()
|
||||
public static async Task Initialize()
|
||||
{
|
||||
File.Delete("log.log");
|
||||
LocManager.Init();
|
||||
LocManager.SetApplicationLocalization(Settings.Instance.Language);
|
||||
Logger = LogManager.GetLogger("Logger");
|
||||
var lp = new NLogLoggerProvider();
|
||||
var logger = lp.CreateLogger("SteamLib");
|
||||
@@ -27,13 +36,18 @@ public static class Shell
|
||||
|
||||
try
|
||||
{
|
||||
TimeAligner.AlignTime();
|
||||
await TimeAligner.AlignTimeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new CantAlignTimeException("", ex);
|
||||
}
|
||||
|
||||
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
|
||||
await Storage.Initialize(threads);
|
||||
MAACStorage.Initialize();
|
||||
MafileExporterStorage.Initialize();
|
||||
|
||||
ExtensionsLogger.LogDebug("Application started");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.Update;
|
||||
|
||||
public class ChangelogEntry
|
||||
{
|
||||
[JsonProperty("version")]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("date")]
|
||||
public string Date { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("changes")]
|
||||
public List<ChangeItem> Changes { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ChangeItem
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("text")]
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("link")]
|
||||
public string? Link { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NebulaAuth.Model.Update;
|
||||
|
||||
public class UpdateSettings
|
||||
{
|
||||
private static readonly string FilePath = Path.Combine("settings", "update.json");
|
||||
|
||||
[JsonProperty("skippedVersion")]
|
||||
public string? SkippedVersion { get; set; }
|
||||
|
||||
[JsonProperty("remindAfter")]
|
||||
public DateTime? RemindAfter { get; set; }
|
||||
|
||||
public static UpdateSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(FilePath)) return new UpdateSettings();
|
||||
var json = File.ReadAllText(FilePath);
|
||||
return JsonConvert.DeserializeObject<UpdateSettings>(json) ?? new UpdateSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new UpdateSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var dir = Path.GetDirectoryName(FilePath)!;
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||
File.WriteAllText(FilePath, json);
|
||||
}
|
||||
|
||||
public void SkipVersion(string version)
|
||||
{
|
||||
SkippedVersion = version;
|
||||
RemindAfter = null;
|
||||
Save();
|
||||
}
|
||||
|
||||
public void SetRemindAfter(DateTime remindAfter)
|
||||
{
|
||||
RemindAfter = remindAfter;
|
||||
Save();
|
||||
}
|
||||
|
||||
public bool ShouldShow(string version)
|
||||
{
|
||||
if (SkippedVersion == version) return false;
|
||||
if (RemindAfter.HasValue && DateTime.Now < RemindAfter.Value) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,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}" />
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
|
||||
<ApplicationIcon>Theme\lock.ico</ApplicationIcon>
|
||||
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
|
||||
<AssemblyVersion>1.8.0</AssemblyVersion>
|
||||
<AssemblyVersion>1.8.2</AssemblyVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<PackageReference Include="CodingSeb.Localization.WPF" Version="1.4.1" />
|
||||
<PackageReference Include="CodingSebLocalization.Fody" Version="1.4.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
|
||||
<PackageReference Include="NLog" Version="5.5.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
|
||||
@@ -53,6 +53,9 @@
|
||||
<Compile Update="View\Dialogs\LoginAgainOnImportDialog.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="View\Dialogs\SdaPasswordDialog.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -11,13 +11,16 @@ public static class LanguageUtility
|
||||
{
|
||||
var userCulture = CultureInfo.CurrentUICulture;
|
||||
var userLang = userCulture.TwoLetterISOLanguageName;
|
||||
var userRegion = userCulture.Name;
|
||||
var userRegion = new RegionInfo(userCulture.Name).TwoLetterISORegionName;
|
||||
|
||||
|
||||
switch (userLang)
|
||||
{
|
||||
case "ru": return LocalizationLanguage.Russian;
|
||||
case "uk": return LocalizationLanguage.Ukrainian;
|
||||
case "en": return LocalizationLanguage.English;
|
||||
case "zh": return LocalizationLanguage.ChineseSimplified;
|
||||
case "fr": return LocalizationLanguage.French;
|
||||
}
|
||||
|
||||
if (userRegion.EndsWith("UA", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -28,9 +31,8 @@ public static class LanguageUtility
|
||||
"RU", "BY", "KZ", "KG", "TJ", "TM", "UZ", "AM", "AZ", "GE", "MD"
|
||||
];
|
||||
|
||||
if (cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase)))
|
||||
return LocalizationLanguage.Russian;
|
||||
|
||||
return LocalizationLanguage.English;
|
||||
return cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase))
|
||||
? LocalizationLanguage.Russian
|
||||
: LocalizationLanguage.English;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
@@ -6,7 +6,6 @@
|
||||
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
|
||||
xmlns:vm="clr-namespace:NebulaAuth.ViewModel">
|
||||
|
||||
|
||||
<DataTemplate DataType="{x:Type confirmations:Confirmation}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -175,36 +174,96 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type entities:MarketMultiConfirmation}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="1">
|
||||
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
|
||||
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
|
||||
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
|
||||
</TextBlock>
|
||||
<Expander Padding="0" Background="Transparent" materialDesign:ExpanderAssist.ExpanderButtonPosition="Start" materialDesign:ExpanderAssist.HorizontalHeaderPadding="0" HorizontalAlignment="Stretch">
|
||||
<Expander.Header>
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="1">
|
||||
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
|
||||
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
|
||||
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left" Margin="5,0,0,0"
|
||||
Text="{Binding Time, StringFormat=t}" />
|
||||
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="Check" />
|
||||
</Button>
|
||||
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Center" Margin="5,0,0,0"
|
||||
Text="{Binding Time, StringFormat=t}" />
|
||||
|
||||
<!-- Confirm all -->
|
||||
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="4"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmAll}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="CheckAll" />
|
||||
</Button>
|
||||
<!-- Cancel all -->
|
||||
<Button Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelAll}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
|
||||
CommandParameter="{Binding }">
|
||||
<materialDesign:PackIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Expander.Header>
|
||||
|
||||
<Border Margin="30,6,0,0" Background="{DynamicResource MaterialDesignPaper}" CornerRadius="4">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
MaxHeight="320">
|
||||
<ItemsControl ItemsSource="{Binding Confirmations}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type confirmations:MarketConfirmation}">
|
||||
<Grid Margin="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="32"
|
||||
MaxHeight="32" Margin="0,0,10,0"
|
||||
Source="{Binding ItemImageUri}" />
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left"
|
||||
Margin="8,0,0,0"
|
||||
Text="{Binding Time, StringFormat=t}" />
|
||||
|
||||
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmOne}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
|
||||
CommandParameter="{Binding}">
|
||||
|
||||
<materialDesign:PackIcon Kind="Check" />
|
||||
</Button>
|
||||
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelOne}"
|
||||
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
|
||||
CommandParameter="{Binding}">
|
||||
<materialDesign:PackIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Expander>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type confirmations:PurchaseConfirmation}">
|
||||
<Grid>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
materialDesign:HintAssist.IsFloating="False"
|
||||
materialDesign:TextFieldAssist.LeadingIcon="Key"
|
||||
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
|
||||
<CheckBox FontSize="15" Grid.Row="1" Margin="10,10,10,0"
|
||||
<CheckBox FontSize="15" Grid.Row="1" Margin="10,5,10,0"
|
||||
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
|
||||
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
|
||||
<Grid Grid.Row="2" Margin="10,10,10,0">
|
||||
|
||||
@@ -74,10 +74,10 @@
|
||||
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
|
||||
</UIElement.InputBindings>
|
||||
</ComboBox>
|
||||
<CheckBox FontSize="15" Grid.Row="2" Margin="10,10,10,0"
|
||||
<CheckBox FontSize="15" Grid.Row="2" Margin="10,5,10,0"
|
||||
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
|
||||
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
|
||||
<CheckBox FontSize="15" Grid.Row="3" Margin="10,10,10,0" IsEnabled="{Binding MafileHasProxy}"
|
||||
<CheckBox FontSize="15" Grid.Row="3" Margin="10,5,10,0" IsEnabled="{Binding MafileHasProxy}"
|
||||
Content="{Tr LoginAgainDialog.UseMafileProxy}" IsChecked="{Binding UseMafileProxy}" />
|
||||
<Grid Grid.Row="4" Margin="10,10,10,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<UserControl x:Class="NebulaAuth.View.Dialogs.SdaPasswordDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||
mc:Ignorable="d"
|
||||
TextElement.Foreground="{DynamicResource BaseContentBrush}"
|
||||
d:DataContext="{d:DesignInstance other:SdaPasswordDialogVM}"
|
||||
Background="{DynamicResource WindowBackground}">
|
||||
|
||||
<Grid MinHeight="200" MinWidth="400" MaxWidth="500">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Lock" Width="20" Height="20" Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />
|
||||
<TextBlock FontSize="16" VerticalAlignment="Center" Foreground="{DynamicResource BaseContentBrush}"
|
||||
Margin="7,0,0,0" HorizontalAlignment="Left"
|
||||
Text="{Tr SdaPasswordDialog.Title}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Separator Grid.Row="1" Opacity="0.7" />
|
||||
<Grid Grid.Row="2" Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="{Tr SdaPasswordDialog.Message}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource BaseContentBrush}" Margin="10,10,10,8" />
|
||||
<TextBox Grid.Row="1" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
materialDesign:TextFieldAssist.HasClearButton="True"
|
||||
Padding="10" FontSize="15" Margin="10,0,10,0"
|
||||
Style="{StaticResource MaterialDesignOutlinedTextBox}"
|
||||
materialDesign:HintAssist.Hint="{Tr SdaPasswordDialog.PasswordHint}"
|
||||
materialDesign:HintAssist.IsFloating="False"
|
||||
materialDesign:TextFieldAssist.LeadingIcon="Key"
|
||||
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
|
||||
<Grid Grid.Row="2" Margin="10,16,10,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button IsEnabled="{Binding IsFormValid}" IsDefault="True" Margin="0,5,5,5"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource True}" Content="{Tr Common.OK}" />
|
||||
<Button IsCancel="True" Grid.Column="1" Margin="5,5,0,5"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
CommandParameter="{StaticResource False}" Content="{Tr Common.Cancel}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
public partial class SdaPasswordDialog
|
||||
{
|
||||
public SdaPasswordDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<UserControl x:Class="NebulaAuth.View.Dialogs.UpdateDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||
mc:Ignorable="d"
|
||||
TextElement.Foreground="{DynamicResource BaseContentBrush}"
|
||||
d:DataContext="{d:DesignInstance other:UpdateDialogVM}"
|
||||
Background="{DynamicResource WindowBackground}"
|
||||
MinWidth="420" MaxWidth="600">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Grid Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Update" Width="20" Height="20" Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />
|
||||
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" FontSize="18">
|
||||
<Run Text="{Tr UpdateDialog.Title, IsDynamic=False}" />
|
||||
<Run FontWeight="Bold" Text="{Binding Version, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Separator Grid.Row="1" Opacity="0.7" />
|
||||
|
||||
<!-- Changelog -->
|
||||
<Grid Grid.Row="2" Margin="12,10,12,4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="{Tr UpdateDialog.Changelog}" FontSize="14" FontWeight="SemiBold"
|
||||
Margin="0,0,0,8"
|
||||
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||
|
||||
<!-- JSON changelog items -->
|
||||
<materialDesign:Card Grid.Row="1" Padding="10">
|
||||
<ScrollViewer MaxHeight="350" VerticalScrollBarVisibility="Auto"
|
||||
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<ItemsControl ItemsSource="{Binding Changelog.Changes}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,6">
|
||||
<Border CornerRadius="3" Padding="4,2,4,2" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource MaterialDesign.Brush.Primary}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Type}" Value="NEW">
|
||||
<Setter Property="Background" Value="{DynamicResource SuccessBrush}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Type}" Value="FIX">
|
||||
<Setter Property="Background" Value="{DynamicResource WarningBrush}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Type}" Value="IMPROVEMENT">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Type}" Value="SECURITY">
|
||||
<Setter Property="Background" Value="{DynamicResource ErrorBrush}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="{Binding Type}" FontSize="11" FontWeight="Bold"
|
||||
Foreground="White" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Text}" TextWrapping="Wrap"
|
||||
VerticalAlignment="Center" FontSize="14" MaxWidth="420" />
|
||||
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
Width="20" Height="20"
|
||||
Padding="2"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding DataContext.OpenLinkCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding Link}"
|
||||
ToolTip="Open Link"
|
||||
Visibility="{Binding Link, Converter={StaticResource NullableToVisibilityConverter}}">
|
||||
<materialDesign:PackIcon Kind="OpenInNew" Width="14" Height="14" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<Separator Opacity="0.2" Margin="0,2,0,2"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</materialDesign:Card>
|
||||
<!-- No changelog fallback -->
|
||||
<TextBlock Grid.Row="1" Text="{Tr UpdateDialog.NoChangelog}" FontSize="13" Opacity="0.6"
|
||||
Visibility="{Binding HasJsonChangelog, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid Grid.Row="3" Margin="12,6,12,14">
|
||||
|
||||
<!-- Normal buttons -->
|
||||
<StackPanel Visibility="{Binding ShowRemindOptions, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
|
||||
<Button Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Command="{Binding UpdateNowCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0,0,0,8"
|
||||
Content="{Tr UpdateDialog.UpdateNow}" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
Command="{Binding ToggleRemindOptionsCommand}"
|
||||
Content="{Tr UpdateDialog.RemindLater}" />
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Command="{Binding SkipVersionCommand}"
|
||||
Content="{Tr UpdateDialog.SkipVersion}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Remind later options -->
|
||||
<StackPanel Visibility="{Binding ShowRemindOptions, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock Text="{Tr UpdateDialog.RemindAfter}" Margin="0,0,0,8" FontSize="14" />
|
||||
<WrapPanel>
|
||||
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
|
||||
Command="{Binding SelectRemindOptionCommand}"
|
||||
CommandParameter="1H"
|
||||
Content="{Tr UpdateDialog.RemindOptions.1Hour}" />
|
||||
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
|
||||
Command="{Binding SelectRemindOptionCommand}"
|
||||
CommandParameter="1D"
|
||||
Content="{Tr UpdateDialog.RemindOptions.1Day}" />
|
||||
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
|
||||
Command="{Binding SelectRemindOptionCommand}"
|
||||
CommandParameter="3D"
|
||||
Content="{Tr UpdateDialog.RemindOptions.3Days}" />
|
||||
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
|
||||
Command="{Binding SelectRemindOptionCommand}"
|
||||
CommandParameter="7D"
|
||||
Content="{Tr UpdateDialog.RemindOptions.7Days}" />
|
||||
</WrapPanel>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
HorizontalAlignment="Left"
|
||||
Command="{Binding ToggleRemindOptionsCommand}"
|
||||
Content="{Tr Common.Cancel}" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NebulaAuth.View.Dialogs;
|
||||
|
||||
public partial class UpdateDialog
|
||||
{
|
||||
public UpdateDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@
|
||||
</Button>
|
||||
|
||||
<Button Margin="0,0,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Click="Website_Click" IsEnabled="False">
|
||||
Click="Documentation_Click" IsEnabled="False">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Viewbox Width="22" Height="22" Margin="0 0 8 0">
|
||||
<Canvas Width="22" Height="22">
|
||||
@@ -72,6 +72,15 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Margin="0,5,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Click="CheckForUpdates_Click">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="Update" Width="22" Height="22" Margin="0 0 8 0"
|
||||
Foreground="{DynamicResource MaterialDesign.Brush.Primary}" />
|
||||
<TextBlock Text="{Tr LinksView.CheckForUpdates}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" IsCancel="True" Width="0"
|
||||
Height="0" Visibility="Visible" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using NebulaAuth.Core;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
@@ -31,4 +33,10 @@ public partial class LinksView : UserControl
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("https://yourwebsite.com") {UseShellExecute = true});
|
||||
}
|
||||
|
||||
private void CheckForUpdates_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogHost.Close(null);
|
||||
UpdateManager.CheckForUpdates(manual: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<UserControl x:Class="NebulaAuth.View.MafileExporterView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
|
||||
mc:Ignorable="d"
|
||||
TextElement.Foreground="{DynamicResource BaseContentBrush}"
|
||||
FontFamily="{materialDesign:MaterialDesignFont}"
|
||||
MinWidth="400"
|
||||
MaxWidth="400"
|
||||
Background="{DynamicResource WindowBackground}"
|
||||
d:DataContext="{d:DesignInstance other:MafileExporterVM}"
|
||||
d:Background="#141119"
|
||||
d:Foreground="White"
|
||||
FontSize="18">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="FileExport" Width="20" Height="20" Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryContentBrush}" />
|
||||
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" FontSize="18" Text="{Tr ExportDialog.ExportTitle}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||
IsCancel="True">
|
||||
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Separator Background="DarkGray" Opacity="0.4" Grid.Row="1" />
|
||||
<Grid Grid.Row="2" Margin="15" TextElement.FontSize="14">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ComboBox Grid.Column="0"
|
||||
Padding="10"
|
||||
Style="{StaticResource MaterialDesignOutlinedComboBox}"
|
||||
Margin="0,0,5,0"
|
||||
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
|
||||
ItemsSource="{Binding Templates}"
|
||||
DisplayMemberPath="Name"
|
||||
SelectedItem="{Binding CurrentTemplate}"
|
||||
Visibility="{Binding EditMode, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
|
||||
|
||||
<TextBox x:Name="EditTemplateNameTextBox"
|
||||
Grid.Column="0"
|
||||
Padding="10"
|
||||
Margin="0,0,5,0"
|
||||
Style="{StaticResource MaterialDesignOutlinedTextBox}"
|
||||
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
|
||||
Text="{Binding CurrentTemplate.Name,
|
||||
FallbackValue=''}"
|
||||
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
|
||||
Visibility="{Binding EditMode, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter"
|
||||
Command="{Binding ToggleEditModeCommand}" />
|
||||
</TextBox.InputBindings>
|
||||
|
||||
</TextBox>
|
||||
|
||||
|
||||
<Button Command="{Binding AddTemplateCommand}" Grid.Column="1">
|
||||
<materialDesign:PackIcon Kind="Add" />
|
||||
</Button>
|
||||
|
||||
<Button Command="{Binding ToggleEditModeCommand}" Click="EditButton_Click" Grid.Column="2">
|
||||
<materialDesign:PackIcon Kind="Edit" />
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="3" Command="{Binding RemoveCurrentTemplateCommand}">
|
||||
<materialDesign:PackIcon Kind="Trash" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="0,15,0,0" Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
|
||||
MouseDoubleClick="PathTB_DoubleClick" materialDesign:TextFieldAssist.HasClearButton="True"
|
||||
Text="{Binding CurrentTemplate.Path, FallbackValue=''}" Padding="10"
|
||||
Style="{StaticResource MaterialDesignOutlinedTextBox}" Grid.Column="0"
|
||||
materialDesign:HintAssist.Hint="{Tr ExportDialog.SelectPathHint}" />
|
||||
<Button Command="{Binding OpenSelectDirectoryDialogCommand}" Grid.Column="1" Margin="5,0,0,0">
|
||||
<materialDesign:PackIcon Kind="FolderOpen" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="2" BorderThickness="1" CornerRadius="4" Margin="0,15,0,0" Padding="2">
|
||||
<Border.BorderBrush>
|
||||
<SolidColorBrush Color="{DynamicResource BaseContentColor}" Opacity="0.4" />
|
||||
</Border.BorderBrush>
|
||||
<Expander x:Name="TemplateExpander"
|
||||
Header="{Tr ExportDialog.ExportSettingsHeader}"
|
||||
materialDesign:ExpanderAssist.HorizontalHeaderPadding="5"
|
||||
materialDesign:ExpanderAssist.ExpanderButtonPosition="Start"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="5,0,5,5"
|
||||
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock FontWeight="DemiBold"
|
||||
Text="{Tr ExportDialog.ExportSettingsCommon}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding CurrentTemplate.UseLoginAsMafileName, FallbackValue=False}"
|
||||
Margin="3,0,0,0"
|
||||
Content="{Tr ExportDialog.ExportOptions.UseLoginAsMafileName}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.UseLoginAsMafileName}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSharedSecret, FallbackValue=False}"
|
||||
Margin="3,0,0,0"
|
||||
Content="{Tr ExportDialog.ExportOptions.IncludeSharedSecret}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.IncludeSharedSecret}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeIdentitySecret, FallbackValue=False}"
|
||||
Margin="3,0,0,0"
|
||||
Content="{Tr ExportDialog.ExportOptions.IncludeIdentitySecret}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.IncludeIdentitySecret}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeRCode, FallbackValue=False}"
|
||||
Margin="3,0,0,0"
|
||||
Content="{Tr ExportDialog.ExportOptions.IncludeRCode}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.IncludeRCode}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSessionData, FallbackValue=False}"
|
||||
Margin="3,0,0,0"
|
||||
Content="{Tr ExportDialog.ExportOptions.IncludeSessionData}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.IncludeSessionData}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeOtherInfo, FallbackValue=False}"
|
||||
Margin="3,0,0,0"
|
||||
Content="{Tr ExportDialog.ExportOptions.IncludeOtherInfo}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.IncludeOtherInfo}" />
|
||||
|
||||
<TextBlock FontWeight="DemiBold"
|
||||
Margin="0,10,0,0"
|
||||
Text="{Tr ExportDialog.ExportSettingsNebula}" />
|
||||
|
||||
<Grid Margin="3,0,3,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<CheckBox Grid.Column="0"
|
||||
IsChecked="{Binding CurrentTemplate.IncludeNebulaProxy, FallbackValue=False}"
|
||||
Content="{Tr ExportDialog.ExportOptions.NebulaProxy}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.NebulaProxy}" />
|
||||
|
||||
<CheckBox Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
IsChecked="{Binding CurrentTemplate.IncludeNebulaPassword, FallbackValue=False}"
|
||||
Content="{Tr ExportDialog.ExportOptions.NebulaPassword}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.NebulaPassword}" />
|
||||
|
||||
<CheckBox Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{Binding CurrentTemplate.IncludeNebulaGroup, FallbackValue=False}"
|
||||
Content="{Tr ExportDialog.ExportOptions.NebulaGroup}"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTip="{Tr ExportDialog.Tooltips.NebulaGroup}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
</Expander>
|
||||
</Border>
|
||||
<TextBox GotFocus="ExportTB_GotFocus" PreviewMouseUp="ExportTB_GotFocus" Grid.Row="3"
|
||||
Style="{StaticResource MaterialDesignOutlinedTextBox}" MaxHeight="600" Margin="0,10,0,0"
|
||||
MinHeight="250" Height="250" AcceptsReturn="True"
|
||||
Text="{Binding AccountsText, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding ExportCommand.IsRunning, Converter={StaticResource ReverseBooleanConverter}}"
|
||||
Name="AccountsTB" />
|
||||
|
||||
|
||||
<TextBlock Grid.Row="3"
|
||||
Text="{Tr ExportDialog.ExportAccountsPlaceholder}"
|
||||
Margin="18,27,10,10"
|
||||
Opacity="0.6"
|
||||
IsHitTestVisible="False"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{Binding Text.IsEmpty,
|
||||
ElementName=AccountsTB,
|
||||
Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||
<nebulaAuth:HintBox Margin="0,10,0,0" Severity="{Binding HintBoxSeverity}" Grid.Row="4"
|
||||
Text="{Binding HintText}" />
|
||||
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
<Separator Grid.Row="3" />
|
||||
<Button Command="{Binding ExportCommand}" Style="{StaticResource MaterialDesignFlatDarkBgButton}" Grid.Row="4"
|
||||
Margin="20,7,20,7" MaxWidth="150" Content="{Tr ExportDialog.ExportButton}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace NebulaAuth.View;
|
||||
|
||||
public partial class MafileExporterView : UserControl
|
||||
{
|
||||
public MafileExporterView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void PathTB_DoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is TextBox textBox)
|
||||
{
|
||||
textBox.SelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExportTB_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
TemplateExpander.IsExpanded = false;
|
||||
}
|
||||
|
||||
private void EditButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Dispatcher.BeginInvoke(() => { EditTemplateNameTextBox.Focus(); }, DispatcherPriority.Input);
|
||||
}
|
||||
}
|
||||
@@ -55,27 +55,19 @@
|
||||
ItemsSource="{Binding Languages}" DisplayMemberPath="Value" SelectedValuePath="Key"
|
||||
SelectedValue="{Binding Language}"
|
||||
materialDesign:HintAssist.Hint="{Tr LanguageWord}" />
|
||||
<CheckBox Margin="0,18,0,0" IsChecked="{Binding HideToTray}"
|
||||
<CheckBox Margin="0,5,0,0" IsChecked="{Binding HideToTray}"
|
||||
Content="{Tr SettingsDialog.MinimizeToTray}" />
|
||||
|
||||
|
||||
<CheckBox Margin="0,18,0,0" IsChecked="{Binding LegacyMode}"
|
||||
<CheckBox Margin="0,5,0,0" IsChecked="{Binding LegacyMode}"
|
||||
Content="{Tr SettingsDialog.LegacyMafileMode}"
|
||||
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
|
||||
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" BorderBrush="AliceBlue"
|
||||
Margin="0,18,0,0"
|
||||
BorderThickness="2"
|
||||
IsChecked="{Binding IgnorePatchTuesdayErrors}"
|
||||
ToolTip="{Tr SettingsDialog.IgnorePatchTuesdayErrorsHint}">
|
||||
<TextBlock TextWrapping="WrapWithOverflow"
|
||||
Text="{Tr SettingsDialog.IgnorePatchTuesdayErrors}" />
|
||||
</CheckBox>
|
||||
|
||||
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
|
||||
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
|
||||
</CheckBox>-->
|
||||
<CheckBox Margin="0,20,0,0" IsChecked="{Binding UseIcon}"
|
||||
<CheckBox Margin="0,5,0,0" IsChecked="{Binding UseIcon}"
|
||||
Content="{Tr SettingsDialog.UseIndicator}"
|
||||
ToolTip="{Tr SettingsDialog.UseIndicatorHint}" />
|
||||
<materialDesign:ColorPicker Margin="-5,0,-5,0" IsEnabled="{Binding UseIcon}"
|
||||
@@ -191,10 +183,10 @@
|
||||
PreviewMouseUp="Slider_OnMouseUp" />
|
||||
|
||||
|
||||
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
|
||||
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
|
||||
IsChecked="{Binding ApplyBlurBackground}"
|
||||
FontSize="15" />
|
||||
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
|
||||
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
|
||||
FontSize="15"
|
||||
IsChecked="{Binding RippleDisabled}" />
|
||||
<Button Margin="0,15,0,0" Command="{Binding ResetThemeDefaultsCommand}"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<UserControl x:Class="NebulaAuth.View.UpdaterView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
|
||||
mc:Ignorable="d"
|
||||
Foreground="WhiteSmoke"
|
||||
FontFamily="{materialDesign:MaterialDesignFont}"
|
||||
d:DataContext="{d:DesignInstance other:UpdaterVM}"
|
||||
Background="{DynamicResource WindowBackground}"
|
||||
Padding="10"
|
||||
MaxWidth="500">
|
||||
<d:DesignerProperties.DesignStyle>
|
||||
<Style TargetType="UserControl">
|
||||
<Setter Property="Background" Value="{DynamicResource WindowBackground}" />
|
||||
</Style>
|
||||
</d:DesignerProperties.DesignStyle>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock FontSize="24" Text="Обновления" />
|
||||
<Separator Grid.Row="1" />
|
||||
|
||||
<TextBlock FontSize="16" Margin="5,10,0,10" TextWrapping="WrapWithOverflow" Grid.Row="2">
|
||||
<Run Text="Доступна новая версия:" />
|
||||
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.CurrentVersion}" />
|
||||
<Run Text="
Вы используете версию" />
|
||||
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.InstalledVersion}" />
|
||||
<Run Text="Хотите обновить программу?" />
|
||||
</TextBlock>
|
||||
<Expander Header="Что изменилось?" Grid.Row="3">
|
||||
<!--<wpf:WebView2 HorizontalAlignment="Stretch" Height="300"
|
||||
Source="{Binding UpdateInfoEventArgs.ChangelogURL}"
|
||||
/>-->
|
||||
</Expander>
|
||||
<Grid Grid.Row="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Content="OK" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
|
||||
<Button IsCancel="True" Grid.Column="1" Content="Cancel"
|
||||
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace NebulaAuth.View;
|
||||
|
||||
public partial class UpdaterView
|
||||
{
|
||||
public UpdaterView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using NebulaAuth.Utility;
|
||||
using NLog;
|
||||
using SteamLib;
|
||||
@@ -224,7 +225,7 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
|
||||
Logger.Error(ex, "Error during saving Nebula data to mafile");
|
||||
}
|
||||
|
||||
Storage.SaveMafile(mafile);
|
||||
await Storage.SaveMafileAsync(mafile);
|
||||
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString(), login);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using NebulaAuth.Utility;
|
||||
using NebulaAuth.ViewModel.Linker;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -10,8 +10,8 @@ using MaterialDesignThemes.Wpf;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using NebulaAuth.Utility;
|
||||
using NebulaAuth.View;
|
||||
using NebulaAuth.View.Dialogs;
|
||||
using SteamLib.SteamMobile;
|
||||
using SteamLibForked.Exceptions.Authorization;
|
||||
@@ -22,7 +22,6 @@ public partial class MainVM : ObservableObject
|
||||
{
|
||||
[UsedImplicitly] public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
|
||||
|
||||
|
||||
public Mafile? SelectedMafile
|
||||
{
|
||||
get => _selectedMafile;
|
||||
@@ -32,7 +31,7 @@ public partial class MainVM : ObservableObject
|
||||
public bool IsMafileSelected => SelectedMafile != null;
|
||||
public DialogHost CurrentDialogHost { get; set; } = null!;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = Storage.MaFiles;
|
||||
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = new(Storage.MaFiles);
|
||||
|
||||
private Mafile? _selectedMafile;
|
||||
|
||||
@@ -43,16 +42,14 @@ public partial class MainVM : ObservableObject
|
||||
new MaProxy(kvp.Key, kvp.Value)));
|
||||
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
|
||||
QueryGroups();
|
||||
UpdateManager.CheckForUpdates();
|
||||
UpdateManager.CheckForUpdates(manual: false);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task Debug()
|
||||
{
|
||||
await DialogsController.ShowSetAccountsPasswordDialog();
|
||||
}
|
||||
|
||||
|
||||
private void SetMafile(Mafile? mafile)
|
||||
{
|
||||
if (mafile != SelectedMafile)
|
||||
@@ -71,7 +68,9 @@ public partial class MainVM : ObservableObject
|
||||
LoginAgainCommand.NotifyCanExecuteChanged();
|
||||
RemoveAuthenticatorCommand.NotifyCanExecuteChanged();
|
||||
ConfirmLoginCommand.NotifyCanExecuteChanged();
|
||||
RemoveProxyCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +113,7 @@ public partial class MainVM : ObservableObject
|
||||
|
||||
private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
SearchText = string.Empty;
|
||||
PerformQuery();
|
||||
}
|
||||
|
||||
|
||||
@@ -134,12 +133,6 @@ public partial class MainVM : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task LinkAccount()
|
||||
{
|
||||
await DialogsController.ShowLinkerDialog();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task MoveAccount()
|
||||
{
|
||||
@@ -172,7 +165,6 @@ public partial class MainVM : ObservableObject
|
||||
if (result.Success)
|
||||
{
|
||||
Storage.MoveToRemoved(selectedMafile);
|
||||
MaFiles.Remove(selectedMafile);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -218,16 +210,6 @@ public partial class MainVM : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenLinksView()
|
||||
{
|
||||
CurrentDialogHost.CloseOnClickAway = true;
|
||||
var view = new LinksView();
|
||||
await DialogHost.Show(view);
|
||||
CurrentDialogHost.CloseOnClickAway = false;
|
||||
}
|
||||
|
||||
|
||||
private static string GetLocalization(string key)
|
||||
{
|
||||
const string locPath = "MainVM";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
@@ -76,18 +76,27 @@ public partial class MainVM //Confirmations
|
||||
return SendConfirmation(SelectedMafile, confirmation, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
|
||||
{
|
||||
bool result;
|
||||
|
||||
try
|
||||
{
|
||||
if (confirmation is MarketMultiConfirmation multi)
|
||||
switch (confirmation)
|
||||
{
|
||||
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
|
||||
case MarketMultiConfirmation multi:
|
||||
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
|
||||
break;
|
||||
|
||||
case MarketConfirmation market:
|
||||
result = await MaClient.SendConfirmation(mafile, market, confirm);
|
||||
break;
|
||||
|
||||
default:
|
||||
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -96,13 +105,49 @@ public partial class MainVM //Confirmations
|
||||
return;
|
||||
}
|
||||
|
||||
if (result)
|
||||
{
|
||||
Confirmations.Remove(confirmation);
|
||||
}
|
||||
else
|
||||
if (!result)
|
||||
{
|
||||
SnackbarController.SendSnackbar(GetLocalization("ConfirmationError"));
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateConfirmationState(confirmation);
|
||||
}
|
||||
|
||||
private void UpdateConfirmationState(Confirmation confirmation)
|
||||
{
|
||||
if (confirmation is MarketConfirmation item)
|
||||
{
|
||||
var parent = Confirmations
|
||||
.OfType<MarketMultiConfirmation>()
|
||||
.FirstOrDefault(p => p.Confirmations.Contains(item));
|
||||
|
||||
if (parent == null)
|
||||
{
|
||||
Confirmations.Remove(item);
|
||||
return;
|
||||
}
|
||||
|
||||
parent.Confirmations.Remove(item);
|
||||
|
||||
if (parent.Confirmations.Count == 0)
|
||||
{
|
||||
Confirmations.Remove(parent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parent.Confirmations.Count == 1)
|
||||
{
|
||||
var remaining = parent.Confirmations[0];
|
||||
var idx = Confirmations.IndexOf(parent);
|
||||
Confirmations[idx] = remaining;
|
||||
return;
|
||||
}
|
||||
|
||||
parent.Time = parent.Confirmations.FirstOrDefault()?.Time ?? parent.Time;
|
||||
return;
|
||||
}
|
||||
|
||||
Confirmations.Remove(confirmation);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -13,6 +13,8 @@ using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Exceptions;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using NebulaAuth.Model.MafilesLegacy;
|
||||
using NebulaAuth.Utility;
|
||||
using NebulaAuth.View;
|
||||
using NebulaAuth.View.Dialogs;
|
||||
@@ -25,6 +27,7 @@ public partial class MainVM //File //TODO: Refactor
|
||||
{
|
||||
public Settings Settings => Settings.Instance;
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenMafileFolder()
|
||||
{
|
||||
@@ -57,48 +60,50 @@ public partial class MainVM //File //TODO: Refactor
|
||||
[RelayCommand]
|
||||
private Task AddMafile()
|
||||
{
|
||||
var openFileDialog = new OpenFileDialog
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "Mafile|*.mafile;*.maFile",
|
||||
Multiselect = false
|
||||
};
|
||||
var fs = openFileDialog.ShowDialog();
|
||||
if (fs != true) return Task.CompletedTask;
|
||||
var path = openFileDialog.FileName;
|
||||
return AddMafile([path]);
|
||||
|
||||
return dialog.ShowDialog() == true
|
||||
? AddMafile([dialog.FileName])
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task AddMafile(string[] path)
|
||||
{
|
||||
bool? confirmOverwrite = null;
|
||||
var added = 0;
|
||||
var notAdded = 0;
|
||||
var errors = 0;
|
||||
var summary = new MafileImportSummary();
|
||||
SDAEncryptionHelper.Context? sdaContext = null;
|
||||
var sdaPasswordPrompted = false;
|
||||
foreach (var str in path)
|
||||
{
|
||||
try
|
||||
{
|
||||
Storage.AddNewMafile(str, confirmOverwrite ?? false);
|
||||
added++;
|
||||
var (mafile, passwordPrompted, context) = await TryReadMafile(str, sdaContext, sdaPasswordPrompted);
|
||||
sdaContext = context;
|
||||
sdaPasswordPrompted = passwordPrompted;
|
||||
if (mafile == null)
|
||||
{
|
||||
summary.ErrorOne();
|
||||
continue;
|
||||
}
|
||||
|
||||
var overwrite = confirmOverwrite ?? false;
|
||||
var res = await Storage.AddNewMafileFromData(mafile, overwrite);
|
||||
if (res == AddMafileResult.AlreadyExist && confirmOverwrite == null)
|
||||
{
|
||||
confirmOverwrite =
|
||||
await DialogsController.ShowConfirmCancelDialog(GetLocalization("ConfirmMafileOverwrite"));
|
||||
res = await Storage.AddNewMafileFromData(mafile, confirmOverwrite.Value);
|
||||
}
|
||||
|
||||
summary.Apply(res);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
errors++;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
confirmOverwrite ??=
|
||||
await DialogsController.ShowConfirmCancelDialog(GetLocalization("ConfirmMafileOverwrite"));
|
||||
|
||||
if (confirmOverwrite == true)
|
||||
{
|
||||
Storage.AddNewMafile(str, true);
|
||||
added++;
|
||||
}
|
||||
else if (confirmOverwrite == false)
|
||||
{
|
||||
notAdded++;
|
||||
}
|
||||
summary.ErrorOne();
|
||||
}
|
||||
catch (MafileNeedReloginException ex)
|
||||
{
|
||||
@@ -107,11 +112,11 @@ public partial class MainVM //File //TODO: Refactor
|
||||
var mafile = ex.Mafile;
|
||||
if (await HandleAddMafileWithoutSession(mafile))
|
||||
{
|
||||
added++;
|
||||
summary.AddedOne();
|
||||
}
|
||||
else
|
||||
{
|
||||
errors++;
|
||||
summary.ErrorOne();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -123,23 +128,50 @@ public partial class MainVM //File //TODO: Refactor
|
||||
}
|
||||
}
|
||||
|
||||
var msg = GetLocalization("Import");
|
||||
if (added > 0)
|
||||
{
|
||||
msg += $" {GetLocalization("ImportAdded")} {added}.";
|
||||
}
|
||||
ShowImportSummary(summary);
|
||||
}
|
||||
|
||||
if (notAdded > 0)
|
||||
{
|
||||
msg += $" {GetLocalization("ImportSkipped")} {notAdded}.";
|
||||
}
|
||||
|
||||
if (errors > 0)
|
||||
private async Task<MafileReadResult> TryReadMafile(string path, SDAEncryptionHelper.Context? sdaContext,
|
||||
bool sdaPasswordPrompted)
|
||||
{
|
||||
try
|
||||
{
|
||||
msg += $" {GetLocalization("ImportErrors")} {errors}.";
|
||||
}
|
||||
var content = await File.ReadAllTextAsync(path);
|
||||
Mafile mafile;
|
||||
if (!SDAEncryptionHelper.LooksLikeSdaEncryptedBlob(content))
|
||||
{
|
||||
mafile = NebulaSerializer.Deserialize(content, path);
|
||||
return new MafileReadResult(mafile, sdaPasswordPrompted, sdaContext);
|
||||
}
|
||||
|
||||
SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2));
|
||||
// Looks like encrypted, let's see if we have manifest
|
||||
sdaContext ??= SDAEncryptionHelper.TryDetect(path, sdaContext?.SdaManifest);
|
||||
if (sdaContext != null)
|
||||
{
|
||||
// We have manifest, but we must ensure we have password
|
||||
if (sdaContext.Password == null && !sdaPasswordPrompted)
|
||||
{
|
||||
sdaPasswordPrompted = true;
|
||||
var password = await DialogsController.ShowSdaPasswordDialog();
|
||||
sdaContext = sdaContext.WithPassword(password);
|
||||
}
|
||||
|
||||
var decrypted = SDAEncryptionHelper.TryDecrypt(content, path, sdaContext);
|
||||
if (decrypted == null) return new MafileReadResult(null, sdaPasswordPrompted, sdaContext);
|
||||
content = decrypted;
|
||||
}
|
||||
|
||||
// If we are here, it means that either file is not encrypted, or we successfully decrypted it
|
||||
mafile = NebulaSerializer.Deserialize(content, path);
|
||||
return new MafileReadResult(mafile, sdaPasswordPrompted, sdaContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is not MafileNeedReloginException)
|
||||
{
|
||||
Shell.Logger.Warn(ex, "Failed to import mafile");
|
||||
throw new FormatException($"Failed to read mafile {Path.GetFileName(path)}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HandleAddMafileWithoutSession(Mafile data)
|
||||
@@ -190,6 +222,28 @@ public partial class MainVM //File //TODO: Refactor
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ShowImportSummary(MafileImportSummary summary)
|
||||
{
|
||||
var msg = GetLocalization("Import");
|
||||
if (summary.Added > 0)
|
||||
{
|
||||
msg += $" {GetLocalization("ImportAdded")} {summary.Added}.";
|
||||
}
|
||||
|
||||
if (summary.NotAdded > 0)
|
||||
{
|
||||
msg += $" {GetLocalization("ImportSkipped")} {summary.NotAdded}.";
|
||||
}
|
||||
|
||||
if (summary.Errors > 0)
|
||||
{
|
||||
msg += $" {GetLocalization("ImportErrors")} {summary.Errors}.";
|
||||
}
|
||||
|
||||
SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RemoveMafile()
|
||||
{
|
||||
@@ -239,7 +293,7 @@ public partial class MainVM //File //TODO: Refactor
|
||||
}
|
||||
|
||||
var arr = files.Cast<string>().ToArray();
|
||||
if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return;
|
||||
if (arr.All(p => !p.ContainsIgnoreCase("mafile"))) return;
|
||||
|
||||
|
||||
await AddMafile(arr);
|
||||
@@ -293,4 +347,9 @@ public partial class MainVM //File //TODO: Refactor
|
||||
if (mafile is not Mafile maf) return false;
|
||||
return maf.Password != null && PHandler.IsPasswordSet;
|
||||
}
|
||||
|
||||
private record MafileReadResult(
|
||||
Mafile? Mafile,
|
||||
bool SdaPasswordPrompted,
|
||||
SDAEncryptionHelper.Context? SdaContext);
|
||||
}
|
||||
@@ -5,8 +5,8 @@ using AchiesUtilities.Extensions;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
|
||||
namespace NebulaAuth.ViewModel;
|
||||
|
||||
@@ -88,7 +88,7 @@ public partial class MainVM //Groups
|
||||
Storage.UpdateMafile(mafile);
|
||||
OnPropertyChanged(nameof(SelectedMafile)); //For bindings
|
||||
QueryGroups();
|
||||
if (Groups.All(g => g.Equals(mafGroup) == false))
|
||||
if (Groups.All(g => !g.Equals(mafGroup)))
|
||||
{
|
||||
SelectedGroup = null;
|
||||
}
|
||||
@@ -104,7 +104,7 @@ public partial class MainVM //Groups
|
||||
private void QueryGroups()
|
||||
{
|
||||
var groups = Storage.MaFiles
|
||||
.Where(m => string.IsNullOrWhiteSpace(m.Group) == false)
|
||||
.Where(m => !string.IsNullOrWhiteSpace(m.Group))
|
||||
.Select(m => m.Group)
|
||||
.Distinct()
|
||||
.Order()
|
||||
|
||||
@@ -130,7 +130,7 @@ public partial class MainVM //MAAC
|
||||
{
|
||||
mafiles = mafiles.ToArray();
|
||||
|
||||
var turnOn = mafiles.All(m => m.LinkedClient == null || GetCurrentMode(m.LinkedClient) == false);
|
||||
var turnOn = mafiles.All(m => m.LinkedClient == null || !GetCurrentMode(m.LinkedClient));
|
||||
if (turnOn)
|
||||
{
|
||||
foreach (var mafile in mafiles)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.View;
|
||||
|
||||
namespace NebulaAuth.ViewModel;
|
||||
|
||||
public partial class MainVM : ObservableObject
|
||||
{
|
||||
[RelayCommand]
|
||||
private Task OpenSetPasswordsDialog()
|
||||
{
|
||||
return DialogsController.ShowSetAccountsPasswordDialog();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task OpenExporterDialog()
|
||||
{
|
||||
return DialogsController.ShowExportDialog();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public Task LinkAccount()
|
||||
{
|
||||
return DialogsController.ShowLinkerDialog();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenLinksView()
|
||||
{
|
||||
CurrentDialogHost.CloseOnClickAway = true;
|
||||
var view = new LinksView();
|
||||
await DialogHost.Show(view);
|
||||
CurrentDialogHost.CloseOnClickAway = false;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
|
||||
namespace NebulaAuth.ViewModel;
|
||||
|
||||
@@ -68,13 +69,20 @@ public partial class MainVM
|
||||
CheckProxyExist();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveProxy()
|
||||
[RelayCommand(CanExecute = nameof(RemoveProxyCanExecute))]
|
||||
private void RemoveProxy(Mafile? mafile)
|
||||
{
|
||||
if (SelectedProxy == null) return;
|
||||
if (SelectedMafile == null) return;
|
||||
SelectedMafile.Proxy = null;
|
||||
SetProxy(null, false); //Not system, triggered by user
|
||||
mafile ??= SelectedMafile;
|
||||
if (mafile?.Proxy == null) return;
|
||||
mafile.Proxy = null;
|
||||
if (mafile == SelectedMafile)
|
||||
SetProxy(null, false); //Not system, triggered by user
|
||||
}
|
||||
|
||||
private bool RemoveProxyCanExecute(Mafile? mafile)
|
||||
{
|
||||
mafile ??= SelectedMafile;
|
||||
return mafile is { Proxy: not null };
|
||||
}
|
||||
|
||||
private void CheckProxyExist()
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.MafileExport;
|
||||
|
||||
namespace NebulaAuth.ViewModel.Other;
|
||||
|
||||
public partial class MafileExporterVM : ObservableObject
|
||||
{
|
||||
public MafileExportTemplateVM? CurrentTemplate
|
||||
{
|
||||
get => _currentTemplate;
|
||||
set => SetCurrentTemplate(value);
|
||||
}
|
||||
|
||||
|
||||
public ObservableCollection<MafileExportTemplateVM> Templates { get; }
|
||||
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ExportCommand))]
|
||||
private string? _accountsText;
|
||||
|
||||
private string? _cachedName;
|
||||
private MafileExportTemplateVM? _currentTemplate;
|
||||
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddTemplateCommand), nameof(RemoveCurrentTemplateCommand))]
|
||||
private bool _editMode;
|
||||
|
||||
[ObservableProperty] private HintBoxSeverity _hintBoxSeverity = HintBoxSeverity.Error;
|
||||
|
||||
[ObservableProperty] private string? _hintText;
|
||||
|
||||
public MafileExporterVM()
|
||||
{
|
||||
var templates = MafileExporterStorage.Templates
|
||||
.Select(MafileExportTemplateVM.FromModel);
|
||||
|
||||
|
||||
Templates = new ObservableCollection<MafileExportTemplateVM>(templates);
|
||||
MafileExporterStorage.Templates.CollectionChanged += OnTemplatesOnCollectionChanged;
|
||||
CurrentTemplate = Templates.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void SetCurrentTemplate(MafileExportTemplateVM? value)
|
||||
{
|
||||
if (_currentTemplate != null)
|
||||
{
|
||||
_currentTemplate.PropertyChanged -= OnCurrentTemplatePropertyChanged;
|
||||
}
|
||||
|
||||
SetProperty(ref _currentTemplate, value, nameof(CurrentTemplate));
|
||||
if (value != null)
|
||||
{
|
||||
value.PropertyChanged += OnCurrentTemplatePropertyChanged;
|
||||
}
|
||||
|
||||
ToggleEditModeCommand.NotifyCanExecuteChanged();
|
||||
RemoveCurrentTemplateCommand.NotifyCanExecuteChanged();
|
||||
OpenSelectDirectoryDialogCommand.NotifyCanExecuteChanged();
|
||||
ExportCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void OnTemplatesOnCollectionChanged(object? s, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (MafileExportTemplate newItem in e.NewItems!)
|
||||
{
|
||||
Templates.Add(MafileExportTemplateVM.FromModel(newItem));
|
||||
}
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (MafileExportTemplate oldItem in e.OldItems!)
|
||||
{
|
||||
var vmToRemove = Templates.FirstOrDefault(x => x.Name == oldItem.Name);
|
||||
if (vmToRemove != null)
|
||||
{
|
||||
Templates.Remove(vmToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
foreach (MafileExportTemplate newItem in e.NewItems!)
|
||||
{
|
||||
var vmToReplace = Templates.FirstOrDefault(x => x.Name == newItem.Name);
|
||||
if (vmToReplace != null)
|
||||
{
|
||||
var index = Templates.IndexOf(vmToReplace);
|
||||
Templates[index] = MafileExportTemplateVM.FromModel(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
Templates.Clear();
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCurrentTemplatePropertyChanged(object? sender, PropertyChangedEventArgs propertyChangedEventArgs)
|
||||
{
|
||||
var currentTemplate = CurrentTemplate;
|
||||
if (currentTemplate == null) return;
|
||||
var key = propertyChangedEventArgs.PropertyName == nameof(MafileExportTemplateVM.Name)
|
||||
? _cachedName!
|
||||
: currentTemplate.Name;
|
||||
|
||||
var existed = MafileExporterStorage.GetTemplate(key);
|
||||
if (existed != null)
|
||||
{
|
||||
currentTemplate.ApplyTo(existed);
|
||||
MafileExporterStorage.Save();
|
||||
}
|
||||
else
|
||||
{
|
||||
MafileExporterStorage.AddTemplate(currentTemplate.ToModel());
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNull))]
|
||||
private void ToggleEditMode()
|
||||
{
|
||||
EditMode = !EditMode;
|
||||
if (EditMode) _cachedName = CurrentTemplate?.Name;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(NotEditMode))]
|
||||
private void AddTemplate()
|
||||
{
|
||||
ResetHintText();
|
||||
var name = "New Template";
|
||||
var i = 0;
|
||||
while (Templates.Any(x => x.Name == name))
|
||||
{
|
||||
name = $"New Template ({++i})";
|
||||
}
|
||||
|
||||
var template = new MafileExportTemplate
|
||||
{
|
||||
Name = name,
|
||||
UseLoginAsMafileName = false,
|
||||
IncludeSharedSecret = true,
|
||||
IncludeIdentitySecret = true,
|
||||
IncludeRCode = true,
|
||||
IncludeSessionData = true,
|
||||
IncludeOtherInfo = true,
|
||||
IncludeNebulaProxy = true,
|
||||
IncludeNebulaPassword = true,
|
||||
IncludeNebulaGroup = true,
|
||||
Path = null
|
||||
};
|
||||
MafileExporterStorage.AddTemplate(template);
|
||||
CurrentTemplate = Templates.FirstOrDefault(x => x.Name == name);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNullAndNotEditMode))]
|
||||
private void RemoveCurrentTemplate()
|
||||
{
|
||||
if (CurrentTemplate != null)
|
||||
{
|
||||
ResetHintText();
|
||||
var index = Templates.IndexOf(CurrentTemplate);
|
||||
var existed = MafileExporterStorage.GetTemplate(CurrentTemplate.Name);
|
||||
if (existed != null)
|
||||
{
|
||||
MafileExporterStorage.DeleteTemplate(existed);
|
||||
}
|
||||
|
||||
if (Templates.Count > 0)
|
||||
{
|
||||
if (index >= Templates.Count)
|
||||
{
|
||||
index = Templates.Count - 1;
|
||||
}
|
||||
|
||||
CurrentTemplate = Templates[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentTemplate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNull))]
|
||||
private void OpenSelectDirectoryDialog()
|
||||
{
|
||||
if (CurrentTemplate == null) return;
|
||||
var dialog = new FolderBrowserDialog
|
||||
{
|
||||
UseDescriptionForTitle = true,
|
||||
ShowNewFolderButton = true
|
||||
};
|
||||
var result = dialog.ShowDialog();
|
||||
if (result == DialogResult.OK)
|
||||
{
|
||||
CurrentTemplate!.Path = dialog.SelectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(ExportCanExecute))]
|
||||
private async Task Export()
|
||||
{
|
||||
var lines = AccountsText;
|
||||
var template = CurrentTemplate;
|
||||
if (string.IsNullOrWhiteSpace(lines) || template == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var split = Regex
|
||||
.Split(lines, "\r\n|\r|\n")
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToArray();
|
||||
|
||||
ResetHintText();
|
||||
ExportResult res;
|
||||
try
|
||||
{
|
||||
res = await MafileExporter.ExportMafiles(split, template.ToModel());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetHintText(ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.Success)
|
||||
{
|
||||
SetHintText(res.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var hint = string.Format(LocManager.GetCodeBehindOrDefault("Exported", "MafileExporterVM.Result.Exported"),
|
||||
res.Exported.Count);
|
||||
|
||||
if (res.NotFound.Count > 0)
|
||||
{
|
||||
hint += string.Format(LocManager.GetCodeBehindOrDefault("NotFound", "MafileExporterVM.Result.NotFound"),
|
||||
res.NotFound.Count);
|
||||
}
|
||||
|
||||
if (res.Conflict.Count > 0)
|
||||
{
|
||||
hint += string.Format(LocManager.GetCodeBehindOrDefault("Conflict", "MafileExporterVM.Result.Conflict"),
|
||||
res.Conflict.Count);
|
||||
}
|
||||
|
||||
SetHintText(hint, HintBoxSeverity.Info);
|
||||
var errors = res.NotFound.Concat(res.Conflict).ToHashSet();
|
||||
var errorLines = split.Where(errors.Contains);
|
||||
var text = string.Join(Environment.NewLine, errorLines);
|
||||
|
||||
AccountsText = text;
|
||||
}
|
||||
|
||||
private void SetHintText(string? text, HintBoxSeverity severity = HintBoxSeverity.Error)
|
||||
{
|
||||
HintText = text;
|
||||
HintBoxSeverity = severity;
|
||||
}
|
||||
|
||||
private void ResetHintText()
|
||||
{
|
||||
SetHintText(null);
|
||||
}
|
||||
|
||||
#region CanExecute
|
||||
|
||||
private bool ExportCanExecute()
|
||||
{
|
||||
return CurrentTemplateNotNull() && !string.IsNullOrWhiteSpace(AccountsText);
|
||||
}
|
||||
|
||||
|
||||
private bool CurrentTemplateNotNull()
|
||||
{
|
||||
return CurrentTemplate != null;
|
||||
}
|
||||
|
||||
private bool CurrentTemplateNotNullAndNotEditMode()
|
||||
{
|
||||
return CurrentTemplateNotNull() && NotEditMode();
|
||||
}
|
||||
|
||||
private bool NotEditMode()
|
||||
{
|
||||
return !EditMode;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public partial class MafileExportTemplateVM : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private bool _includeIdentitySecret;
|
||||
[ObservableProperty] private bool _includeNebulaGroup;
|
||||
[ObservableProperty] private bool _includeNebulaPassword;
|
||||
[ObservableProperty] private bool _includeNebulaProxy;
|
||||
[ObservableProperty] private bool _includeOtherInfo;
|
||||
[ObservableProperty] private bool _includeRCode;
|
||||
[ObservableProperty] private bool _includeSessionData;
|
||||
[ObservableProperty] private bool _includeSharedSecret;
|
||||
[ObservableProperty] private string _name;
|
||||
[ObservableProperty] private string? _path;
|
||||
[ObservableProperty] private bool _useLoginAsMafileName;
|
||||
|
||||
|
||||
public static MafileExportTemplateVM FromModel(MafileExportTemplate x)
|
||||
{
|
||||
return new MafileExportTemplateVM
|
||||
{
|
||||
Name = x.Name,
|
||||
UseLoginAsMafileName = x.UseLoginAsMafileName,
|
||||
IncludeSharedSecret = x.IncludeSharedSecret,
|
||||
IncludeIdentitySecret = x.IncludeIdentitySecret,
|
||||
IncludeRCode = x.IncludeRCode,
|
||||
IncludeSessionData = x.IncludeSessionData,
|
||||
IncludeOtherInfo = x.IncludeOtherInfo,
|
||||
IncludeNebulaProxy = x.IncludeNebulaProxy,
|
||||
IncludeNebulaPassword = x.IncludeNebulaPassword,
|
||||
IncludeNebulaGroup = x.IncludeNebulaGroup,
|
||||
Path = x.Path
|
||||
};
|
||||
}
|
||||
|
||||
public MafileExportTemplate ToModel()
|
||||
{
|
||||
return new MafileExportTemplate
|
||||
{
|
||||
Name = Name,
|
||||
UseLoginAsMafileName = UseLoginAsMafileName,
|
||||
IncludeSharedSecret = IncludeSharedSecret,
|
||||
IncludeIdentitySecret = IncludeIdentitySecret,
|
||||
IncludeRCode = IncludeRCode,
|
||||
IncludeSessionData = IncludeSessionData,
|
||||
IncludeOtherInfo = IncludeOtherInfo,
|
||||
IncludeNebulaProxy = IncludeNebulaProxy,
|
||||
IncludeNebulaPassword = IncludeNebulaPassword,
|
||||
IncludeNebulaGroup = IncludeNebulaGroup,
|
||||
Path = Path
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyTo(MafileExportTemplate model)
|
||||
{
|
||||
model.Name = Name;
|
||||
model.UseLoginAsMafileName = UseLoginAsMafileName;
|
||||
model.IncludeSharedSecret = IncludeSharedSecret;
|
||||
model.IncludeIdentitySecret = IncludeIdentitySecret;
|
||||
model.IncludeRCode = IncludeRCode;
|
||||
model.IncludeSessionData = IncludeSessionData;
|
||||
model.IncludeOtherInfo = IncludeOtherInfo;
|
||||
model.IncludeNebulaProxy = IncludeNebulaProxy;
|
||||
model.IncludeNebulaPassword = IncludeNebulaPassword;
|
||||
model.IncludeNebulaGroup = IncludeNebulaGroup;
|
||||
model.Path = Path;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public partial class ProxyManagerVM : ObservableObject
|
||||
|
||||
var split = input
|
||||
.Split(Environment.NewLine)
|
||||
.Where(s => string.IsNullOrWhiteSpace(s) == false)
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Select(x => x.Trim())
|
||||
.ToArray();
|
||||
|
||||
@@ -72,7 +72,7 @@ public partial class ProxyManagerVM : ObservableObject
|
||||
var i = 0;
|
||||
|
||||
|
||||
foreach (var s in split.Where(s => string.IsNullOrWhiteSpace(s) == false))
|
||||
foreach (var s in split.Where(s => !string.IsNullOrWhiteSpace(s)))
|
||||
{
|
||||
i++;
|
||||
var str = s;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace NebulaAuth.ViewModel.Other;
|
||||
|
||||
public partial class SdaPasswordDialogVM : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsFormValid))]
|
||||
private string _password = string.Empty;
|
||||
|
||||
public bool IsFormValid => !string.IsNullOrWhiteSpace(Password);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using SteamLibForked.Models.SteamIds;
|
||||
|
||||
namespace NebulaAuth.ViewModel.Other;
|
||||
|
||||
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
|
||||
namespace NebulaAuth.ViewModel.Other;
|
||||
|
||||
@@ -53,7 +54,7 @@ public partial class SettingsVM : ObservableObject
|
||||
var targetValue = UseAccountNameAsMafileNamePreview;
|
||||
if (UseAccountNameAsMafileName == targetValue) return;
|
||||
RenameMafilesProgress = 0;
|
||||
Storage.MafileRenameResult? result = null;
|
||||
MafilesBulkRenameResult? result = null;
|
||||
try
|
||||
{
|
||||
result = await Storage.RenameMafiles(targetValue, new Progress<double>(p => RenameMafilesProgress = p));
|
||||
@@ -85,7 +86,7 @@ public partial class SettingsVM : ObservableObject
|
||||
{
|
||||
var l = GetLoc("PartialSuccess");
|
||||
RenameResultText =
|
||||
string.Format(l, result.Total, result.Renamed, result.Errors, result.Conflict, result.BackupFileName);
|
||||
string.Format(l, result.Total, result.Renamed, result.Conflict, result.Errors, result.BackupFileName);
|
||||
}
|
||||
|
||||
string GetLoc(string key)
|
||||
@@ -127,7 +128,9 @@ public partial class SettingsVM : ObservableObject
|
||||
{
|
||||
{LocalizationLanguage.English, "English"},
|
||||
{LocalizationLanguage.Russian, "Русский"},
|
||||
{LocalizationLanguage.Ukrainian, "Українська"}
|
||||
{LocalizationLanguage.Ukrainian, "Українська"},
|
||||
{LocalizationLanguage.ChineseSimplified, "简体中文"},
|
||||
{LocalizationLanguage.French, "Français"}
|
||||
};
|
||||
|
||||
public Color? IconColor
|
||||
@@ -141,7 +144,7 @@ public partial class SettingsVM : ObservableObject
|
||||
get => IconColor != null;
|
||||
set
|
||||
{
|
||||
if (value == false)
|
||||
if (!value)
|
||||
IconColor = null;
|
||||
else
|
||||
IconColor = Color.FromRgb(202, 39, 39);
|
||||
@@ -184,12 +187,6 @@ public partial class SettingsVM : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnorePatchTuesdayErrors
|
||||
{
|
||||
get => Settings.IgnorePatchTuesdayErrors;
|
||||
set => Settings.IgnorePatchTuesdayErrors = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Theme
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using AutoUpdaterDotNET;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using NebulaAuth.Model.Update;
|
||||
|
||||
namespace NebulaAuth.ViewModel.Other;
|
||||
|
||||
public partial class UpdateDialogVM : ObservableObject
|
||||
{
|
||||
public UpdateInfoEventArgs Args { get; }
|
||||
public ChangelogEntry? Changelog { get; }
|
||||
public string? HtmlFallbackUrl { get; }
|
||||
public string Version => Args.CurrentVersion.ToString();
|
||||
public bool HasJsonChangelog => Changelog?.Changes?.Count > 0;
|
||||
|
||||
[ObservableProperty] private bool _showRemindOptions;
|
||||
|
||||
public UpdateDialogVM(UpdateInfoEventArgs args, ChangelogEntry? changelog, string? htmlFallbackUrl)
|
||||
{
|
||||
Args = args;
|
||||
Changelog = changelog;
|
||||
HtmlFallbackUrl = htmlFallbackUrl;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void UpdateNow()
|
||||
{
|
||||
DialogHost.Close(null);
|
||||
if (AutoUpdater.DownloadUpdate(Args))
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleRemindOptions()
|
||||
{
|
||||
ShowRemindOptions = !ShowRemindOptions;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectRemindOption(string option)
|
||||
{
|
||||
var delay = option switch
|
||||
{
|
||||
"1H" => TimeSpan.FromHours(1),
|
||||
"1D" => TimeSpan.FromDays(1),
|
||||
"3D" => TimeSpan.FromDays(3),
|
||||
"7D" => TimeSpan.FromDays(7),
|
||||
_ => TimeSpan.FromDays(1)
|
||||
};
|
||||
Core.UpdateManager.SetRemindAfter(delay);
|
||||
DialogHost.Close(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SkipVersion()
|
||||
{
|
||||
Core.UpdateManager.SkipVersion(Version);
|
||||
DialogHost.Close(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenLink(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) return;
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using AutoUpdaterDotNET;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace NebulaAuth.ViewModel.Other;
|
||||
|
||||
public class UpdaterVM : ObservableObject
|
||||
{
|
||||
public UpdateInfoEventArgs UpdateInfoEventArgs { get; }
|
||||
|
||||
public UpdaterVM(UpdateInfoEventArgs args)
|
||||
{
|
||||
UpdateInfoEventArgs = args;
|
||||
}
|
||||
}
|
||||
+1232
-314
File diff suppressed because it is too large
Load Diff
@@ -152,7 +152,7 @@ public static class SteamAuthenticatorLinkerApi
|
||||
var content = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||
var j = JObject.Parse(content);
|
||||
|
||||
if (j["success"]!.Value<bool>() == false)
|
||||
if (!j["success"]!.Value<bool>())
|
||||
return CheckPhoneResult.GeneralFailure;
|
||||
|
||||
if (j["is_voip"]!.Value<bool>())
|
||||
@@ -187,7 +187,7 @@ public static class SteamAuthenticatorLinkerApi
|
||||
var resp = await client.PostProto<IsAccountWaitingForEmailConfirmation_Response>(reqUri,
|
||||
new EmptyMessage());
|
||||
|
||||
if (resp.IsWaiting == false) return true;
|
||||
if (!resp.IsWaiting) return true;
|
||||
|
||||
await Task.Delay(resp.SecondsToWait * 1000);
|
||||
}
|
||||
|
||||
@@ -44,10 +44,15 @@ public static class AdmissionHelper
|
||||
/// <summary>
|
||||
/// Clear and set new session
|
||||
/// </summary>
|
||||
public static void SetSteamCookies(this CookieContainer container, ISessionData sessionData,
|
||||
public static void SetSteamCookies(this CookieContainer container, ISessionData? sessionData,
|
||||
string setLanguage = "english")
|
||||
{
|
||||
container.ClearSteamCookies(setLanguage);
|
||||
if (sessionData == null)
|
||||
{
|
||||
TransferCommunityCookies(container);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AddRefreshToken(container, sessionData.RefreshToken);
|
||||
@@ -68,7 +73,7 @@ public static class AdmissionHelper
|
||||
{
|
||||
var uri = SteamDomains.GetDomainUri(domain);
|
||||
foreach (var cookie in container.GetCookies(uri)
|
||||
.Where(c => c.Expired == false && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
|
||||
.Where(c => !c.Expired && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
|
||||
{
|
||||
cookie.Expired = true;
|
||||
}
|
||||
@@ -79,11 +84,16 @@ public static class AdmissionHelper
|
||||
/// <summary>
|
||||
/// Clear and set new session
|
||||
/// </summary>
|
||||
public static void SetSteamMobileCookies(this CookieContainer container, ISessionData mobileSession,
|
||||
public static void SetSteamMobileCookies(this CookieContainer container, ISessionData? mobileSession,
|
||||
string setLanguage = "english")
|
||||
{
|
||||
container.ClearSteamCookies(setLanguage);
|
||||
container.AddMinimalMobileCookies();
|
||||
if (mobileSession == null)
|
||||
{
|
||||
TransferCommunityCookies(container);
|
||||
return;
|
||||
}
|
||||
|
||||
AddRefreshToken(container, mobileSession.RefreshToken);
|
||||
|
||||
@@ -106,12 +116,19 @@ public static class AdmissionHelper
|
||||
/// Market, Trading and other pages won't be authorized
|
||||
/// </summary>
|
||||
public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container,
|
||||
IMobileSessionData mobileSession,
|
||||
IMobileSessionData? mobileSession,
|
||||
string setLanguage = "english")
|
||||
{
|
||||
container.ClearSteamCookies(setLanguage);
|
||||
container.AddMinimalMobileCookies();
|
||||
|
||||
if (mobileSession == null)
|
||||
{
|
||||
TransferCommunityCookies(container);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AddRefreshToken(container, mobileSession.RefreshToken);
|
||||
|
||||
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||
@@ -131,7 +148,7 @@ public static class AdmissionHelper
|
||||
}
|
||||
|
||||
var mobileToken = mobileSession.GetMobileToken();
|
||||
if (domainCookieSet == false && mobileToken is {IsExpired: false})
|
||||
if (!domainCookieSet && mobileToken is {IsExpired: false})
|
||||
{
|
||||
var domain = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, mobileToken.Value.SignedToken)
|
||||
@@ -193,7 +210,7 @@ public static class AdmissionHelper
|
||||
|
||||
foreach (Cookie cookie in cookies)
|
||||
{
|
||||
if (cookie.Domain.Contains("steamcommunity.com") == false || cookie.Expired ||
|
||||
if (!cookie.Domain.Contains("steamcommunity.com") || cookie.Expired ||
|
||||
cookie.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)) continue;
|
||||
|
||||
|
||||
@@ -253,7 +270,7 @@ public static class AdmissionHelper
|
||||
var cookies = container.GetAllCookies();
|
||||
return cookies
|
||||
.FirstOrDefault(c => c.Name.Equals(SESSION_ID_COOKIE_NAME, StringComparison.InvariantCultureIgnoreCase)
|
||||
&& c.Expired == false
|
||||
&& !c.Expired
|
||||
&& c.Domain.Contains(domain, StringComparison.InvariantCultureIgnoreCase))?
|
||||
.Value;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public static class SteamTokenHelper
|
||||
{
|
||||
result = default;
|
||||
var match = SteamTokenRegex.Match(input);
|
||||
if (match.Success == false)
|
||||
if (!match.Success)
|
||||
{
|
||||
if (trying) return false;
|
||||
throw new ArgumentException("Provided Token is not valid SteamLoginSecure or SteamAccess token");
|
||||
@@ -64,7 +64,7 @@ public static class SteamTokenHelper
|
||||
}
|
||||
|
||||
var steamIdStr = jwt.Subject;
|
||||
if (steamIdStr == null || long.TryParse(steamIdStr, out var steamId) == false || steamId < SteamId64.SEED)
|
||||
if (steamIdStr == null || !long.TryParse(steamIdStr, out var steamId) || steamId < SteamId64.SEED)
|
||||
{
|
||||
if (trying) return false;
|
||||
throw new ArgumentException(
|
||||
@@ -101,7 +101,7 @@ public static class SteamTokenHelper
|
||||
if (jwt.Audiences.ToList().Count > 0)
|
||||
{
|
||||
var aud = audiences[0];
|
||||
if (AudDomains.TryGetValue(aud, out domain) == false && aud != "web")
|
||||
if (!AudDomains.TryGetValue(aud, out domain) && aud != "web")
|
||||
{
|
||||
if (trying)
|
||||
{
|
||||
@@ -180,13 +180,13 @@ public static class SteamTokenHelper
|
||||
|
||||
internal static string CombineLoginValueIfNeeded(long steamId, string loginValue)
|
||||
{
|
||||
return CheckIfProperLoginValue(loginValue) == false ? CombineJwtWithSteamId(steamId, loginValue) : loginValue;
|
||||
return !CheckIfProperLoginValue(loginValue) ? CombineJwtWithSteamId(steamId, loginValue) : loginValue;
|
||||
}
|
||||
|
||||
public static string ExtractJwtToken(string steamLoginSecure)
|
||||
{
|
||||
var match = SteamTokenRegex.Match(steamLoginSecure);
|
||||
if (match.Success == false) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure");
|
||||
if (!match.Success) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure");
|
||||
return match.Groups["jwt"].Value;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public class SessionInvalidException : Exception
|
||||
{
|
||||
}
|
||||
|
||||
public SessionInvalidException(string message, Exception? inner) : base(message, inner)
|
||||
public SessionInvalidException(string? message, Exception? inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ public class SessionPermanentlyExpiredException : SessionInvalidException
|
||||
{
|
||||
}
|
||||
|
||||
public SessionPermanentlyExpiredException(string message, Exception inner) : base(message, inner)
|
||||
public SessionPermanentlyExpiredException(string? message, Exception? inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public static class ProtoHttpClientExtension
|
||||
this HttpClient client, Uri uri, HttpMethod method, string? protoMsg, CancellationToken cancellationToken)
|
||||
where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var resp = await SendProto(client, uri, method, protoMsg, cancellationToken);
|
||||
var resp = await client.SendProto(uri, method, protoMsg, cancellationToken);
|
||||
return await ProtoResponse<TProtoResponse>.FromHttpResponseAsync(resp, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public static class ProtoHttpClientExtension
|
||||
string? protoMsg,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var resp = await SendProto(client, uri, method, protoMsg, cancellationToken);
|
||||
var resp = await client.SendProto(uri, method, protoMsg, cancellationToken);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
return ProtoHelpers.GetEResult(resp);
|
||||
}
|
||||
@@ -97,7 +97,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
return SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Post, str, cancellationToken);
|
||||
return client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Post, str, cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<ProtoResponse<TProtoResponse>> PostProtoMsg<TProtoResponse>(this HttpClient client, string uri,
|
||||
@@ -105,7 +105,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
return SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Post, str, cancellationToken);
|
||||
return client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Post, str, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -117,7 +117,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
var res = await SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Post, str, cancellationToken);
|
||||
var res = await client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Post, str, cancellationToken);
|
||||
return res.GetResponseEnsureSuccess();
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
var res = await SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Post, str,
|
||||
var res = await client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Post, str,
|
||||
cancellationToken);
|
||||
return res.GetResponseEnsureSuccess();
|
||||
}
|
||||
@@ -179,7 +179,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
return SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Get, str, cancellationToken);
|
||||
return client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Get, str, cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<ProtoResponse<TProtoResponse>> GetProtoMsg<TProtoResponse>(this HttpClient client, string uri,
|
||||
@@ -187,7 +187,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
return SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Get, str, cancellationToken);
|
||||
return client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Get, str, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -199,7 +199,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
var res = await SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Get, str, cancellationToken);
|
||||
var res = await client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Get, str, cancellationToken);
|
||||
res.Result.EnsureSuccessEResult();
|
||||
|
||||
return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null");
|
||||
@@ -210,7 +210,7 @@ public static class ProtoHttpClientExtension
|
||||
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
|
||||
{
|
||||
var str = ProtoHelpers.ProtoToString(request);
|
||||
var res = await SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Get, str,
|
||||
var res = await client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Get, str,
|
||||
cancellationToken);
|
||||
res.Result.EnsureSuccessEResult();
|
||||
return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null");
|
||||
|
||||
@@ -74,18 +74,18 @@ public class SteamAuthenticatorLinker
|
||||
|
||||
long? phoneNumber = null;
|
||||
|
||||
if (hasPhone == false && Options.PhoneNumberProvider != null)
|
||||
if (!hasPhone && Options.PhoneNumberProvider != null)
|
||||
{
|
||||
phoneNumber = await Options.PhoneNumberProvider.GetPhoneNumber(Consumer);
|
||||
}
|
||||
|
||||
if (hasPhone == false && phoneNumber != null)
|
||||
if (!hasPhone && phoneNumber != null)
|
||||
{
|
||||
Logger?.LogInformation("Attaching phone number {phoneNumber}", phoneNumber);
|
||||
//if (await this.IsValidPhoneNumber(phoneNumber.Value) == false)
|
||||
// throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidPhoneNumber);
|
||||
|
||||
if (await this.AttachPhone(phoneNumber.Value) == false)
|
||||
if (!await this.AttachPhone(phoneNumber.Value))
|
||||
throw new AuthenticatorLinkerException(AuthenticatorLinkerError.CantAttachPhone);
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ public class SteamAuthenticatorLinker
|
||||
Logger?.LogInformation("Finalizing link");
|
||||
var result = await this.FinalizeLink(code, resp.SharedSecret, isPhone);
|
||||
|
||||
if (result.Success == false)
|
||||
if (!result.Success)
|
||||
{
|
||||
var error = result.Error switch
|
||||
{
|
||||
|
||||
@@ -5,24 +5,21 @@ namespace SteamLib.Utility.MafileSerialization;
|
||||
internal class LegacyMafile
|
||||
{
|
||||
[JsonProperty("shared_secret")]
|
||||
[JsonRequired]
|
||||
public string SharedSecret { get; set; } = default!;
|
||||
public string SharedSecret { get; set; } = null!;
|
||||
|
||||
[JsonProperty("identity_secret")]
|
||||
[JsonRequired]
|
||||
public string IdentitySecret { get; set; } = default!;
|
||||
public string IdentitySecret { get; set; } = null!;
|
||||
|
||||
[JsonProperty("device_id")]
|
||||
[JsonRequired]
|
||||
public string DeviceId { get; set; } = default!;
|
||||
public string DeviceId { get; set; } = null!;
|
||||
|
||||
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = default!;
|
||||
[JsonProperty("account_name")] public string AccountName { get; set; } = default!;
|
||||
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = null!;
|
||||
[JsonProperty("account_name")] public string AccountName { get; set; } = null!;
|
||||
[JsonProperty("Session")] public object? SessionData { get; set; }
|
||||
[JsonProperty("server_time")] public long ServerTime { get; set; } //Unused
|
||||
[JsonProperty("steamid")] public long SteamId { get; set; }
|
||||
[JsonProperty("serial_number")] public string SerialNumber { get; set; } = default!; //Unused
|
||||
[JsonProperty("uri")] public string Uri { get; set; } = default!; //Unused
|
||||
[JsonProperty("token_gid")] public string TokenGid { get; set; } = default!; //Unused
|
||||
[JsonProperty("secret_1")] public string Secret1 { get; set; } = default!; //Unused
|
||||
[JsonProperty("serial_number")] public string SerialNumber { get; set; } = null!; //Unused
|
||||
[JsonProperty("uri")] public string Uri { get; set; } = null!; //Unused
|
||||
[JsonProperty("token_gid")] public string TokenGid { get; set; } = null!; //Unused
|
||||
[JsonProperty("secret_1")] public string Secret1 { get; set; } = null!; //Unused
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public partial class MafileSerializer //Validate
|
||||
public static void IsValidBase64(string name, string base64)
|
||||
{
|
||||
var buffer = new Span<byte>(new byte[base64.Length]);
|
||||
if (Convert.TryFromBase64String(base64, buffer, out _) == false)
|
||||
if (!Convert.TryFromBase64String(base64, buffer, out _))
|
||||
throw new ArgumentException($"{name} is not valid base64 string");
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public static class MobileConfirmationScrapper
|
||||
throw new SessionInvalidException();
|
||||
}
|
||||
|
||||
if (conf.Success == false)
|
||||
if (!conf.Success)
|
||||
{
|
||||
var error = LoadConfirmationsError.Unknown;
|
||||
if (conf.Message != null && ErrorMessages.TryGetValue(conf.Message, out var e))
|
||||
|
||||
Reference in New Issue
Block a user