mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-25 06:14:31 +00:00
feat(update): introduce modern update system with JSON changelog, reminders and checksum validation
- Replace legacy AutoUpdater dialog with a custom update UI integrated into the application theme - Add support for JSON-based changelog with HTML fallback for backward compatibility - Implement "Remind later" and "Skip version" options to reduce intrusive update prompts - Add visual update indicator and manual "Check for updates" action in the links menu - Persist user update preferences (skipped versions and reminder delays) - Introduce checksum validation (SHA256) for downloaded update packages - Migrate changelog source format from HTML to JSON - Automatically generate HTML changelog and GitHub release notes from JSON via CI - Consolidate release automation into a single GitHub Actions workflow - Remove obsolete legacy update dialog and related code - Expand localization coverage for update-related UI
This commit is contained in:
@@ -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
|
||||
@@ -1,9 +1,11 @@
|
||||
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;
|
||||
@@ -143,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)
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -694,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",
|
||||
@@ -730,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)"
|
||||
}
|
||||
},
|
||||
@@ -988,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",
|
||||
@@ -1034,6 +1025,10 @@
|
||||
"zh": "文档",
|
||||
"ua": "Документація",
|
||||
"fr": "Documentation"
|
||||
},
|
||||
"CheckForUpdates": {
|
||||
"en": "Check for updates",
|
||||
"ru": "Проверить обновления"
|
||||
}
|
||||
},
|
||||
"MafileMoverView": {
|
||||
@@ -1233,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": {
|
||||
@@ -1259,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": {
|
||||
@@ -1306,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(每行一个)。"
|
||||
}
|
||||
},
|
||||
@@ -1355,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": {
|
||||
@@ -1379,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": "Аутентификатор удален",
|
||||
@@ -1499,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, возможно в папке уже существует мафайл с таким именем.",
|
||||
@@ -1542,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",
|
||||
@@ -1582,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": {
|
||||
@@ -1941,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",
|
||||
@@ -2013,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": {
|
||||
@@ -2153,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} была отправлена СМС",
|
||||
@@ -2171,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": {
|
||||
@@ -2212,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": {
|
||||
@@ -2265,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",
|
||||
@@ -2309,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}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2364,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": "没有权限写入指定的文件夹。请检查访问权限或选择其他路径。"
|
||||
}
|
||||
},
|
||||
@@ -2407,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