Skip to content

Commit 706c6c5

Browse files
committed
*优化项目配置,更新目标框架和平台设置;重构KitopiaExplorerCommand以支持新的配置加载逻辑;增强MainWindowViewModel以处理Kitopia路径和配置文件的加载;更新UI以改善用户体验。
1 parent 6ff60bd commit 706c6c5

File tree

12 files changed

+966
-175
lines changed

12 files changed

+966
-175
lines changed

ContextMenu.Avalonia/ContextMenu.Avalonia.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>WinExe</OutputType>
4-
<TargetFramework>net10.0</TargetFramework>
4+
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ApplicationManifest>app.manifest</ApplicationManifest>
77
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
8+
<PlatformTarget>x64</PlatformTarget>
9+
<Platforms>AnyCPU;x64</Platforms>
810
</PropertyGroup>
911

1012
<ItemGroup>
Lines changed: 310 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,314 @@
1-
namespace ContextMenu.Avalonia.ViewModels
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Text.Json;
7+
using System.Threading.Tasks;
8+
using Avalonia;
9+
using Avalonia.Controls.ApplicationLifetimes;
10+
using Avalonia.Platform.Storage;
11+
using CommunityToolkit.Mvvm.ComponentModel;
12+
using CommunityToolkit.Mvvm.Input;
13+
using Microsoft.Win32;
14+
using Windows.Storage;
15+
16+
namespace ContextMenu.Avalonia.ViewModels;
17+
18+
public partial class MainWindowViewModel : ViewModelBase
219
{
3-
public partial class MainWindowViewModel : ViewModelBase
20+
private const string SettingsFileName = "ContextMenuSettings.json";
21+
private const string ConfigFileName = "KitopiaContextMenu.json";
22+
23+
[ObservableProperty] private string _statusMessage = "Initializing...";
24+
[ObservableProperty] private string _kitopiaPath = string.Empty;
25+
26+
public ObservableCollection<ContextMenuItemViewModel> Items { get; } = new();
27+
28+
public MainWindowViewModel()
429
{
5-
public string Greeting { get; } = "Welcome to Avalonia!";
30+
InitializeAsync();
631
}
32+
33+
private async void InitializeAsync()
34+
{
35+
await LoadDataAsync();
36+
}
37+
38+
private async Task LoadDataAsync()
39+
{
40+
try
41+
{
42+
// 0. Check for Saved Manual Path
43+
var settings = LoadSettings();
44+
string? installPath = null;
45+
46+
if (!string.IsNullOrEmpty(settings.ExternalConfigPath))
47+
{
48+
// Try to infer install path from config path (..\configs\file.json)
49+
var configDir = Path.GetDirectoryName(settings.ExternalConfigPath);
50+
if (configDir != null)
51+
{
52+
var inferredPath = Path.GetDirectoryName(configDir); // Parent of configs
53+
if (inferredPath != null && Directory.Exists(inferredPath))
54+
{
55+
installPath = inferredPath;
56+
}
57+
}
58+
}
59+
60+
// 1. If no manual path, Find Kitopia Path from Registry
61+
if (string.IsNullOrEmpty(installPath))
62+
{
63+
installPath = FindKitopiaInstallPath();
64+
}
65+
66+
if (string.IsNullOrEmpty(installPath))
67+
{
68+
StatusMessage = "Kitopia installation not found. Please set manually.";
69+
return;
70+
}
71+
KitopiaPath = installPath;
72+
73+
// 2. Locate Config
74+
var configPath = Path.Combine(installPath, "configs", ConfigFileName);
75+
if (!File.Exists(configPath))
76+
{
77+
StatusMessage = $"Config not found at: {configPath}";
78+
return;
79+
}
80+
81+
// 3. Load Config Items
82+
var json = await File.ReadAllTextAsync(configPath);
83+
var config = JsonSerializer.Deserialize<ContextMenuConfigModel>(json);
84+
85+
if (config?.Items == null)
86+
{
87+
StatusMessage = "Failed to parse config.";
88+
return;
89+
}
90+
91+
Items.Clear();
92+
foreach (var item in config.Items)
93+
{
94+
bool isVisible = true;
95+
if (settings.Visibility.TryGetValue(item.Title, out bool savedVis))
96+
{
97+
isVisible = savedVis;
98+
}
99+
100+
var vm = new ContextMenuItemViewModel(item, isVisible);
101+
vm.PropertyChanged += (s, e) =>
102+
{
103+
if (e.PropertyName == nameof(ContextMenuItemViewModel.IsVisible))
104+
{
105+
SaveSettings();
106+
}
107+
};
108+
Items.Add(vm);
109+
}
110+
111+
// 5. Save the path to settings immediately so DLL can find it
112+
if (settings.ExternalConfigPath != configPath)
113+
{
114+
settings.ExternalConfigPath = configPath;
115+
SaveSettings(settings);
116+
}
117+
118+
StatusMessage = "Loaded successfully.";
119+
}
120+
catch (Exception ex)
121+
{
122+
StatusMessage = $"Error: {ex.Message}";
123+
}
124+
}
125+
126+
[RelayCommand]
127+
private async Task BrowsePath()
128+
{
129+
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
130+
{
131+
var folders = await desktop.MainWindow.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
132+
{
133+
Title = "Select Kitopia Installation Folder",
134+
AllowMultiple = false
135+
});
136+
137+
if (folders.Count > 0)
138+
{
139+
var path = folders[0].Path.LocalPath;
140+
141+
// Verify it looks like Kitopia (check for executable or configs folder)
142+
if (File.Exists(Path.Combine(path, "Kitopia.StoreCompanion.exe")) ||
143+
File.Exists(Path.Combine(path, "KitopiaAvalonia.exe")) ||
144+
Directory.Exists(Path.Combine(path, "configs")))
145+
{
146+
KitopiaPath = path;
147+
148+
// Update settings with new config path
149+
var settings = LoadSettings();
150+
settings.ExternalConfigPath = Path.Combine(KitopiaPath, "configs", ConfigFileName);
151+
SaveSettings(settings);
152+
153+
// Reload
154+
await LoadDataAsync();
155+
}
156+
else
157+
{
158+
StatusMessage = "Selected folder does not appear to be a valid Kitopia installation.";
159+
}
160+
}
161+
}
162+
}
163+
164+
private string? FindKitopiaInstallPath()
165+
{
166+
try
167+
{
168+
// Check HKLM and HKCU Uninstall keys
169+
string[] roots = {
170+
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
171+
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
172+
};
173+
174+
foreach (var root in roots)
175+
{
176+
using var key = Registry.LocalMachine.OpenSubKey(root);
177+
if (key != null)
178+
{
179+
SearchRegistryKey(key, out var path);
180+
if (!string.IsNullOrEmpty(path)) return path;
181+
}
182+
}
183+
184+
// Try HKCU
185+
using var cuKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall");
186+
if (cuKey != null)
187+
{
188+
SearchRegistryKey(cuKey, out var path);
189+
if (!string.IsNullOrEmpty(path)) return path;
190+
}
191+
192+
}
193+
catch { }
194+
return null;
195+
196+
void SearchRegistryKey(RegistryKey root, out string? foundPath)
197+
{
198+
foundPath = null;
199+
foreach (var subKeyName in root.GetSubKeyNames())
200+
{
201+
using var subKey = root.OpenSubKey(subKeyName);
202+
if (subKey == null) continue;
203+
204+
var displayName = subKey.GetValue("DisplayName") as string;
205+
if (displayName != null && displayName.Contains("Kitopia") && !displayName.Contains("Packing"))
206+
{
207+
var installLocation = subKey.GetValue("InstallLocation") as string;
208+
if (string.IsNullOrEmpty(installLocation))
209+
{
210+
installLocation = subKey.GetValue("Path") as string; // ModernInstaller uses "Path"
211+
}
212+
213+
if (!string.IsNullOrEmpty(installLocation) && Directory.Exists(installLocation))
214+
{
215+
foundPath = installLocation;
216+
return;
217+
}
218+
}
219+
}
220+
}
221+
}
222+
223+
private ContextMenuSettings LoadSettings()
224+
{
225+
try
226+
{
227+
var path = GetSettingsPath();
228+
if (File.Exists(path))
229+
{
230+
var json = File.ReadAllText(path);
231+
return JsonSerializer.Deserialize<ContextMenuSettings>(json) ?? new ContextMenuSettings();
232+
}
233+
}
234+
catch { }
235+
return new ContextMenuSettings();
236+
}
237+
238+
private void SaveSettings(ContextMenuSettings? settings = null)
239+
{
240+
try
241+
{
242+
if (settings == null)
243+
{
244+
settings = new ContextMenuSettings
245+
{
246+
ExternalConfigPath = Path.Combine(KitopiaPath, "configs", ConfigFileName)
247+
};
248+
foreach (var item in Items)
249+
{
250+
settings.Visibility[item.Title] = item.IsVisible;
251+
}
252+
}
253+
254+
var path = GetSettingsPath();
255+
var dir = Path.GetDirectoryName(path);
256+
if (dir != null && !Directory.Exists(dir)) Directory.CreateDirectory(dir);
257+
258+
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
259+
File.WriteAllText(path, json);
260+
}
261+
catch (Exception ex)
262+
{
263+
StatusMessage = $"Failed to save settings: {ex.Message}";
264+
}
265+
}
266+
267+
private string GetSettingsPath()
268+
{
269+
// Try Package LocalState first
270+
try
271+
{
272+
return Path.Combine(ApplicationData.Current.LocalFolder.Path, SettingsFileName);
273+
}
274+
catch
275+
{
276+
// Fallback for unpackaged debug
277+
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, SettingsFileName);
278+
}
279+
}
280+
}
281+
282+
public partial class ContextMenuItemViewModel : ObservableObject
283+
{
284+
public string Title { get; }
285+
public string Command { get; }
286+
287+
[ObservableProperty] private bool _isVisible;
288+
289+
public ContextMenuItemViewModel(ContextMenuItem item, bool isVisible)
290+
{
291+
Title = item.Title;
292+
Command = item.Command;
293+
IsVisible = isVisible;
294+
}
295+
}
296+
297+
public class ContextMenuConfigModel
298+
{
299+
public List<ContextMenuItem> Items { get; set; } = new();
300+
}
301+
302+
public class ContextMenuItem
303+
{
304+
public string Title { get; set; } = string.Empty;
305+
public string Icon { get; set; } = string.Empty;
306+
public string Command { get; set; } = string.Empty;
307+
public string Arguments { get; set; } = string.Empty;
308+
}
309+
310+
public class ContextMenuSettings
311+
{
312+
public string ExternalConfigPath { get; set; } = string.Empty;
313+
public Dictionary<string, bool> Visibility { get; set; } = new();
7314
}

ContextMenu.Avalonia/Views/MainWindow.axaml

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,38 @@
33
xmlns:vm="using:ContextMenu.Avalonia.ViewModels"
44
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
55
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6-
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
6+
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
77
x:Class="ContextMenu.Avalonia.Views.MainWindow"
88
x:DataType="vm:MainWindowViewModel"
9+
Width="600" Height="400"
10+
WindowStartupLocation="CenterScreen"
911
Icon="/Assets/avalonia-logo.ico"
10-
Title="ContextMenu.Avalonia">
12+
Title="Kitopia Context Menu Manager">
1113

1214
<Design.DataContext>
13-
<!-- This only sets the DataContext for the previewer in an IDE,
14-
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
1515
<vm:MainWindowViewModel/>
1616
</Design.DataContext>
1717

18-
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
18+
<Grid RowDefinitions="Auto, *, Auto" Margin="20">
19+
<StackPanel Grid.Row="0" Spacing="10">
20+
<TextBlock Text="Kitopia Context Menu Manager" FontSize="20" FontWeight="Bold"/>
21+
<TextBlock Text="{Binding StatusMessage}" Foreground="Gray" TextWrapping="Wrap"/>
22+
23+
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto">
24+
<TextBlock Grid.Column="0" Text="{Binding KitopiaPath, StringFormat='Installation: {0}'}" FontSize="12" Foreground="Gray" TextWrapping="Wrap" VerticalAlignment="Center"/>
25+
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowsePathCommand}" Margin="10,0,0,0"/>
26+
</Grid>
27+
</StackPanel>
28+
29+
<ListBox Grid.Row="1" ItemsSource="{Binding Items}" Margin="0,20">
30+
<ListBox.ItemTemplate>
31+
<DataTemplate>
32+
<CheckBox IsChecked="{Binding IsVisible}" Content="{Binding Title}"/>
33+
</DataTemplate>
34+
</ListBox.ItemTemplate>
35+
</ListBox>
36+
37+
<TextBlock Grid.Row="2" Text="Changes are saved automatically." HorizontalAlignment="Center" Foreground="Gray"/>
38+
</Grid>
1939

2040
</Window>

0 commit comments

Comments
 (0)