mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-26 14:51:42 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ac4ffdc34 | |||
| 17635ccba0 | |||
| 69589075e5 | |||
| 4b547e19c4 | |||
| 053faec343 |
@@ -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.event.repository.name }}/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
|
||||
+2
-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
|
||||
@@ -30,8 +30,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{
|
||||
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
|
||||
@@ -44,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,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();
|
||||
}
|
||||
@@ -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();
|
||||
@@ -123,6 +145,17 @@ public static class DialogsController
|
||||
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)
|
||||
@@ -130,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)
|
||||
|
||||
@@ -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);
|
||||
//}
|
||||
}
|
||||
@@ -519,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,6 +27,43 @@ 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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -51,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
|
||||
@@ -73,26 +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 async Task AddNewMafile(string path, bool overwrite)
|
||||
public static async Task<AddMafileResult> AddNewMafile(string path, bool overwrite)
|
||||
{
|
||||
Mafile data;
|
||||
|
||||
try
|
||||
{
|
||||
data = await ReadMafileAsync(path);
|
||||
data.Filename = null;
|
||||
}
|
||||
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
|
||||
{
|
||||
@@ -100,16 +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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
await SaveMafileAsync(data);
|
||||
return AddMafileResult.Added;
|
||||
}
|
||||
|
||||
public static async Task<Mafile> ReadMafileAsync(string path)
|
||||
@@ -218,7 +241,7 @@ public static class Storage
|
||||
|
||||
if (counter % 5 == 0)
|
||||
{
|
||||
progress?.Report(counter / (double) files.Count);
|
||||
progress?.Report(counter / (double)files.Count);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public partial class MainVM : ObservableObject
|
||||
new MaProxy(kvp.Key, kvp.Value)));
|
||||
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
|
||||
QueryGroups();
|
||||
UpdateManager.CheckForUpdates();
|
||||
UpdateManager.CheckForUpdates(manual: false);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -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;
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
@@ -26,6 +27,7 @@ public partial class MainVM //File //TODO: Refactor
|
||||
{
|
||||
public Settings Settings => Settings.Instance;
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenMafileFolder()
|
||||
{
|
||||
@@ -58,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
|
||||
{
|
||||
await 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)
|
||||
{
|
||||
await Storage.AddNewMafile(str, true);
|
||||
added++;
|
||||
}
|
||||
else if (confirmOverwrite == false)
|
||||
{
|
||||
notAdded++;
|
||||
}
|
||||
summary.ErrorOne();
|
||||
}
|
||||
catch (MafileNeedReloginException ex)
|
||||
{
|
||||
@@ -108,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
|
||||
@@ -124,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)
|
||||
@@ -191,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()
|
||||
{
|
||||
@@ -294,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);
|
||||
}
|
||||
@@ -221,7 +221,12 @@ public partial class MafileExporterVM : ObservableObject
|
||||
return;
|
||||
}
|
||||
|
||||
var split = Regex.Split(lines, "\r\n|\r|\n").Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
|
||||
var split = Regex
|
||||
.Split(lines, "\r\n|\r|\n")
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToArray();
|
||||
|
||||
ResetHintText();
|
||||
ExportResult res;
|
||||
try
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -410,6 +410,34 @@
|
||||
"zh": "购买",
|
||||
"ua": "Покупка",
|
||||
"fr": "Acheter"
|
||||
},
|
||||
"ConfirmAll": {
|
||||
"en": "Confirm all",
|
||||
"ru": "Подтвердить все",
|
||||
"zh": "全部确认",
|
||||
"ua": "Підтвердити все",
|
||||
"fr": "Tout confirmer"
|
||||
},
|
||||
"CancelAll": {
|
||||
"en": "Cancel all",
|
||||
"ru": "Отклонить все",
|
||||
"zh": "全部取消",
|
||||
"ua": "Відхилити все",
|
||||
"fr": "Tout annuler"
|
||||
},
|
||||
"ConfirmOne": {
|
||||
"en": "Confirm this item",
|
||||
"ru": "Подтвердить этот предмет",
|
||||
"zh": "确认此物品",
|
||||
"ua": "Підтвердити цей предмет",
|
||||
"fr": "Confirmer cet objet"
|
||||
},
|
||||
"CancelOne": {
|
||||
"en": "Cancel this item",
|
||||
"ru": "Отклонить этот предмет",
|
||||
"zh": "取消此物品",
|
||||
"ua": "Відхилити цей предмет",
|
||||
"fr": "Annuler cet objet"
|
||||
}
|
||||
},
|
||||
"CodeCopied": {
|
||||
@@ -666,12 +694,10 @@
|
||||
},
|
||||
"LegacyMafileModeHint": {
|
||||
"en": "Save mafiles in compatibility format with Steam Desktop Authenticator and other applications",
|
||||
"ru":
|
||||
"Сохранять мафайлы в старом формате для совместимости со Steam Desktop Authenticator и другими приложениями",
|
||||
"ru": "Сохранять мафайлы в старом формате для совместимости со Steam Desktop Authenticator и другими приложениями",
|
||||
"zh": "以与 Steam 桌面认证器及其他应用程序兼容的格式保存 mafiles",
|
||||
"ua": "Зберігати мафайли у старому форматі для сумісності зі Steam Desktop Authenticator та іншими додатками",
|
||||
"fr":
|
||||
"Enregistrer les mafiles dans un format compatible avec Steam Desktop Authenticator et d'autres applications"
|
||||
"fr": "Enregistrer les mafiles dans un format compatible avec Steam Desktop Authenticator et d'autres applications"
|
||||
},
|
||||
"AllowAutoUpdate": {
|
||||
"en": "Allow auto update",
|
||||
@@ -702,13 +728,10 @@
|
||||
"fr": "Ignorer les erreurs dues à la maintenance Steam du mardi soir dans le timer (exp.)"
|
||||
},
|
||||
"IgnorePatchTuesdayErrorsHint": {
|
||||
"en":
|
||||
"Ignore errors that occur every Tuesday (Wednesday night in GMT) when Steam doing technical works and auto-confirm timers turns off unnecessarily",
|
||||
"ru":
|
||||
"Игнорировать ошибки, которые возникают каждый вторник (ночь среды по GMT) когда Steam проводит технические работы и автоподтверждения отключаются без надобности",
|
||||
"en": "Ignore errors that occur every Tuesday (Wednesday night in GMT) when Steam doing technical works and auto-confirm timers turns off unnecessarily",
|
||||
"ru": "Игнорировать ошибки, которые возникают каждый вторник (ночь среды по GMT) когда Steam проводит технические работы и автоподтверждения отключаются без надобности",
|
||||
"zh": "忽略每周三(格林尼治标准时间周三夜间)发生的错误,因为 Steam 进行技术维护时自动确认功能会被无故关闭",
|
||||
"ua":
|
||||
"Ігнорувати помилки, які виникають щосереди (ніч середи за GMT) коли Steam проводить технічні роботи і автопідтвердження вимикаються без потреби",
|
||||
"ua": "Ігнорувати помилки, які виникають щосереди (ніч середи за GMT) коли Steam проводить технічні роботи і автопідтвердження вимикаються без потреби",
|
||||
"fr": "Ignorer les erreurs dues à la maintenance Steam du mardi soir (Mercredi à minuit en GMT)"
|
||||
}
|
||||
},
|
||||
@@ -960,15 +983,11 @@
|
||||
"fr": "Mot de passe de chiffrement"
|
||||
},
|
||||
"DialogText": {
|
||||
"en":
|
||||
"You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session.\r\n(Optional)",
|
||||
"ru":
|
||||
"Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией.\r\n(Не обязательно)",
|
||||
"en": "You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session.\r\n(Optional)",
|
||||
"ru": "Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией.\r\n(Не обязательно)",
|
||||
"zh": "之前您为密码文件设置了加密密码,请输入它,以便应用程序在会话出现问题时可以自动登录。\r\n(非必填)",
|
||||
"ua":
|
||||
"Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією.\r\n(Не обов'язково)",
|
||||
"fr":
|
||||
"Vous avez déjà défini un mot de passe de chiffrement pour les mots de passe des mafiles, spécifiez-le pour que l'appli puisse se connecter automatiquement en cas de problèmes avec la session.\r\n(Facultatif)"
|
||||
"ua": "Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією.\r\n(Не обов'язково)",
|
||||
"fr": "Vous avez déjà défini un mot de passe de chiffrement pour les mots de passe des mafiles, spécifiez-le pour que l'appli puisse se connecter automatiquement en cas de problèmes avec la session.\r\n(Facultatif)"
|
||||
},
|
||||
"Ok": {
|
||||
"en": "Ok",
|
||||
@@ -1006,6 +1025,10 @@
|
||||
"zh": "文档",
|
||||
"ua": "Документація",
|
||||
"fr": "Documentation"
|
||||
},
|
||||
"CheckForUpdates": {
|
||||
"en": "Check for updates",
|
||||
"ru": "Проверить обновления"
|
||||
}
|
||||
},
|
||||
"MafileMoverView": {
|
||||
@@ -1033,6 +1056,44 @@
|
||||
"fr": "Confirmer l'action"
|
||||
}
|
||||
},
|
||||
"SdaPasswordDialog": {
|
||||
"Title": {
|
||||
"en": "SDA encryption password",
|
||||
"ru": "Пароль шифрования SDA",
|
||||
"ua": "Пароль шифрування SDA",
|
||||
"fr": "Mot de passe de chiffrement SDA"
|
||||
},
|
||||
"Message": {
|
||||
"en": "This mafile is encrypted with Steam Desktop Authenticator. Enter your SDA encryption password to import it.",
|
||||
"ru": "Этот mafile зашифрован Steam Desktop Authenticator. Введите пароль шифрования SDA для импорта.",
|
||||
"ua": "Цей mafile зашифровано Steam Desktop Authenticator. Введіть пароль шифрування SDA для імпорту.",
|
||||
"fr": "Ce mafile est chiffré avec Steam Desktop Authenticator. Entrez votre mot de passe SDA pour l'importer."
|
||||
},
|
||||
"PasswordHint": {
|
||||
"en": "SDA encryption password",
|
||||
"ru": "Пароль шифрования SDA",
|
||||
"ua": "Пароль шифрування SDA",
|
||||
"fr": "Mot de passe de chiffrement SDA"
|
||||
},
|
||||
"WrongPassword": {
|
||||
"en": "Wrong SDA password. Please try again.",
|
||||
"ru": "Неверный пароль SDA. Попробуйте снова.",
|
||||
"ua": "Невірний пароль SDA. Спробуйте ще раз.",
|
||||
"fr": "Mauvais mot de passe SDA. Veuillez réessayer."
|
||||
},
|
||||
"ManifestMissing": {
|
||||
"en": "This mafile looks like it was encrypted by SDA, but manifest.json was not found. Please select your SDA manifest.json to continue.",
|
||||
"ru": "Похоже, этот mafile зашифрован SDA, но manifest.json не найден. Выберите manifest.json от SDA, чтобы продолжить.",
|
||||
"ua": "Схоже, цей mafile зашифровано SDA, але manifest.json не знайдено. Виберіть manifest.json від SDA, щоб продовжити.",
|
||||
"fr": "Ce mafile semble chiffré par SDA, mais manifest.json est introuvable. Sélectionnez manifest.json de SDA pour continuer."
|
||||
},
|
||||
"ManifestEntryNotFound": {
|
||||
"en": "Selected manifest.json does not contain an entry for this mafile.",
|
||||
"ru": "Выбранный manifest.json не содержит запись для этого mafile.",
|
||||
"ua": "Вибраний manifest.json не містить запису для цього mafile.",
|
||||
"fr": "Le manifest.json sélectionné ne contient pas d'entrée pour ce mafile."
|
||||
}
|
||||
},
|
||||
"TextFieldDialog": {
|
||||
"Title": {
|
||||
"ru": "Введите значение",
|
||||
@@ -1167,22 +1228,16 @@
|
||||
},
|
||||
"IncludeSharedSecret": {
|
||||
"ru": "Позволяет генерировать коды подтверждения и входить в аккаунт при наличии пароля или активной сессии.",
|
||||
"en":
|
||||
"Allows generating confirmation codes and logging into the account if a password or active session is available.",
|
||||
"en": "Allows generating confirmation codes and logging into the account if a password or active session is available.",
|
||||
"ua": "Дозволяє генерувати коди підтвердження та входити в акаунт за наявності пароля або активної сесії.",
|
||||
"fr":
|
||||
"Permet de générer des codes de confirmation et de se connecter au compte si un mot de passe ou une session active est disponible.",
|
||||
"fr": "Permet de générer des codes de confirmation et de se connecter au compte si un mot de passe ou une session active est disponible.",
|
||||
"zh": "允许生成确认代码,并在有密码或活动会话的情况下登录账户。"
|
||||
},
|
||||
"IncludeIdentitySecret": {
|
||||
"ru":
|
||||
"Включает identity_secret и device_id. Необходимы для отправки подтверждений и дают доступ к трейдам и торговой площадке.",
|
||||
"en":
|
||||
"Includes identity_secret and device_id. Required for confirmations and provides access to trades and the market.",
|
||||
"ua":
|
||||
"Включає identity_secret і device_id. Потрібні для надсилання підтверджень і надають доступ до трейдів та торгового майданчика.",
|
||||
"fr":
|
||||
"Inclut identity_secret et device_id. Nécessaires pour les confirmations et donnent accès aux échanges et au marché.",
|
||||
"ru": "Включает identity_secret и device_id. Необходимы для отправки подтверждений и дают доступ к трейдам и торговой площадке.",
|
||||
"en": "Includes identity_secret and device_id. Required for confirmations and provides access to trades and the market.",
|
||||
"ua": "Включає identity_secret і device_id. Потрібні для надсилання підтверджень і надають доступ до трейдів та торгового майданчика.",
|
||||
"fr": "Inclut identity_secret et device_id. Nécessaires pour les confirmations et donnent accès aux échanges et au marché.",
|
||||
"zh": "包含 identity_secret 和 device_id。用于发送确认,并可访问交易和市场。"
|
||||
},
|
||||
"IncludeRCode": {
|
||||
@@ -1193,25 +1248,17 @@
|
||||
"zh": "恢复代码。在失去访问权限时可将 Steam Guard 与账户解绑。"
|
||||
},
|
||||
"IncludeSessionData": {
|
||||
"ru":
|
||||
"Включает данные текущей сессии. Если сессия активна и не истекла, позволяет получить доступ к аккаунту без ввода пароля с помощью mafile.",
|
||||
"en":
|
||||
"Includes current session data. If the session is active and valid, allows accessing the account without entering a password using the mafile.",
|
||||
"ua":
|
||||
"Включає дані поточної сесії. Якщо сесія активна й не прострочена, дозволяє отримати доступ до акаунта без введення пароля за допомогою mafile.",
|
||||
"fr":
|
||||
"Inclut les données de la session actuelle. Si la session est active et valide, permet d’accéder au compte sans saisir de mot de passe via le mafile.",
|
||||
"ru": "Включает данные текущей сессии. Если сессия активна и не истекла, позволяет получить доступ к аккаунту без ввода пароля с помощью mafile.",
|
||||
"en": "Includes current session data. If the session is active and valid, allows accessing the account without entering a password using the mafile.",
|
||||
"ua": "Включає дані поточної сесії. Якщо сесія активна й не прострочена, дозволяє отримати доступ до акаунта без введення пароля за допомогою mafile.",
|
||||
"fr": "Inclut les données de la session actuelle. Si la session est active et valide, permet d’accéder au compte sans saisir de mot de passe via le mafile.",
|
||||
"zh": "包含当前会话数据。如果会话处于活动状态且未过期,可通过 mafile 在无需输入密码的情况下访问账户。"
|
||||
},
|
||||
"IncludeOtherInfo": {
|
||||
"ru":
|
||||
"Включает дополнительные необязательные поля, которые в данный момент не используются Steam, но могут применяться в будущем и содержать чувствительную информацию.",
|
||||
"en":
|
||||
"Includes additional optional fields not currently used by Steam, but potentially usable in the future and may contain sensitive information.",
|
||||
"ua":
|
||||
"Включає додаткові необов’язкові поля, які наразі не використовуються Steam, але можуть бути задіяні в майбутньому та містити чутливу інформацію.",
|
||||
"fr":
|
||||
"Inclut des champs supplémentaires optionnels actuellement non utilisés par Steam, mais susceptibles d’être exploités à l’avenir et de contenir des informations sensibles.",
|
||||
"ru": "Включает дополнительные необязательные поля, которые в данный момент не используются Steam, но могут применяться в будущем и содержать чувствительную информацию.",
|
||||
"en": "Includes additional optional fields not currently used by Steam, but potentially usable in the future and may contain sensitive information.",
|
||||
"ua": "Включає додаткові необов’язкові поля, які наразі не використовуються Steam, але можуть бути задіяні в майбутньому та містити чутливу інформацію.",
|
||||
"fr": "Inclut des champs supplémentaires optionnels actuellement non utilisés par Steam, mais susceptibles d’être exploités à l’avenir et de contenir des informations sensibles.",
|
||||
"zh": "包含目前未被 Steam 使用的可选附加字段,这些字段将来可能会被使用,并可能包含敏感信息。"
|
||||
},
|
||||
"NebulaProxy": {
|
||||
@@ -1240,8 +1287,7 @@
|
||||
"ru": "Введите логины аккаунтов или SteamID (по одному на строку) для экспорта в выбранную директорию.",
|
||||
"en": "Enter account logins or SteamIDs (one per line) to export to the selected directory.",
|
||||
"ua": "Введіть логіни акаунтів або SteamID (по одному на рядок) для експорту у вибрану директорію.",
|
||||
"fr":
|
||||
"Entrez les identifiants de compte ou les SteamID (un par ligne) pour exporter vers le répertoire sélectionné.",
|
||||
"fr": "Entrez les identifiants de compte ou les SteamID (un par ligne) pour exporter vers le répertoire sélectionné.",
|
||||
"zh": "输入要导出到所选目录的账户登录名或 SteamID(每行一个)。"
|
||||
}
|
||||
},
|
||||
@@ -1289,6 +1335,54 @@
|
||||
"fr": "Formats pris en charge:\r\n\r\nlogin:password\r\nsteamid:password"
|
||||
}
|
||||
},
|
||||
"UpdateDialog": {
|
||||
"Title": {
|
||||
"en": "New version available:",
|
||||
"ru": "Доступна новая версия:"
|
||||
},
|
||||
"Changelog": {
|
||||
"en": "What's new",
|
||||
"ru": "Что нового"
|
||||
},
|
||||
"NoChangelog": {
|
||||
"en": "No changelog available",
|
||||
"ru": "Нет информации об изменениях"
|
||||
},
|
||||
"UpdateNow": {
|
||||
"en": "Update now",
|
||||
"ru": "Обновить сейчас"
|
||||
},
|
||||
"RemindLater": {
|
||||
"en": "Remind later",
|
||||
"ru": "Напомнить позже"
|
||||
},
|
||||
"SkipVersion": {
|
||||
"en": "Skip version",
|
||||
"ru": "Пропустить версию"
|
||||
},
|
||||
"RemindAfter": {
|
||||
"en": "Remind after:",
|
||||
"ru": "Напомнить через:"
|
||||
},
|
||||
"RemindOptions": {
|
||||
"1Hour": {
|
||||
"en": "1 hour",
|
||||
"ru": "1 час"
|
||||
},
|
||||
"1Day": {
|
||||
"en": "1 day",
|
||||
"ru": "1 день"
|
||||
},
|
||||
"3Days": {
|
||||
"en": "3 days",
|
||||
"ru": "3 дня"
|
||||
},
|
||||
"7Days": {
|
||||
"en": "7 days",
|
||||
"ru": "7 дней"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CodeBehind": {
|
||||
"MainVM": {
|
||||
"SuccessfulLogin": {
|
||||
@@ -1313,15 +1407,11 @@
|
||||
"fr": "RCode manquant"
|
||||
},
|
||||
"ConfirmRemovingAuthenticator": {
|
||||
"ru":
|
||||
"Вы точно уверены, что хотите удалить аутентификатор?\r\nПосле этого вы не сможете обмениваться, использовать торговую площадку 15 дней. Мафайл будет удалён навсегда",
|
||||
"en":
|
||||
"Are you sure you want to remove the authenticator?\r\nAfter that, you will not be able to trade or use the marketplace for 15 days. Mafile will be deleted permanently",
|
||||
"ru": "Вы точно уверены, что хотите удалить аутентификатор?\r\nПосле этого вы не сможете обмениваться, использовать торговую площадку 15 дней. Мафайл будет удалён навсегда",
|
||||
"en": "Are you sure you want to remove the authenticator?\r\nAfter that, you will not be able to trade or use the marketplace for 15 days. Mafile will be deleted permanently",
|
||||
"zh": "您确定要移除身份验证器吗?\r\n之后,您将在15天内无法进行交易或使用市场。Mafile 将被永久删除。",
|
||||
"ua":
|
||||
"Ви точно впевнені, що хочете видалити автентифікатор?\r\nПісля цього ви не зможете обмінюватися, використовувати торгову площадку 15 днів. Мафайл буде видалено назавжди",
|
||||
"fr":
|
||||
"Êtes-vous sûr de vouloir supprimer l'authenticateur?\r\nAprès cela, vous ne pourrez pas échanger ou utiliser le marché pendant 15 jours. Le mafile sera supprimé définitivement"
|
||||
"ua": "Ви точно впевнені, що хочете видалити автентифікатор?\r\nПісля цього ви не зможете обмінюватися, використовувати торгову площадку 15 днів. Мафайл буде видалено назавжди",
|
||||
"fr": "Êtes-vous sûr de vouloir supprimer l'authenticateur?\r\nAprès cela, vous ne pourrez pas échanger ou utiliser le marché pendant 15 jours. Le mafile sera supprimé définitivement"
|
||||
},
|
||||
"AuthenticatorRemoved": {
|
||||
"ru": "Аутентификатор удален",
|
||||
@@ -1379,6 +1469,24 @@
|
||||
"ua": "Помилка імпорту мафайла",
|
||||
"fr": "Erreur d'importation du mafile"
|
||||
},
|
||||
"SdaManifestMissing": {
|
||||
"en": "This .mafile is encrypted by Steam Desktop Authenticator (SDA).\r\n\r\nTo import it, select the .mafile from the SDA folder (so Nebula can find the matching manifest.json), or click OK and manually select SDA's manifest.json.\r\n\r\nWithout manifest.json, decrypting is impossible because it contains the encryption salt and IV.",
|
||||
"ru": "Этот .mafile зашифрован Steam Desktop Authenticator (SDA).\r\n\r\nЧтобы импортировать его, выберите .mafile из папки SDA (чтобы Nebula нашла соответствующий manifest.json), либо нажмите OK и вручную выберите manifest.json от SDA.\r\n\r\nБез manifest.json расшифровка невозможна, так как в нём хранятся salt и IV.",
|
||||
"ua": "Цей .mafile зашифровано Steam Desktop Authenticator (SDA).\r\n\r\nЩоб імпортувати його, виберіть .mafile з папки SDA (щоб Nebula знайшла відповідний manifest.json), або натисніть OK і вручну виберіть manifest.json від SDA.\r\n\r\nБез manifest.json розшифрування неможливе, бо там зберігаються salt та IV.",
|
||||
"fr": "Ce .mafile est chiffré par Steam Desktop Authenticator (SDA).\r\n\r\nPour l'importer, sélectionnez le .mafile depuis le dossier SDA (afin que Nebula trouve le manifest.json correspondant), ou cliquez sur OK et sélectionnez manuellement le manifest.json de SDA.\r\n\r\nSans manifest.json, le déchiffrement est impossible car il contient le sel et l'IV."
|
||||
},
|
||||
"SdaManifestEntryNotFound": {
|
||||
"en": "The selected manifest.json does not contain an entry for this .mafile.",
|
||||
"ru": "Выбранный manifest.json не содержит запись для этого .mafile.",
|
||||
"ua": "Вибраний manifest.json не містить запису для цього .mafile.",
|
||||
"fr": "Le manifest.json sélectionné ne contient pas d'entrée pour ce .mafile."
|
||||
},
|
||||
"SdaWrongPassword": {
|
||||
"en": "Wrong SDA password. Please try again.",
|
||||
"ru": "Неверный пароль SDA. Попробуйте снова.",
|
||||
"ua": "Невірний пароль SDA. Спробуйте ще раз.",
|
||||
"fr": "Mauvais mot de passe SDA. Veuillez réessayer."
|
||||
},
|
||||
"MissingSessionInMafile": {
|
||||
"ru": ". У него отсутствует сессия. Добавьте этот файл отдельно от остальных",
|
||||
"en": ". It has no session. Add this file separately from the others",
|
||||
@@ -1415,15 +1523,11 @@
|
||||
"fr": "erreurs:"
|
||||
},
|
||||
"RemoveMafileConfirmation": {
|
||||
"ru":
|
||||
"Удалить mafile?\r\nМафайл будет перемещен в папку 'mafiles_removed'\r\nЭто действие НЕ удаляет Steam Guard с аккаунта",
|
||||
"en":
|
||||
"Remove mafile?\r\nMafile will be moved to 'mafiles_removed' directory\r\nThis action will NOT remove the Steam Guard from the account",
|
||||
"ru": "Удалить mafile?\r\nМафайл будет перемещен в папку 'mafiles_removed'\r\nЭто действие НЕ удаляет Steam Guard с аккаунта",
|
||||
"en": "Remove mafile?\r\nMafile will be moved to 'mafiles_removed' directory\r\nThis action will NOT remove the Steam Guard from the account",
|
||||
"zh": "是否删除mafile?\r\nMafile将被移动到“mafiles_removed”目录\r\n此操作不会从账户中移除令牌",
|
||||
"ua":
|
||||
"Видалити mafile?\r\nМафайл буде переміщено у каталог 'mafiles_removed'\r\nЦя дія НЕ видаляє автентифікатор з облікового запису",
|
||||
"fr":
|
||||
"Supprimer le mafile?\r\nLe mafile sera déplacé dans le dossier 'mafiles_removed'\r\nCette action NE SUPPRIME PAS le Steam Guard du compte"
|
||||
"ua": "Видалити mafile?\r\nМафайл буде переміщено у каталог 'mafiles_removed'\r\nЦя дія НЕ видаляє автентифікатор з облікового запису",
|
||||
"fr": "Supprimer le mafile?\r\nLe mafile sera déplacé dans le dossier 'mafiles_removed'\r\nCette action NE SUPPRIME PAS le Steam Guard du compte"
|
||||
},
|
||||
"CantRemoveAlreadyExist": {
|
||||
"ru": "Не удалось переместить mafile, возможно в папке уже существует мафайл с таким именем.",
|
||||
@@ -1458,8 +1562,7 @@
|
||||
"en": "Can't retrieve SteamID from mafile to save changes. Need to relogin. Canceled",
|
||||
"zh": "无法从配置文件获取 SteamID 并保存更改。需要重新登录。已取消",
|
||||
"ua": "Не вдалося отримати SteamID з мафайла та зберегти зміни. Потрібно зробити релогін. Скасовано",
|
||||
"fr":
|
||||
"Impossible de récupérer le SteamID du mafile pour sauvegarder les modifications. Reconnexion nécessaire. Annulé"
|
||||
"fr": "Impossible de récupérer le SteamID du mafile pour sauvegarder les modifications. Reconnexion nécessaire. Annulé"
|
||||
},
|
||||
"LoginCopied": {
|
||||
"en": "Login copied",
|
||||
@@ -1498,11 +1601,9 @@
|
||||
},
|
||||
"CantDecryptPassword": {
|
||||
"en": "Can't decrypt password. Maybe password from this mafile was encrypted with another encryption password",
|
||||
"ru":
|
||||
"Не удалось расшифровать пароль. Возможно пароль из этого мафайла был зашифрован другим паролем шифрования",
|
||||
"ru": "Не удалось расшифровать пароль. Возможно пароль из этого мафайла был зашифрован другим паролем шифрования",
|
||||
"zh": "无法解密密码。可能此文件中的密码是用其他加密密码加密的。",
|
||||
"ua":
|
||||
"Не вдалося розшифрувати пароль. Можливо пароль з цього мафайла був зашифрований іншим паролем шифрування",
|
||||
"ua": "Не вдалося розшифрувати пароль. Можливо пароль з цього мафайла був зашифрований іншим паролем шифрування",
|
||||
"fr": "Impossible de déchiffrer le mot de passe. Le mot de passe de chiffrement est peut-être différent"
|
||||
},
|
||||
"CreateGroupTitle": {
|
||||
@@ -1857,13 +1958,10 @@
|
||||
},
|
||||
"InvalidStateWithStatus2": {
|
||||
"ru": "Неверное состояние (InvalidState) со статусом 2. Откройте ссылку на руководство сверху этого окна.",
|
||||
"en":
|
||||
"Invalid state (InvalidState) with status 2. Open link to the troubleshooting guide at the top of this window.",
|
||||
"en": "Invalid state (InvalidState) with status 2. Open link to the troubleshooting guide at the top of this window.",
|
||||
"zh": "状态无效(InvalidState),状态码为 2. 请打开本窗口顶部的故障排除指南链接。",
|
||||
"ua":
|
||||
"Невірний стан (InvalidState) зі статусом 2. Відкрийте посилання на посібник з усунення несправностей у верхній частині цього вікна.",
|
||||
"fr":
|
||||
"État invalide (InvalidState) avec le statut 2. Ouvrez le lien vers le guide de dépannage en haut de cette fenêtre."
|
||||
"ua": "Невірний стан (InvalidState) зі статусом 2. Відкрийте посилання на посібник з усунення несправностей у верхній частині цього вікна.",
|
||||
"fr": "État invalide (InvalidState) avec le statut 2. Ouvrez le lien vers le guide de dépannage en haut de cette fenêtre."
|
||||
},
|
||||
"GeneralFailure": {
|
||||
"ru": "General Failure",
|
||||
@@ -1929,8 +2027,7 @@
|
||||
"ru": "Ошибка: Был запрошен вход с помощью Steam Guard. На аккаунте уже активирован мобильный аутентификатор",
|
||||
"en": "Error: Login with Steam Guard was requested. Mobile authenticator is already activated on the account",
|
||||
"zh": "错误:请求使用令牌登录。该账户已激活移动验证器",
|
||||
"ua":
|
||||
"Помилка: Був запрошений вхід за допомогою Steam Guard. На акаунті вже активовано мобільний автентифікатор",
|
||||
"ua": "Помилка: Був запрошений вхід за допомогою Steam Guard. На акаунті вже активовано мобільний автентифікатор",
|
||||
"fr": "Erreur: Connexion avec Steam Guard demandée. Authenticateur mobile est déjà activé sur le compte"
|
||||
},
|
||||
"Unknown": {
|
||||
@@ -2069,15 +2166,11 @@
|
||||
"fr": "Un email a été envoyé. Ouvrez le lien de l'email, puis cliquez sur 'Continuer'"
|
||||
},
|
||||
"ClickOnEmailLinkRetry": {
|
||||
"ru":
|
||||
"Не удалось подтвердить переход по ссылке. Убедитесь, что вы открыли ссылку из письма, затем нажмите — 'Продолжить'",
|
||||
"en":
|
||||
"We couldn’t verify that you clicked the link. Make sure you opened the link from the email, then click 'Proceed'",
|
||||
"ru": "Не удалось подтвердить переход по ссылке. Убедитесь, что вы открыли ссылку из письма, затем нажмите — 'Продолжить'",
|
||||
"en": "We couldn’t verify that you clicked the link. Make sure you opened the link from the email, then click 'Proceed'",
|
||||
"zh": "无法确认通过链接的跳转。请确保您是从邮件中打开的链接,然后点击——“继续”",
|
||||
"ua":
|
||||
"Не вдалося підтвердити перехід за посиланням. Переконайтесь, що ви відкрили посилання з листа, а потім натисніть — 'Продовжити'",
|
||||
"fr":
|
||||
"Nous n'avons pas pu vérifier que vous avez cliqué sur le lien. Assurez-vous d'avoir ouvert le lien de l'email, puis cliquez sur 'Continuer'"
|
||||
"ua": "Не вдалося підтвердити перехід за посиланням. Переконайтесь, що ви відкрили посилання з листа, а потім натисніть — 'Продовжити'",
|
||||
"fr": "Nous n'avons pas pu vérifier que vous avez cliqué sur le lien. Assurez-vous d'avoir ouvert le lien de l'email, puis cliquez sur 'Continuer'"
|
||||
},
|
||||
"PhoneHint": {
|
||||
"ru": "На телефон {0} была отправлена СМС",
|
||||
@@ -2087,15 +2180,11 @@
|
||||
"fr": "SMS envoyé au téléphone {0}"
|
||||
},
|
||||
"EnterPhoneNumber": {
|
||||
"ru":
|
||||
"Введите номер в международном формате, только цифры (без '+', пробелов и скобок), или оставьте поле пустым, чтобы привязать без номера",
|
||||
"en":
|
||||
"Enter number in international format, digits only (no '+', spaces or brackets), or leave the field empty to link without a number",
|
||||
"ru": "Введите номер в международном формате, только цифры (без '+', пробелов и скобок), или оставьте поле пустым, чтобы привязать без номера",
|
||||
"en": "Enter number in international format, digits only (no '+', spaces or brackets), or leave the field empty to link without a number",
|
||||
"zh": "请输入国际格式的号码,仅数字(不含空格和括号),或留空以绑定无号码",
|
||||
"ua":
|
||||
"Введіть номер у міжнародному форматі, лише цифри (без '+', пробілів і дужок), або залиште поле порожнім, щоб прив’язати без номера",
|
||||
"fr":
|
||||
"Entrez un numéro au format international, uniquement des chiffres (sans '+', espaces ou parenthèses), ou laissez le champ vide pour lier sans numéro"
|
||||
"ua": "Введіть номер у міжнародному форматі, лише цифри (без '+', пробілів і дужок), або залиште поле порожнім, щоб прив’язати без номера",
|
||||
"fr": "Entrez un numéro au format international, uniquement des chiffres (sans '+', espaces ou parenthèses), ou laissez le champ vide pour lier sans numéro"
|
||||
}
|
||||
},
|
||||
"MafileMoverVM": {
|
||||
@@ -2128,12 +2217,10 @@
|
||||
"fr": "RCode: {0}\r\nNom du fichier: {1}.mafile"
|
||||
},
|
||||
"GuardIsNotActive": {
|
||||
"ru":
|
||||
"Steam Guard установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"",
|
||||
"ru": "Steam Guard установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"",
|
||||
"en": "Steam Guard is set on this account. To link mafile to the account, use the 'Link' window",
|
||||
"zh": "此账户已启用令牌。要将 mafile 绑定到此账户,请使用“绑定”窗口。",
|
||||
"ua":
|
||||
"Steam Guard встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'",
|
||||
"ua": "Steam Guard встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'",
|
||||
"fr": "Steam Guard est configuré sur ce compte. Pour lier mafile au compte, utilisez la fenêtre 'Lien'"
|
||||
},
|
||||
"SeemsNoPhoneNumber": {
|
||||
@@ -2181,15 +2268,11 @@
|
||||
"fr": "Auto "
|
||||
},
|
||||
"TimerPreventedOverlap": {
|
||||
"ru":
|
||||
"Авто: таймер подтверждений пытался начать новый цикл, когда предыдущий еще не завершился. Советуем увеличить задержку",
|
||||
"en":
|
||||
"Auto: confirmation timer tried to start new cycle when previous one was not finished yet. We recommend to increase delay",
|
||||
"ru": "Авто: таймер подтверждений пытался начать новый цикл, когда предыдущий еще не завершился. Советуем увеличить задержку",
|
||||
"en": "Auto: confirmation timer tried to start new cycle when previous one was not finished yet. We recommend to increase delay",
|
||||
"zh": "自动: 确认计时器在前一个周期尚未完成时尝试启动新周期。我们建议增加延迟。",
|
||||
"ua":
|
||||
"Авто: таймер підтверджень намагався запустити новий цикл, коли попередній ще не закінчився. Рекомендуємо збільшити затримку",
|
||||
"fr":
|
||||
"Auto: le timer de confirmation a tenté de démarrer un nouveau cycle alors que le précédent n'était pas encore terminé. Nous recommandons d'augmenter le délai"
|
||||
"ua": "Авто: таймер підтверджень намагався запустити новий цикл, коли попередній ще не закінчився. Рекомендуємо збільшити затримку",
|
||||
"fr": "Auto: le timer de confirmation a tenté de démarrer un nouveau cycle alors que le précédent n'était pas encore terminé. Nous recommandons d'augmenter le délai"
|
||||
},
|
||||
"FailedToLoadStorage": {
|
||||
"en": "Failed to load auto-confirm timers. File seems to be corrupted",
|
||||
@@ -2225,15 +2308,11 @@
|
||||
"fr": "Tous les mafiles ont été renommés avec succès ({0}).\r\nUn backup a été créé: {1}"
|
||||
},
|
||||
"PartialSuccess": {
|
||||
"en":
|
||||
"Total mafiles: {0}\r\nRenamed: {1}\r\nConflicts: {2}\r\nErrors: {3}\r\n\r\nBackup of mafiles saved in file: {4}",
|
||||
"ru":
|
||||
"Всего мафайлов: {0}\r\nПереименовано: {1}\r\nКонфликты: {2}\r\nОшибок: {3}\r\n\r\nРезервная копия мафайлов сохранена в файле: {4}",
|
||||
"en": "Total mafiles: {0}\r\nRenamed: {1}\r\nConflicts: {2}\r\nErrors: {3}\r\n\r\nBackup of mafiles saved in file: {4}",
|
||||
"ru": "Всего мафайлов: {0}\r\nПереименовано: {1}\r\nКонфликты: {2}\r\nОшибок: {3}\r\n\r\nРезервная копия мафайлов сохранена в файле: {4}",
|
||||
"zh": "总文件数: {0}\r\n重命名: {1}\r\n冲突: {2}\r\n错误: {3}\r\n\r\n文件备份已保存至: {4}",
|
||||
"ua":
|
||||
"Всього мафайлів: {0}\r\nПерейменовано: {1}\r\nКонфлікти: {2}\r\nПомилок: {3}\r\n\r\nРезервна копія мафайлів збережена у файлі: {4}",
|
||||
"fr":
|
||||
"Total des mafiles: {0}\r\nRenommés: {1}\r\nConflits: {2}\r\nErreurs: {3}\r\n\r\nBackup des mafiles enregistré dans le fichier: {4}"
|
||||
"ua": "Всього мафайлів: {0}\r\nПерейменовано: {1}\r\nКонфлікти: {2}\r\nПомилок: {3}\r\n\r\nРезервна копія мафайлів збережена у файлі: {4}",
|
||||
"fr": "Total des mafiles: {0}\r\nRenommés: {1}\r\nConflits: {2}\r\nErreurs: {3}\r\n\r\nBackup des mafiles enregistré dans le fichier: {4}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2280,22 +2359,17 @@
|
||||
"zh": "指定的导出路径无效。"
|
||||
},
|
||||
"NebulaUtilizedDirectory": {
|
||||
"ru":
|
||||
"Невозможно выполнить экспорт в папку, которая используется приложением. Рекомендуется выбрать отдельную директорию.",
|
||||
"en":
|
||||
"Cannot export to a directory that is currently used by the application. Please choose a separate folder.",
|
||||
"ua":
|
||||
"Неможливо виконати експорт у папку, яка використовується застосунком. Рекомендується вибрати окрему директорію.",
|
||||
"ru": "Невозможно выполнить экспорт в папку, которая используется приложением. Рекомендуется выбрать отдельную директорию.",
|
||||
"en": "Cannot export to a directory that is currently used by the application. Please choose a separate folder.",
|
||||
"ua": "Неможливо виконати експорт у папку, яка використовується застосунком. Рекомендується вибрати окрему директорію.",
|
||||
"fr": "Impossible d’exporter vers un dossier utilisé par l’application. Veuillez choisir un dossier distinct.",
|
||||
"zh": "无法导出到应用程序正在使用的文件夹。请选择一个单独的目录。"
|
||||
},
|
||||
"AccessDenied": {
|
||||
"ru": "Недостаточно прав для записи в указанную папку. Проверьте права доступа или выберите другой путь.",
|
||||
"en":
|
||||
"Insufficient permissions to write to the specified directory. Please check access rights or choose another path.",
|
||||
"en": "Insufficient permissions to write to the specified directory. Please check access rights or choose another path.",
|
||||
"ua": "Недостатньо прав для запису в указану папку. Перевірте права доступу або виберіть інший шлях.",
|
||||
"fr":
|
||||
"Autorisation insuffisante pour écrire dans le dossier spécifié. Vérifiez les droits d’accès ou choisissez un autre chemin.",
|
||||
"fr": "Autorisation insuffisante pour écrire dans le dossier spécifié. Vérifiez les droits d’accès ou choisissez un autre chemin.",
|
||||
"zh": "没有权限写入指定的文件夹。请检查访问权限或选择其他路径。"
|
||||
}
|
||||
},
|
||||
@@ -2323,17 +2397,23 @@
|
||||
"zh": "。冲突:{0}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateService": {
|
||||
"LatestVersion": {
|
||||
"en": "You are using the latest version",
|
||||
"ru": "У вас установлена последняя версия"
|
||||
},
|
||||
"UpdateIndicatorHint": {
|
||||
"en": "Update available. Click 'by achies' to check",
|
||||
"ru": "Доступно обновление. Нажмите 'by achies' для проверки"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CantAlignTimeError": {
|
||||
"ru":
|
||||
"Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log",
|
||||
"en":
|
||||
"Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log",
|
||||
"ru": "Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log",
|
||||
"en": "Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log",
|
||||
"zh": "无法与 Steam 对齐时间。可能没有互联网连接或 Steam 不可用。详细错误已保存到 log.log 中",
|
||||
"ua":
|
||||
"Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log",
|
||||
"fr":
|
||||
"Impossible de synchroniser l'heure avec Steam. Peut-être qu'il n'y a pas de connexion internet ou Steam est indisponible. Détails de l'erreur enregistrés dans log.log"
|
||||
"ua": "Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log",
|
||||
"fr": "Impossible de synchroniser l'heure avec Steam. Peut-être qu'il n'y a pas de connexion internet ou Steam est indisponible. Détails de l'erreur enregistrés dans log.log"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user