feat: 插件导出/导入

This commit is contained in:
denglihong2007
2025-07-29 15:06:01 +08:00
parent b051e4757f
commit e41ed32db4
6 changed files with 141 additions and 25 deletions

View File

@@ -24,6 +24,8 @@ public interface IPluginService
public static ObservableCollection<PluginInfo> OnlinePlugins => OnlinePluginsInternal;
Task InstallPluginAsync(PluginInfo plugin);
void LoadOnlinePlugins();
Task InstallPluginOnlineAsync(PluginInfo plugin);
Task InstallPluginLocalAsync(string filePath);
Task PackPluginAsync(PluginInfo plugin, string filePath);
Task LoadOnlinePluginsAsync();
}

View File

@@ -29,7 +29,7 @@ public class PluginManifest
/// <summary>
/// 项目 Url
/// </summary>
public string? Url { get; set; }
public string Url { get; set; } = "";
/// <summary>
/// 插件版本

View File

@@ -64,7 +64,7 @@ public class PluginService : IPluginService
Manifest = manifest,
PluginFolderPath = Path.GetFullPath(pluginDir),
RealIconPath = Path.Combine(Path.GetFullPath(pluginDir), "icon.png"),
StyleInfo = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(Path.GetFullPath(pluginDir), StyleInfoFileName)),JsonContextWithCamelCase.Default.StyleInfo),
StyleInfo = manifest.Type == "ScreenStyle" ? JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(Path.GetFullPath(pluginDir), StyleInfoFileName)),JsonContextWithCamelCase.Default.StyleInfo) : null,
};
if (info.IsUninstalling)
{
@@ -127,9 +127,8 @@ public class PluginService : IPluginService
{
_networkService = networkService;
_settings = settingsService.GetSettings();
LoadOnlinePlugins();
}
public void LoadOnlinePlugins()
public async Task LoadOnlinePluginsAsync()
{
var pluginManifests = _networkService.GetOnlinePlugins(IndexUrl);
IPluginService.OnlinePluginsInternal.Clear();
@@ -145,9 +144,10 @@ public class PluginService : IPluginService
};
IPluginService.OnlinePluginsInternal.Add(info);
}
await Task.CompletedTask;
}
public async Task InstallPluginAsync(PluginInfo plugin)
public async Task InstallPluginOnlineAsync(PluginInfo plugin)
{
var id = plugin.Manifest.Id;
var packageUrl = $"{_settings.ApiUri}/GetFile?fileName=plugins/{id}{IPluginService.PluginPackageExtension}";
@@ -189,4 +189,79 @@ public class PluginService : IPluginService
plugin.DownloadProgress = 0;
plugin.RestartRequired = true;
}
public async Task InstallPluginLocalAsync(string filePath)
{
var tempDir = Path.Combine(AppPaths.TempPath, "Plugins", Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
System.IO.Compression.ZipFile.ExtractToDirectory(filePath, tempDir);
var manifestPath = Path.Combine(tempDir, PluginManifestFileName);
if (!File.Exists(manifestPath))
{
throw new FileNotFoundException("插件包中缺少 manifest.json 文件。");
}
var manifestYaml = File.ReadAllText(manifestPath);
if( JsonSerializer.Deserialize(manifestYaml, JsonContextWithCamelCase.Default.PluginManifest) is not PluginManifest manifest || manifest.Id == "")
{
throw new InvalidDataException("插件包中的 manifest.json 文件格式不正确。");
}
// 解压到插件目录
var destDir = Path.Combine(AppPaths.PluginsRootPath, manifest.Id);
if (Directory.Exists(destDir))
Directory.Delete(destDir, true);
System.IO.Compression.ZipFile.ExtractToDirectory(filePath, destDir);
var info = new PluginInfo
{
Manifest = manifest,
PluginFolderPath = destDir,
RealIconPath = Path.Combine(destDir, "icon.png"),
StyleInfo = manifest.Type == "ScreenStyle" ? JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(destDir, StyleInfoFileName)), JsonContextWithCamelCase.Default.StyleInfo) : null,
RestartRequired = true
};
// 清理临时文件
try { Directory.Delete(tempDir, true); } catch { }
IPluginService.LoadedPluginsInternal.Add(info);
await Task.CompletedTask;
}
public async Task PackPluginAsync(PluginInfo plugin, string filePath)
{
var pluginDir = plugin.PluginFolderPath;
if (string.IsNullOrWhiteSpace(pluginDir) || !Directory.Exists(pluginDir))
throw new DirectoryNotFoundException("插件目录不存在。");
// 临时目录用于打包
var tempDir = Path.Combine(AppPaths.TempPath, "Pack", Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
// 复制所有文件到临时目录排除指定dll
foreach (var file in Directory.EnumerateFiles(pluginDir, "*", SearchOption.AllDirectories))
{
var fileName = Path.GetFileName(file);
if (fileName.Equals("Microsoft.Windows.SDK.NET.dll", StringComparison.OrdinalIgnoreCase) ||
fileName.Equals("WinRT.Runtime.dll", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var relativePath = Path.GetRelativePath(pluginDir, file);
var destPath = Path.Combine(tempDir, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
File.Copy(file, destPath, true);
}
// 如果目标文件已存在,先删除
if (File.Exists(filePath))
File.Delete(filePath);
// 打包为zip
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, filePath);
// 清理临时目录
try { Directory.Delete(tempDir, true); } catch { }
await Task.CompletedTask;
}
}

View File

@@ -1,15 +1,15 @@
using CRSim.Core.Abstractions;
using CRSim.Core.Enums;
using CRSim.Core.Models;
using CRSim.Core.Models.Plugin;
using CRSim.ScreenSimulator.Views;
using System.Windows;
using System.Windows.Controls;
using static System.Net.Mime.MediaTypeNames;
namespace CRSim.ScreenSimulator
{
public class StyleManager
{
public static List<PluginInfo> StyleInfos => [.. IPluginService.LoadedPlugins.Where(x => x.Manifest.Type == "ScreenStyle")];
public static List<PluginInfo> StyleInfos => [.. IPluginService.LoadedPlugins.Where(x => x.Manifest.Type == "ScreenStyle" && x.LoadStatus == PluginLoadStatus.Loaded)];
public static IServiceProvider ServiceProvider;
private static IDatabaseService _databaseService;
public StyleManager(IEnumerable<PluginBase> pluginBases,IServiceProvider serviceProvider,IDatabaseService databaseService)

View File

@@ -8,7 +8,6 @@ namespace CRSim.ViewModels;
public partial class PluginManagementPageViewModel : ObservableObject
{
[ObservableProperty]
public partial string PageTitle { get; set; } = "插件管理";
@@ -17,11 +16,14 @@ public partial class PluginManagementPageViewModel : ObservableObject
public ObservableCollection<PluginInfo> Plugins = IPluginService.OnlinePlugins;
private IPluginService _pluginService;
private readonly IPluginService _pluginService;
private readonly IDialogService _dialogService;
public PluginManagementPageViewModel(IPluginService pluginService)
public PluginManagementPageViewModel(IPluginService pluginService,IDialogService dialogService)
{
_pluginService = pluginService;
_dialogService = dialogService;
_pluginService.LoadOnlinePluginsAsync();
}
[RelayCommand]
@@ -34,12 +36,30 @@ public partial class PluginManagementPageViewModel : ObservableObject
}
[RelayCommand]
public async Task InstallPlugin()
public async Task InstallPluginOnline()
{
await _pluginService.InstallPluginAsync(SelectedPlugin);
await _pluginService.InstallPluginOnlineAsync(SelectedPlugin);
OnPropertyChanged(nameof(SelectedPlugin));
}
[RelayCommand]
public async Task InstallPluginLocal()
{
if (await _dialogService.GetFileAsync([".crsp"]) is string filePath)
{
try
{
await _pluginService.InstallPluginLocalAsync(filePath);
}
catch (Exception ex)
{
await _dialogService.ShowTextAsync("安装失败", $"{ex}");
}
}
else
{
await _dialogService.ShowMessageAsync("未选择文件", "请先选择一个插件包文件。");
}
}
[RelayCommand]
public void UninstallPlugin()
{
@@ -53,11 +73,25 @@ public partial class PluginManagementPageViewModel : ObservableObject
SelectedPlugin.IsUninstalling = false;
OnPropertyChanged(nameof(SelectedPlugin));
}
[RelayCommand]
public async Task PackPlugin()
{
if (SelectedPlugin == null) return;
string? filePath = await _dialogService.SaveFileAsync(".crsp", $"{SelectedPlugin.Manifest.Id}");
if (filePath == null) return;
try
{
await _pluginService.PackPluginAsync(SelectedPlugin, filePath);
}
catch (Exception ex)
{
await _dialogService.ShowTextAsync("打包失败", $"{ex}");
}
}
[RelayCommand]
public static void Restart()
{
string exePath = Process.GetCurrentProcess().MainModule.FileName;
string exePath = Environment.ProcessPath;
Process.Start(new ProcessStartInfo(exePath)
{
UseShellExecute = true
@@ -87,8 +121,8 @@ public partial class PluginManagementPageViewModel : ObservableObject
}
[RelayCommand]
public void Refresh()
public async Task Refresh()
{
_pluginService.LoadOnlinePlugins();
await _pluginService.LoadOnlinePluginsAsync();
}
}

View File

@@ -45,7 +45,6 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<SelectorBar x:Name="SelectorBar" Grid.Column="0" SelectionChanged="SelectorBar_SelectionChanged">
<interactivity:Interaction.Behaviors>
@@ -56,10 +55,13 @@
<SelectorBarItem Text="在线" Icon="{winui:FontIcon Glyph=&#xEDE4;}" IsSelected="True"/>
<SelectorBarItem Text="本地" Icon="{winui:FontIcon Glyph=&#xEDA2;}" />
</SelectorBar>
<Button Grid.Column="2" Content="{winui:FontIcon Glyph=&#xE72C;}" Style="{StaticResource EllipsisButton}"
Command="{x:Bind ViewModel.RefreshCommand}"/>
<Button Grid.Column="3" Content="{winui:FontIcon Glyph=&#xE8B7;}" Style="{StaticResource EllipsisButton}"
Command="{x:Bind ViewModel.OpenPluginFolderCommand}"/>
<CommandBar Grid.Column="2" IsOpen="False" DefaultLabelPosition="Right">
<AppBarButton Icon="{winui:FontIcon Glyph=&#xE72C;}" Label="刷新" Command="{x:Bind ViewModel.RefreshCommand}"/>
<CommandBar.SecondaryCommands>
<AppBarButton Icon="{winui:FontIcon Glyph=&#xE8B7;}" Label="打开插件目录" Command="{x:Bind ViewModel.OpenPluginFolderCommand}"/>
<AppBarButton Icon="{winui:FontIcon Glyph=&#xE7B8;}" Label="安装外部插件" Command="{x:Bind ViewModel.InstallPluginLocalCommand}"/>
</CommandBar.SecondaryCommands>
</CommandBar>
</Grid>
<Border Style="{StaticResource ListViewBorder}" Grid.Row="1">
<ListView ItemsSource="{x:Bind ViewModel.Plugins,Mode=OneWay}" Width="290" SelectionMode="Single">
@@ -113,12 +115,15 @@
<TextBlock Text="重启应用" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Style="{StaticResource AccentButtonStyle}" Command="{x:Bind ViewModel.InstallPluginCommand}" Visibility="{x:Bind ViewModel.SelectedPlugin,Mode=OneWay,Converter={StaticResource PluginLoadStatusToVisibilityConverter},ConverterParameter=NotLoaded}">
<Button Style="{StaticResource AccentButtonStyle}" Command="{x:Bind ViewModel.InstallPluginOnlineCommand}" Visibility="{x:Bind ViewModel.SelectedPlugin,Mode=OneWay,Converter={StaticResource PluginLoadStatusToVisibilityConverter},ConverterParameter=NotLoaded}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<FontIcon Glyph="&#xE896;" Margin="0,0,8,0"/>
<TextBlock Text="下载" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Command="{x:Bind ViewModel.PackPluginCommand}" Visibility="{x:Bind ViewModel.SelectedPlugin,Mode=OneWay,Converter={StaticResource PluginLoadStatusToVisibilityConverter},ConverterParameter=Loaded}">
<FontIcon Glyph="&#xE72D;"/>
</Button>
<Button Command="{x:Bind ViewModel.UninstallPluginCommand}" Visibility="{x:Bind ViewModel.SelectedPlugin,Mode=OneWay,Converter={StaticResource PluginLoadStatusToVisibilityConverter},ConverterParameter=Loaded}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<FontIcon Glyph="&#xE74D;" Margin="0,0,8,0"/>