From 204f0e3e2d235a85b1762668d01a9eee535a7400 Mon Sep 17 00:00:00 2001 From: Spike Curtis <spike@coder.com> Date: Thu, 22 May 2025 20:29:49 +0400 Subject: [PATCH 1/7] fix: change dummy workspace logic to include starting and stopping (#107) fixes #104 Includes starting, stopping, pending and unknown (rare!) workspaces as "dummy" grayed workspaces. TBD what we want to do with failed or canceled workspaces. --- App/ViewModels/TrayWindowViewModel.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index cfa5163..4d493c9 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -207,7 +207,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) // For every stopped workspace that doesn't have any agents, add a // dummy agent row. foreach (var workspace in rpcModel.Workspaces.Where(w => - w.Status == Workspace.Types.Status.Stopped && !workspacesWithAgents.Contains(w.Id))) + ShouldShowDummy(w) && !workspacesWithAgents.Contains(w.Id))) { if (!Uuid.TryFrom(workspace.Id.Span, out var uuid)) continue; @@ -372,4 +372,21 @@ public void Exit() { _ = ((App)Application.Current).ExitApplication(); } + + private static bool ShouldShowDummy(Workspace workspace) + { + switch (workspace.Status) + { + case Workspace.Types.Status.Unknown: + case Workspace.Types.Status.Pending: + case Workspace.Types.Status.Starting: + case Workspace.Types.Status.Stopping: + case Workspace.Types.Status.Stopped: + return true; + // TODO: should we include and show a different color than Gray for workspaces that are + // failed, canceled or deleting? + default: + return false; + } + } } From 2301c75d7a6526bb83a9b91a953df7f99ef1070e Mon Sep 17 00:00:00 2001 From: Atif Ali <atif@coder.com> Date: Tue, 27 May 2025 20:55:15 -0700 Subject: [PATCH 2/7] chore: publish to winget in release workflow (#108) --- .github/workflows/release.yaml | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e6849aa..9ad6c16 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,6 +18,8 @@ permissions: jobs: release: runs-on: ${{ github.repository_owner == 'coder' && 'windows-latest-16-cores' || 'windows-latest' }} + outputs: + version: ${{ steps.version.outputs.VERSION }} timeout-minutes: 15 steps: @@ -117,3 +119,78 @@ jobs: ${{ steps.release.outputs.ARM64_OUTPUT_PATH }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + winget: + runs-on: depot-windows-latest + needs: release + steps: + - name: Sync fork + run: gh repo sync cdrci/winget-pkgs -b master + env: + GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + # If the event that triggered the build was an annotated tag (which our + # tags are supposed to be), actions/checkout has a bug where the tag in + # question is only a lightweight tag and not a full annotated tag. This + # command seems to fix it. + # https://github.com/actions/checkout/issues/290 + - name: Fetch git tags + run: git fetch --tags --force + + - name: Install wingetcreate + run: | + Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe + + - name: Submit updated manifest to winget-pkgs + run: | + $version = "${{ needs.release.outputs.version }}" + + $release_assets = gh release view --repo coder/coder-desktop-windows "v${version}" --json assets | ` + ConvertFrom-Json + # Get the installer URLs from the release assets. + $amd64_installer_url = $release_assets.assets | ` + Where-Object name -Match ".*-x64.exe$" | ` + Select -ExpandProperty url + $arm64_installer_url = $release_assets.assets | ` + Where-Object name -Match ".*-arm64.exe$" | ` + Select -ExpandProperty url + + echo "amd64 Installer URL: ${amd64_installer_url}" + echo "arm64 Installer URL: ${arm64_installer_url}" + echo "Package version: ${version}" + + .\wingetcreate.exe update Coder.CoderDesktop ` + --submit ` + --version "${version}" ` + --urls "${amd64_installer_url}" "${arm64_installer_url}" ` + --token "$env:WINGET_GH_TOKEN" + + env: + # For gh CLI: + GH_TOKEN: ${{ github.token }} + # For wingetcreate. We need a real token since we're pushing a commit + # to GitHub and then making a PR in a different repo. + WINGET_GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }} + + + - name: Comment on PR + run: | + # wait 30 seconds + Start-Sleep -Seconds 30.0 + # Find the PR that wingetcreate just made. + $version = "${{ needs.release.outputs.version }}" + $pr_list = gh pr list --repo microsoft/winget-pkgs --search "author:cdrci Coder.CoderDesktop version ${version}" --limit 1 --json number | ` + ConvertFrom-Json + $pr_number = $pr_list[0].number + + gh pr comment --repo microsoft/winget-pkgs "${pr_number}" --body "🤖 cc: @deansheather @matifali" + + env: + # For gh CLI. We need a real token since we're commenting on a PR in a + # different repo. + GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }} \ No newline at end of file From d6cbf716130b96ece46f646ef8013967d00faf73 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 28 May 2025 13:17:22 +0200 Subject: [PATCH 3/7] feat: enabled sign out and animated window resize (#109) Closes: #96 --------- Co-authored-by: Dean Sheather <dean@deansheather.com> --- App/Controls/ExpandContent.xaml | 51 ++++---- App/Controls/ExpandContent.xaml.cs | 46 +++++-- App/Services/RpcController.cs | 2 + App/ViewModels/AgentViewModel.cs | 12 +- .../TrayWindowLoginRequiredViewModel.cs | 7 ++ App/ViewModels/TrayWindowViewModel.cs | 9 +- .../Pages/TrayWindowLoginRequiredPage.xaml | 9 ++ App/Views/Pages/TrayWindowMainPage.xaml | 1 - App/Views/TrayWindow.xaml | 7 ++ App/Views/TrayWindow.xaml.cs | 114 ++++++++++++++---- 10 files changed, 188 insertions(+), 70 deletions(-) diff --git a/App/Controls/ExpandContent.xaml b/App/Controls/ExpandContent.xaml index d36170d..2cc0eb4 100644 --- a/App/Controls/ExpandContent.xaml +++ b/App/Controls/ExpandContent.xaml @@ -9,42 +9,43 @@ xmlns:toolkit="using:CommunityToolkit.WinUI" mc:Ignorable="d"> - <Grid x:Name="CollapsiblePanel" Opacity="0" Visibility="Collapsed" toolkit:UIElementExtensions.ClipToBounds="True"> + <Grid x:Name="CollapsiblePanel" Opacity="0" Visibility="Collapsed" MaxHeight="0" toolkit:UIElementExtensions.ClipToBounds="True"> <Grid.RenderTransform> - <TranslateTransform x:Name="SlideTransform" Y="-10" /> + <TranslateTransform x:Name="SlideTransform" Y="-16"/> </Grid.RenderTransform> <VisualStateManager.VisualStateGroups> <VisualStateGroup> <VisualState x:Name="ExpandedState"> - <Storyboard> - <DoubleAnimation - Storyboard.TargetName="CollapsiblePanel" - Storyboard.TargetProperty="Opacity" - To="1" - Duration="0:0:0.2" /> - <DoubleAnimation - Storyboard.TargetName="SlideTransform" - Storyboard.TargetProperty="Y" - To="0" - Duration="0:0:0.2" /> + <Storyboard x:Name="ExpandSb"> + <DoubleAnimation Storyboard.TargetName="CollapsiblePanel" + Storyboard.TargetProperty="MaxHeight" + To="10000" Duration="0:0:0.16" BeginTime="0:0:0.16" + EnableDependentAnimation="True"/> + <DoubleAnimation Storyboard.TargetName="CollapsiblePanel" + Storyboard.TargetProperty="Opacity" BeginTime="0:0:0.16" + To="1" Duration="0:0:0.16"/> + <DoubleAnimation Storyboard.TargetName="SlideTransform" + Storyboard.TargetProperty="Y" BeginTime="0:0:0.16" + To="0" Duration="0:0:0.16"/> </Storyboard> </VisualState> - <VisualState x:Name="CollapsedState"> - <Storyboard Completed="{x:Bind CollapseAnimation_Completed}"> - <DoubleAnimation - Storyboard.TargetName="CollapsiblePanel" - Storyboard.TargetProperty="Opacity" - To="0" - Duration="0:0:0.2" /> - <DoubleAnimation - Storyboard.TargetName="SlideTransform" - Storyboard.TargetProperty="Y" - To="-10" - Duration="0:0:0.2" /> + <Storyboard x:Name="CollapseSb" + Completed="{x:Bind CollapseStoryboard_Completed}"> + <DoubleAnimation Storyboard.TargetName="CollapsiblePanel" + Storyboard.TargetProperty="MaxHeight" + To="0" Duration="0:0:0.16" + EnableDependentAnimation="True"/> + <DoubleAnimation Storyboard.TargetName="CollapsiblePanel" + Storyboard.TargetProperty="Opacity" + To="0" Duration="0:0:0.16"/> + <DoubleAnimation Storyboard.TargetName="SlideTransform" + Storyboard.TargetProperty="Y" + To="-16" Duration="0:0:0.16"/> </Storyboard> </VisualState> + </VisualStateGroup> </VisualStateManager.VisualStateGroups> </Grid> diff --git a/App/Controls/ExpandContent.xaml.cs b/App/Controls/ExpandContent.xaml.cs index 1cd5d2f..926af9a 100644 --- a/App/Controls/ExpandContent.xaml.cs +++ b/App/Controls/ExpandContent.xaml.cs @@ -2,38 +2,60 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Markup; +using System; +using System.Threading.Tasks; namespace Coder.Desktop.App.Controls; + [ContentProperty(Name = nameof(Children))] [DependencyProperty<bool>("IsOpen", DefaultValue = false)] public sealed partial class ExpandContent : UserControl { public UIElementCollection Children => CollapsiblePanel.Children; + private readonly string _expandedState = "ExpandedState"; + private readonly string _collapsedState = "CollapsedState"; + public ExpandContent() { InitializeComponent(); - } + Loaded += (_, __) => + { + // When we load the control for the first time (after panel swapping) + // we need to set the initial state based on IsOpen. + VisualStateManager.GoToState( + this, + IsOpen ? _expandedState : _collapsedState, + useTransitions: false); // NO animation yet - public void CollapseAnimation_Completed(object? sender, object args) - { - // Hide the panel completely when the collapse animation is done. This - // cannot be done with keyframes for some reason. - // - // Without this, the space will still be reserved for the panel. - CollapsiblePanel.Visibility = Visibility.Collapsed; + // If IsOpen was already true we must also show the panel + if (IsOpen) + { + CollapsiblePanel.Visibility = Visibility.Visible; + // This makes the panel expand to its full height + CollapsiblePanel.ClearValue(FrameworkElement.MaxHeightProperty); + } + }; } partial void OnIsOpenChanged(bool oldValue, bool newValue) { - var newState = newValue ? "ExpandedState" : "CollapsedState"; - - // The animation can't set visibility when starting or ending the - // animation. + var newState = newValue ? _expandedState : _collapsedState; if (newValue) + { CollapsiblePanel.Visibility = Visibility.Visible; + // We use BeginTime to ensure other panels are collapsed first. + // If the user clicks the expand button quickly, we want to avoid + // the panel expanding to its full height before the collapse animation completes. + CollapseSb.SkipToFill(); + } VisualStateManager.GoToState(this, newState, true); } + + private void CollapseStoryboard_Completed(object sender, object e) + { + CollapsiblePanel.Visibility = Visibility.Collapsed; + } } diff --git a/App/Services/RpcController.cs b/App/Services/RpcController.cs index 70dfe9f..7beff66 100644 --- a/App/Services/RpcController.cs +++ b/App/Services/RpcController.cs @@ -234,6 +234,8 @@ public async Task StopVpn(CancellationToken ct = default) MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); throw new VpnLifecycleException($"Failed to stop VPN. Service reported failure: {reply.Stop.ErrorMessage}"); } + + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); } public async ValueTask DisposeAsync() diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index 34b01d7..cd5907b 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -237,12 +237,20 @@ public AgentViewModel(ILogger<AgentViewModel> logger, ICoderApiClientFactory cod Id = id; - PropertyChanged += (_, args) => + PropertyChanging += (x, args) => { if (args.PropertyName == nameof(IsExpanded)) { - _expanderHost.HandleAgentExpanded(Id, IsExpanded); + var value = !IsExpanded; + if (value) + _expanderHost.HandleAgentExpanded(Id, value); + } + }; + PropertyChanged += (x, args) => + { + if (args.PropertyName == nameof(IsExpanded)) + { // Every time the drawer is expanded, re-fetch all apps. if (IsExpanded && !FetchingApps) FetchApps(); diff --git a/App/ViewModels/TrayWindowLoginRequiredViewModel.cs b/App/ViewModels/TrayWindowLoginRequiredViewModel.cs index 628be72..abc1257 100644 --- a/App/ViewModels/TrayWindowLoginRequiredViewModel.cs +++ b/App/ViewModels/TrayWindowLoginRequiredViewModel.cs @@ -2,6 +2,7 @@ using Coder.Desktop.App.Views; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; namespace Coder.Desktop.App.ViewModels; @@ -31,4 +32,10 @@ public void Login() _signInWindow.Closed += (_, _) => _signInWindow = null; _signInWindow.Activate(); } + + [RelayCommand] + public void Exit() + { + _ = ((App)Application.Current).ExitApplication(); + } } diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 4d493c9..d8b3182 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -126,7 +126,7 @@ public void HandleAgentExpanded(Uuid id, bool expanded) if (!expanded) return; _hasExpandedAgent = true; // Collapse every other agent. - foreach (var otherAgent in Agents.Where(a => a.Id != id)) + foreach (var otherAgent in Agents.Where(a => a.Id != id && a.IsExpanded == true)) otherAgent.SetExpanded(false); } @@ -360,11 +360,10 @@ private void ShowFileSyncListWindow() } [RelayCommand] - private void SignOut() + private async Task SignOut() { - if (VpnLifecycle is not VpnLifecycle.Stopped) - return; - _credentialManager.ClearCredentials(); + await _rpcController.StopVpn(); + await _credentialManager.ClearCredentials(); } [RelayCommand] diff --git a/App/Views/Pages/TrayWindowLoginRequiredPage.xaml b/App/Views/Pages/TrayWindowLoginRequiredPage.xaml index ce161e3..c1d69aa 100644 --- a/App/Views/Pages/TrayWindowLoginRequiredPage.xaml +++ b/App/Views/Pages/TrayWindowLoginRequiredPage.xaml @@ -34,5 +34,14 @@ <TextBlock Text="Sign in" Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" /> </HyperlinkButton> + + <HyperlinkButton + Command="{x:Bind ViewModel.ExitCommand, Mode=OneWay}" + Margin="-12,-8,-12,-5" + HorizontalAlignment="Stretch" + HorizontalContentAlignment="Left"> + + <TextBlock Text="Exit" Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" /> + </HyperlinkButton> </StackPanel> </Page> diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index f3549c2..283867d 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -333,7 +333,6 @@ <HyperlinkButton Command="{x:Bind ViewModel.SignOutCommand, Mode=OneWay}" - IsEnabled="{x:Bind ViewModel.VpnLifecycle, Converter={StaticResource StoppedBoolConverter}, Mode=OneWay}" Margin="-12,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"> diff --git a/App/Views/TrayWindow.xaml b/App/Views/TrayWindow.xaml index 0d87874..cfc4214 100644 --- a/App/Views/TrayWindow.xaml +++ b/App/Views/TrayWindow.xaml @@ -20,5 +20,12 @@ <!-- This is where the current Page is displayed --> <controls:SizedFrame x:Name="RootFrame" /> + + <!-- proxy for animating resize --> + <Border x:Name="SizeProxy" + Width="0" + Height="0" + IsHitTestVisible="False" + Opacity="0" /> </Grid> </Window> diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 5d1755c..ef55095 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -1,8 +1,3 @@ -using System; -using System.Runtime.InteropServices; -using Windows.Graphics; -using Windows.System; -using Windows.UI.Core; using Coder.Desktop.App.Controls; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; @@ -15,6 +10,13 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Animation; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Graphics; +using Windows.System; +using Windows.UI.Core; using WinRT.Interop; using WindowActivatedEventArgs = Microsoft.UI.Xaml.WindowActivatedEventArgs; @@ -24,8 +26,15 @@ public sealed partial class TrayWindow : Window { private const int WIDTH = 300; + private readonly AppWindow _aw; + + public double ProxyHeight { get; private set; } + + // This is used to know the "start point of the animation" + private int _lastWindowHeight; + private Storyboard? _currentSb; + private NativeApi.POINT? _lastActivatePosition; - private int _maxHeightSinceLastActivation; private readonly IRpcController _rpcController; private readonly ICredentialManager _credentialManager; @@ -82,8 +91,34 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan var value = 2; // Best effort. This does not work on Windows 10. _ = NativeApi.DwmSetWindowAttribute(windowHandle, 33, ref value, Marshal.SizeOf<int>()); + + _aw = AppWindow.GetFromWindowId( + Win32Interop.GetWindowIdFromWindow( + WindowNative.GetWindowHandle(this))); + SizeProxy.SizeChanged += (_, e) => + { + if (_currentSb is null) return; // nothing running + + int newHeight = (int)Math.Round( + e.NewSize.Height * DisplayScale.WindowScale(this)); + + int delta = newHeight - _lastWindowHeight; + if (delta == 0) return; + + var pos = _aw.Position; + var size = _aw.Size; + + pos.Y -= delta; // grow upward + size.Height = newHeight; + + _aw.MoveAndResize( + new RectInt32(pos.X, pos.Y, size.Width, size.Height)); + + _lastWindowHeight = newHeight; + }; } + private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel, SyncSessionControllerStateModel syncSessionModel) { @@ -140,22 +175,62 @@ public void SetRootFrame(Page page) private void RootFrame_SizeChanged(object sender, SizedFrameEventArgs e) { - MoveAndResize(e.NewSize.Height); + AnimateWindowHeight(e.NewSize.Height); } - private void MoveAndResize(double height) + // We need to animate the height change in code-behind, because XAML + // storyboard animation timeline is immutable - it cannot be changed + // mid-run to accomodate a new height. + private void AnimateWindowHeight(double targetHeight) { - var size = CalculateWindowSize(height); - var pos = CalculateWindowPosition(size); - var rect = new RectInt32(pos.X, pos.Y, size.Width, size.Height); - AppWindow.MoveAndResize(rect); + // If another animation is already running we need to fast forward it. + if (_currentSb is { } oldSb) + { + oldSb.Completed -= OnStoryboardCompleted; + // We need to use SkipToFill, because Stop actually sets Height to 0, which + // makes the window go haywire. + oldSb.SkipToFill(); + } + + _lastWindowHeight = AppWindow.Size.Height; + + var anim = new DoubleAnimation + { + To = targetHeight, + Duration = TimeSpan.FromMilliseconds(200), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }, + EnableDependentAnimation = true + }; + + Storyboard.SetTarget(anim, SizeProxy); + Storyboard.SetTargetProperty(anim, "Height"); + + var sb = new Storyboard { Children = { anim } }; + sb.Completed += OnStoryboardCompleted; + sb.Begin(); + + _currentSb = sb; + } + + private void OnStoryboardCompleted(object? sender, object e) + { + // We need to remove the event handler after the storyboard completes, + // to avoid memory leaks and multiple calls. + if (sender is Storyboard sb) + sb.Completed -= OnStoryboardCompleted; + + // SizeChanged handler will stop forwarding resize ticks + // until we start the next storyboard. + _currentSb = null; } private void MoveResizeAndActivate() { SaveCursorPos(); - _maxHeightSinceLastActivation = 0; - MoveAndResize(RootFrame.GetContentSize().Height); + var size = CalculateWindowSize(RootFrame.GetContentSize().Height); + var pos = CalculateWindowPosition(size); + var rect = new RectInt32(pos.X, pos.Y, size.Width, size.Height); + AppWindow.MoveAndResize(rect); AppWindow.Show(); NativeApi.SetForegroundWindow(WindowNative.GetWindowHandle(this)); } @@ -179,9 +254,6 @@ private SizeInt32 CalculateWindowSize(double height) var scale = DisplayScale.WindowScale(this); var newWidth = (int)(WIDTH * scale); var newHeight = (int)(height * scale); - // Store the maximum height we've seen for positioning purposes. - if (newHeight > _maxHeightSinceLastActivation) - _maxHeightSinceLastActivation = newHeight; return new SizeInt32(newWidth, newHeight); } @@ -190,14 +262,6 @@ private PointInt32 CalculateWindowPosition(SizeInt32 size) { var width = size.Width; var height = size.Height; - // For positioning purposes, pretend the window is the maximum size it - // has been since it was last activated. This has the affect of - // allowing the window to move up to accomodate more content, but - // prevents it from moving back down when the window shrinks again. - // - // Prevents a lot of jittery behavior with app drawers. - if (height < _maxHeightSinceLastActivation) - height = _maxHeightSinceLastActivation; var cursorPosition = _lastActivatePosition; if (cursorPosition is null) From 22c9bcdb8cfe884179f07f47ab8b2c27e86d775f Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 28 May 2025 13:17:53 +0200 Subject: [PATCH 4/7] feat: disabled 'new sync form' when creating a mutagen sync (#110) Closes: #82 --- App/ViewModels/FileSyncListViewModel.cs | 13 ++++++++++--- App/Views/Pages/FileSyncListMainPage.xaml | 4 +++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/App/ViewModels/FileSyncListViewModel.cs b/App/ViewModels/FileSyncListViewModel.cs index da40e5c..4777183 100644 --- a/App/ViewModels/FileSyncListViewModel.cs +++ b/App/ViewModels/FileSyncListViewModel.cs @@ -48,7 +48,11 @@ public partial class FileSyncListViewModel : ObservableObject [NotifyPropertyChangedFor(nameof(ShowSessions))] public partial string? Error { get; set; } = null; - [ObservableProperty] public partial bool OperationInProgress { get; set; } = false; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanOpenLocalPath))] + [NotifyPropertyChangedFor(nameof(NewSessionRemoteHostEnabled))] + [NotifyPropertyChangedFor(nameof(NewSessionRemotePathDialogEnabled))] + public partial bool OperationInProgress { get; set; } = false; [ObservableProperty] public partial IReadOnlyList<SyncSessionViewModel> Sessions { get; set; } = []; @@ -60,6 +64,7 @@ public partial class FileSyncListViewModel : ObservableObject [ObservableProperty] [NotifyPropertyChangedFor(nameof(NewSessionCreateEnabled))] + [NotifyPropertyChangedFor(nameof(CanOpenLocalPath))] public partial bool NewSessionLocalPathDialogOpen { get; set; } = false; [ObservableProperty] @@ -80,10 +85,12 @@ public partial class FileSyncListViewModel : ObservableObject [NotifyPropertyChangedFor(nameof(NewSessionRemotePathDialogEnabled))] public partial bool NewSessionRemotePathDialogOpen { get; set; } = false; - public bool NewSessionRemoteHostEnabled => AvailableHosts.Count > 0; + public bool CanOpenLocalPath => !NewSessionLocalPathDialogOpen && !OperationInProgress; + + public bool NewSessionRemoteHostEnabled => AvailableHosts.Count > 0 && !OperationInProgress; public bool NewSessionRemotePathDialogEnabled => - !string.IsNullOrWhiteSpace(NewSessionRemoteHost) && !NewSessionRemotePathDialogOpen; + !string.IsNullOrWhiteSpace(NewSessionRemoteHost) && !NewSessionRemotePathDialogOpen && !OperationInProgress; [ObservableProperty] public partial string NewSessionStatus { get; set; } = ""; diff --git a/App/Views/Pages/FileSyncListMainPage.xaml b/App/Views/Pages/FileSyncListMainPage.xaml index cb9f2bb..0872c1a 100644 --- a/App/Views/Pages/FileSyncListMainPage.xaml +++ b/App/Views/Pages/FileSyncListMainPage.xaml @@ -318,11 +318,12 @@ Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Stretch" + IsEnabled="{x:Bind ViewModel.OperationInProgress,Converter={StaticResource InverseBoolConverter}, Mode=OneWay}" Text="{x:Bind ViewModel.NewSessionLocalPath, Mode=TwoWay}" /> <Button Grid.Column="1" - IsEnabled="{x:Bind ViewModel.NewSessionLocalPathDialogOpen, Converter={StaticResource InverseBoolConverter}, Mode=OneWay}" + IsEnabled="{x:Bind ViewModel.CanOpenLocalPath, Mode=OneWay}" Command="{x:Bind ViewModel.OpenLocalPathSelectDialogCommand}" VerticalAlignment="Stretch"> @@ -350,6 +351,7 @@ Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Stretch" + IsEnabled="{x:Bind ViewModel.NewSessionRemotePathDialogEnabled, Mode=OneWay}" Text="{x:Bind ViewModel.NewSessionRemotePath, Mode=TwoWay}" /> <Button From 5afd747c52bba2a3deef27507b5ca95976af6d31 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:01:00 +0200 Subject: [PATCH 5/7] feat: removed external dependencies (VPN, mutagen) connection management (#111) Closes: #68 --------- Co-authored-by: Dean Sheather <dean@deansheather.com> --- App/App.xaml.cs | 28 ++++++++++--------- App/Services/RpcController.cs | 2 +- App/ViewModels/FileSyncListViewModel.cs | 21 +++++++++++--- .../TrayWindowDisconnectedViewModel.cs | 17 +++++++++-- .../Pages/TrayWindowDisconnectedPage.xaml | 13 +++++++++ App/Views/TrayWindow.xaml.cs | 3 +- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/App/App.xaml.cs b/App/App.xaml.cs index 5b82ced..06ab676 100644 --- a/App/App.xaml.cs +++ b/App/App.xaml.cs @@ -165,20 +165,22 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) }, CancellationToken.None); // Initialize file sync. - var syncSessionCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var syncSessionController = _services.GetRequiredService<ISyncSessionController>(); - _ = syncSessionController.RefreshState(syncSessionCts.Token).ContinueWith(t => - { - if (t.IsCanceled || t.Exception != null) - { - _logger.LogError(t.Exception, "failed to refresh sync state (canceled = {canceled})", t.IsCanceled); -#if DEBUG - Debugger.Break(); -#endif - } + // We're adding a 5s delay here to avoid race conditions when loading the mutagen binary. - syncSessionCts.Dispose(); - }, CancellationToken.None); + _ = Task.Delay(5000).ContinueWith((_) => + { + var syncSessionCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var syncSessionController = _services.GetRequiredService<ISyncSessionController>(); + syncSessionController.RefreshState(syncSessionCts.Token).ContinueWith( + t => + { + if (t.IsCanceled || t.Exception != null) + { + _logger.LogError(t.Exception, "failed to refresh sync state (canceled = {canceled})", t.IsCanceled); + } + syncSessionCts.Dispose(); + }, CancellationToken.None); + }); // Prevent the TrayWindow from closing, just hide it. var trayWindow = _services.GetRequiredService<TrayWindow>(); diff --git a/App/Services/RpcController.cs b/App/Services/RpcController.cs index 7beff66..7461ba8 100644 --- a/App/Services/RpcController.cs +++ b/App/Services/RpcController.cs @@ -313,7 +313,7 @@ private void SpeakerOnError(Exception e) Debug.WriteLine($"Error: {e}"); try { - Reconnect(CancellationToken.None).Wait(); + using var _ = Reconnect(CancellationToken.None); } catch { diff --git a/App/ViewModels/FileSyncListViewModel.cs b/App/ViewModels/FileSyncListViewModel.cs index 4777183..cb84f56 100644 --- a/App/ViewModels/FileSyncListViewModel.cs +++ b/App/ViewModels/FileSyncListViewModel.cs @@ -143,9 +143,9 @@ public void Initialize(Window window, DispatcherQueue dispatcherQueue) var rpcModel = _rpcController.GetState(); var credentialModel = _credentialManager.GetCachedCredentials(); - MaybeSetUnavailableMessage(rpcModel, credentialModel); var syncSessionState = _syncSessionController.GetState(); UpdateSyncSessionState(syncSessionState); + MaybeSetUnavailableMessage(rpcModel, credentialModel, syncSessionState); } private void RpcControllerStateChanged(object? sender, RpcModel rpcModel) @@ -159,7 +159,8 @@ private void RpcControllerStateChanged(object? sender, RpcModel rpcModel) } var credentialModel = _credentialManager.GetCachedCredentials(); - MaybeSetUnavailableMessage(rpcModel, credentialModel); + var syncSessionState = _syncSessionController.GetState(); + MaybeSetUnavailableMessage(rpcModel, credentialModel, syncSessionState); } private void CredentialManagerCredentialsChanged(object? sender, CredentialModel credentialModel) @@ -173,7 +174,8 @@ private void CredentialManagerCredentialsChanged(object? sender, CredentialModel } var rpcModel = _rpcController.GetState(); - MaybeSetUnavailableMessage(rpcModel, credentialModel); + var syncSessionState = _syncSessionController.GetState(); + MaybeSetUnavailableMessage(rpcModel, credentialModel, syncSessionState); } private void SyncSessionStateChanged(object? sender, SyncSessionControllerStateModel syncSessionState) @@ -189,7 +191,7 @@ private void SyncSessionStateChanged(object? sender, SyncSessionControllerStateM UpdateSyncSessionState(syncSessionState); } - private void MaybeSetUnavailableMessage(RpcModel rpcModel, CredentialModel credentialModel) + private void MaybeSetUnavailableMessage(RpcModel rpcModel, CredentialModel credentialModel, SyncSessionControllerStateModel syncSessionState) { var oldMessage = UnavailableMessage; if (rpcModel.RpcLifecycle != RpcLifecycle.Connected) @@ -205,6 +207,10 @@ private void MaybeSetUnavailableMessage(RpcModel rpcModel, CredentialModel crede { UnavailableMessage = "Please start Coder Connect from the tray window to access file sync."; } + else if (syncSessionState.Lifecycle == SyncSessionControllerLifecycle.Uninitialized) + { + UnavailableMessage = "Sync session controller is not initialized. Please wait..."; + } else { UnavailableMessage = null; @@ -219,6 +225,13 @@ private void MaybeSetUnavailableMessage(RpcModel rpcModel, CredentialModel crede private void UpdateSyncSessionState(SyncSessionControllerStateModel syncSessionState) { + // This should never happen. + if (syncSessionState == null) + return; + if (syncSessionState.Lifecycle == SyncSessionControllerLifecycle.Uninitialized) + { + MaybeSetUnavailableMessage(_rpcController.GetState(), _credentialManager.GetCachedCredentials(), syncSessionState); + } Error = syncSessionState.DaemonError; Sessions = syncSessionState.SyncSessions.Select(s => new SyncSessionViewModel(this, s)).ToList(); } diff --git a/App/ViewModels/TrayWindowDisconnectedViewModel.cs b/App/ViewModels/TrayWindowDisconnectedViewModel.cs index 5fe16a2..ce6582c 100644 --- a/App/ViewModels/TrayWindowDisconnectedViewModel.cs +++ b/App/ViewModels/TrayWindowDisconnectedViewModel.cs @@ -1,8 +1,9 @@ -using System.Threading.Tasks; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using System; +using System.Threading.Tasks; namespace Coder.Desktop.App.ViewModels; @@ -11,6 +12,8 @@ public partial class TrayWindowDisconnectedViewModel : ObservableObject private readonly IRpcController _rpcController; [ObservableProperty] public partial bool ReconnectButtonEnabled { get; set; } = true; + [ObservableProperty] public partial string ErrorMessage { get; set; } = string.Empty; + [ObservableProperty] public partial bool ReconnectFailed { get; set; } = false; public TrayWindowDisconnectedViewModel(IRpcController rpcController) { @@ -26,6 +29,16 @@ private void UpdateFromRpcModel(RpcModel rpcModel) [RelayCommand] public async Task Reconnect() { - await _rpcController.Reconnect(); + try + { + ReconnectFailed = false; + ErrorMessage = string.Empty; + await _rpcController.Reconnect(); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + ReconnectFailed = true; + } } } diff --git a/App/Views/Pages/TrayWindowDisconnectedPage.xaml b/App/Views/Pages/TrayWindowDisconnectedPage.xaml index 6675f8d..936c65f 100644 --- a/App/Views/Pages/TrayWindowDisconnectedPage.xaml +++ b/App/Views/Pages/TrayWindowDisconnectedPage.xaml @@ -30,6 +30,19 @@ <controls:HorizontalRule /> + <TextBlock FontWeight="semibold" + TextWrapping="Wrap" + Foreground="Red" + Visibility="{x:Bind ViewModel.ReconnectFailed, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" + Text="Reconnect failed"/> + + <TextBlock + TextWrapping="Wrap" + Margin="0,0,0,10" + Foreground="Red" + Visibility="{x:Bind ViewModel.ReconnectFailed, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" + Text="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}" /> + <HyperlinkButton HorizontalContentAlignment="Left" HorizontalAlignment="Stretch" diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index ef55095..7ecd75c 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -122,8 +122,7 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel, SyncSessionControllerStateModel syncSessionModel) { - if (credentialModel.State == CredentialState.Unknown || - syncSessionModel.Lifecycle == SyncSessionControllerLifecycle.Uninitialized) + if (credentialModel.State == CredentialState.Unknown) { SetRootFrame(_loadingPage); return; From 23f78193b4e4b8cfe57733882ae31a7e5d82492e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:02:52 +0200 Subject: [PATCH 6/7] feat: added icon to installer's bootstrap application (#112) Closes: #38 WiX 5 introduced a bug that stopped respecting the icon file on the theme's Window. This is described in this issue: https://github.com/wixtoolset/issues/issues/8104 This PR introduces: - an additional option in our `build-bootstrapper` to allow optional setting of the `theme xml`, - adds a copy of the `RtfLargeTheme.xml` from WiX `5.0.2` with the added attribute of `IconFile`  --- Installer/Program.cs | 19 ++++ scripts/Publish.ps1 | 1 + scripts/files/RtfThemeLarge.xml | 152 ++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 scripts/files/RtfThemeLarge.xml diff --git a/Installer/Program.cs b/Installer/Program.cs index 10a09a7..f02f9b2 100644 --- a/Installer/Program.cs +++ b/Installer/Program.cs @@ -119,6 +119,9 @@ public class BootstrapperOptions : SharedOptions [Option('w', "windows-app-sdk-path", Required = true, HelpText = "Path to the Windows App Sdk package to embed")] public string WindowsAppSdkPath { get; set; } + [Option('t', "theme-xml-path", Required = false, HelpText = "Path to the theme .xml file to use for the installer")] + public string ThemeXmlPath { get; set; } + public new void Validate() { base.Validate(); @@ -130,6 +133,8 @@ public class BootstrapperOptions : SharedOptions if (!SystemFile.Exists(WindowsAppSdkPath)) throw new ArgumentException($"Windows App Sdk package not found at '{WindowsAppSdkPath}'", nameof(WindowsAppSdkPath)); + if (ThemeXmlPath != null && !SystemFile.Exists(ThemeXmlPath)) + throw new ArgumentException($"Theme XML file not found at '{ThemeXmlPath}'", nameof(ThemeXmlPath)); } } @@ -415,6 +420,20 @@ private static int BuildBundle(BootstrapperOptions opts) bundle.Application.LicensePath = opts.LicenseFile; bundle.Application.LogoFile = opts.LogoPng; + if (opts.ThemeXmlPath != null) + { + bundle.Application.ThemeFile = opts.ThemeXmlPath; + bundle.Application.Payloads = + [ + new ExePackagePayload + { + Name = "icon.ico", + SourceFile = opts.IconFile, + Compressed = true, + }, + ]; + } + // Set the default install folder, which will eventually be passed into // the MSI. bundle.Variables = diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1 index 4390dfa..ee86980 100644 --- a/scripts/Publish.ps1 +++ b/scripts/Publish.ps1 @@ -189,6 +189,7 @@ $windowsAppSdkPath = Join-Path $scriptRoot "files\windows-app-sdk-$($arch).exe" --icon-file "App\coder.ico" ` --msi-path $msiOutputPath ` --windows-app-sdk-path $windowsAppSdkPath ` + --theme-xml-path "scripts\files\RtfThemeLarge.xml" ` --logo-png "scripts\files\logo.png" if ($LASTEXITCODE -ne 0) { throw "Failed to build bootstrapper" } diff --git a/scripts/files/RtfThemeLarge.xml b/scripts/files/RtfThemeLarge.xml new file mode 100644 index 0000000..a704810 --- /dev/null +++ b/scripts/files/RtfThemeLarge.xml @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. --> +<!-- +Copyright (c) .NET Foundation and contributors. +This software is released under the Microsoft Reciprocal License (MS-RL) (the "License"); you may not use the software except in compliance with the License. + +The text of the Microsoft Reciprocal License (MS-RL) can be found online at: + http://opensource.org/licenses/ms-rl + + +Microsoft Reciprocal License (MS-RL) + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software. + +1. Definitions + The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law. + A "contribution" is the original software, or any additions or changes to the software. + A "contributor" is any person that distributes its contribution under this license. + "Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights + (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations + (A) Reciprocal Grants- For any file you distribute that contains code from the software (in source code or binary format), you must provide recipients the source code to that file along with a copy of this license, which license will govern that file. You may license other files that are entirely your own work and do not contain code from the software under any terms you choose. + (B) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + (C) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. + (D) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. + (E) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. + (F) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. +--> +<!-- Downloaded from https://github.com/wixtoolset/wix/blob/v5.0.2/src/ext/Bal/stdbas/Resources/RtfLargeTheme.xml + This needed to be modified, because WiX 5 introduced an issue that doesn't fill IconFile attribute on the main Window + in the theme. WiX issue: https://github.com/wixtoolset/issues/issues/8104 +--> + +<Theme xmlns="http://wixtoolset.org/schemas/v4/thmutil"> + <Font Id="0" Height="-12" Weight="500" Foreground="windowtext" Background="window">Segoe UI</Font> + <Font Id="1" Height="-24" Weight="500" Foreground="windowtext">Segoe UI</Font> + <Font Id="2" Height="-22" Weight="500" Foreground="graytext">Segoe UI</Font> + <Font Id="3" Height="-12" Weight="500" Foreground="windowtext" Background="window">Segoe UI</Font> + + <Window Width="500" Height="390" HexStyle="100a0000" FontId="0" Caption="#(loc.Caption)" IconFile="icon.ico"> + <ImageControl X="11" Y="11" Width="64" Height="64" ImageFile="logo.png" Visible="yes"/> + <Label X="80" Y="11" Width="-11" Height="64" FontId="1" Visible="yes" DisablePrefix="yes">#(loc.Title)</Label> + + <Page Name="Help"> + <Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Label> + <Label X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Label> + <Button Name="HelpCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0"> + <Text>#(loc.HelpCloseButton)</Text> + <CloseWindowAction /> + </Button> + </Page> + <Page Name="Loading"> + <Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes" Visible="no" Name="CheckingForUpdatesLabel" /> + </Page> + <Page Name="Install"> + <Label X="11" Y="80" Width="-11" Height="-70" TabStop="no" FontId="2" HexStyle="800000" DisablePrefix="yes" /> + <Richedit Name="EulaRichedit" X="12" Y="81" Width="-12" Height="-71" TabStop="yes" FontId="0" /> + <Label Name="InstallVersion" X="11" Y="-41" Width="210" Height="17" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBAShowVersion">#(loc.InstallVersion)</Label> + <Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox> + <Button Name="InstallUpdateButton" X="11" Y="-11" Width="200" Height="23" TabStop="yes" FontId="0" EnableCondition="WixStdBAUpdateAvailable" HideWhenDisabled="yes">#(loc.UpdateButton)</Button> + <Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" VisibleCondition="NOT WixStdBASuppressOptionsUI"> + <Text>#(loc.InstallOptionsButton)</Text> + <ChangePageAction Page="Options" /> + </Button> + <Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button> + <Button Name="InstallCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0"> + <Text>#(loc.InstallCancelButton)</Text> + <CloseWindowAction /> + </Button> + </Page> + <Page Name="Options"> + <Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Label> + <Label X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Label> + <Editbox Name="InstallFolder" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" /> + <Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3"> + <Text>#(loc.OptionsBrowseButton)</Text> + <BrowseDirectoryAction VariableName="InstallFolder" /> + </Button> + <Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0"> + <Text>#(loc.OptionsOkButton)</Text> + <ChangePageAction Page="Install" /> + </Button> + <Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0"> + <Text>#(loc.OptionsCancelButton)</Text> + <ChangePageAction Page="Install" Cancel="yes" /> + </Button> + </Page> + <Page Name="Progress"> + <Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Label> + <Label X="11" Y="121" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Label> + <Label Name="OverallProgressPackageText" X="85" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Label> + <Progressbar Name="OverallCalculatedProgressbar" X="11" Y="143" Width="-11" Height="15" /> + <Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button> + </Page> + <Page Name="Modify"> + <Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Label> + <Button Name="ModifyUpdateButton" X="11" Y="-11" Width="200" Height="23" TabStop="yes" FontId="0" EnableCondition="WixStdBAUpdateAvailable" HideWhenDisabled="yes">#(loc.UpdateButton)</Button> + <Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.ModifyRepairButton)</Button> + <Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button> + <Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0"> + <Text>#(loc.ModifyCancelButton)</Text> + <CloseWindowAction /> + </Button> + </Page> + <Page Name="Success"> + <Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes"> + <Text>#(loc.SuccessHeader)</Text> + <Text Condition="WixBundleAction = 2">#(loc.SuccessLayoutHeader)</Text> + <Text Condition="WixBundleAction = 3">#(loc.SuccessUnsafeUninstallHeader)</Text> + <Text Condition="WixBundleAction = 4">#(loc.SuccessUninstallHeader)</Text> + <Text Condition="WixBundleAction = 5">#(loc.SuccessCacheHeader)</Text> + <Text Condition="WixBundleAction = 6">#(loc.SuccessInstallHeader)</Text> + <Text Condition="WixBundleAction = 7">#(loc.SuccessModifyHeader)</Text> + <Text Condition="WixBundleAction = 8">#(loc.SuccessRepairHeader)</Text> + </Label> + <Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button> + <Label X="-11" Y="-51" Width="400" Height="34" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBARestartRequired"> + <Text>#(loc.SuccessRestartText)</Text> + <Text Condition="WixBundleAction = 3">#(loc.SuccessUninstallRestartText)</Text> + </Label> + <Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button> + <Button Name="SuccessCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0"> + <Text>#(loc.SuccessCloseButton)</Text> + <CloseWindowAction /> + </Button> + </Page> + <Page Name="Failure"> + <Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes"> + <Text>#(loc.FailureHeader)</Text> + <Text Condition="WixBundleAction = 2">#(loc.FailureLayoutHeader)</Text> + <Text Condition="WixBundleAction = 3">#(loc.FailureUnsafeUninstallHeader)</Text> + <Text Condition="WixBundleAction = 4">#(loc.FailureUninstallHeader)</Text> + <Text Condition="WixBundleAction = 5">#(loc.FailureCacheHeader)</Text> + <Text Condition="WixBundleAction = 6">#(loc.FailureInstallHeader)</Text> + <Text Condition="WixBundleAction = 7">#(loc.FailureModifyHeader)</Text> + <Text Condition="WixBundleAction = 8">#(loc.FailureRepairHeader)</Text> + </Label> + <Hypertext Name="FailureLogFileLink" X="11" Y="121" Width="-11" Height="42" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext> + <Hypertext Name="FailureMessageText" X="22" Y="163" Width="-11" Height="51" FontId="3" TabStop="yes" HideWhenDisabled="yes" /> + <Label Name="FailureRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Label> + <Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button> + <Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0"> + <Text>#(loc.FailureCloseButton)</Text> + <CloseWindowAction /> + </Button> + </Page> + </Window> +</Theme> From 0b27367bb4cc7a809877efab6cd3d4f0483d415b Mon Sep 17 00:00:00 2001 From: Dean Sheather <dean@deansheather.com> Date: Tue, 3 Jun 2025 18:12:18 +1000 Subject: [PATCH 7/7] fix: avoid build errors in CI (#115) --- .github/workflows/ci.yaml | 4 ++-- scripts/Publish.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 459579c..ac57947 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: cache: true cache-dependency-path: '**/packages.lock.json' - name: dotnet restore - run: dotnet restore --locked-mode + run: dotnet restore --locked-mode /p:BuildWithNetFrameworkHostedCompiler=true - name: dotnet format run: dotnet format --verify-no-changes --no-restore @@ -75,7 +75,7 @@ jobs: cache: true cache-dependency-path: '**/packages.lock.json' - name: dotnet restore - run: dotnet restore --locked-mode + run: dotnet restore --locked-mode /p:BuildWithNetFrameworkHostedCompiler=true # This doesn't call `dotnet publish` on the entire solution, just the # projects we care about building. Doing a full publish includes test # libraries and stuff which is pointless. diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1 index ee86980..6c0c101 100644 --- a/scripts/Publish.ps1 +++ b/scripts/Publish.ps1 @@ -113,7 +113,7 @@ if (Test-Path $buildPath) { New-Item -ItemType Directory -Path $buildPath -Force # Build in release mode -& dotnet.exe restore +& dotnet.exe restore /p:BuildWithNetFrameworkHostedCompiler=true if ($LASTEXITCODE -ne 0) { throw "Failed to dotnet restore" } $servicePublishDir = Join-Path $buildPath "service" & dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir /p:Version=$version