mirror of
https://github.com/denglihong2007/CRSim
synced 2026-08-05 08:55:52 +08:00
feat: 插件导出/导入
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public class PluginManifest
|
||||
/// <summary>
|
||||
/// 项目 Url
|
||||
/// </summary>
|
||||
public string? Url { get; set; }
|
||||
public string Url { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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=}" IsSelected="True"/>
|
||||
<SelectorBarItem Text="本地" Icon="{winui:FontIcon Glyph=}" />
|
||||
</SelectorBar>
|
||||
<Button Grid.Column="2" Content="{winui:FontIcon Glyph=}" Style="{StaticResource EllipsisButton}"
|
||||
Command="{x:Bind ViewModel.RefreshCommand}"/>
|
||||
<Button Grid.Column="3" Content="{winui:FontIcon Glyph=}" Style="{StaticResource EllipsisButton}"
|
||||
Command="{x:Bind ViewModel.OpenPluginFolderCommand}"/>
|
||||
<CommandBar Grid.Column="2" IsOpen="False" DefaultLabelPosition="Right">
|
||||
<AppBarButton Icon="{winui:FontIcon Glyph=}" Label="刷新" Command="{x:Bind ViewModel.RefreshCommand}"/>
|
||||
<CommandBar.SecondaryCommands>
|
||||
<AppBarButton Icon="{winui:FontIcon Glyph=}" Label="打开插件目录" Command="{x:Bind ViewModel.OpenPluginFolderCommand}"/>
|
||||
<AppBarButton Icon="{winui:FontIcon Glyph=}" 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="" 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=""/>
|
||||
</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="" Margin="0,0,8,0"/>
|
||||
|
||||
Reference in New Issue
Block a user