modified layout (#2)
Reviewed-on: #2 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #2.
This commit is contained in:
@@ -137,10 +137,6 @@ public sealed class AppSettingsTests
|
||||
bool flag => !flag,
|
||||
Color color => Color.FromArgb((byte)(color.R + 1), color.G, color.B),
|
||||
Enum value => NextEnumValue(value),
|
||||
DateTimeOffset moment => moment.AddDays(1),
|
||||
|
||||
// A setting never set yet: the app has not checked for updates once
|
||||
null when property.PropertyType == typeof(DateTimeOffset?) => DateTimeOffset.UnixEpoch,
|
||||
|
||||
_ => throw new NotSupportedException($"Unknown kind of setting: {property.PropertyType}"),
|
||||
};
|
||||
|
||||
@@ -29,10 +29,4 @@ public sealed class UpdateOptionsTests
|
||||
Assert.Equal("git.example.com", options.ServiceUri.Host);
|
||||
Assert.Equal("team/app", options.Project);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_app_asks_about_releases_no_more_than_once_a_day()
|
||||
{
|
||||
Assert.Equal(TimeSpan.FromDays(1), new UpdateOptions().CheckInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,20 +87,6 @@ public sealed partial class AppSettings : ObservableObject
|
||||
[ObservableProperty]
|
||||
private double _capsLockHoldMilliseconds = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Ask the repository about new versions on startup. A check can always be
|
||||
/// started by hand — this setting turns off only the automatic one.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _checkForUpdates = true;
|
||||
|
||||
/// <summary>
|
||||
/// When the application last asked about new versions successfully.
|
||||
/// Stored so as not to go to the network on every startup.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private DateTimeOffset? _lastUpdateCheck;
|
||||
|
||||
/// <summary>The fill of the popup. The opacity is set by <see cref="Opacity"/>.</summary>
|
||||
[ObservableProperty]
|
||||
private Color _backgroundColor = Color.FromArgb(0xFF, 0x20, 0x20, 0x20);
|
||||
@@ -142,8 +128,6 @@ public sealed partial class AppSettings : ObservableObject
|
||||
DurationMilliseconds = other.DurationMilliseconds;
|
||||
UseCapsLockHotkey = other.UseCapsLockHotkey;
|
||||
CapsLockHoldMilliseconds = other.CapsLockHoldMilliseconds;
|
||||
CheckForUpdates = other.CheckForUpdates;
|
||||
LastUpdateCheck = other.LastUpdateCheck;
|
||||
BackgroundColor = other.BackgroundColor;
|
||||
ForegroundColor = other.ForegroundColor;
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
<data name="AppTheme_Dark" xml:space="preserve">
|
||||
<value>Dark</value>
|
||||
</data>
|
||||
<data name="SectionPlacement" xml:space="preserve">
|
||||
<value>Placement</value>
|
||||
<data name="SectionPopup" xml:space="preserve">
|
||||
<value>Layout popup</value>
|
||||
</data>
|
||||
<data name="PlacementModeLabel" xml:space="preserve">
|
||||
<value>Mode</value>
|
||||
@@ -151,9 +151,6 @@
|
||||
<data name="ScreenMarginLabel" xml:space="preserve">
|
||||
<value>Margin from screen edge</value>
|
||||
</data>
|
||||
<data name="SectionAppearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="FontSizeLabel" xml:space="preserve">
|
||||
<value>Font size</value>
|
||||
</data>
|
||||
@@ -214,8 +211,8 @@
|
||||
<data name="CheckUpdatesButton" xml:space="preserve">
|
||||
<value>Check for updates</value>
|
||||
</data>
|
||||
<data name="UpdateAutoCheck" xml:space="preserve">
|
||||
<value>Check for updates</value>
|
||||
<data name="UpdateNotChecked" xml:space="preserve">
|
||||
<value>Updates have not been checked yet.</value>
|
||||
</data>
|
||||
<data name="UpdateChecking" xml:space="preserve">
|
||||
<value>Checking for updates…</value>
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
<data name="AppTheme_Dark" xml:space="preserve">
|
||||
<value>Тёмная</value>
|
||||
</data>
|
||||
<data name="SectionPlacement" xml:space="preserve">
|
||||
<value>Расположение</value>
|
||||
<data name="SectionPopup" xml:space="preserve">
|
||||
<value>Подсказка о раскладке</value>
|
||||
</data>
|
||||
<data name="PlacementModeLabel" xml:space="preserve">
|
||||
<value>Режим</value>
|
||||
@@ -151,9 +151,6 @@
|
||||
<data name="ScreenMarginLabel" xml:space="preserve">
|
||||
<value>Отступ от края экрана</value>
|
||||
</data>
|
||||
<data name="SectionAppearance" xml:space="preserve">
|
||||
<value>Внешний вид</value>
|
||||
</data>
|
||||
<data name="FontSizeLabel" xml:space="preserve">
|
||||
<value>Размер шрифта</value>
|
||||
</data>
|
||||
@@ -214,8 +211,8 @@
|
||||
<data name="CheckUpdatesButton" xml:space="preserve">
|
||||
<value>Проверить обновления</value>
|
||||
</data>
|
||||
<data name="UpdateAutoCheck" xml:space="preserve">
|
||||
<value>Проверять обновления</value>
|
||||
<data name="UpdateNotChecked" xml:space="preserve">
|
||||
<value>Обновления ещё не проверялись.</value>
|
||||
</data>
|
||||
<data name="UpdateChecking" xml:space="preserve">
|
||||
<value>Идёт проверка обновлений…</value>
|
||||
|
||||
@@ -21,9 +21,6 @@ public sealed class UpdateOptions
|
||||
/// <summary>The project: <c>owner/repository</c>.</summary>
|
||||
public string Project { get; init; } = "alrakis/cursor-lang";
|
||||
|
||||
/// <summary>How often the application checks the releases on its own.</summary>
|
||||
public TimeSpan CheckInterval { get; init; } = TimeSpan.FromDays(1);
|
||||
|
||||
/// <summary>
|
||||
/// An access token for a private repository.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CursorLang.Core.Models;
|
||||
using CursorLang.Core.Services;
|
||||
using CursorLang.Settings.Services;
|
||||
using CursorLang.Settings.ViewModels;
|
||||
using CursorLang.Tests.Shared;
|
||||
@@ -12,7 +11,7 @@ namespace CursorLang.Settings.Tests.Infrastructure;
|
||||
internal static class Fake
|
||||
{
|
||||
internal static UpdateViewModel Updates() =>
|
||||
new(new FakeUpdateService(), new FakeLocalizationService(), new AppSettings(), new UpdateOptions());
|
||||
new(new FakeUpdateService(), new FakeLocalizationService());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,13 +13,15 @@ namespace CursorLang.Settings.Tests.ViewModels;
|
||||
/// </summary>
|
||||
public sealed class UpdateViewModelTests
|
||||
{
|
||||
// The section has something to say at every moment, the one before the first
|
||||
// answer included: the status line is never an empty spot in the window
|
||||
[Fact]
|
||||
public void Before_the_first_check_the_section_says_nothing()
|
||||
public void Before_the_first_check_the_section_says_so()
|
||||
{
|
||||
using UpdateViewModel viewModel = Create(new FakeUpdateService());
|
||||
|
||||
Assert.Equal(UpdateStatus.Idle, viewModel.Status);
|
||||
Assert.False(viewModel.HasStatus);
|
||||
Assert.Equal("en:UpdateNotChecked", viewModel.StatusText);
|
||||
Assert.False(viewModel.IsDownloadOffered);
|
||||
Assert.False(viewModel.IsInstallOffered);
|
||||
Assert.True(viewModel.CanCheck);
|
||||
@@ -62,96 +64,36 @@ public sealed class UpdateViewModelTests
|
||||
await viewModel.CheckCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(UpdateStatus.Failed, viewModel.Status);
|
||||
Assert.True(viewModel.HasStatus);
|
||||
Assert.Equal("en:UpdateFailed", viewModel.StatusText);
|
||||
}
|
||||
|
||||
// The app does not always start with a live network, and the user who never
|
||||
// asked about updates has no use for the complaint
|
||||
// The window is opened by hand, and the answer is what the section is there
|
||||
// for: a dead network is part of the answer rather than a reason to say nothing
|
||||
[Fact]
|
||||
public async Task A_check_at_startup_keeps_a_failure_to_itself()
|
||||
public async Task A_check_when_the_window_opens_says_when_it_did_not_work_out()
|
||||
{
|
||||
var updates = new FakeUpdateService { Failure = new HttpRequestException("no network") };
|
||||
using UpdateViewModel viewModel = Create(updates);
|
||||
|
||||
await viewModel.StartAsync();
|
||||
|
||||
Assert.Equal(UpdateStatus.Idle, viewModel.Status);
|
||||
Assert.False(viewModel.HasStatus);
|
||||
Assert.Equal(UpdateStatus.Failed, viewModel.Status);
|
||||
Assert.Equal("en:UpdateFailed", viewModel.StatusText);
|
||||
}
|
||||
|
||||
// Nothing is remembered between openings: an answer from yesterday is worth
|
||||
// less than today's, and the request costs nothing at this rate
|
||||
[Fact]
|
||||
public async Task A_successful_check_is_remembered_in_the_settings()
|
||||
public async Task Every_opening_of_the_window_asks_anew()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
var updates = new FakeUpdateService { Release = Release("2.0.0.0") };
|
||||
using UpdateViewModel viewModel = Create(updates, settings);
|
||||
|
||||
await viewModel.CheckCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.NotNull(settings.LastUpdateCheck);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_check_that_did_not_work_out_is_not_remembered()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
var updates = new FakeUpdateService { Failure = new HttpRequestException("no network") };
|
||||
using UpdateViewModel viewModel = Create(updates, settings);
|
||||
|
||||
await viewModel.CheckCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Null(settings.LastUpdateCheck);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_recent_check_is_not_repeated_at_startup()
|
||||
{
|
||||
var settings = new AppSettings { LastUpdateCheck = DateTimeOffset.UtcNow };
|
||||
var updates = new FakeUpdateService();
|
||||
using UpdateViewModel viewModel = Create(updates, settings);
|
||||
using UpdateViewModel viewModel = Create(updates);
|
||||
|
||||
await viewModel.StartAsync();
|
||||
|
||||
Assert.Equal(0, updates.CheckCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_check_of_yesterday_is_repeated_at_startup()
|
||||
{
|
||||
var settings = new AppSettings { LastUpdateCheck = DateTimeOffset.UtcNow - TimeSpan.FromDays(2) };
|
||||
var updates = new FakeUpdateService();
|
||||
using UpdateViewModel viewModel = Create(updates, settings);
|
||||
|
||||
await viewModel.StartAsync();
|
||||
|
||||
Assert.Equal(1, updates.CheckCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_ban_on_checking_by_itself_is_obeyed()
|
||||
{
|
||||
var settings = new AppSettings { CheckForUpdates = false };
|
||||
var updates = new FakeUpdateService();
|
||||
using UpdateViewModel viewModel = Create(updates, settings);
|
||||
|
||||
await viewModel.StartAsync();
|
||||
|
||||
Assert.Equal(0, updates.CheckCalls);
|
||||
|
||||
// The button still works: the setting is about the app doing it on its own
|
||||
await viewModel.CheckCommand.ExecuteAsync(null);
|
||||
Assert.Equal(1, updates.CheckCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_ban_on_checking_travels_to_the_settings()
|
||||
{
|
||||
var settings = new AppSettings { CheckForUpdates = true };
|
||||
using UpdateViewModel viewModel = Create(new FakeUpdateService(), settings);
|
||||
|
||||
viewModel.CheckAutomatically = false;
|
||||
|
||||
Assert.False(settings.CheckForUpdates);
|
||||
Assert.Equal(2, updates.CheckCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -283,10 +225,6 @@ public sealed class UpdateViewModelTests
|
||||
|
||||
private static UpdateViewModel Create(
|
||||
IUpdateService updates,
|
||||
AppSettings? settings = null,
|
||||
ILocalizationService? localization = null) =>
|
||||
new(updates,
|
||||
localization ?? new FakeLocalizationService(),
|
||||
settings ?? new AppSettings(),
|
||||
new UpdateOptions());
|
||||
new(updates, localization ?? new FakeLocalizationService());
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ public sealed class MainWindowTests
|
||||
}
|
||||
|
||||
private static UpdateViewModel CreateUpdates(IUpdateService updates) =>
|
||||
new(updates, new FakeLocalizationService(), new AppSettings(), new UpdateOptions());
|
||||
new(updates, new FakeLocalizationService());
|
||||
|
||||
private static IEnumerable<Button> FindButtonsBoundTo(DependencyObject root, string path) =>
|
||||
FindAll<Button>(root).Where(button =>
|
||||
|
||||
@@ -15,17 +15,15 @@ namespace CursorLang.Settings.ViewModels;
|
||||
/// The updates section of the settings window.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A check made on opening the window passes its failures in silence: the machine does
|
||||
/// not always have a live network, and there is no point complaining about it to a user
|
||||
/// who came to change the popup colour. A check started by the button does report a
|
||||
/// failure — it is awaited and watched.
|
||||
/// The section always says where things stand: a check flies off the moment the window
|
||||
/// appears, and its outcome — including an unreachable network — stays written in the
|
||||
/// status line. Nothing here is silent, and nothing disappears: a user who opened the
|
||||
/// window is told about updates whether they came looking for them or not.
|
||||
/// </remarks>
|
||||
public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IUpdateService _updates;
|
||||
private readonly ILocalizationService _localization;
|
||||
private readonly AppSettings _settings;
|
||||
private readonly UpdateOptions _options;
|
||||
|
||||
private CancellationTokenSource? _work;
|
||||
private ReleaseInfo? _release;
|
||||
@@ -35,7 +33,6 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
[NotifyPropertyChangedFor(nameof(StatusText))]
|
||||
[NotifyPropertyChangedFor(nameof(IsBusy))]
|
||||
[NotifyPropertyChangedFor(nameof(CanCheck))]
|
||||
[NotifyPropertyChangedFor(nameof(HasStatus))]
|
||||
[NotifyPropertyChangedFor(nameof(IsProgressUnknown))]
|
||||
[NotifyPropertyChangedFor(nameof(IsDownloadOffered))]
|
||||
[NotifyPropertyChangedFor(nameof(IsInstallOffered))]
|
||||
@@ -48,16 +45,10 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
[NotifyPropertyChangedFor(nameof(IsProgressUnknown))]
|
||||
private double _progress;
|
||||
|
||||
public UpdateViewModel(
|
||||
IUpdateService updates,
|
||||
ILocalizationService localization,
|
||||
AppSettings settings,
|
||||
UpdateOptions options)
|
||||
public UpdateViewModel(IUpdateService updates, ILocalizationService localization)
|
||||
{
|
||||
_updates = updates;
|
||||
_localization = localization;
|
||||
_settings = settings;
|
||||
_options = options;
|
||||
|
||||
_localization.PropertyChanged += OnLocalizationChanged;
|
||||
}
|
||||
@@ -67,29 +58,11 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
|
||||
public string CurrentVersion => _updates.CurrentVersion.ToString();
|
||||
|
||||
/// <summary>Check for new versions at startup.</summary>
|
||||
public bool CheckAutomatically
|
||||
{
|
||||
get => _settings.CheckForUpdates;
|
||||
set
|
||||
{
|
||||
if (value == _settings.CheckForUpdates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.CheckForUpdates = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A request or a download is in flight — the buttons freeze for that time.</summary>
|
||||
public bool IsBusy => Status is UpdateStatus.Checking or UpdateStatus.Downloading;
|
||||
|
||||
public bool CanCheck => !IsBusy;
|
||||
|
||||
public bool HasStatus => Status != UpdateStatus.Idle;
|
||||
|
||||
/// <summary>
|
||||
/// The package size is unknown, and the bar shows only the fact of the download.
|
||||
/// Its start looks the same until the first report arrives.
|
||||
@@ -107,6 +80,10 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
/// <summary>The release page: the release notes live there too.</summary>
|
||||
public Uri? ReleaseUrl => _release?.PageUrl;
|
||||
|
||||
/// <summary>
|
||||
/// What the section says. There is a line for every state, the one before the
|
||||
/// first check included: the status is never an empty spot in the window.
|
||||
/// </summary>
|
||||
public string StatusText => Status switch
|
||||
{
|
||||
UpdateStatus.Checking => _localization["UpdateChecking"],
|
||||
@@ -115,33 +92,24 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
UpdateStatus.Downloading => _localization["UpdateDownloading"],
|
||||
UpdateStatus.Ready => _localization["UpdateReady"],
|
||||
UpdateStatus.Failed => _localization["UpdateFailed"],
|
||||
_ => string.Empty,
|
||||
_ => _localization["UpdateNotChecked"],
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates when the user has not forbidden it and enough time has
|
||||
/// passed since the previous check. Called once when the window opens.
|
||||
/// Asks about new versions. Called once, when the window has just appeared.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It used to be called when the application started, which back then meant when
|
||||
/// the machine was switched on. The background half is a separate process now and
|
||||
/// does not go to the network at all — nothing in it could show the answer — so the
|
||||
/// question is asked when there is a window to answer into.
|
||||
///
|
||||
/// And it is asked every time that window appears: opening it is a deliberate act
|
||||
/// of the user, rare enough that a request costs nothing, and the answer is what
|
||||
/// the section exists for. A remembered answer from yesterday is worth less than
|
||||
/// today's, so nothing is remembered.
|
||||
/// </remarks>
|
||||
public async Task StartAsync()
|
||||
{
|
||||
if (!IsSupported || !_settings.CheckForUpdates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settings.LastUpdateCheck is { } last && DateTimeOffset.UtcNow - last < _options.CheckInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunCheckAsync(quiet: true);
|
||||
}
|
||||
public Task StartAsync() => CheckAsync();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -153,7 +121,36 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task CheckAsync() => RunCheckAsync(quiet: false);
|
||||
private async Task CheckAsync()
|
||||
{
|
||||
if (!IsSupported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancellationToken token = StartWork();
|
||||
Status = UpdateStatus.Checking;
|
||||
|
||||
try
|
||||
{
|
||||
_release = await _updates.CheckAsync(token);
|
||||
_packagePath = null;
|
||||
|
||||
Status = _release is null ? UpdateStatus.UpToDate : UpdateStatus.Available;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
// The check was cancelled by the next piece of work: it has already set its own state
|
||||
return;
|
||||
}
|
||||
catch (Exception e) when (IsExpected(e))
|
||||
{
|
||||
Status = UpdateStatus.Failed;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ReleaseUrl));
|
||||
OnPropertyChanged(nameof(IsReleaseLinkShown));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DownloadAsync()
|
||||
@@ -216,38 +213,6 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
|
||||
or NotSupportedException or InvalidOperationException or Win32Exception
|
||||
or OperationCanceledException;
|
||||
|
||||
private async Task RunCheckAsync(bool quiet)
|
||||
{
|
||||
if (!IsSupported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancellationToken token = StartWork();
|
||||
Status = UpdateStatus.Checking;
|
||||
|
||||
try
|
||||
{
|
||||
_release = await _updates.CheckAsync(token);
|
||||
_packagePath = null;
|
||||
|
||||
_settings.LastUpdateCheck = DateTimeOffset.UtcNow;
|
||||
Status = _release is null ? UpdateStatus.UpToDate : UpdateStatus.Available;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
// The check was cancelled by the next piece of work: it has already set its own state
|
||||
return;
|
||||
}
|
||||
catch (Exception e) when (IsExpected(e))
|
||||
{
|
||||
Status = quiet ? UpdateStatus.Idle : UpdateStatus.Failed;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ReleaseUrl));
|
||||
OnPropertyChanged(nameof(IsReleaseLinkShown));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new piece of work, cancelling the previous one: the user may have
|
||||
/// pressed "Check" in the middle of a download.
|
||||
|
||||
@@ -51,7 +51,10 @@
|
||||
|
||||
<!-- The settings are laid out in two columns: this way the window fits on the
|
||||
screen entirely and does without scrolling. Scrolling is kept for the case of
|
||||
a large system font, with which the content is taller than the monitor after all -->
|
||||
a large system font, with which the content is taller than the monitor after all.
|
||||
|
||||
The left column holds what the popup does; the right one holds the single
|
||||
section describing how and where the popup looks -->
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -91,7 +94,7 @@
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{Binding Localization[SectionPlacement]}">
|
||||
<GroupBox Header="{Binding Localization[SectionBehavior]}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="164" />
|
||||
@@ -105,6 +108,177 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[DurationLabel]}" />
|
||||
<Slider Grid.Column="1" Minimum="200" Maximum="5000" TickFrequency="100"
|
||||
Value="{Binding Settings.DurationMilliseconds}" />
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource FieldValue}"
|
||||
Text="{Binding Settings.DurationMilliseconds, StringFormat={}{0:F0}}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CapsLockLabel]}" />
|
||||
<CheckBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
IsChecked="{Binding Settings.UseCapsLockHotkey}"
|
||||
Content="{Binding Localization[CapsLockHotkeyCheck]}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CapsLockHoldLabel]}" />
|
||||
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||
Minimum="150" Maximum="1500" TickFrequency="50"
|
||||
Value="{Binding Settings.CapsLockHoldMilliseconds}"
|
||||
IsEnabled="{Binding Settings.UseCapsLockHotkey}" />
|
||||
<StackPanel Grid.Row="2" Grid.Column="2" Margin="0,12,0,0"
|
||||
Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource FieldValue}"
|
||||
Text="{Binding Settings.CapsLockHoldMilliseconds, StringFormat={}{0:F0}}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}">
|
||||
<Run Text="{Binding Localization[CapsLockHoldHint], Mode=OneWay}" />
|
||||
<InlineUIContainer BaselineAlignment="Baseline">
|
||||
<!-- The elevation caveat lives in the tooltip to keep the section compact -->
|
||||
<TextBlock Text="{Binding Localization[MoreInfoLink]}"
|
||||
Foreground="{DynamicResource Theme.Accent}"
|
||||
TextDecorations="Underline"
|
||||
Cursor="Help"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTipService.ShowDuration="60000"
|
||||
Visibility="{Binding Settings.UseCapsLockHotkey, Converter={StaticResource BooleanToVisibility}}">
|
||||
<TextBlock.ToolTip>
|
||||
<ToolTip>
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Text="{Binding Localization[CapsLockElevationHint]}" />
|
||||
</ToolTip>
|
||||
</TextBlock.ToolTip>
|
||||
</TextBlock>
|
||||
</InlineUIContainer>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
||||
Text="{Binding Localization[StartupLabel]}" />
|
||||
<CheckBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
||||
IsEnabled="{Binding CanChangeStartup}"
|
||||
IsChecked="{Binding RunAtStartup}"
|
||||
Content="{Binding Localization[StartupCheck]}" />
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Visibility="{Binding IsStartupLocked, Converter={StaticResource BooleanToVisibility}}"
|
||||
Text="{Binding Localization[StartupLockedHint]}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<!-- The section is absent for an app from the Store: the Store updates it itself -->
|
||||
<GroupBox Header="{Binding Localization[SectionUpdates]}"
|
||||
Visibility="{Binding Updates.IsSupported, Converter={StaticResource BooleanToVisibility}}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="164" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CurrentVersionLabel]}" />
|
||||
<TextBlock Grid.Column="1" VerticalAlignment="Center"
|
||||
Text="{Binding Updates.CurrentVersion}" />
|
||||
<Button Grid.Column="2" Padding="12,4"
|
||||
Command="{Binding Updates.CheckCommand}"
|
||||
IsEnabled="{Binding Updates.CanCheck}"
|
||||
Content="{Binding Localization[CheckUpdatesButton]}" />
|
||||
|
||||
<!-- The status is always on show: the check flies off with the
|
||||
window, and where things stand is the answer the user came for -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,12,0,0" TextWrapping="Wrap"
|
||||
Text="{Binding Updates.StatusText}" />
|
||||
|
||||
<!-- The downloaded fraction is not always known: not every hosting reports the file size -->
|
||||
<ProgressBar Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,8,0,0" Height="4" Maximum="1"
|
||||
Value="{Binding Updates.Progress, Mode=OneWay}"
|
||||
IsIndeterminate="{Binding Updates.IsProgressUnknown}"
|
||||
Visibility="{Binding Updates.IsProgressShown, Converter={StaticResource BooleanToVisibility}}" />
|
||||
|
||||
<StackPanel Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,12,0,0" Orientation="Horizontal">
|
||||
<Button Padding="12,4"
|
||||
Command="{Binding Updates.DownloadCommand}"
|
||||
Visibility="{Binding Updates.IsDownloadOffered, Converter={StaticResource BooleanToVisibility}}"
|
||||
Content="{Binding Localization[DownloadUpdateButton]}" />
|
||||
<Button Padding="12,4"
|
||||
Command="{Binding Updates.InstallCommand}"
|
||||
Visibility="{Binding Updates.IsInstallOffered, Converter={StaticResource BooleanToVisibility}}"
|
||||
Content="{Binding Localization[InstallUpdateButton]}" />
|
||||
<TextBlock Margin="12,0,0,0" VerticalAlignment="Center"
|
||||
Visibility="{Binding Updates.IsReleaseLinkShown, Converter={StaticResource BooleanToVisibility}}">
|
||||
<Hyperlink NavigateUri="{Binding Updates.ReleaseUrl}"
|
||||
RequestNavigate="OnReleaseLinkNavigate">
|
||||
<Run Text="{Binding Localization[ReleasePageLink], Mode=OneWay}" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Visibility="{Binding Updates.IsInstallOffered, Converter={StaticResource BooleanToVisibility}}"
|
||||
Text="{Binding Localization[UpdateInstallHint]}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="16,0,0,0">
|
||||
|
||||
<!-- Where the popup appears and how it looks are one question for the
|
||||
user, so both live in a single section: the rule is picked at the
|
||||
top and its result is seen in the preview at the bottom -->
|
||||
<GroupBox Header="{Binding Localization[SectionPopup]}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="164" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
@@ -220,160 +394,56 @@
|
||||
Visibility="{Binding Settings.PlacementMode,
|
||||
Converter={StaticResource EnumToVisibility},
|
||||
ConverterParameter=FixedPoint}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{Binding Localization[SectionBehavior]}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="164" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- A hairline between the two halves of the section: the
|
||||
placement above, the look of the popup below -->
|
||||
<Border Grid.Row="7" Grid.ColumnSpan="3"
|
||||
Margin="0,16,0,0" Height="1"
|
||||
Background="{DynamicResource Theme.ControlBorder}" />
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[DurationLabel]}" />
|
||||
<Slider Grid.Column="1" Minimum="200" Maximum="5000" TickFrequency="100"
|
||||
Value="{Binding Settings.DurationMilliseconds}" />
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource FieldValue}"
|
||||
Text="{Binding Settings.DurationMilliseconds, StringFormat={}{0:F0}}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||
<TextBlock Grid.Row="8" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CapsLockLabel]}" />
|
||||
<CheckBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
IsChecked="{Binding Settings.UseCapsLockHotkey}"
|
||||
Content="{Binding Localization[CapsLockHotkeyCheck]}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CapsLockHoldLabel]}" />
|
||||
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||
Minimum="150" Maximum="1500" TickFrequency="50"
|
||||
Value="{Binding Settings.CapsLockHoldMilliseconds}"
|
||||
IsEnabled="{Binding Settings.UseCapsLockHotkey}" />
|
||||
<StackPanel Grid.Row="2" Grid.Column="2" Margin="0,12,0,0"
|
||||
Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource FieldValue}"
|
||||
Text="{Binding Settings.CapsLockHoldMilliseconds, StringFormat={}{0:F0}}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}">
|
||||
<Run Text="{Binding Localization[CapsLockHoldHint], Mode=OneWay}" />
|
||||
<InlineUIContainer BaselineAlignment="Baseline">
|
||||
<!-- The elevation caveat lives in the tooltip to keep the section compact -->
|
||||
<TextBlock Text="{Binding Localization[MoreInfoLink]}"
|
||||
Foreground="{DynamicResource Theme.Accent}"
|
||||
TextDecorations="Underline"
|
||||
Cursor="Help"
|
||||
ToolTipService.InitialShowDelay="200"
|
||||
ToolTipService.ShowDuration="60000"
|
||||
Visibility="{Binding Settings.UseCapsLockHotkey, Converter={StaticResource BooleanToVisibility}}">
|
||||
<TextBlock.ToolTip>
|
||||
<ToolTip>
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Text="{Binding Localization[CapsLockElevationHint]}" />
|
||||
</ToolTip>
|
||||
</TextBlock.ToolTip>
|
||||
</TextBlock>
|
||||
</InlineUIContainer>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
||||
Text="{Binding Localization[StartupLabel]}" />
|
||||
<CheckBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
||||
IsEnabled="{Binding CanChangeStartup}"
|
||||
IsChecked="{Binding RunAtStartup}"
|
||||
Content="{Binding Localization[StartupCheck]}" />
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Visibility="{Binding IsStartupLocked, Converter={StaticResource BooleanToVisibility}}"
|
||||
Text="{Binding Localization[StartupLockedHint]}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="16,0,0,0">
|
||||
|
||||
<GroupBox Header="{Binding Localization[SectionAppearance]}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="164" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[FontSizeLabel]}" />
|
||||
<Slider Grid.Column="1" Minimum="10" Maximum="72" TickFrequency="1"
|
||||
<Slider Grid.Row="8" Grid.Column="1" Margin="0,12,0,0"
|
||||
Minimum="10" Maximum="72" TickFrequency="1"
|
||||
Value="{Binding Settings.FontSize}" />
|
||||
<TextBlock Grid.Column="2" Style="{StaticResource FieldValue}"
|
||||
<TextBlock Grid.Row="8" Grid.Column="2" Margin="12,12,0,0"
|
||||
Style="{StaticResource FieldValue}"
|
||||
Text="{Binding Settings.FontSize, StringFormat={}{0:F0}}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||
<TextBlock Grid.Row="9" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[OpacityLabel]}" />
|
||||
<Slider Grid.Row="1" Grid.Column="1" Margin="0,12,0,0"
|
||||
<Slider Grid.Row="9" Grid.Column="1" Margin="0,12,0,0"
|
||||
Minimum="0.1" Maximum="1" TickFrequency="0.05"
|
||||
Value="{Binding Settings.Opacity}" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" Margin="12,12,0,0"
|
||||
<TextBlock Grid.Row="9" Grid.Column="2" Margin="12,12,0,0"
|
||||
Style="{StaticResource FieldValue}"
|
||||
Text="{Binding Settings.Opacity, StringFormat={}{0:P0}}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||
<TextBlock Grid.Row="10" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[BackgroundColorLabel]}" />
|
||||
<ComboBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
<ComboBox Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
ItemsSource="{Binding BackgroundPalette}"
|
||||
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
||||
SelectedItem="{Binding Settings.BackgroundColor}" />
|
||||
|
||||
<TextBlock Grid.Row="3" Margin="0,12,12,0"
|
||||
<TextBlock Grid.Row="11" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[TextColorLabel]}" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
<ComboBox Grid.Row="11" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
ItemsSource="{Binding TextPalette}"
|
||||
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
||||
SelectedItem="{Binding Settings.ForegroundColor}" />
|
||||
|
||||
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
||||
<TextBlock Grid.Row="12" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[PreviewLabel]}" />
|
||||
<!-- The height of the area is sized for the largest font
|
||||
available: the sample text is fully visible at any
|
||||
position of the slider -->
|
||||
<Border Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
<Border Grid.Row="12" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,12,0,0" Padding="16"
|
||||
Height="152" ClipToBounds="True"
|
||||
Background="{DynamicResource Theme.SurfaceStrong}" CornerRadius="4"
|
||||
@@ -392,76 +462,6 @@
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<!-- The section is absent for an app from the Store: the Store updates it itself -->
|
||||
<GroupBox Header="{Binding Localization[SectionUpdates]}"
|
||||
Visibility="{Binding Updates.IsSupported, Converter={StaticResource BooleanToVisibility}}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="164" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CurrentVersionLabel]}" />
|
||||
<TextBlock Grid.Column="1" VerticalAlignment="Center"
|
||||
Text="{Binding Updates.CurrentVersion}" />
|
||||
<Button Grid.Column="2" Padding="12,4"
|
||||
Command="{Binding Updates.CheckCommand}"
|
||||
IsEnabled="{Binding Updates.CanCheck}"
|
||||
Content="{Binding Localization[CheckUpdatesButton]}" />
|
||||
|
||||
<CheckBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
IsChecked="{Binding Updates.CheckAutomatically}"
|
||||
Content="{Binding Localization[UpdateAutoCheck]}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,12,0,0" TextWrapping="Wrap"
|
||||
Visibility="{Binding Updates.HasStatus, Converter={StaticResource BooleanToVisibility}}"
|
||||
Text="{Binding Updates.StatusText}" />
|
||||
|
||||
<!-- The downloaded fraction is not always known: not every hosting reports the file size -->
|
||||
<ProgressBar Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,8,0,0" Height="4" Maximum="1"
|
||||
Value="{Binding Updates.Progress, Mode=OneWay}"
|
||||
IsIndeterminate="{Binding Updates.IsProgressUnknown}"
|
||||
Visibility="{Binding Updates.IsProgressShown, Converter={StaticResource BooleanToVisibility}}" />
|
||||
|
||||
<StackPanel Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,12,0,0" Orientation="Horizontal">
|
||||
<Button Padding="12,4"
|
||||
Command="{Binding Updates.DownloadCommand}"
|
||||
Visibility="{Binding Updates.IsDownloadOffered, Converter={StaticResource BooleanToVisibility}}"
|
||||
Content="{Binding Localization[DownloadUpdateButton]}" />
|
||||
<Button Padding="12,4"
|
||||
Command="{Binding Updates.InstallCommand}"
|
||||
Visibility="{Binding Updates.IsInstallOffered, Converter={StaticResource BooleanToVisibility}}"
|
||||
Content="{Binding Localization[InstallUpdateButton]}" />
|
||||
<TextBlock Margin="12,0,0,0" VerticalAlignment="Center"
|
||||
Visibility="{Binding Updates.IsReleaseLinkShown, Converter={StaticResource BooleanToVisibility}}">
|
||||
<Hyperlink NavigateUri="{Binding Updates.ReleaseUrl}"
|
||||
RequestNavigate="OnReleaseLinkNavigate">
|
||||
<Run Text="{Binding Localization[ReleasePageLink], Mode=OneWay}" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Visibility="{Binding Updates.IsInstallOffered, Converter={StaticResource BooleanToVisibility}}"
|
||||
Text="{Binding Localization[UpdateInstallHint]}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -18,10 +18,19 @@
|
||||
section. It is handed out there together with the reserved application name;
|
||||
the default is only good enough for a check on your own machine.
|
||||
|
||||
.PARAMETER Install
|
||||
Puts the package onto this machine to see it working. Developer mode has to
|
||||
be on; a signature is not needed, because what gets registered is the layout
|
||||
the package is made of rather than the package file.
|
||||
|
||||
.EXAMPLE
|
||||
# A check on your own machine: your architecture alone
|
||||
pwsh -File Packaging\build-msix.ps1 -Architectures x64
|
||||
|
||||
.EXAMPLE
|
||||
# Build and install in one go, to click through the application
|
||||
pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
|
||||
|
||||
.EXAMPLE
|
||||
# A build for the Store — the identity comes from Partner Center
|
||||
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||
@@ -39,6 +48,8 @@ param(
|
||||
[ValidateSet('x64', 'arm64')]
|
||||
[string[]] $Architectures = @('x64', 'arm64'),
|
||||
|
||||
[switch] $Install,
|
||||
|
||||
[string] $OutputPath
|
||||
)
|
||||
|
||||
@@ -65,6 +76,25 @@ if (-not (Test-Path $assets)) {
|
||||
throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first."
|
||||
}
|
||||
|
||||
if ($Install) {
|
||||
# Both things below are checked before the build rather than after it: the
|
||||
# build takes minutes, and neither of them gets any truer while it runs
|
||||
|
||||
$developerMode = Get-ItemPropertyValue `
|
||||
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' `
|
||||
-Name 'AllowDevelopmentWithoutDevLicense' -ErrorAction SilentlyContinue
|
||||
|
||||
if ($developerMode -ne 1) {
|
||||
throw 'Installing needs developer mode: Settings - System - For developers - Developer mode. Without a signature Windows registers a package no other way.'
|
||||
}
|
||||
|
||||
$machineArchitecture = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' }
|
||||
|
||||
if ($Architectures -notcontains $machineArchitecture) {
|
||||
throw "This machine is $machineArchitecture, and that architecture is not being built. Add it to -Architectures, or drop -Install."
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Tool {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -136,6 +166,26 @@ Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
|
||||
$sdkTools = Get-SdkToolsPath
|
||||
$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools
|
||||
|
||||
# A package registered out of the layout runs straight from that folder, and
|
||||
# the folder is about to be wiped. Left in place, the registration would point
|
||||
# at files that are gone — an application that neither starts nor uninstalls
|
||||
# the usual way. This happens whatever the build was asked for, so -Install has
|
||||
# no say in it.
|
||||
#
|
||||
# Only a registration made from this very folder is removed. A copy installed
|
||||
# from the Store answers to the same name and is none of our business
|
||||
foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) {
|
||||
$location = $installed.InstallLocation
|
||||
if (-not $location) { continue }
|
||||
|
||||
if (-not $location.TrimEnd('\').StartsWith($layoutRoot.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Removing the registered $($installed.PackageFullName)..." -ForegroundColor Cyan
|
||||
Remove-AppxPackage -Package $installed.PackageFullName
|
||||
}
|
||||
|
||||
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
|
||||
|
||||
@@ -198,9 +248,25 @@ if ($built.Count -gt 1) {
|
||||
Remove-Item $bundleInput -Recurse -Force
|
||||
}
|
||||
|
||||
if ($Install) {
|
||||
Write-Host "Installing the $machineArchitecture build..." -ForegroundColor Cyan
|
||||
|
||||
# The layout is registered rather than the package file: the two hold the
|
||||
# same thing, but a package file Windows only installs when it is signed
|
||||
Add-AppxPackage -Register (Join-Path $layoutRoot "$machineArchitecture\AppxManifest.xml")
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Done.' -ForegroundColor Green
|
||||
Write-Host " $result"
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' This file is uploaded to Partner Center as it is.'
|
||||
|
||||
if ($Install) {
|
||||
Write-Host ''
|
||||
Write-Host ' The application is installed and runs out of the layout folder, so'
|
||||
Write-Host ' building again would pull the files out from under it. This script'
|
||||
Write-Host ' takes care of that itself; to remove the application by hand:'
|
||||
Write-Host " Remove-AppxPackage (Get-AppxPackage -Name $IdentityName).PackageFullName"
|
||||
}
|
||||
|
||||
+21
-7
@@ -170,9 +170,11 @@ Windows: тот показывает издателя, спрашивает по
|
||||
У приложения, установленного из Store, раздела обновлений нет вовсе: его
|
||||
обновляет Store, а пакет со стороны Windows поверх него всё равно не примет.
|
||||
|
||||
Само по себе приложение спрашивает о выпусках раз в сутки, при запуске, и хранит
|
||||
дату последней удачной проверки в настройках. Там же её можно выключить — тогда
|
||||
остаётся кнопка в окне настроек, делающая то же самое по требованию.
|
||||
Приложение спрашивает о выпусках при каждом появлении окна настроек, и ответ
|
||||
остаётся в разделе как есть — включая недоступную сеть. Открытие этого окна —
|
||||
намеренное действие, достаточно редкое, чтобы запрос ничего не стоил, поэтому
|
||||
между открытиями ничего не запоминается и выключать нечего. Кнопка рядом с
|
||||
версией задаёт тот же вопрос заново по требованию.
|
||||
|
||||
Где искать выпуски, задаётся в `UpdateOptions`: репозиторий принадлежит тому, кто
|
||||
выпускает приложение, а не пользователю, поэтому значения живут в сборке, а не в
|
||||
@@ -283,10 +285,22 @@ Center как есть.
|
||||
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
||||
MSIX не может установить среду выполнения как зависимость пакета.
|
||||
|
||||
Чтобы попробовать пакет без установки, зарегистрируйте опубликованный layout —
|
||||
для этого нужен режим разработчика и не нужна подпись вовсе:
|
||||
Чтобы посмотреть, как пакет работает на этой машине, соберите его с `-Install`:
|
||||
|
||||
```powershell
|
||||
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
|
||||
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName
|
||||
pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
|
||||
```
|
||||
|
||||
Приложение появится в меню «Пуск» как любое установленное. Регистрируется не сам
|
||||
файл пакета, а layout, из которого пакет собирается, поэтому подпись не нужна —
|
||||
достаточно режима разработчика. Обратная сторона в том, что приложение работает
|
||||
прямо из `artifacts\layout`, и пересборка вытащила бы файлы у него из-под ног:
|
||||
такую регистрацию скрипт снимает перед очисткой папки независимо от того,
|
||||
просили ли `-Install`. Копию, установленную из Store, он не трогает, хотя
|
||||
называется она так же.
|
||||
|
||||
Удалить вручную:
|
||||
|
||||
```powershell
|
||||
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName
|
||||
```
|
||||
|
||||
@@ -166,9 +166,11 @@ An app installed from the Store has no updates section at all: the Store
|
||||
updates it, and a package from the side is something Windows would not accept
|
||||
over it anyway.
|
||||
|
||||
By itself the app asks about releases once a day, at startup, and remembers the
|
||||
date of the last successful check in the settings. The check can be turned off
|
||||
there, which leaves the button in the settings window doing the same on demand.
|
||||
The app asks about releases every time the settings window appears, and the
|
||||
answer stays in the section as it is — an unreachable network included. Opening
|
||||
that window is a deliberate act, rare enough for a request to cost nothing, so
|
||||
nothing is remembered between openings and there is nothing to turn off. The
|
||||
button next to the version asks the same question again on demand.
|
||||
|
||||
Where the releases are looked for is set in `UpdateOptions` — the repository
|
||||
belongs to whoever publishes the app, not to the user, so the values live in the
|
||||
@@ -277,10 +279,22 @@ to Partner Center as it is.
|
||||
The app ships with its own copy of .NET: Windows does not include .NET 10, and
|
||||
MSIX cannot install a runtime as a package dependency.
|
||||
|
||||
To try the package without installing it, register the published layout — this
|
||||
needs Developer Mode and no signature at all:
|
||||
To see the package working on this machine, build it with `-Install`:
|
||||
|
||||
```powershell
|
||||
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
|
||||
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName
|
||||
pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
|
||||
```
|
||||
|
||||
The application then shows up in the Start menu like any installed one. What
|
||||
gets registered is the layout the package is made of rather than the package
|
||||
file, so no signature is needed — only Developer Mode. The flip side is that the
|
||||
application runs straight out of `artifacts\layout`, and a rebuild would pull
|
||||
the files out from under it; the script removes such a registration before it
|
||||
wipes the folder, whether or not `-Install` was asked for. A copy installed from
|
||||
the Store answers to the same name and is left alone.
|
||||
|
||||
To remove it by hand:
|
||||
|
||||
```powershell
|
||||
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user