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:
2026-08-12 14:25:05 +00:00
committed by alex
parent 55ac8e6556
commit 68b3d544d2
14 changed files with 376 additions and 415 deletions
@@ -137,10 +137,6 @@ public sealed class AppSettingsTests
bool flag => !flag, bool flag => !flag,
Color color => Color.FromArgb((byte)(color.R + 1), color.G, color.B), Color color => Color.FromArgb((byte)(color.R + 1), color.G, color.B),
Enum value => NextEnumValue(value), 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}"), _ => 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("git.example.com", options.ServiceUri.Host);
Assert.Equal("team/app", options.Project); 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);
}
} }
-16
View File
@@ -87,20 +87,6 @@ public sealed partial class AppSettings : ObservableObject
[ObservableProperty] [ObservableProperty]
private double _capsLockHoldMilliseconds = 300; 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> /// <summary>The fill of the popup. The opacity is set by <see cref="Opacity"/>.</summary>
[ObservableProperty] [ObservableProperty]
private Color _backgroundColor = Color.FromArgb(0xFF, 0x20, 0x20, 0x20); private Color _backgroundColor = Color.FromArgb(0xFF, 0x20, 0x20, 0x20);
@@ -142,8 +128,6 @@ public sealed partial class AppSettings : ObservableObject
DurationMilliseconds = other.DurationMilliseconds; DurationMilliseconds = other.DurationMilliseconds;
UseCapsLockHotkey = other.UseCapsLockHotkey; UseCapsLockHotkey = other.UseCapsLockHotkey;
CapsLockHoldMilliseconds = other.CapsLockHoldMilliseconds; CapsLockHoldMilliseconds = other.CapsLockHoldMilliseconds;
CheckForUpdates = other.CheckForUpdates;
LastUpdateCheck = other.LastUpdateCheck;
BackgroundColor = other.BackgroundColor; BackgroundColor = other.BackgroundColor;
ForegroundColor = other.ForegroundColor; ForegroundColor = other.ForegroundColor;
} }
+4 -7
View File
@@ -85,8 +85,8 @@
<data name="AppTheme_Dark" xml:space="preserve"> <data name="AppTheme_Dark" xml:space="preserve">
<value>Dark</value> <value>Dark</value>
</data> </data>
<data name="SectionPlacement" xml:space="preserve"> <data name="SectionPopup" xml:space="preserve">
<value>Placement</value> <value>Layout popup</value>
</data> </data>
<data name="PlacementModeLabel" xml:space="preserve"> <data name="PlacementModeLabel" xml:space="preserve">
<value>Mode</value> <value>Mode</value>
@@ -151,9 +151,6 @@
<data name="ScreenMarginLabel" xml:space="preserve"> <data name="ScreenMarginLabel" xml:space="preserve">
<value>Margin from screen edge</value> <value>Margin from screen edge</value>
</data> </data>
<data name="SectionAppearance" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="FontSizeLabel" xml:space="preserve"> <data name="FontSizeLabel" xml:space="preserve">
<value>Font size</value> <value>Font size</value>
</data> </data>
@@ -214,8 +211,8 @@
<data name="CheckUpdatesButton" xml:space="preserve"> <data name="CheckUpdatesButton" xml:space="preserve">
<value>Check for updates</value> <value>Check for updates</value>
</data> </data>
<data name="UpdateAutoCheck" xml:space="preserve"> <data name="UpdateNotChecked" xml:space="preserve">
<value>Check for updates</value> <value>Updates have not been checked yet.</value>
</data> </data>
<data name="UpdateChecking" xml:space="preserve"> <data name="UpdateChecking" xml:space="preserve">
<value>Checking for updates…</value> <value>Checking for updates…</value>
+4 -7
View File
@@ -85,8 +85,8 @@
<data name="AppTheme_Dark" xml:space="preserve"> <data name="AppTheme_Dark" xml:space="preserve">
<value>Тёмная</value> <value>Тёмная</value>
</data> </data>
<data name="SectionPlacement" xml:space="preserve"> <data name="SectionPopup" xml:space="preserve">
<value>Расположение</value> <value>Подсказка о раскладке</value>
</data> </data>
<data name="PlacementModeLabel" xml:space="preserve"> <data name="PlacementModeLabel" xml:space="preserve">
<value>Режим</value> <value>Режим</value>
@@ -151,9 +151,6 @@
<data name="ScreenMarginLabel" xml:space="preserve"> <data name="ScreenMarginLabel" xml:space="preserve">
<value>Отступ от края экрана</value> <value>Отступ от края экрана</value>
</data> </data>
<data name="SectionAppearance" xml:space="preserve">
<value>Внешний вид</value>
</data>
<data name="FontSizeLabel" xml:space="preserve"> <data name="FontSizeLabel" xml:space="preserve">
<value>Размер шрифта</value> <value>Размер шрифта</value>
</data> </data>
@@ -214,8 +211,8 @@
<data name="CheckUpdatesButton" xml:space="preserve"> <data name="CheckUpdatesButton" xml:space="preserve">
<value>Проверить обновления</value> <value>Проверить обновления</value>
</data> </data>
<data name="UpdateAutoCheck" xml:space="preserve"> <data name="UpdateNotChecked" xml:space="preserve">
<value>Проверять обновления</value> <value>Обновления ещё не проверялись.</value>
</data> </data>
<data name="UpdateChecking" xml:space="preserve"> <data name="UpdateChecking" xml:space="preserve">
<value>Идёт проверка обновлений…</value> <value>Идёт проверка обновлений…</value>
@@ -21,9 +21,6 @@ public sealed class UpdateOptions
/// <summary>The project: <c>owner/repository</c>.</summary> /// <summary>The project: <c>owner/repository</c>.</summary>
public string Project { get; init; } = "alrakis/cursor-lang"; 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> /// <summary>
/// An access token for a private repository. /// An access token for a private repository.
/// </summary> /// </summary>
@@ -1,5 +1,4 @@
using CursorLang.Core.Models; using CursorLang.Core.Models;
using CursorLang.Core.Services;
using CursorLang.Settings.Services; using CursorLang.Settings.Services;
using CursorLang.Settings.ViewModels; using CursorLang.Settings.ViewModels;
using CursorLang.Tests.Shared; using CursorLang.Tests.Shared;
@@ -12,7 +11,7 @@ namespace CursorLang.Settings.Tests.Infrastructure;
internal static class Fake internal static class Fake
{ {
internal static UpdateViewModel Updates() => internal static UpdateViewModel Updates() =>
new(new FakeUpdateService(), new FakeLocalizationService(), new AppSettings(), new UpdateOptions()); new(new FakeUpdateService(), new FakeLocalizationService());
} }
/// <summary> /// <summary>
@@ -13,13 +13,15 @@ namespace CursorLang.Settings.Tests.ViewModels;
/// </summary> /// </summary>
public sealed class UpdateViewModelTests 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] [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()); using UpdateViewModel viewModel = Create(new FakeUpdateService());
Assert.Equal(UpdateStatus.Idle, viewModel.Status); Assert.Equal(UpdateStatus.Idle, viewModel.Status);
Assert.False(viewModel.HasStatus); Assert.Equal("en:UpdateNotChecked", viewModel.StatusText);
Assert.False(viewModel.IsDownloadOffered); Assert.False(viewModel.IsDownloadOffered);
Assert.False(viewModel.IsInstallOffered); Assert.False(viewModel.IsInstallOffered);
Assert.True(viewModel.CanCheck); Assert.True(viewModel.CanCheck);
@@ -62,96 +64,36 @@ public sealed class UpdateViewModelTests
await viewModel.CheckCommand.ExecuteAsync(null); await viewModel.CheckCommand.ExecuteAsync(null);
Assert.Equal(UpdateStatus.Failed, viewModel.Status); 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 // The window is opened by hand, and the answer is what the section is there
// asked about updates has no use for the complaint // for: a dead network is part of the answer rather than a reason to say nothing
[Fact] [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") }; var updates = new FakeUpdateService { Failure = new HttpRequestException("no network") };
using UpdateViewModel viewModel = Create(updates); using UpdateViewModel viewModel = Create(updates);
await viewModel.StartAsync(); await viewModel.StartAsync();
Assert.Equal(UpdateStatus.Idle, viewModel.Status); Assert.Equal(UpdateStatus.Failed, viewModel.Status);
Assert.False(viewModel.HasStatus); 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] [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(); var updates = new FakeUpdateService();
using UpdateViewModel viewModel = Create(updates, settings); using UpdateViewModel viewModel = Create(updates);
await viewModel.StartAsync(); 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); 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(); await viewModel.StartAsync();
Assert.Equal(2, updates.CheckCalls);
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);
} }
[Fact] [Fact]
@@ -283,10 +225,6 @@ public sealed class UpdateViewModelTests
private static UpdateViewModel Create( private static UpdateViewModel Create(
IUpdateService updates, IUpdateService updates,
AppSettings? settings = null,
ILocalizationService? localization = null) => ILocalizationService? localization = null) =>
new(updates, new(updates, localization ?? new FakeLocalizationService());
localization ?? new FakeLocalizationService(),
settings ?? new AppSettings(),
new UpdateOptions());
} }
@@ -353,7 +353,7 @@ public sealed class MainWindowTests
} }
private static UpdateViewModel CreateUpdates(IUpdateService updates) => 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) => private static IEnumerable<Button> FindButtonsBoundTo(DependencyObject root, string path) =>
FindAll<Button>(root).Where(button => FindAll<Button>(root).Where(button =>
@@ -15,17 +15,15 @@ namespace CursorLang.Settings.ViewModels;
/// The updates section of the settings window. /// The updates section of the settings window.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// A check made on opening the window passes its failures in silence: the machine does /// The section always says where things stand: a check flies off the moment the window
/// not always have a live network, and there is no point complaining about it to a user /// appears, and its outcome — including an unreachable network — stays written in the
/// who came to change the popup colour. A check started by the button does report a /// status line. Nothing here is silent, and nothing disappears: a user who opened the
/// failure — it is awaited and watched. /// window is told about updates whether they came looking for them or not.
/// </remarks> /// </remarks>
public sealed partial class UpdateViewModel : ObservableObject, IDisposable public sealed partial class UpdateViewModel : ObservableObject, IDisposable
{ {
private readonly IUpdateService _updates; private readonly IUpdateService _updates;
private readonly ILocalizationService _localization; private readonly ILocalizationService _localization;
private readonly AppSettings _settings;
private readonly UpdateOptions _options;
private CancellationTokenSource? _work; private CancellationTokenSource? _work;
private ReleaseInfo? _release; private ReleaseInfo? _release;
@@ -35,7 +33,6 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
[NotifyPropertyChangedFor(nameof(StatusText))] [NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(IsBusy))] [NotifyPropertyChangedFor(nameof(IsBusy))]
[NotifyPropertyChangedFor(nameof(CanCheck))] [NotifyPropertyChangedFor(nameof(CanCheck))]
[NotifyPropertyChangedFor(nameof(HasStatus))]
[NotifyPropertyChangedFor(nameof(IsProgressUnknown))] [NotifyPropertyChangedFor(nameof(IsProgressUnknown))]
[NotifyPropertyChangedFor(nameof(IsDownloadOffered))] [NotifyPropertyChangedFor(nameof(IsDownloadOffered))]
[NotifyPropertyChangedFor(nameof(IsInstallOffered))] [NotifyPropertyChangedFor(nameof(IsInstallOffered))]
@@ -48,16 +45,10 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
[NotifyPropertyChangedFor(nameof(IsProgressUnknown))] [NotifyPropertyChangedFor(nameof(IsProgressUnknown))]
private double _progress; private double _progress;
public UpdateViewModel( public UpdateViewModel(IUpdateService updates, ILocalizationService localization)
IUpdateService updates,
ILocalizationService localization,
AppSettings settings,
UpdateOptions options)
{ {
_updates = updates; _updates = updates;
_localization = localization; _localization = localization;
_settings = settings;
_options = options;
_localization.PropertyChanged += OnLocalizationChanged; _localization.PropertyChanged += OnLocalizationChanged;
} }
@@ -67,29 +58,11 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
public string CurrentVersion => _updates.CurrentVersion.ToString(); 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> /// <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 IsBusy => Status is UpdateStatus.Checking or UpdateStatus.Downloading;
public bool CanCheck => !IsBusy; public bool CanCheck => !IsBusy;
public bool HasStatus => Status != UpdateStatus.Idle;
/// <summary> /// <summary>
/// The package size is unknown, and the bar shows only the fact of the download. /// 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. /// 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> /// <summary>The release page: the release notes live there too.</summary>
public Uri? ReleaseUrl => _release?.PageUrl; 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 public string StatusText => Status switch
{ {
UpdateStatus.Checking => _localization["UpdateChecking"], UpdateStatus.Checking => _localization["UpdateChecking"],
@@ -115,33 +92,24 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
UpdateStatus.Downloading => _localization["UpdateDownloading"], UpdateStatus.Downloading => _localization["UpdateDownloading"],
UpdateStatus.Ready => _localization["UpdateReady"], UpdateStatus.Ready => _localization["UpdateReady"],
UpdateStatus.Failed => _localization["UpdateFailed"], UpdateStatus.Failed => _localization["UpdateFailed"],
_ => string.Empty, _ => _localization["UpdateNotChecked"],
}; };
/// <summary> /// <summary>
/// Checks for updates when the user has not forbidden it and enough time has /// Asks about new versions. Called once, when the window has just appeared.
/// passed since the previous check. Called once when the window opens.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// It used to be called when the application started, which back then meant when /// 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 /// 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 /// 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. /// 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> /// </remarks>
public async Task StartAsync() public Task StartAsync() => CheckAsync();
{
if (!IsSupported || !_settings.CheckForUpdates)
{
return;
}
if (_settings.LastUpdateCheck is { } last && DateTimeOffset.UtcNow - last < _options.CheckInterval)
{
return;
}
await RunCheckAsync(quiet: true);
}
public void Dispose() public void Dispose()
{ {
@@ -153,7 +121,36 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
} }
[RelayCommand] [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] [RelayCommand]
private async Task DownloadAsync() private async Task DownloadAsync()
@@ -216,38 +213,6 @@ public sealed partial class UpdateViewModel : ObservableObject, IDisposable
or NotSupportedException or InvalidOperationException or Win32Exception or NotSupportedException or InvalidOperationException or Win32Exception
or OperationCanceledException; 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> /// <summary>
/// Starts a new piece of work, cancelling the previous one: the user may have /// Starts a new piece of work, cancelling the previous one: the user may have
/// pressed "Check" in the middle of a download. /// pressed "Check" in the middle of a download.
+195 -195
View File
@@ -51,7 +51,10 @@
<!-- The settings are laid out in two columns: this way the window fits on the <!-- 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 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"> <ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -91,7 +94,7 @@
</Grid> </Grid>
</GroupBox> </GroupBox>
<GroupBox Header="{Binding Localization[SectionPlacement]}"> <GroupBox Header="{Binding Localization[SectionBehavior]}">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="164" /> <ColumnDefinition Width="164" />
@@ -105,6 +108,177 @@
<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[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" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
@@ -220,160 +394,56 @@
Visibility="{Binding Settings.PlacementMode, Visibility="{Binding Settings.PlacementMode,
Converter={StaticResource EnumToVisibility}, Converter={StaticResource EnumToVisibility},
ConverterParameter=FixedPoint}" /> ConverterParameter=FixedPoint}" />
</Grid>
</GroupBox>
<GroupBox Header="{Binding Localization[SectionBehavior]}"> <!-- A hairline between the two halves of the section: the
<Grid> placement above, the look of the popup below -->
<Grid.ColumnDefinitions> <Border Grid.Row="7" Grid.ColumnSpan="3"
<ColumnDefinition Width="164" /> Margin="0,16,0,0" Height="1"
<ColumnDefinition Width="*" /> Background="{DynamicResource Theme.ControlBorder}" />
<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}" <TextBlock Grid.Row="8" Margin="0,12,12,0"
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}" 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]}" /> 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}" /> 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}}" /> 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}" Style="{StaticResource FieldLabel}"
Text="{Binding Localization[OpacityLabel]}" /> 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" Minimum="0.1" Maximum="1" TickFrequency="0.05"
Value="{Binding Settings.Opacity}" /> 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}" Style="{StaticResource FieldValue}"
Text="{Binding Settings.Opacity, StringFormat={}{0:P0}}" /> 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}" Style="{StaticResource FieldLabel}"
Text="{Binding Localization[BackgroundColorLabel]}" /> 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}" ItemsSource="{Binding BackgroundPalette}"
ItemTemplate="{StaticResource ColorSwatchTemplate}" ItemTemplate="{StaticResource ColorSwatchTemplate}"
SelectedItem="{Binding Settings.BackgroundColor}" /> 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}" Style="{StaticResource FieldLabel}"
Text="{Binding Localization[TextColorLabel]}" /> 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}" ItemsSource="{Binding TextPalette}"
ItemTemplate="{StaticResource ColorSwatchTemplate}" ItemTemplate="{StaticResource ColorSwatchTemplate}"
SelectedItem="{Binding Settings.ForegroundColor}" /> 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}" Style="{StaticResource FieldLabel}"
Text="{Binding Localization[PreviewLabel]}" /> Text="{Binding Localization[PreviewLabel]}" />
<!-- The height of the area is sized for the largest font <!-- The height of the area is sized for the largest font
available: the sample text is fully visible at any available: the sample text is fully visible at any
position of the slider --> 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" Margin="0,12,0,0" Padding="16"
Height="152" ClipToBounds="True" Height="152" ClipToBounds="True"
Background="{DynamicResource Theme.SurfaceStrong}" CornerRadius="4" Background="{DynamicResource Theme.SurfaceStrong}" CornerRadius="4"
@@ -392,76 +462,6 @@
</Grid> </Grid>
</GroupBox> </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> </StackPanel>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
+66
View File
@@ -18,10 +18,19 @@
section. It is handed out there together with the reserved application name; 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. 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 .EXAMPLE
# A check on your own machine: your architecture alone # A check on your own machine: your architecture alone
pwsh -File Packaging\build-msix.ps1 -Architectures x64 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 .EXAMPLE
# A build for the Store — the identity comes from Partner Center # A build for the Store — the identity comes from Partner Center
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 ` pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
@@ -39,6 +48,8 @@ param(
[ValidateSet('x64', 'arm64')] [ValidateSet('x64', 'arm64')]
[string[]] $Architectures = @('x64', 'arm64'), [string[]] $Architectures = @('x64', 'arm64'),
[switch] $Install,
[string] $OutputPath [string] $OutputPath
) )
@@ -65,6 +76,25 @@ if (-not (Test-Path $assets)) {
throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first." 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 { function Invoke-Tool {
<# <#
.SYNOPSIS .SYNOPSIS
@@ -136,6 +166,26 @@ Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
$sdkTools = Get-SdkToolsPath $sdkTools = Get-SdkToolsPath
$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools $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 Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
@@ -198,9 +248,25 @@ if ($built.Count -gt 1) {
Remove-Item $bundleInput -Recurse -Force 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 ''
Write-Host 'Done.' -ForegroundColor Green Write-Host 'Done.' -ForegroundColor Green
Write-Host " $result" Write-Host " $result"
Write-Host '' Write-Host ''
Write-Host ' This file is uploaded to Partner Center as it is.' 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
View File
@@ -170,9 +170,11 @@ Windows: тот показывает издателя, спрашивает по
У приложения, установленного из Store, раздела обновлений нет вовсе: его У приложения, установленного из Store, раздела обновлений нет вовсе: его
обновляет Store, а пакет со стороны Windows поверх него всё равно не примет. обновляет Store, а пакет со стороны Windows поверх него всё равно не примет.
Само по себе приложение спрашивает о выпусках раз в сутки, при запуске, и хранит Приложение спрашивает о выпусках при каждом появлении окна настроек, и ответ
дату последней удачной проверки в настройках. Там же её можно выключить — тогда остаётся в разделе как есть — включая недоступную сеть. Открытие этого окна —
остаётся кнопка в окне настроек, делающая то же самое по требованию. намеренное действие, достаточно редкое, чтобы запрос ничего не стоил, поэтому
между открытиями ничего не запоминается и выключать нечего. Кнопка рядом с
версией задаёт тот же вопрос заново по требованию.
Где искать выпуски, задаётся в `UpdateOptions`: репозиторий принадлежит тому, кто Где искать выпуски, задаётся в `UpdateOptions`: репозиторий принадлежит тому, кто
выпускает приложение, а не пользователю, поэтому значения живут в сборке, а не в выпускает приложение, а не пользователю, поэтому значения живут в сборке, а не в
@@ -283,10 +285,22 @@ Center как есть.
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
MSIX не может установить среду выполнения как зависимость пакета. MSIX не может установить среду выполнения как зависимость пакета.
Чтобы попробовать пакет без установки, зарегистрируйте опубликованный layout — Чтобы посмотреть, как пакет работает на этой машине, соберите его с `-Install`:
для этого нужен режим разработчика и не нужна подпись вовсе:
```powershell ```powershell
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName ```
Приложение появится в меню «Пуск» как любое установленное. Регистрируется не сам
файл пакета, а layout, из которого пакет собирается, поэтому подпись не нужна —
достаточно режима разработчика. Обратная сторона в том, что приложение работает
прямо из `artifacts\layout`, и пересборка вытащила бы файлы у него из-под ног:
такую регистрацию скрипт снимает перед очисткой папки независимо от того,
просили ли `-Install`. Копию, установленную из Store, он не трогает, хотя
называется она так же.
Удалить вручную:
```powershell
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName
``` ```
+21 -7
View File
@@ -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 updates it, and a package from the side is something Windows would not accept
over it anyway. over it anyway.
By itself the app asks about releases once a day, at startup, and remembers the The app asks about releases every time the settings window appears, and the
date of the last successful check in the settings. The check can be turned off answer stays in the section as it is — an unreachable network included. Opening
there, which leaves the button in the settings window doing the same on demand. 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 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 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 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. MSIX cannot install a runtime as a package dependency.
To try the package without installing it, register the published layout — this To see the package working on this machine, build it with `-Install`:
needs Developer Mode and no signature at all:
```powershell ```powershell
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName ```
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
``` ```