mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-16 23:49:19 +02:00
Drop the update check - Steam updates the starter
The daily GitHub check, the top-bar button, the stable/staging channel and "check now" go, with their settings and texts.
This commit is contained in:
@@ -1,291 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using StarterNG.Application;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain.Settings;
|
||||
using StarterNG.Infrastructure.Adapters;
|
||||
using StarterNG.Tests.Fakes;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class UpdateCheckTests
|
||||
{
|
||||
private const string Page = "https://github.com/MaSzyna-EU07/StarterNG/releases/tag/v1.4.107";
|
||||
private const string StagingPage = "https://github.com/MaSzyna-EU07/StarterNG/releases/tag/staging";
|
||||
private const string RecordPath = "/home/user/.config/starter/updatecheck.txt";
|
||||
private const string RunningCommit = "9d981cb0932c372074eaba25d5c917acbacc25d8";
|
||||
private const string OtherCommit = "f7219cf7055670ad0f39fc18e0640358b8fff8f7";
|
||||
|
||||
private static readonly Version Current = new(1, 4, 100, 0);
|
||||
|
||||
[Theory]
|
||||
[InlineData("v1.4.107")]
|
||||
[InlineData("v1.5.0")]
|
||||
[InlineData("1.4.101")]
|
||||
public async Task A_higher_release_is_an_update(string tag)
|
||||
{
|
||||
var check = new Rig(new StubFeed(Stable(tag))).Check;
|
||||
|
||||
Assert.Equal(UpdateCheckOutcome.UpdateAvailable, await check.CheckNowAsync(UpdateChannel.Stable));
|
||||
Assert.Equal(UpdateCheck.ParseTag(tag)!.ToString(), check.Available!.Label);
|
||||
Assert.Equal(Page, check.Available.PageUrl);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("v1.4.100")]
|
||||
[InlineData("v1.4.99")]
|
||||
[InlineData("v1.3.500")]
|
||||
[InlineData("staging")]
|
||||
[InlineData("v1.5.0-rc1")]
|
||||
public async Task The_same_an_older_or_an_unversioned_release_is_not(string tag)
|
||||
{
|
||||
var check = new Rig(new StubFeed(Stable(tag))).Check;
|
||||
|
||||
Assert.Equal(UpdateCheckOutcome.UpToDate, await check.CheckNowAsync(UpdateChannel.Stable));
|
||||
Assert.Null(check.Available);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task No_answer_is_reported_as_such_and_not_remembered()
|
||||
{
|
||||
var rig = new Rig(new StubFeed(lookup: ReleaseLookup.NoAnswer));
|
||||
|
||||
Assert.Equal(UpdateCheckOutcome.NoAnswer, await rig.Check.CheckNowAsync(UpdateChannel.Stable));
|
||||
Assert.False(rig.Files.FileExists(RecordPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_daily_check_asks_once_a_day_and_keeps_showing_the_answer()
|
||||
{
|
||||
var feed = new StubFeed(Stable("v1.4.107"));
|
||||
var rig = new Rig(feed);
|
||||
await rig.Check.CheckDailyAsync();
|
||||
|
||||
// A second start the same day: no request, the update still shown.
|
||||
var again = rig.Restart();
|
||||
await again.CheckDailyAsync();
|
||||
|
||||
Assert.Equal(1, feed.Calls);
|
||||
Assert.Equal("1.4.107", again.Available!.Label);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_next_day_it_asks_again()
|
||||
{
|
||||
var feed = new StubFeed(Stable("v1.4.107"));
|
||||
var rig = new Rig(feed);
|
||||
await rig.Check.CheckDailyAsync();
|
||||
|
||||
rig.Clock.Now = rig.Clock.Now.AddDays(1);
|
||||
await rig.Restart().CheckDailyAsync();
|
||||
|
||||
Assert.Equal(2, feed.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_remembered_release_already_installed_is_not_offered()
|
||||
{
|
||||
var rig = new Rig(new StubFeed(Stable("v1.4.107")));
|
||||
await rig.Check.CheckDailyAsync();
|
||||
|
||||
var upgraded = rig.Restart(current: new Version(1, 4, 107, 0));
|
||||
await upgraded.CheckDailyAsync();
|
||||
|
||||
Assert.Null(upgraded.Available);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Switching_the_channel_asks_again_the_same_day()
|
||||
{
|
||||
var feed = new StubFeed(Stable("v1.4.107"));
|
||||
var rig = new Rig(feed);
|
||||
await rig.Check.CheckDailyAsync();
|
||||
|
||||
rig.Settings.UpdateChannel = UpdateChannel.Staging;
|
||||
await rig.Restart().CheckDailyAsync();
|
||||
|
||||
Assert.Equal(2, feed.Calls);
|
||||
Assert.Equal(UpdateChannel.Staging, feed.LastChannel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Switched_off_the_daily_check_does_not_ask_but_the_button_does()
|
||||
{
|
||||
var feed = new StubFeed(Stable("v9.0.0"));
|
||||
var rig = new Rig(feed);
|
||||
rig.Settings.CheckForUpdates = false;
|
||||
|
||||
await rig.Check.CheckDailyAsync();
|
||||
Assert.Equal(0, feed.Calls);
|
||||
|
||||
Assert.Equal(UpdateCheckOutcome.UpdateAvailable, await rig.Check.CheckNowAsync(UpdateChannel.Stable));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_staging_build_from_another_commit_is_offered()
|
||||
{
|
||||
var check = new Rig(new StubFeed(Staging(OtherCommit))).Check;
|
||||
|
||||
Assert.Equal(UpdateCheckOutcome.UpdateAvailable, await check.CheckNowAsync(UpdateChannel.Staging));
|
||||
Assert.Equal("staging f7219cf", check.Available!.Label);
|
||||
Assert.Equal(StagingPage, check.Available.PageUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_staging_build_already_running_is_not()
|
||||
{
|
||||
var check = new Rig(new StubFeed(Staging(RunningCommit))).Check;
|
||||
|
||||
Assert.Equal(UpdateCheckOutcome.UpToDate, await check.CheckNowAsync(UpdateChannel.Staging));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Finding_an_update_tells_the_top_bar()
|
||||
{
|
||||
var check = new Rig(new StubFeed(Stable("v1.4.107"))).Check;
|
||||
int changes = 0;
|
||||
check.Changed += () => changes++;
|
||||
|
||||
await check.CheckNowAsync(UpdateChannel.Stable);
|
||||
await check.CheckNowAsync(UpdateChannel.Stable);
|
||||
|
||||
Assert.Equal(1, changes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_commit_is_read_from_the_informational_version()
|
||||
{
|
||||
Assert.Equal(RunningCommit, UpdateCheck.CommitOf("1.4.113+" + RunningCommit));
|
||||
Assert.Null(UpdateCheck.CommitOf("1.4.113"));
|
||||
Assert.Null(UpdateCheck.CommitOf(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_four_part_assembly_version_matches_a_three_part_tag()
|
||||
{
|
||||
Assert.Equal(new Version(1, 4, 0), UpdateCheck.ParseTag("v1.4"));
|
||||
Assert.Equal(new Version(1, 4, 107), UpdateCheck.ParseTag("V1.4.107"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_feed_reads_a_stable_release()
|
||||
{
|
||||
var feed = Feed(HttpStatusCode.OK,
|
||||
$$"""{"tag_name":"v1.4.107","html_url":"{{Page}}","target_commitish":"{{OtherCommit}}","prerelease":false}""");
|
||||
|
||||
var lookup = await feed.LatestAsync(UpdateChannel.Stable);
|
||||
|
||||
Assert.True(lookup.Answered);
|
||||
Assert.Equal(new ReleaseInfo("v1.4.107", Page, OtherCommit), lookup.Release);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_stable_feed_skips_a_pre_release_and_the_staging_feed_takes_it()
|
||||
{
|
||||
string json = $$"""{"tag_name":"staging","html_url":"{{StagingPage}}","target_commitish":"{{OtherCommit}}","prerelease":true}""";
|
||||
|
||||
Assert.Null((await Feed(HttpStatusCode.OK, json).LatestAsync(UpdateChannel.Stable)).Release);
|
||||
Assert.Equal(OtherCommit, (await Feed(HttpStatusCode.OK, json).LatestAsync(UpdateChannel.Staging)).Release!.Commit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_staging_feed_asks_for_the_staging_tag()
|
||||
{
|
||||
string? asked = null;
|
||||
var feed = new GitHubReleaseFeed(new TestInstallation().Log, Current, new StubHandler(request =>
|
||||
{
|
||||
asked = request.RequestUri!.AbsolutePath;
|
||||
return Respond(HttpStatusCode.NotFound, "{}");
|
||||
}));
|
||||
|
||||
await feed.LatestAsync(UpdateChannel.Staging);
|
||||
|
||||
Assert.EndsWith("/releases/tags/staging", asked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Nothing_released_yet_is_an_answer()
|
||||
{
|
||||
var lookup = await Feed(HttpStatusCode.NotFound, """{"message":"Not Found"}""").LatestAsync(UpdateChannel.Stable);
|
||||
|
||||
Assert.True(lookup.Answered);
|
||||
Assert.Null(lookup.Release);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task No_network_is_no_answer_and_nothing_in_the_log()
|
||||
{
|
||||
var log = new TestInstallation().Log;
|
||||
var feed = new GitHubReleaseFeed(log, Current, new StubHandler(_ => throw new HttpRequestException("offline")));
|
||||
|
||||
Assert.False((await feed.LatestAsync(UpdateChannel.Stable)).Answered);
|
||||
Assert.Equal("", log.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_broken_answer_is_logged()
|
||||
{
|
||||
var log = new TestInstallation().Log;
|
||||
var feed = new GitHubReleaseFeed(log, Current, new StubHandler(_ => Respond(HttpStatusCode.OK, "<html>")));
|
||||
|
||||
Assert.False((await feed.LatestAsync(UpdateChannel.Stable)).Answered);
|
||||
Assert.Contains("update check", log.Text);
|
||||
}
|
||||
|
||||
private static ReleaseLookup Stable(string tag) => ReleaseLookup.Found(new ReleaseInfo(tag, Page));
|
||||
|
||||
private static ReleaseLookup Staging(string commit) =>
|
||||
ReleaseLookup.Found(new ReleaseInfo("staging", StagingPage, commit));
|
||||
|
||||
private static GitHubReleaseFeed Feed(HttpStatusCode status, string body) =>
|
||||
new(new TestInstallation().Log, Current, new StubHandler(_ => Respond(status, body)));
|
||||
|
||||
private static HttpResponseMessage Respond(HttpStatusCode status, string body) =>
|
||||
new(status) { Content = new StringContent(body) };
|
||||
|
||||
/// <summary>One installation's worth of update checking, restartable on the same disk.</summary>
|
||||
private sealed class Rig
|
||||
{
|
||||
private readonly IReleaseFeed _feed;
|
||||
private readonly TestInstallation _installation;
|
||||
|
||||
public Rig(IReleaseFeed feed)
|
||||
{
|
||||
_feed = feed;
|
||||
_installation = new TestInstallation(Files, new DateTime(2026, 10, 4, 9, 0, 0));
|
||||
Check = Restart();
|
||||
}
|
||||
|
||||
public InMemoryFileSystem Files { get; } = new();
|
||||
|
||||
public SimulatorSettings Settings { get; } = new();
|
||||
|
||||
public FixedClock Clock => _installation.Clock;
|
||||
|
||||
public UpdateCheck Check { get; }
|
||||
|
||||
public UpdateCheck Restart(Version? current = null) =>
|
||||
new(Settings, _feed, current ?? Current, RunningCommit, Files, Clock, _installation.Log, RecordPath);
|
||||
}
|
||||
|
||||
private sealed class StubFeed(ReleaseLookup lookup) : IReleaseFeed
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
|
||||
public UpdateChannel? LastChannel { get; private set; }
|
||||
|
||||
public Task<ReleaseLookup> LatestAsync(UpdateChannel channel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Calls++;
|
||||
LastChannel = channel;
|
||||
return Task.FromResult(lookup);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> respond) : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Task.FromResult(respond(request));
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StarterNG.Domain.Settings;
|
||||
|
||||
namespace StarterNG.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// A published build of the starter: its tag, the page it is downloaded from and
|
||||
/// the commit it was built from - the only way to tell rolling builds apart.
|
||||
/// </summary>
|
||||
public sealed record ReleaseInfo(string Tag, string PageUrl, string? Commit = null);
|
||||
|
||||
/// <summary>
|
||||
/// What asking for the newest release came back with. No answer - offline, rate
|
||||
/// limited, timed out - is kept apart from an answer of "nothing released", so a
|
||||
/// failed check is not remembered as a done one.
|
||||
/// </summary>
|
||||
public sealed record ReleaseLookup(bool Answered, ReleaseInfo? Release)
|
||||
{
|
||||
public static ReleaseLookup NoAnswer { get; } = new(false, null);
|
||||
|
||||
public static ReleaseLookup Found(ReleaseInfo? release) => new(true, release);
|
||||
}
|
||||
|
||||
public interface IReleaseFeed
|
||||
{
|
||||
Task<ReleaseLookup> LatestAsync(UpdateChannel channel, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain.Settings;
|
||||
using StarterNG.Infrastructure.Adapters;
|
||||
@@ -54,12 +53,6 @@ public sealed class AppServices
|
||||
SettingsStore = new SettingsStore(files, clock, log, settingsPaths, new SettingsSerializer(), Executables);
|
||||
MissingVehicleLog = new MissingVehicleLog(SettingsStore.Settings, MissingAssets, Library, log);
|
||||
Favorites = new FavoriteSceneries(files, log, settingsPaths.FavoritesPath());
|
||||
var assembly = typeof(AppServices).Assembly;
|
||||
var version = assembly.GetName().Version ?? new Version(0, 0);
|
||||
string? commit = UpdateCheck.CommitOf(
|
||||
assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion);
|
||||
Updates = new UpdateCheck(SettingsStore.Settings, new GitHubReleaseFeed(log, version), version, commit,
|
||||
files, clock, log, settingsPaths.UpdateCheckPath());
|
||||
StartSimulation = new StartSimulation(State, SettingsStore, Library.Vehicles, Executables, files, paths,
|
||||
Processes, Random, log);
|
||||
Aftermath = new SimulatorAftermath(files, paths, log);
|
||||
@@ -134,8 +127,6 @@ public sealed class AppServices
|
||||
|
||||
public MissingVehicleLog MissingVehicleLog { get; }
|
||||
|
||||
public UpdateCheck Updates { get; }
|
||||
|
||||
public FavoriteSceneries Favorites { get; }
|
||||
|
||||
public StartSimulation StartSimulation { get; }
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain.Settings;
|
||||
|
||||
namespace StarterNG.Application;
|
||||
|
||||
/// <summary>A build to offer: what to call it on the top bar and where to get it.</summary>
|
||||
public sealed record AvailableUpdate(string Label, string PageUrl);
|
||||
|
||||
public enum UpdateCheckOutcome
|
||||
{
|
||||
UpToDate,
|
||||
UpdateAvailable,
|
||||
NoAnswer
|
||||
}
|
||||
|
||||
public sealed class UpdateCheck
|
||||
{
|
||||
private const string DayFormat = "yyyy-MM-dd";
|
||||
private const int ShortCommit = 7;
|
||||
|
||||
private readonly SimulatorSettings _settings;
|
||||
private readonly IReleaseFeed _feed;
|
||||
private readonly string? _commit;
|
||||
private readonly IFileSystem _files;
|
||||
private readonly IClock _clock;
|
||||
private readonly IDiagnosticsLog _log;
|
||||
private readonly string _recordPath;
|
||||
|
||||
public UpdateCheck(SimulatorSettings settings, IReleaseFeed feed, Version current, string? commit,
|
||||
IFileSystem files, IClock clock, IDiagnosticsLog log, string recordPath)
|
||||
{
|
||||
_settings = settings;
|
||||
_feed = feed;
|
||||
_commit = commit;
|
||||
_files = files;
|
||||
_clock = clock;
|
||||
_log = log;
|
||||
_recordPath = recordPath;
|
||||
Current = Normalize(current);
|
||||
}
|
||||
|
||||
public Version Current { get; }
|
||||
|
||||
/// <summary>The build to offer, as last heard of; null when up to date or not asked yet.</summary>
|
||||
public AvailableUpdate? Available { get; private set; }
|
||||
|
||||
public event Action? Changed;
|
||||
|
||||
/// <summary>
|
||||
/// The check made on start. GitHub is asked at most once a day, and its answer
|
||||
/// is remembered, so a second start the same day still shows the update.
|
||||
/// </summary>
|
||||
public async Task CheckDailyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_settings.CheckForUpdates)
|
||||
return;
|
||||
|
||||
var channel = _settings.UpdateChannel;
|
||||
if (ReadRecord() is { } record && record.Day == Today() && record.Channel == channel)
|
||||
{
|
||||
Publish(channel, record.Release);
|
||||
return;
|
||||
}
|
||||
|
||||
await CheckNowAsync(channel, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Asks straight away, whatever the switch in the settings says.</summary>
|
||||
public async Task<UpdateCheckOutcome> CheckNowAsync(UpdateChannel channel,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var lookup = await _feed.LatestAsync(channel, cancellationToken);
|
||||
|
||||
// Not recorded, so the next start asks again instead of waiting a day.
|
||||
if (!lookup.Answered)
|
||||
return UpdateCheckOutcome.NoAnswer;
|
||||
|
||||
WriteRecord(channel, lookup.Release);
|
||||
Publish(channel, lookup.Release);
|
||||
return Available is null ? UpdateCheckOutcome.UpToDate : UpdateCheckOutcome.UpdateAvailable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "v1.4.107" as 1.4.107. Anything that is not a plain version - a suffix, a
|
||||
/// rolling tag like "staging" - is not an update.
|
||||
/// </summary>
|
||||
public static Version? ParseTag(string tag)
|
||||
{
|
||||
string text = tag.StartsWith('v') || tag.StartsWith('V') ? tag[1..] : tag;
|
||||
return Version.TryParse(text, out var version) ? Normalize(version) : null;
|
||||
}
|
||||
|
||||
/// <summary>The commit a build carries after the "+" of its informational version.</summary>
|
||||
public static string? CommitOf(string? informationalVersion)
|
||||
{
|
||||
int plus = informationalVersion?.IndexOf('+') ?? -1;
|
||||
return plus >= 0 && plus < informationalVersion!.Length - 1 ? informationalVersion[(plus + 1)..] : null;
|
||||
}
|
||||
|
||||
private void Publish(UpdateChannel channel, ReleaseInfo? release)
|
||||
{
|
||||
// Compared again on every start: the build remembered this morning may be
|
||||
// the very one installed since.
|
||||
var available = channel == UpdateChannel.Staging ? StagingUpdate(release) : StableUpdate(release);
|
||||
if (available == Available)
|
||||
return;
|
||||
|
||||
Available = available;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private AvailableUpdate? StableUpdate(ReleaseInfo? release) =>
|
||||
release is not null && ParseTag(release.Tag) is { } version && version > Current
|
||||
? new AvailableUpdate(version.ToString(), release.PageUrl)
|
||||
: null;
|
||||
|
||||
// The staging build has no version of its own, only its commit, and the newest
|
||||
// one is the only one there is: any build but the running one is worth offering.
|
||||
private AvailableUpdate? StagingUpdate(ReleaseInfo? release) =>
|
||||
release is { Commit: { Length: > 0 } commit } && !SameCommit(commit, _commit)
|
||||
? new AvailableUpdate($"{release.Tag} {commit[..Math.Min(ShortCommit, commit.Length)]}", release.PageUrl)
|
||||
: null;
|
||||
|
||||
private static bool SameCommit(string a, string? b) =>
|
||||
!string.IsNullOrEmpty(b) &&
|
||||
(a.StartsWith(b, StringComparison.OrdinalIgnoreCase) || b.StartsWith(a, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private string Today() => _clock.Now.ToString(DayFormat, CultureInfo.InvariantCulture);
|
||||
|
||||
private (string Day, UpdateChannel Channel, ReleaseInfo? Release)? ReadRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_files.FileExists(_recordPath))
|
||||
return null;
|
||||
|
||||
string[] lines = _files.ReadAllText(_recordPath).Split('\n', StringSplitOptions.TrimEntries);
|
||||
if (lines.Length < 5 || !Enum.TryParse(lines[1], out UpdateChannel channel))
|
||||
return null;
|
||||
|
||||
var release = lines[2].Length > 0 && lines[3].Length > 0
|
||||
? new ReleaseInfo(lines[2], lines[3], lines[4].Length > 0 ? lines[4] : null)
|
||||
: null;
|
||||
return (lines[0], channel, release);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Log($"reading {_recordPath}", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteRecord(UpdateChannel channel, ReleaseInfo? release)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Path.GetDirectoryName(_recordPath) is { Length: > 0 } directory)
|
||||
_files.CreateDirectory(directory);
|
||||
|
||||
_files.WriteAllText(_recordPath, string.Join('\n', Today(), channel, release?.Tag ?? "",
|
||||
release?.PageUrl ?? "", release?.Commit ?? ""));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Log($"writing {_recordPath}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// The assembly carries four parts and the tags three; an unset part reads as -1.
|
||||
private static Version Normalize(Version version) =>
|
||||
new(version.Major, version.Minor, Math.Max(version.Build, 0));
|
||||
}
|
||||
@@ -36,8 +36,6 @@ public sealed class SimulatorSettings
|
||||
public bool VirtualShunting = true;
|
||||
public bool LogMissingVehicleFiles;
|
||||
public bool DeveloperTools;
|
||||
public bool CheckForUpdates = true;
|
||||
public UpdateChannel UpdateChannel = UpdateChannel.Stable;
|
||||
|
||||
public int RenderEngine;
|
||||
public int Width = 1280;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace StarterNG.Domain.Settings;
|
||||
|
||||
/// <summary>Which builds the starter offers as updates.</summary>
|
||||
public enum UpdateChannel
|
||||
{
|
||||
/// <summary>The versioned releases of main.</summary>
|
||||
Stable,
|
||||
|
||||
/// <summary>The rolling pre-release of the staging branch, for testers.</summary>
|
||||
Staging
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain.Settings;
|
||||
|
||||
namespace StarterNG.Infrastructure.Adapters;
|
||||
|
||||
public sealed class GitHubReleaseFeed : IReleaseFeed
|
||||
{
|
||||
private const string ReleasesUrl = "https://api.github.com/repos/MaSzyna-EU07/StarterNG/releases/";
|
||||
|
||||
/// <summary>The tag CI keeps recreating for every build of the staging branch.</summary>
|
||||
public const string StagingTag = "staging";
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly IDiagnosticsLog _log;
|
||||
|
||||
public GitHubReleaseFeed(IDiagnosticsLog log, Version current, HttpMessageHandler? handler = null)
|
||||
{
|
||||
_log = log;
|
||||
_http = handler is null ? new HttpClient() : new HttpClient(handler);
|
||||
_http.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
// The API turns away requests without a User-Agent.
|
||||
_http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StarterNG", current.ToString()));
|
||||
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||
}
|
||||
|
||||
public async Task<ReleaseLookup> LatestAsync(UpdateChannel channel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// "latest" is GitHub's own pick: the newest release that is not a pre-release.
|
||||
string url = ReleasesUrl + (channel == UpdateChannel.Staging ? "tags/" + StagingTag : "latest");
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await _http.GetAsync(url, cancellationToken);
|
||||
|
||||
// Nothing released yet.
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return ReleaseLookup.Found(null);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return ReleaseLookup.Found(Parse(await response.Content.ReadAsStringAsync(cancellationToken), channel));
|
||||
}
|
||||
// Offline, rate limited or timed out: there is simply no answer this time,
|
||||
// and a log entry on every start without a network would only be noise.
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return ReleaseLookup.NoAnswer;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return ReleaseLookup.NoAnswer;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Log("update check", ex);
|
||||
return ReleaseLookup.NoAnswer;
|
||||
}
|
||||
}
|
||||
|
||||
public static ReleaseInfo? Parse(string json, UpdateChannel channel)
|
||||
{
|
||||
var release = JsonSerializer.Deserialize(json, GitHubReleaseContext.Default.GitHubReleaseDocument);
|
||||
if (release is null || release.Draft ||
|
||||
string.IsNullOrEmpty(release.TagName) || string.IsNullOrEmpty(release.HtmlUrl))
|
||||
return null;
|
||||
|
||||
bool belongs = channel == UpdateChannel.Staging
|
||||
? release.TagName == StagingTag
|
||||
: !release.Prerelease;
|
||||
|
||||
return belongs ? new ReleaseInfo(release.TagName, release.HtmlUrl, release.TargetCommitish) : null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GitHubReleaseDocument
|
||||
{
|
||||
[JsonPropertyName("tag_name")] public string? TagName { get; set; }
|
||||
|
||||
[JsonPropertyName("html_url")] public string? HtmlUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("target_commitish")] public string? TargetCommitish { get; set; }
|
||||
|
||||
[JsonPropertyName("draft")] public bool Draft { get; set; }
|
||||
|
||||
[JsonPropertyName("prerelease")] public bool Prerelease { get; set; }
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(GitHubReleaseDocument))]
|
||||
internal partial class GitHubReleaseContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -53,9 +53,6 @@ public sealed class SettingsPaths
|
||||
/// <summary>The sceneries starred in the list.</summary>
|
||||
public string FavoritesPath() => Path.Combine(UserConfigDirectory(), "favorites.txt");
|
||||
|
||||
/// <summary>The day of the last update check and the release it found.</summary>
|
||||
public string UpdateCheckPath() => Path.Combine(UserConfigDirectory(), "updatecheck.txt");
|
||||
|
||||
public string UserConfigDirectory() =>
|
||||
Path.GetDirectoryName(UserConfigPath()) ?? _environment.BaseDirectory;
|
||||
}
|
||||
|
||||
@@ -57,11 +57,6 @@ public sealed class SettingsSerializer
|
||||
s.VirtualShunting = c.GetBool("ai.trainman", true);
|
||||
s.LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false);
|
||||
s.DeveloperTools = c.GetBool("starter.developertools", false);
|
||||
s.CheckForUpdates = c.GetBool("starter.checkupdates", true);
|
||||
s.UpdateChannel = string.Equals(c.GetString("starter.updatechannel", "stable"), "staging",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
? UpdateChannel.Staging
|
||||
: UpdateChannel.Stable;
|
||||
|
||||
s.RenderEngine = IndexOf(SimulatorSettings.RenderEngines, c.GetString("gfxrenderer", "full"), 0);
|
||||
s.Width = c.GetInt("width", 1280);
|
||||
@@ -202,8 +197,6 @@ public sealed class SettingsSerializer
|
||||
c.SetBool("ai.trainman", s.VirtualShunting);
|
||||
c.SetBool("starter.logmissingvehicles", s.LogMissingVehicleFiles);
|
||||
c.SetBool("starter.developertools", s.DeveloperTools);
|
||||
c.SetBool("starter.checkupdates", s.CheckForUpdates);
|
||||
c.Set("starter.updatechannel", s.UpdateChannel == UpdateChannel.Staging ? "staging" : "stable");
|
||||
|
||||
c.Set("gfxrenderer", SimulatorSettings.RenderEngines[Clamp(s.RenderEngine, 0, SimulatorSettings.RenderEngines.Length - 1)]);
|
||||
c.SetInt("width", s.Width);
|
||||
|
||||
@@ -55,16 +55,6 @@
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal"
|
||||
VerticalAlignment="Center" Margin="0,0,3,0" Spacing="0">
|
||||
<Button x:Name="UpdateButton" Classes="Basic" Cursor="Hand" IsVisible="False"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0" Click="UpdateButton_OnClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<materialIcons:MaterialIcon Kind="ArrowUpBoldCircle" Height="16" Width="16"
|
||||
Foreground="{StaticResource CarbonAccentBright}" />
|
||||
<TextBlock x:Name="UpdateText" FontSize="12" VerticalAlignment="Center"
|
||||
Foreground="{StaticResource CarbonAccentBright}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,6,0">
|
||||
<Label Content="{Binding [Version], Source={x:Static starterNg:App.Loc}}"
|
||||
FontSize="12" Foreground="{StaticResource CarbonFgDim}" Padding="0" />
|
||||
|
||||
@@ -57,7 +57,6 @@ public partial class MainWindow : Window
|
||||
RoutingStrategies.Tunnel);
|
||||
|
||||
SettingsView.DeveloperToolsChanged += ApplyDeveloperTab;
|
||||
AppServices.Current.Updates.Changed += ShowUpdate;
|
||||
|
||||
SettingsView.ThumbnailSizeChanged += () =>
|
||||
{
|
||||
@@ -86,11 +85,6 @@ public partial class MainWindow : Window
|
||||
|
||||
_openedUtc = DateTime.UtcNow;
|
||||
UpdateStartButton();
|
||||
#if !DEBUG
|
||||
// A local build carries the bare major.minor, older than any release;
|
||||
// the button in the settings still checks on demand.
|
||||
_ = AppServices.Current.Updates.CheckDailyAsync();
|
||||
#endif
|
||||
};
|
||||
Activated += (_, _) => CheckExternalSettings();
|
||||
}
|
||||
@@ -258,26 +252,8 @@ public partial class MainWindow : Window
|
||||
App.ApplyLanguage(code);
|
||||
}
|
||||
|
||||
private void ShowUpdate()
|
||||
{
|
||||
var update = AppServices.Current.Updates.Available;
|
||||
UpdateButton.IsVisible = update is not null;
|
||||
if (update is null)
|
||||
return;
|
||||
|
||||
UpdateText.Text = string.Format(App.Loc["UpdateAvailable"], update.Label);
|
||||
ToolTip.SetTip(UpdateButton, string.Format(App.Loc["UpdateAvailableTip"], update.Label));
|
||||
}
|
||||
|
||||
private void UpdateButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (AppServices.Current.Updates.Available is { } update)
|
||||
AppServices.Current.Processes.OpenInShell(update.PageUrl);
|
||||
}
|
||||
|
||||
private void OnLanguageChanged()
|
||||
{
|
||||
ShowUpdate();
|
||||
SelectActiveLanguage();
|
||||
ScenariosView?.RebuildAfterLanguageChange();
|
||||
DepotView?.RebuildAfterLanguageChange();
|
||||
|
||||
@@ -109,26 +109,6 @@
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="starter.checkupdates, starter.updatechannel" Description="{Binding [CheckUpdatesDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel Spacing="6">
|
||||
<CheckBox x:Name="CheckUpdatesCb" Content="{Binding [CheckUpdates], Source={x:Static starterNg:App.Loc}}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<ComboBox x:Name="UpdateChannelCb" SelectedIndex="0" MinWidth="160"
|
||||
SelectionChanged="UpdateChannelCb_OnSelectionChanged">
|
||||
<ComboBoxItem Content="{Binding [UpdateChannelStable], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Content="{Binding [UpdateChannelStaging], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ComboBox>
|
||||
<Button x:Name="CheckUpdatesNowButton" Classes="Flat" Cursor="Hand"
|
||||
Click="CheckUpdatesNowButton_OnClick"
|
||||
Content="{Binding [CheckUpdatesNow], Source={x:Static starterNg:App.Loc}}" />
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="UpdateChannelWarning" IsVisible="False" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource CarbonAccentBright}"
|
||||
Text="{Binding [UpdateChannelWarning], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock x:Name="CheckUpdatesStatus" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource CarbonFgDim}" />
|
||||
</StackPanel>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="starter.autoclose">
|
||||
<CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
|
||||
@@ -260,8 +260,6 @@ public partial class Settings : UserControl, ISettingsCapture
|
||||
VirtualShuntingCb.IsChecked = s.VirtualShunting;
|
||||
LogMissingVehicleFilesCb.IsChecked = s.LogMissingVehicleFiles;
|
||||
DeveloperToolsCb.IsChecked = s.DeveloperTools;
|
||||
CheckUpdatesCb.IsChecked = s.CheckForUpdates;
|
||||
UpdateChannelCb.SelectedIndex = s.UpdateChannel == UpdateChannel.Staging ? 1 : 0;
|
||||
|
||||
RenderEngineCb.SelectedIndex = s.RenderEngine;
|
||||
SelectResolution(s.Width, s.Height);
|
||||
@@ -361,8 +359,6 @@ public partial class Settings : UserControl, ISettingsCapture
|
||||
s.VirtualShunting = IsChecked(VirtualShuntingCb);
|
||||
s.LogMissingVehicleFiles = IsChecked(LogMissingVehicleFilesCb);
|
||||
s.DeveloperTools = IsChecked(DeveloperToolsCb);
|
||||
s.CheckForUpdates = IsChecked(CheckUpdatesCb);
|
||||
s.UpdateChannel = SelectedChannel();
|
||||
|
||||
s.RenderEngine = Math.Max(0, RenderEngineCb.SelectedIndex);
|
||||
ReadResolution(s);
|
||||
@@ -455,36 +451,6 @@ public partial class Settings : UserControl, ISettingsCapture
|
||||
SimulatorSettings.RenderEngines.ElementAtOrDefault(RenderEngineCb.SelectedIndex) == "experimental";
|
||||
}
|
||||
|
||||
private UpdateChannel SelectedChannel() =>
|
||||
UpdateChannelCb.SelectedIndex == 1 ? UpdateChannel.Staging : UpdateChannel.Stable;
|
||||
|
||||
private void UpdateChannelCb_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (UpdateChannelWarning is not null)
|
||||
UpdateChannelWarning.IsVisible = SelectedChannel() == UpdateChannel.Staging;
|
||||
}
|
||||
|
||||
/// <summary>Checks the channel picked in the list, saved or not, so it can be tried first.</summary>
|
||||
private async void CheckUpdatesNowButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var updates = AppServices.Current.Updates;
|
||||
CheckUpdatesNowButton.IsEnabled = false;
|
||||
CheckUpdatesStatus.Text = App.Loc["UpdateChecking"];
|
||||
try
|
||||
{
|
||||
CheckUpdatesStatus.Text = await updates.CheckNowAsync(SelectedChannel()) switch
|
||||
{
|
||||
UpdateCheckOutcome.UpdateAvailable => string.Format(App.Loc["UpdateFound"], updates.Available!.Label),
|
||||
UpdateCheckOutcome.UpToDate => string.Format(App.Loc["UpdateUpToDate"], updates.Current),
|
||||
_ => App.Loc["UpdateCheckFailed"]
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
CheckUpdatesNowButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RedrawNavIfNeeded()
|
||||
{
|
||||
bool developer = AppServices.Current.Settings.DeveloperTools;
|
||||
|
||||
@@ -356,18 +356,6 @@
|
||||
<String key="VirtualShunting">Virtuální posun</String>
|
||||
<String key="LogMissingVehicleFiles">Zaznamenat chybějící soubory vozidel při exportu</String>
|
||||
<String key="DeveloperTools">Show the package author's tab</String>
|
||||
<String key="CheckUpdates">Kontrolovat aktualizace starteru</String>
|
||||
<String key="CheckUpdatesDesc">Jednou denně zjistí na GitHubu, zda vyšla novější verze starteru, a zobrazí ji v horní liště. Testovací kanál nabízí buildy z větve staging.</String>
|
||||
<String key="UpdateAvailable">Aktualizace {0}</String>
|
||||
<String key="UpdateAvailableTip">Je k dispozici verze {0}. Kliknutím otevřete stránku ke stažení.</String>
|
||||
<String key="UpdateChannelStable">Stabilní</String>
|
||||
<String key="UpdateChannelStaging">Testovací (staging)</String>
|
||||
<String key="UpdateChannelWarning">Upozornění: kanál se týká pouze starteru. Simulátor MaSzyna a jeho soubory se aktualizují zvlášť - tato volba je nemění.</String>
|
||||
<String key="CheckUpdatesNow">Zkontrolovat nyní</String>
|
||||
<String key="UpdateChecking">Kontroluji…</String>
|
||||
<String key="UpdateFound">K dispozici je verze {0} - tlačítko je v horní liště.</String>
|
||||
<String key="UpdateUpToDate">Máte nejnovější verzi ({0}).</String>
|
||||
<String key="UpdateCheckFailed">Nepodařilo se připojit ke GitHubu. Zkuste to později.</String>
|
||||
|
||||
<String key="Graphic">Grafika</String>
|
||||
<String key="RenderEngine">Vykreslovací engine</String>
|
||||
|
||||
@@ -363,18 +363,6 @@
|
||||
<String key="LogMissingVehicleFilesDesc">Writes vehicles the starter could not find into the log.</String>
|
||||
<String key="DeveloperTools">Show the package author's tab</String>
|
||||
<String key="DeveloperToolsDesc">Adds a tab with the state of the vehicle database, for preparing rolling stock packages.</String>
|
||||
<String key="CheckUpdates">Check for starter updates</String>
|
||||
<String key="CheckUpdatesDesc">Once a day, asks GitHub whether a newer starter has been released and shows it on the top bar. The testing channel offers builds of the staging branch.</String>
|
||||
<String key="UpdateAvailable">Update {0}</String>
|
||||
<String key="UpdateAvailableTip">Version {0} is available. Click to open its download page.</String>
|
||||
<String key="UpdateChannelStable">Stable</String>
|
||||
<String key="UpdateChannelStaging">Testing (staging)</String>
|
||||
<String key="UpdateChannelWarning">Note: the channel applies to the starter only. The MaSzyna simulator and its files are updated separately - this choice does not change them.</String>
|
||||
<String key="CheckUpdatesNow">Check now</String>
|
||||
<String key="UpdateChecking">Checking…</String>
|
||||
<String key="UpdateFound">Version {0} is available - see the button on the top bar.</String>
|
||||
<String key="UpdateUpToDate">You have the newest version ({0}).</String>
|
||||
<String key="UpdateCheckFailed">Could not reach GitHub. Try again later.</String>
|
||||
|
||||
<String key="Graphic">Graphics</String>
|
||||
<String key="RenderEngine">Render engine</String>
|
||||
|
||||
@@ -356,18 +356,6 @@
|
||||
<String key="VirtualShunting">Virtuális tolatás</String>
|
||||
<String key="LogMissingVehicleFiles">Hiányzó járműfájlok naplózása exportáláskor</String>
|
||||
<String key="DeveloperTools">Show the package author's tab</String>
|
||||
<String key="CheckUpdates">Starter frissítéseinek keresése</String>
|
||||
<String key="CheckUpdatesDesc">Naponta egyszer ellenőrzi a GitHubon, hogy megjelent-e újabb starter-verzió, és megjeleníti a felső sávon. A teszt csatorna a staging ág buildjeit kínálja.</String>
|
||||
<String key="UpdateAvailable">Frissítés: {0}</String>
|
||||
<String key="UpdateAvailableTip">Elérhető a(z) {0} verzió. Kattints a letöltési oldal megnyitásához.</String>
|
||||
<String key="UpdateChannelStable">Stabil</String>
|
||||
<String key="UpdateChannelStaging">Teszt (staging)</String>
|
||||
<String key="UpdateChannelWarning">Figyelem: a csatorna csak a startert érinti. A MaSzyna szimulátort és fájljait külön kell frissíteni - ez a választás nem módosítja őket.</String>
|
||||
<String key="CheckUpdatesNow">Ellenőrzés most</String>
|
||||
<String key="UpdateChecking">Ellenőrzés…</String>
|
||||
<String key="UpdateFound">Elérhető a(z) {0} verzió - a gomb a felső sávon található.</String>
|
||||
<String key="UpdateUpToDate">A legújabb verzió van telepítve ({0}).</String>
|
||||
<String key="UpdateCheckFailed">Nem sikerült elérni a GitHubot. Próbáld újra később.</String>
|
||||
|
||||
<String key="Graphic">Grafika</String>
|
||||
<String key="RenderEngine">Megjelenítő motor</String>
|
||||
|
||||
@@ -363,18 +363,6 @@
|
||||
<String key="LogMissingVehicleFilesDesc">Zapisuje w logu pojazdy, których starter nie znalazł.</String>
|
||||
<String key="DeveloperTools">Pokaż zakładkę dla twórców paczek</String>
|
||||
<String key="DeveloperToolsDesc">Dodaje zakładkę ze stanem bazy pojazdów, przydatną przy przygotowywaniu paczek z taborem.</String>
|
||||
<String key="CheckUpdates">Sprawdzaj aktualizacje startera</String>
|
||||
<String key="CheckUpdatesDesc">Raz dziennie sprawdza na GitHubie, czy wyszła nowsza wersja startera, i pokazuje ją na górnym pasku. Kanał testowy to buildy z gałęzi staging.</String>
|
||||
<String key="UpdateAvailable">Aktualizacja {0}</String>
|
||||
<String key="UpdateAvailableTip">Dostępna jest wersja {0}. Kliknij, aby otworzyć stronę pobierania.</String>
|
||||
<String key="UpdateChannelStable">Stabilne</String>
|
||||
<String key="UpdateChannelStaging">Testowe (staging)</String>
|
||||
<String key="UpdateChannelWarning">Uwaga: kanał dotyczy tylko startera. Symulator MaSzyna i jego pliki aktualizujesz osobno - ten wybór ich nie zmienia.</String>
|
||||
<String key="CheckUpdatesNow">Sprawdź teraz</String>
|
||||
<String key="UpdateChecking">Sprawdzanie…</String>
|
||||
<String key="UpdateFound">Dostępna jest wersja {0} - przycisk pojawił się na górnym pasku.</String>
|
||||
<String key="UpdateUpToDate">Masz najnowszą wersję ({0}).</String>
|
||||
<String key="UpdateCheckFailed">Nie udało się połączyć z GitHubem. Spróbuj później.</String>
|
||||
|
||||
<String key="Graphic">Grafika</String>
|
||||
<String key="RenderEngine">Silnik renderowania</String>
|
||||
|
||||
@@ -356,18 +356,6 @@
|
||||
<String key="VirtualShunting">Виртуальный манёвр</String>
|
||||
<String key="LogMissingVehicleFiles">Записывать в журнал отсутствующие файлы состава при экспорте</String>
|
||||
<String key="DeveloperTools">Show the package author's tab</String>
|
||||
<String key="CheckUpdates">Проверять обновления стартера</String>
|
||||
<String key="CheckUpdatesDesc">Раз в день проверяет на GitHub, вышла ли новая версия стартера, и показывает её на верхней панели. Тестовый канал предлагает сборки ветки staging.</String>
|
||||
<String key="UpdateAvailable">Обновление {0}</String>
|
||||
<String key="UpdateAvailableTip">Доступна версия {0}. Нажмите, чтобы открыть страницу загрузки.</String>
|
||||
<String key="UpdateChannelStable">Стабильные</String>
|
||||
<String key="UpdateChannelStaging">Тестовые (staging)</String>
|
||||
<String key="UpdateChannelWarning">Внимание: канал относится только к стартеру. Симулятор MaSzyna и его файлы обновляются отдельно - этот выбор их не меняет.</String>
|
||||
<String key="CheckUpdatesNow">Проверить сейчас</String>
|
||||
<String key="UpdateChecking">Проверка…</String>
|
||||
<String key="UpdateFound">Доступна версия {0} - кнопка появилась на верхней панели.</String>
|
||||
<String key="UpdateUpToDate">У вас последняя версия ({0}).</String>
|
||||
<String key="UpdateCheckFailed">Не удалось связаться с GitHub. Попробуйте позже.</String>
|
||||
|
||||
<String key="Graphic">Графика</String>
|
||||
<String key="RenderEngine">Движок отрисовки</String>
|
||||
|
||||
Reference in New Issue
Block a user