返回提交历史
Added
XFEToolBox.Client.Wpf.Test/ToolRuntimeProcessStartInfoFactoryTests.cs
+47
-0
Modified
XFEToolBox.Client.Wpf.Test/XFEToolBox.Client.Wpf.Test.csproj
+1
-0
Modified
XFEToolBox/Profiles/CacheProfiles/AppCacheProfile.cs
+6
-0
Added
XFEToolBox/Utilities/ElevationShieldIcon.cs
+64
-0
Added
XFEToolBox/Utilities/ToolLaunchPreferenceService.cs
+48
-0
Modified
XFEToolBox/Utilities/ToolProjectRunService.cs
+89
-14
Added
XFEToolBox/Utilities/ToolRuntimeProcessStartInfoFactory.cs
+34
-0
Modified
XFEToolBox/ViewModel/Pages/ToolCardViewModel.cs
+14
-1
Added
XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml
+46
-0
Added
XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml.cs
+35
-0
Modified
XFEToolBox/Views/Pages/ToolBoxPage.xaml
+17
-3
Modified
XFEToolBox/Views/Pages/ToolBoxPage.xaml.cs
+44
-5
Modified
XFEToolBox/XFEToolBox.Client.csproj
+1
-1
XFEstudio/XFEToolBox
支持为工具配置管理员权限启动
新增 ToolLaunchPreferenceService,支持每个工具单独配置是否以管理员权限启动,配置持久化至 AppCacheProfile。ToolCardViewModel 增加 RunAsAdministrator 属性及 UAC 图标,UI 支持显示管理员徽标。新增 ToolConfigurationPopupPage,允许用户设置管理员启动选项。调整 ToolProjectRunService 启动流程,支持根据配置请求 UAC 权限。新增 ToolRuntimeProcessStartInfoFactory 及相关测试。版本号升级至 1.1.4。
090bc21
代码差异
13 个文件
+446
-24
@@ -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
}
@@ -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>
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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,
@@ -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
}
@@ -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
{
@@ -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>
@@ -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
}
@@ -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
@@ -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;