XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEToolBox

支持为工具配置管理员权限启动

新增 ToolLaunchPreferenceService,支持每个工具单独配置是否以管理员权限启动,配置持久化至 AppCacheProfile。ToolCardViewModel 增加 RunAsAdministrator 属性及 UAC 图标,UI 支持显示管理员徽标。新增 ToolConfigurationPopupPage,允许用户设置管理员启动选项。调整 ToolProjectRunService 启动流程,支持根据配置请求 UAC 权限。新增 ToolRuntimeProcessStartInfoFactory 及相关测试。版本号升级至 1.1.4。

090bc21
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

13 个文件 +446 -24
Added XFEToolBox.Client.Wpf.Test/ToolRuntimeProcessStartInfoFactoryTests.cs +47 -0
@@ -0,0 +1,47 @@
1 using System.IO;
2 using XFEToolBox.Client.Utilities;
3
4 namespace XFEToolBox.Client.Wpf.Test;
5
6 public static class ToolRuntimeProcessStartInfoFactoryTests
7 {
8 [Test]
9 public static void AdministratorLaunchUsesShellRunAsAndTheCompiledAppHost()
10 {
11 var executable = Path.GetFullPath(Path.Combine("runtime", "Tool.exe"));
12 var workingDirectory = Path.GetFullPath("workspace");
13
14 var startInfo = ToolRuntimeProcessStartInfoFactory.Create(
15 executable,
16 workingDirectory,
17 runAsAdministrator: true);
18
19 Ensure(startInfo.FileName == executable, "管理员启动没有直接使用编译后的工具 AppHost。 ");
20 Ensure(startInfo.WorkingDirectory == workingDirectory, "管理员启动丢失了工具工作目录。 ");
21 Ensure(startInfo.UseShellExecute, "管理员启动没有启用 Windows Shell。 ");
22 Ensure(startInfo.Verb == "runas", "管理员启动没有请求 UAC 提权。 ");
23 Ensure(!startInfo.RedirectStandardOutput && !startInfo.RedirectStandardError,
24 "Shell 提权模式错误地配置了标准流重定向。 ");
25 }
26
27 [Test]
28 public static void StandardLaunchKeepsStartupDiagnosticsEnabled()
29 {
30 var startInfo = ToolRuntimeProcessStartInfoFactory.Create(
31 Path.Combine("runtime", "Tool.exe"),
32 "workspace",
33 runAsAdministrator: false);
34
35 Ensure(!startInfo.UseShellExecute, "普通启动不应经过 Windows Shell。 ");
36 Ensure(string.IsNullOrEmpty(startInfo.Verb), "普通启动不应设置 runas。 ");
37 Ensure(startInfo.CreateNoWindow, "普通工具宿主不应创建控制台窗口。 ");
38 Ensure(startInfo.RedirectStandardOutput && startInfo.RedirectStandardError,
39 "普通启动应保留启动阶段错误诊断。 ");
40 }
41
42 private static void Ensure(bool condition, string message)
43 {
44 if (!condition)
45 throw new InvalidOperationException(message);
46 }
47 }
Modified XFEToolBox.Client.Wpf.Test/XFEToolBox.Client.Wpf.Test.csproj +1 -0
@@ -24,6 +24,7 @@
24 24 <Compile Include="..\XFEToolBox.Client.Installer\Utilities\InstallationService.cs" Link="Installer\InstallationService.cs" />
25 25 <Compile Include="..\XFEToolBox.Client.Installer\Utilities\ZipHelper.cs" Link="Installer\ZipHelper.cs" />
26 26 <Compile Include="..\XFEToolBox\Utilities\WebImageSourceLoader.cs" Link="Utilities\WebImageSourceLoader.cs" />
27 <Compile Include="..\XFEToolBox\Utilities\ToolRuntimeProcessStartInfoFactory.cs" Link="Utilities\ToolRuntimeProcessStartInfoFactory.cs" />
27 28 <Compile Include="..\XFEToolBox\Models\RecentUsageEntry.cs" Link="Models\RecentUsageEntry.cs" />
28 29 <Compile Include="..\XFEToolBox\Utilities\RecentUsageIconCache.cs" Link="Utilities\RecentUsageIconCache.cs" />
29 30 </ItemGroup>
Modified XFEToolBox/Profiles/CacheProfiles/AppCacheProfile.cs +6 -0
@@ -20,5 +20,11 @@ public partial class AppCacheProfile : XFEProfile
20 20 [ProfileProperty]
21 21 private string softwareCatalogJson = "";
22 22
23 /// <summary>
24 /// 各工具的宿主启动配置,例如是否在首次启动时请求管理员权限。
25 /// </summary>
26 [ProfileProperty]
27 private string toolLaunchPreferencesJson = "";
28
23 29 public AppCacheProfile() => ProfilePath = @$"{AppPath.CacheProfile}\{typeof(AppCacheProfile)}.xprofile";
24 30 }
Added XFEToolBox/Utilities/ElevationShieldIcon.cs +64 -0
@@ -0,0 +1,64 @@
1 using System.Runtime.InteropServices;
2 using System.Windows;
3 using System.Windows.Interop;
4 using System.Windows.Media;
5 using System.Windows.Media.Imaging;
6
7 namespace XFEToolBox.Client.Utilities;
8
9 internal static class ElevationShieldIcon
10 {
11 private const uint ShieldStockIconId = 77;
12 private const uint IconFlag = 0x00000100;
13 private const uint SmallIconFlag = 0x00000001;
14 private static readonly Lazy<ImageSource> CachedSource = new(CreateImageSourceCore);
15
16 public static ImageSource Source => CachedSource.Value;
17
18 private static ImageSource CreateImageSourceCore()
19 {
20 var info = new StockIconInfo { Size = (uint)Marshal.SizeOf<StockIconInfo>() };
21 if (SHGetStockIconInfo(ShieldStockIconId, IconFlag | SmallIconFlag, ref info) == 0 && info.IconHandle != IntPtr.Zero)
22 {
23 try
24 {
25 var source = Imaging.CreateBitmapSourceFromHIcon(
26 info.IconHandle,
27 Int32Rect.Empty,
28 BitmapSizeOptions.FromWidthAndHeight(20, 20));
29 source.Freeze();
30 return source;
31 }
32 finally
33 {
34 _ = DestroyIcon(info.IconHandle);
35 }
36 }
37
38 var fallback = new DrawingImage(new GeometryDrawing(
39 new SolidColorBrush(Color.FromRgb(52, 116, 194)),
40 null,
41 Geometry.Parse("M10,1 L18,4 V9 C18,14 14.7,18 10,20 C5.3,18 2,14 2,9 V4 Z")));
42 fallback.Freeze();
43 return fallback;
44 }
45
46 [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
47 private static extern int SHGetStockIconInfo(uint stockIconId, uint flags, ref StockIconInfo stockIconInfo);
48
49 [DllImport("user32.dll")]
50 [return: MarshalAs(UnmanagedType.Bool)]
51 private static extern bool DestroyIcon(IntPtr iconHandle);
52
53 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
54 private struct StockIconInfo
55 {
56 public uint Size;
57 public IntPtr IconHandle;
58 public int SystemImageIndex;
59 public int IconIndex;
60
61 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
62 public string? Path;
63 }
64 }
Added XFEToolBox/Utilities/ToolLaunchPreferenceService.cs +48 -0
@@ -0,0 +1,48 @@
1 using System.Text.Json;
2 using XFEToolBox.Client.Profiles.CacheProfiles;
3
4 namespace XFEToolBox.Client.Utilities;
5
6 internal static class ToolLaunchPreferenceService
7 {
8 private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
9
10 public static bool GetRunAsAdministrator(string toolId)
11 {
12 if (string.IsNullOrWhiteSpace(toolId)) return false;
13 return ReadPreferences().TryGetValue(toolId, out var preference) && preference.RunAsAdministrator;
14 }
15
16 public static void SetRunAsAdministrator(string toolId, bool runAsAdministrator)
17 {
18 ArgumentException.ThrowIfNullOrWhiteSpace(toolId);
19 var preferences = ReadPreferences();
20 if (runAsAdministrator)
21 preferences[toolId] = new ToolLaunchPreference(true);
22 else
23 preferences.Remove(toolId);
24
25 AppCacheProfile.ToolLaunchPreferencesJson = JsonSerializer.Serialize(preferences, JsonOptions);
26 }
27
28 private static Dictionary<string, ToolLaunchPreference> ReadPreferences()
29 {
30 try
31 {
32 var json = AppCacheProfile.ToolLaunchPreferencesJson;
33 if (string.IsNullOrWhiteSpace(json))
34 return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
35
36 var stored = JsonSerializer.Deserialize<Dictionary<string, ToolLaunchPreference>>(json, JsonOptions);
37 return stored is null
38 ? new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase)
39 : new Dictionary<string, ToolLaunchPreference>(stored, StringComparer.OrdinalIgnoreCase);
40 }
41 catch (JsonException)
42 {
43 return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
44 }
45 }
46
47 private sealed record ToolLaunchPreference(bool RunAsAdministrator);
48 }
Modified XFEToolBox/Utilities/ToolProjectRunService.cs +89 -14
@@ -1,3 +1,4 @@
1 using System.ComponentModel;
1 2 using System.Diagnostics;
2 3 using System.IO;
3 4 using System.IO.Compression;
@@ -16,6 +17,7 @@ internal static class ToolProjectRunService
16 17 private const int MaximumPackageEntryCount = 512;
17 18 private const long MaximumExtractedPackageBytes = 128L * 1024 * 1024;
18 19 private static readonly TimeSpan RuntimeStartupObservationWindow = TimeSpan.FromSeconds(2);
20 private static readonly TimeSpan RuntimeRelaunchDetectionWindow = TimeSpan.FromSeconds(1.5);
19 21 private static readonly JsonSerializerOptions ManifestJsonOptions = new(JsonSerializerDefaults.Web);
20 22
21 23 public static async Task<ToolRunResult> BuildAsync(
@@ -29,6 +31,7 @@ internal static class ToolProjectRunService
29 31 $"{manifest.Name} · 生成验证",
30 32 temporaryWorkspaceRoot: null,
31 33 launchAfterBuild: false,
34 runAsAdministrator: false,
32 35 cancellationToken);
33 36 }
34 37
@@ -43,6 +46,7 @@ internal static class ToolProjectRunService
43 46 $"{manifest.Name} · 运行预览",
44 47 temporaryWorkspaceRoot: null,
45 48 launchAfterBuild: true,
49 runAsAdministrator: false,
46 50 cancellationToken);
47 51 }
48 52
@@ -51,6 +55,7 @@ internal static class ToolProjectRunService
51 55 string expectedToolId,
52 56 string expectedVersion,
53 57 string expectedSha256,
58 bool runAsAdministrator = false,
54 59 CancellationToken cancellationToken = default)
55 60 {
56 61 var packageWorkspaceRoot = Path.Combine(
@@ -75,6 +80,7 @@ internal static class ToolProjectRunService
75 80 manifest.Name,
76 81 packageWorkspaceRoot,
77 82 launchAfterBuild: true,
83 runAsAdministrator,
78 84 cancellationToken);
79 85 }
80 86 catch (Exception exception)
@@ -90,6 +96,7 @@ internal static class ToolProjectRunService
90 96 string windowTitle,
91 97 string? temporaryWorkspaceRoot,
92 98 bool launchAfterBuild,
99 bool runAsAdministrator,
93 100 CancellationToken cancellationToken)
94 101 {
95 102 var runtimeRoot = Path.Combine(Path.GetTempPath(), "XFEToolBox", "CodeStudioRuns", Guid.NewGuid().ToString("N"));
@@ -155,19 +162,23 @@ internal static class ToolProjectRunService
155 162 return new ToolRunResult(true, "工具工程已成功生成。", null);
156 163 }
157 164
158 var runInfo = new ProcessStartInfo("dotnet")
159 {
160 WorkingDirectory = workspaceRoot,
161 UseShellExecute = false,
162 CreateNoWindow = true,
163 RedirectStandardOutput = true,
164 RedirectStandardError = true
165 };
166 runInfo.ArgumentList.Add(runtimeAssembly);
165 var runtimeExecutable = Path.Combine(outputRoot, assemblyName + ".exe");
166 if (!File.Exists(runtimeExecutable))
167 throw new FileNotFoundException("编译成功,但没有找到工具运行程序。", runtimeExecutable);
168
169 var runInfo = ToolRuntimeProcessStartInfoFactory.Create(
170 runtimeExecutable,
171 workspaceRoot,
172 runAsAdministrator);
167 173 var runtimeProcess = Process.Start(runInfo)
168 174 ?? throw new InvalidOperationException("工具运行进程启动失败。");
169 var runtimeStandardOutputTask = runtimeProcess.StandardOutput.ReadToEndAsync();
170 var runtimeStandardErrorTask = runtimeProcess.StandardError.ReadToEndAsync();
175 var runtimeStandardOutputTask = runInfo.RedirectStandardOutput
176 ? runtimeProcess.StandardOutput.ReadToEndAsync()
177 : Task.FromResult(string.Empty);
178 var runtimeStandardErrorTask = runInfo.RedirectStandardError
179 ? runtimeProcess.StandardError.ReadToEndAsync()
180 : Task.FromResult(string.Empty);
181 var runtimeProcessName = Path.GetFileNameWithoutExtension(runtimeExecutable);
171 182 var exitTask = runtimeProcess.WaitForExitAsync(cancellationToken);
172 183 var startupObservationTask = Task.Delay(RuntimeStartupObservationWindow, cancellationToken);
173 184 if (await Task.WhenAny(exitTask, startupObservationTask) == exitTask)
@@ -175,7 +186,20 @@ internal static class ToolProjectRunService
175 186 await exitTask;
176 187 var runtimeOutput = (await runtimeStandardOutputTask) + Environment.NewLine + (await runtimeStandardErrorTask);
177 188 var exitCode = runtimeProcess.ExitCode;
189 var replacementProcess = await FindReplacementProcessAsync(runtimeProcessName, runtimeProcess.Id);
178 190 runtimeProcess.Dispose();
191 if (replacementProcess is not null)
192 {
193 _ = CleanupAfterExitAsync(
194 replacementProcess,
195 runtimeRoot,
196 temporaryWorkspaceRoot,
197 Task.FromResult(string.Empty),
198 Task.FromResult(string.Empty),
199 runtimeProcessName);
200 return new ToolRunResult(true, "工具已切换到新的权限进程。", replacementProcess.Id);
201 }
202
179 203 TryDeleteDirectory(runtimeRoot);
180 204 if (temporaryWorkspaceRoot is not null)
181 205 TryDeleteDirectory(temporaryWorkspaceRoot);
@@ -188,9 +212,17 @@ internal static class ToolProjectRunService
188 212 runtimeRoot,
189 213 temporaryWorkspaceRoot,
190 214 runtimeStandardOutputTask,
191 runtimeStandardErrorTask);
215 runtimeStandardErrorTask,
216 runtimeProcessName);
192 217 return new ToolRunResult(true, "工具已完成编译并在独立窗口中运行。", runtimeProcess.Id);
193 218 }
219 catch (Win32Exception exception) when (exception.NativeErrorCode == 1223)
220 {
221 TryDeleteDirectory(runtimeRoot);
222 if (temporaryWorkspaceRoot is not null)
223 TryDeleteDirectory(temporaryWorkspaceRoot);
224 return new ToolRunResult(false, "已取消管理员权限请求,工具没有启动。", null);
225 }
194 226 catch (Exception exception)
195 227 {
196 228 TryDeleteDirectory(runtimeRoot);
@@ -216,7 +248,9 @@ internal static class ToolProjectRunService
216 248 <PropertyGroup>
217 249 <OutputType>WinExe</OutputType>
218 250 <TargetFramework>net10.0-windows</TargetFramework>
219 <UseWPF>true</UseWPF>
251 <UseWPF>true</UseWPF>
252 <UseAppHost>true</UseAppHost>
253 <SelfContained>false</SelfContained>
220 254 <Nullable>enable</Nullable>
221 255 <ImplicitUsings>enable</ImplicitUsings>
222 256 <AssemblyName>{{assemblyName}}</AssemblyName>
@@ -636,13 +670,22 @@ internal static class ToolProjectRunService
636 670 string runtimeRoot,
637 671 string? temporaryWorkspaceRoot,
638 672 Task<string> standardOutputTask,
639 Task<string> standardErrorTask)
673 Task<string> standardErrorTask,
674 string runtimeProcessName)
640 675 {
676 var originalProcessId = process.Id;
641 677 try
642 678 {
643 679 await process.WaitForExitAsync();
644 680 await Task.WhenAll(standardOutputTask, standardErrorTask);
645 681 process.Dispose();
682
683 var replacementProcess = await FindReplacementProcessAsync(runtimeProcessName, originalProcessId);
684 if (replacementProcess is not null)
685 {
686 using (replacementProcess)
687 await replacementProcess.WaitForExitAsync();
688 }
646 689 }
647 690 catch
648 691 {
@@ -656,6 +699,38 @@ internal static class ToolProjectRunService
656 699 }
657 700 }
658 701
702 private static async Task<Process?> FindReplacementProcessAsync(string processName, int excludedProcessId)
703 {
704 var deadline = DateTime.UtcNow + RuntimeRelaunchDetectionWindow;
705 do
706 {
707 foreach (var candidate in Process.GetProcessesByName(processName))
708 {
709 if (candidate.Id == excludedProcessId)
710 {
711 candidate.Dispose();
712 continue;
713 }
714
715 try
716 {
717 if (!candidate.HasExited)
718 return candidate;
719 }
720 catch
721 {
722 // 进程可能在枚举后立即退出,继续等待真正的替代进程。
723 }
724
725 candidate.Dispose();
726 }
727
728 await Task.Delay(100);
729 } while (DateTime.UtcNow < deadline);
730
731 return null;
732 }
733
659 734 private static async Task VerifyPackageHashAsync(
660 735 string packagePath,
661 736 string expectedSha256,
Added XFEToolBox/Utilities/ToolRuntimeProcessStartInfoFactory.cs +34 -0
@@ -0,0 +1,34 @@
1 using System.Diagnostics;
2 using System.IO;
3
4 namespace XFEToolBox.Client.Utilities;
5
6 internal static class ToolRuntimeProcessStartInfoFactory
7 {
8 public static ProcessStartInfo Create(
9 string runtimeExecutable,
10 string workingDirectory,
11 bool runAsAdministrator)
12 {
13 ArgumentException.ThrowIfNullOrWhiteSpace(runtimeExecutable);
14 ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory);
15
16 var startInfo = new ProcessStartInfo(Path.GetFullPath(runtimeExecutable))
17 {
18 WorkingDirectory = Path.GetFullPath(workingDirectory)
19 };
20
21 if (runAsAdministrator)
22 {
23 startInfo.UseShellExecute = true;
24 startInfo.Verb = "runas";
25 return startInfo;
26 }
27
28 startInfo.UseShellExecute = false;
29 startInfo.CreateNoWindow = true;
30 startInfo.RedirectStandardOutput = true;
31 startInfo.RedirectStandardError = true;
32 return startInfo;
33 }
34 }
Modified XFEToolBox/ViewModel/Pages/ToolCardViewModel.cs +14 -1
@@ -4,7 +4,12 @@ using XFEToolBox.Core.Tools;
4 4
5 5 namespace XFEToolBox.Client.ViewModel.Pages;
6 6
7 public sealed class ToolCardViewModel(ToolPackageSummary package, ImageSource iconSource, bool isCached) : ObservableObject
7 public sealed class ToolCardViewModel(
8 ToolPackageSummary package,
9 ImageSource iconSource,
10 ImageSource uacIconSource,
11 bool isCached,
12 bool runAsAdministrator) : ObservableObject
8 13 {
9 14 private bool _isEnabled = true;
10 15 private string _cacheState = isCached ? "点击打开" : "获取并打开";
@@ -12,6 +17,7 @@ public sealed class ToolCardViewModel(ToolPackageSummary package, ImageSource ic
12 17 private bool _isDownloadIndeterminate;
13 18 private double _downloadProgress;
14 19 private string _downloadProgressText = string.Empty;
20 private bool _runAsAdministrator = runAsAdministrator;
15 21
16 22 public ToolPackageSummary Package { get; } = package;
17 23 public string Id => Package.Id;
@@ -21,6 +27,13 @@ public sealed class ToolCardViewModel(ToolPackageSummary package, ImageSource ic
21 27 public string Category => Package.Category;
22 28 public string LatestVersion => Package.LatestVersion;
23 29 public ImageSource IconSource { get; } = iconSource;
30 public ImageSource UacIconSource { get; } = uacIconSource;
31
32 public bool RunAsAdministrator
33 {
34 get => _runAsAdministrator;
35 set => SetProperty(ref _runAsAdministrator, value);
36 }
24 37
25 38 public bool IsEnabled
26 39 {
Added XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml +46 -0
@@ -0,0 +1,46 @@
1 <Page x:Class="XFEToolBox.Client.Views.Pages.Popups.ToolConfigurationPopupPage"
2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4 xmlns:controls="clr-namespace:XFEToolBox.WpfCore.Controls;assembly=XFEToolBox.WpfCore"
5 Background="White">
6 <Grid Margin="22,18,22,18">
7 <Grid.RowDefinitions>
8 <RowDefinition Height="Auto"/>
9 <RowDefinition Height="*"/>
10 <RowDefinition Height="Auto"/>
11 </Grid.RowDefinitions>
12
13 <StackPanel>
14 <TextBlock Text="启动权限" Foreground="#3F3F55" FontSize="16" FontWeight="Bold"/>
15 <TextBlock x:Name="ToolNameText" Foreground="#858598" FontSize="10" Margin="0,5,0,0"
16 TextTrimming="CharacterEllipsis"/>
17 </StackPanel>
18
19 <Border Grid.Row="1" Margin="0,15,0,14" Padding="15" Background="#F7F7FD"
20 BorderBrush="#E2E2F1" BorderThickness="1" CornerRadius="14">
21 <Grid>
22 <Grid.ColumnDefinitions>
23 <ColumnDefinition Width="42"/>
24 <ColumnDefinition Width="*"/>
25 </Grid.ColumnDefinitions>
26 <Border Width="34" Height="34" CornerRadius="10" Background="White"
27 BorderBrush="#E2E2F1" BorderThickness="1" VerticalAlignment="Top">
28 <Image x:Name="ShieldImage" Width="20" Height="20" Stretch="Uniform"/>
29 </Border>
30 <StackPanel Grid.Column="1" Margin="10,0,0,0">
31 <CheckBox x:Name="RunAsAdministratorCheckBox" Content="以管理员身份打开" FontWeight="SemiBold"/>
32 <TextBlock Margin="0,7,0,0" Foreground="#77778C" FontSize="9.5" TextWrapping="Wrap"
33 Text="启用后,工具箱会在工具首次启动时显示 UAC 确认,并直接创建管理员工具进程。编译和下载仍由普通权限完成。"/>
34 </StackPanel>
35 </Grid>
36 </Border>
37
38 <Grid Grid.Row="2">
39 <TextBlock Text="此设置仅应用于当前工具。" Foreground="#9999AA" FontSize="9.5" VerticalAlignment="Center"/>
40 <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
41 <Button Content="取消" MinWidth="82" Margin="0,0,8,0" Click="CancelButton_Click"/>
42 <Button Content="保存配置" MinWidth="96" controls:ButtonAssist.IsPrimary="True" Click="SaveButton_Click"/>
43 </StackPanel>
44 </Grid>
45 </Grid>
46 </Page>
Added XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml.cs +35 -0
@@ -0,0 +1,35 @@
1 using System.Windows;
2 using System.Windows.Controls;
3 using System.Windows.Media;
4 using XFEToolBox.Client.Model;
5 using XFEToolBox.Client.Views.Windows;
6
7 namespace XFEToolBox.Client.Views.Pages.Popups;
8
9 public partial class ToolConfigurationPopupPage : Page, IPopupPage
10 {
11 public ToolConfigurationPopupPage(string toolName, bool runAsAdministrator, ImageSource shieldIcon)
12 {
13 InitializeComponent();
14 ToolNameText.Text = toolName;
15 RunAsAdministratorCheckBox.IsChecked = runAsAdministrator;
16 ShieldImage.Source = shieldIcon;
17 }
18
19 public PopupWindow? PopupWindow { get; set; }
20
21 public bool RunAsAdministrator { get; private set; }
22
23 private async void SaveButton_Click(object sender, RoutedEventArgs e)
24 {
25 RunAsAdministrator = RunAsAdministratorCheckBox.IsChecked == true;
26 if (PopupWindow is not null)
27 await PopupWindow.CloseWithResultAsync(MessageBoxResult.OK);
28 }
29
30 private async void CancelButton_Click(object sender, RoutedEventArgs e)
31 {
32 if (PopupWindow is not null)
33 await PopupWindow.CloseWithResultAsync(MessageBoxResult.Cancel);
34 }
35 }
Modified XFEToolBox/Views/Pages/ToolBoxPage.xaml +17 -3
@@ -212,15 +212,26 @@
212 212 DataContext="{Binding PlacementTarget.CommandParameter, RelativeSource={RelativeSource Self}}">
213 213 <MenuItem Header="打开工具" Style="{StaticResource ToolCardMenuItemStyle}"
214 214 CommandParameter="{Binding}" Click="OpenToolMenuItem_Click"/>
215 <MenuItem Header="工具配置" Style="{StaticResource ToolCardMenuItemStyle}"
216 CommandParameter="{Binding}" Click="ToolConfigurationMenuItem_Click"/>
215 217 <MenuItem Header="清除该工具的数据" Style="{StaticResource ToolCardMenuItemStyle}"
216 218 CommandParameter="{Binding}" Click="ClearToolDataMenuItem_Click"/>
217 219 </ContextMenu>
218 220 </Button.ContextMenu>
219 221 <Grid ClipToBounds="True">
220 222 <Grid.ColumnDefinitions><ColumnDefinition Width="58"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
221 <Border Width="48" Height="48" CornerRadius="14" Background="#EEEEFB" VerticalAlignment="Top" HorizontalAlignment="Left">
222 <Image Source="{Binding IconSource}" Width="29" Height="29" Stretch="Uniform"/>
223 </Border>
223 <Grid Width="52" Height="52" VerticalAlignment="Top" HorizontalAlignment="Left">
224 <Border Width="48" Height="48" CornerRadius="14" Background="#EEEEFB"
225 VerticalAlignment="Top" HorizontalAlignment="Left">
226 <Image Source="{Binding IconSource}" Width="29" Height="29" Stretch="Uniform"/>
227 </Border>
228 <Border x:Name="UacBadge" Width="22" Height="22" Padding="2" Background="White"
229 BorderBrush="#D6D6E8" BorderThickness="1" CornerRadius="8"
230 HorizontalAlignment="Right" VerticalAlignment="Bottom" Visibility="Collapsed"
231 ToolTip="此工具将以管理员身份打开">
232 <Image Source="{Binding UacIconSource}" Stretch="Uniform"/>
233 </Border>
234 </Grid>
224 235 <Grid Grid.Column="1">
225 236 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
226 237 <DockPanel LastChildFill="True">
@@ -260,6 +271,9 @@
260 271 <Setter TargetName="NormalFooter" Property="Visibility" Value="Collapsed"/>
261 272 <Setter TargetName="DownloadFooter" Property="Visibility" Value="Visible"/>
262 273 </DataTrigger>
274 <DataTrigger Binding="{Binding RunAsAdministrator}" Value="True">
275 <Setter TargetName="UacBadge" Property="Visibility" Value="Visible"/>
276 </DataTrigger>
263 277 </DataTemplate.Triggers>
264 278 </DataTemplate>
265 279
Modified XFEToolBox/Views/Pages/ToolBoxPage.xaml.cs +44 -5
@@ -8,11 +8,13 @@ using System.Windows.Controls;
8 8 using System.Windows.Input;
9 9 using System.Windows.Media;
10 10 using System.Windows.Media.Imaging;
11 using XFEToolBox.Client.Model;
11 12 using XFEToolBox.Client.Models;
12 13 using XFEToolBox.Client.Utilities;
13 14 using XFEToolBox.Client.Utilities.Server;
14 15 using XFEToolBox.Client.ViewModel.Pages;
15 16 using XFEToolBox.Client.Profiles.CacheProfiles;
17 using XFEToolBox.Client.Views.Pages.Popups;
16 18 using XFEToolBox.Core.Model;
17 19 using XFEToolBox.Core.Tools;
18 20
@@ -162,7 +164,7 @@ public partial class ToolBoxPage : Page
162 164 string.Equals(card.Id, tool.Id, StringComparison.OrdinalIgnoreCase)
163 165 && ToolSummariesEquivalent(card.Package, tool));
164 166 if (existing is not null) return existing;
165 return new ToolCardViewModel(tool, CreateIconSource(tool.IconDataUrl), File.Exists(GetCachePath(tool)));
167 return CreateToolCard(tool);
166 168 }).ToArray();
167 169
168 170 _tools.Clear();
@@ -332,10 +334,42 @@ public partial class ToolBoxPage : Page
332 334 return false;
333 335 }
334 336
335 card ??= new ToolCardViewModel(summary, CreateIconSource(summary.IconDataUrl), File.Exists(GetCachePath(summary)));
337 card ??= CreateToolCard(summary);
336 338 return await OpenToolAsync(card);
337 339 }
338 340
341 private static ToolCardViewModel CreateToolCard(ToolPackageSummary summary) => new(
342 summary,
343 CreateIconSource(summary.IconDataUrl),
344 ElevationShieldIcon.Source,
345 File.Exists(GetCachePath(summary)),
346 ToolLaunchPreferenceService.GetRunAsAdministrator(summary.Id));
347
348 private void ToolConfigurationMenuItem_Click(object sender, RoutedEventArgs e)
349 {
350 e.Handled = true;
351 if (sender is not MenuItem { CommandParameter: ToolCardViewModel card }) return;
352
353 var configurationPage = new ToolConfigurationPopupPage(
354 card.Name,
355 card.RunAsAdministrator,
356 card.UacIconSource);
357 var result = PopupHelper.ShowDialog(configurationPage, new PopupWindowOptions
358 {
359 Title = "工具配置",
360 Subtitle = card.Name,
361 Width = 470,
362 Height = 330
363 });
364 if (result != MessageBoxResult.OK) return;
365
366 ToolLaunchPreferenceService.SetRunAsAdministrator(card.Id, configurationPage.RunAsAdministrator);
367 card.RunAsAdministrator = configurationPage.RunAsAdministrator;
368 StatusText.Text = card.RunAsAdministrator
369 ? $"{card.Name} 已配置为以管理员身份打开。"
370 : $"{card.Name} 已配置为以普通权限打开。";
371 }
372
339 373 private void ClearToolDataMenuItem_Click(object sender, RoutedEventArgs e)
340 374 {
341 375 e.Handled = true;
@@ -431,17 +465,22 @@ public partial class ToolBoxPage : Page
431 465 }
432 466
433 467 card.CacheState = "正在打开…";
434 StatusText.Text = $"正在编译并打开 {card.Name}…";
468 StatusText.Text = card.RunAsAdministrator
469 ? $"正在准备 {card.Name},随后将请求管理员权限…"
470 : $"正在编译并打开 {card.Name}…";
435 471 var runResult = await ToolProjectRunService.BuildPackageAndRunAsync(
436 472 cachePath,
437 473 card.Id,
438 474 package.Version,
439 package.Sha256);
475 package.Sha256,
476 card.RunAsAdministrator);
440 477 if (!runResult.Success)
441 478 throw new InvalidOperationException(runResult.Message);
442 479
443 480 card.CacheState = "已打开";
444 StatusText.Text = $"{card.Name} {card.LatestVersion} 已在独立窗口中打开。";
481 StatusText.Text = card.RunAsAdministrator
482 ? $"{card.Name} {card.LatestVersion} 已以管理员身份打开。"
483 : $"{card.Name} {card.LatestVersion} 已在独立窗口中打开。";
445 484 RecentUsageIconCache.Remember(RecentUsageKind.Tool, card.Id, card.IconSource);
446 485 RecentUsageService.RecordTool(card.Package);
447 486 return true;
Modified XFEToolBox/XFEToolBox.Client.csproj +1 -1