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:
achiez
2026-03-13 00:17:45 +02:00
parent 17635ccba0
commit 9ac4ffdc34
19 changed files with 899 additions and 431 deletions
-78
View File
@@ -1,78 +0,0 @@
name: Publish release
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
release:
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version
id: ver
run: echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
shell: bash
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build NebulaAuth
run: dotnet publish src/NebulaAuth/NebulaAuth.csproj -c Release -o build
- name: Package release
run: |
cd build
powershell Compress-Archive * "../NebulaAuth.${env:GITHUB_REF_NAME}.zip"
shell: pwsh
- name: Install pandoc
run: |
choco install pandoc -y
- name: Convert changelog HTML to Markdown (ul-only)
id: changelog
shell: bash
run: |
version="${{ steps.ver.outputs.version }}"
file="changelog/${version}.html"
echo "Looking for file: $file"
if [ -f "$file" ]; then
echo "✅ File found, extracting <ul> section"
sed -n '/<ul>/,/<\/ul>/p' "$file" > body.html
echo "Converting with pandoc..."
pandoc body.html -f html -t markdown --wrap=none -o changelog.md
echo "-------- Converted changelog.md --------"
cat changelog.md
echo "----------------------------------------"
echo "body<<EOF" >> $GITHUB_OUTPUT
cat changelog.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "❌ Changelog not found for version $version"
echo "body=No changelog found for $version" >> $GITHUB_OUTPUT
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: Release ${{ env.RELEASE_NAME }}
body: ${{ steps.changelog.outputs.body }}
files: |
NebulaAuth.${{ steps.ver.outputs.version }}.zip
env:
RELEASE_NAME: ${{ steps.ver.outputs.version }}
-65
View File
@@ -1,65 +0,0 @@
name: Prepare release
on:
push:
branches: [ master ]
paths:
- 'src/NebulaAuth/NebulaAuth.csproj'
workflow_dispatch:
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_TOKEN }}
- name: Read version from csproj
id: ver
shell: bash
run: |
VERSION=$(grep -oPm1 "(?<=<AssemblyVersion>)[^<]+" src/NebulaAuth/NebulaAuth.csproj)
echo "Detected version $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Generate update.xml
run: |
cat > NebulaAuth/update.xml <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>${{ steps.ver.outputs.version }}</version>
<url>https://github.com/${{ github.repository }}/releases/download/${{ steps.ver.outputs.version }}/NebulaAuth.${{ steps.ver.outputs.version }}.zip</url>
<changelog>https://achiez.github.io/${{ github.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."
+218
View File
@@ -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
+13
View File
@@ -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)
+107 -58
View File
@@ -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);
//}
}
+25 -9
View File
@@ -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}" />
+47 -8
View File
@@ -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();
}
}
+10 -1
View File
@@ -58,7 +58,7 @@
</Button>
<Button Margin="0,0,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="Website_Click" IsEnabled="False">
Click="Documentation_Click" IsEnabled="False">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Viewbox Width="22" Height="22" Margin="0 0 8 0">
<Canvas Width="22" Height="22">
@@ -72,6 +72,15 @@
</StackPanel>
</Button>
<Button Margin="0,5,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="CheckForUpdates_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<materialDesign:PackIcon Kind="Update" Width="22" Height="22" Margin="0 0 8 0"
Foreground="{DynamicResource MaterialDesign.Brush.Primary}" />
<TextBlock Text="{Tr LinksView.CheckForUpdates}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" IsCancel="True" Width="0"
Height="0" Visibility="Visible" />
</StackPanel>
+9 -1
View File
@@ -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);
}
}
-55
View File
@@ -1,55 +0,0 @@
<UserControl x:Class="NebulaAuth.View.UpdaterView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
mc:Ignorable="d"
Foreground="WhiteSmoke"
FontFamily="{materialDesign:MaterialDesignFont}"
d:DataContext="{d:DesignInstance other:UpdaterVM}"
Background="{DynamicResource WindowBackground}"
Padding="10"
MaxWidth="500">
<d:DesignerProperties.DesignStyle>
<Style TargetType="UserControl">
<Setter Property="Background" Value="{DynamicResource WindowBackground}" />
</Style>
</d:DesignerProperties.DesignStyle>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontSize="24" Text="Обновления" />
<Separator Grid.Row="1" />
<TextBlock FontSize="16" Margin="5,10,0,10" TextWrapping="WrapWithOverflow" Grid.Row="2">
<Run Text="Доступна новая версия:" />
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.CurrentVersion}" />
<Run Text="&#x0a;Вы используете версию" />
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.InstalledVersion}" />
<Run Text="Хотите обновить программу?" />
</TextBlock>
<Expander Header="Что изменилось?" Grid.Row="3">
<!--<wpf:WebView2 HorizontalAlignment="Stretch" Height="300"
Source="{Binding UpdateInfoEventArgs.ChangelogURL}"
/>-->
</Expander>
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Content="OK" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
<Button IsCancel="True" Grid.Column="1" Content="Cancel"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
</Grid>
</Grid>
</UserControl>
-9
View File
@@ -1,9 +0,0 @@
namespace NebulaAuth.View;
public partial class UpdaterView
{
public UpdaterView()
{
InitializeComponent();
}
}
+1 -1
View File
@@ -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;
}
}
+128 -132
View File
@@ -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 daccé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 daccé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 à lavenir 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 à lavenir 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 couldnt verify that you clicked the link. Make sure you opened the link from the email, then click 'Proceed'",
"ru": "Не удалось подтвердить переход по ссылке. Убедитесь, что вы открыли ссылку из письма, затем нажмите — 'Продолжить'",
"en": "We couldnt 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 dexporter vers un dossier utilisé par lapplication. 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 daccès ou choisissez un autre chemin.",
"fr": "Autorisation insuffisante pour écrire dans le dossier spécifié. Vérifiez les droits daccè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"
}
}