mirror of
https://github.com/jackqqq123/luban_ui_internal.git
synced 2025-12-17 21:38:22 +08:00
feat: Luban.Core初步实现,提供能力:
- RegistService 装饰器实现服务注册 - 下载服务 - 文件服务 - 多线程方法 - 解压缩服务
This commit is contained in:
85
.github/instructions/prompt.instructions.md
vendored
85
.github/instructions/prompt.instructions.md
vendored
@@ -3,6 +3,52 @@ applyTo: '**'
|
|||||||
---
|
---
|
||||||
- 说中文
|
- 说中文
|
||||||
- 这是一个基于Avalonia的项目管理工程,目标是让非程序员也能轻松使用luban管理配置表。
|
- 这是一个基于Avalonia的项目管理工程,目标是让非程序员也能轻松使用luban管理配置表。
|
||||||
|
- 命令行是PowerShell使用分号作为命令分隔符
|
||||||
|
|
||||||
|
## 架构说明
|
||||||
|
|
||||||
|
### 🏗️ LubanHub.Core 核心能力库
|
||||||
|
LubanHub.Core 提供了项目的底层核心能力,**优先使用这些服务**来实现功能,避免重复造轮子:
|
||||||
|
|
||||||
|
#### 📥 下载服务 (ICoreDownloadService)
|
||||||
|
- **默认下载目录**: 系统用户数据目录的LubanHub子目录 (`%AppData%\LubanHub\Downloads`)
|
||||||
|
- **目录管理**: 获取/设置下载目录
|
||||||
|
- **进度追踪**: 实时下载进度、速度、文件名显示
|
||||||
|
- **取消支持**: 支持CancellationToken取消下载
|
||||||
|
- **使用场景**: Luban版本下载、模板下载、资源文件下载
|
||||||
|
|
||||||
|
#### 📁 文件管理服务 (ICoreFileService)
|
||||||
|
- **文件操作**: 创建、删除、复制、移动文件
|
||||||
|
- **目录操作**: 创建、删除、遍历目录
|
||||||
|
- **异步读写**: ReadAllTextAsync/WriteAllTextAsync
|
||||||
|
- **信息获取**: FileInfo/DirectoryInfo
|
||||||
|
- **使用场景**: 配置文件操作、项目文件管理、模板文件处理
|
||||||
|
|
||||||
|
#### ⚙️ 进程调用服务 (ICoreProcessService)
|
||||||
|
- **同步执行**: 等待进程完成并获取输出
|
||||||
|
- **异步执行**: 实时获取stdout/stderr输出
|
||||||
|
- **后台进程**: 启动但不等待的进程
|
||||||
|
- **进程管理**: 检查进程状态、杀死进程
|
||||||
|
- **使用场景**: 调用Luban命令行、Git操作、外部工具集成
|
||||||
|
|
||||||
|
#### 🗜️ 解压缩服务 (ICoreCompressionService)
|
||||||
|
- **ZIP支持**: 完整的ZIP解压缩功能
|
||||||
|
- **进度追踪**: 解压进度回调
|
||||||
|
- **格式检测**: 自动识别压缩格式
|
||||||
|
- **扩展支持**: 预留RAR/7Z接口(需第三方库)
|
||||||
|
- **使用场景**: Luban安装包解压、模板包解压、资源包处理
|
||||||
|
|
||||||
|
#### 🔧 依赖注入集成
|
||||||
|
```csharp
|
||||||
|
// 在App.axaml.cs中已配置
|
||||||
|
services.AddCoreServices(); // 自动注册所有Core服务
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ⚠️ 扩展原则
|
||||||
|
- **优先使用**: 实现新功能时,首先检查Core是否已提供相关服务
|
||||||
|
- **必要扩展**: 只有在Core无法满足需求时,才考虑扩展Core
|
||||||
|
- **接口设计**: 新增Core功能需要定义接口,保持架构一致性
|
||||||
|
- **日志集成**: 所有Core服务已集成Microsoft.Extensions.Logging
|
||||||
|
|
||||||
## UI设计规范
|
## UI设计规范
|
||||||
|
|
||||||
@@ -61,4 +107,41 @@ applyTo: '**'
|
|||||||
- 按钮支持按下状态反馈
|
- 按钮支持按下状态反馈
|
||||||
- 输入框支持焦点状态
|
- 输入框支持焦点状态
|
||||||
- 列表项支持选中和悬停状态
|
- 列表项支持选中和悬停状态
|
||||||
- 过渡动画让交互更流畅
|
- 过渡动画让交互更流畅
|
||||||
|
|
||||||
|
## 开发指导
|
||||||
|
|
||||||
|
### 📋 功能实现流程
|
||||||
|
1. **需求分析**: 明确功能需求和用户场景
|
||||||
|
2. **Core检查**: 检查LubanHub.Core是否已提供相关服务
|
||||||
|
3. **服务利用**: 优先使用Core服务实现功能逻辑
|
||||||
|
4. **UI设计**: 按照设计规范创建用户界面
|
||||||
|
5. **进度显示**: 长时间操作使用底部进度条显示状态
|
||||||
|
6. **错误处理**: 合理的异常处理和用户提示
|
||||||
|
|
||||||
|
### 🎯 常见场景示例
|
||||||
|
- **文件下载**: 使用ICoreDownloadService,进度显示在底部
|
||||||
|
- **Luban调用**: 使用ICoreProcessService执行命令行
|
||||||
|
- **配置管理**: 使用ICoreFileService读写配置文件
|
||||||
|
- **安装包处理**: 使用ICoreCompressionService解压
|
||||||
|
- **目录选择**: 集成系统文件对话框(需要时扩展Core)
|
||||||
|
|
||||||
|
### 🔄 服务扩展指南
|
||||||
|
当需要新功能且Core无法满足时:
|
||||||
|
1. 在`LubanHub.Core/Interfaces`添加接口定义
|
||||||
|
2. 在`LubanHub.Core/Services`添加实现类
|
||||||
|
3. 在`ServiceCollectionExtensions`中注册服务
|
||||||
|
4. 更新本文档说明新增能力
|
||||||
|
5. 在App层通过依赖注入使用新服务
|
||||||
|
|
||||||
|
### 📊 UI状态管理
|
||||||
|
- **ViewModelBase**: 所有ViewModel继承此基类
|
||||||
|
- **属性绑定**: 使用SetProperty方法通知UI更新
|
||||||
|
- **命令模式**: 使用RelayCommand处理用户交互
|
||||||
|
- **进度显示**: 使用DownloadProgressViewModel显示长时间操作
|
||||||
|
|
||||||
|
### 🎨 主题和样式
|
||||||
|
- **主题切换**: 使用ThemeManager管理深色/浅色主题
|
||||||
|
- **资源绑定**: 所有颜色使用DynamicResource绑定
|
||||||
|
- **样式复用**: 在Styles.axaml中定义通用样式
|
||||||
|
- **响应式**: 支持运行时主题切换
|
||||||
@@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{06401A04-D86
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.App", "src\LubanHub.App\LubanHub.App.csproj", "{9A66E728-EA8A-4644-9CC2-2C056479AF5A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.App", "src\LubanHub.App\LubanHub.App.csproj", "{9A66E728-EA8A-4644-9CC2-2C056479AF5A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.Core", "src\LubanHub.Core\LubanHub.Core.csproj", "{B8F5E9A2-1234-4567-890A-BCDEF0123456}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -20,8 +22,13 @@ Global
|
|||||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
|
{9A66E728-EA8A-4644-9CC2-2C056479AF5A} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
|
||||||
|
{B8F5E9A2-1234-4567-890A-BCDEF0123456} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -82,9 +82,37 @@ UI层
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、开发建议
|
## 五、架构原则
|
||||||
|
|
||||||
|
### 1. 服务注册原则
|
||||||
|
- **优先使用装饰器注册服务**:所有服务类使用 `[RegistService]` 特性标记,实现自动发现和注册
|
||||||
|
- **避免手工注册**:减少在 DI 容器中手动添加服务,降低耦合度和维护成本
|
||||||
|
- **支持生命周期配置**:通过装饰器参数指定服务生命周期(Singleton/Scoped/Transient)
|
||||||
|
|
||||||
|
### 2. 依赖方向原则
|
||||||
|
- **LubanHub.Core 不能引用任何其他服务**:作为基础能力库,Core 必须保持纯净,只能依赖系统库和第三方基础库
|
||||||
|
- **其他服务可以引用 Core**:业务服务层可以依赖 Core 提供的基础能力
|
||||||
|
- **业务服务间避免相互引用**:各业务服务应保持独立,通过事件或接口解耦
|
||||||
|
|
||||||
|
### 3. 接口设计原则
|
||||||
|
- **Core 服务必须定义接口**:如 `ICoreFileService`、`ICoreDownloadService` 等
|
||||||
|
- **业务服务建议定义接口**:便于测试和扩展
|
||||||
|
- **接口与实现分离**:接口放在 Interfaces 目录,实现放在 Services 目录
|
||||||
|
|
||||||
|
### 4. 装饰器使用示例
|
||||||
|
```csharp
|
||||||
|
[RegistService(ServiceLifetime.Singleton, typeof(ICoreFileService))]
|
||||||
|
public class CoreFileService : ICoreFileService
|
||||||
|
{
|
||||||
|
// 服务实现
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、开发建议
|
||||||
|
|
||||||
- 每个服务建议定义接口(如 IFileService),便于测试和扩展
|
|
||||||
- Core层实现时注意跨平台兼容
|
- Core层实现时注意跨平台兼容
|
||||||
- 业务服务只依赖Core,不直接操作系统API
|
- 业务服务只依赖Core,不直接操作系统API
|
||||||
- UI层只做展示和交互,所有逻辑下沉到服务
|
- UI层只做展示和交互,所有逻辑下沉到服务
|
||||||
|
- 使用装饰器模式实现服务的自动发现和注册
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using LubanHub.App.Services;
|
||||||
|
using LubanHub.Core;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace LubanHub.App;
|
namespace LubanHub.App;
|
||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
public static ServiceProvider? ServiceProvider { get; private set; }
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.WriteLine("正在初始化应用程序...");
|
Console.WriteLine("正在初始化应用程序...");
|
||||||
|
|
||||||
|
// 配置服务
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
ConfigureServices(services);
|
||||||
|
ServiceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
Console.WriteLine("XAML加载完成。");
|
Console.WriteLine("XAML加载完成。");
|
||||||
}
|
}
|
||||||
@@ -23,6 +35,27 @@ public partial class App : Application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureServices(ServiceCollection services)
|
||||||
|
{
|
||||||
|
// 添加日志
|
||||||
|
services.AddLogging(builder =>
|
||||||
|
{
|
||||||
|
builder.AddConsole();
|
||||||
|
builder.SetMinimumLevel(LogLevel.Debug);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加Core服务
|
||||||
|
services.AddCoreServices();
|
||||||
|
|
||||||
|
// 添加App层服务
|
||||||
|
services.AddSingleton<IDownloadProgressService, DownloadProgressService>();
|
||||||
|
services.AddSingleton<IAppDownloadService, AppDownloadService>();
|
||||||
|
services.AddSingleton<IExampleModuleService, ExampleModuleService>();
|
||||||
|
|
||||||
|
// 添加ViewModels等其他服务
|
||||||
|
// services.AddSingleton<MainWindowViewModel>();
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -31,6 +64,7 @@ public partial class App : Application
|
|||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow();
|
desktop.MainWindow = new MainWindow();
|
||||||
|
desktop.Exit += (s, e) => ServiceProvider?.Dispose();
|
||||||
Console.WriteLine("主窗口已创建。");
|
Console.WriteLine("主窗口已创建。");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<RootNamespace>LubanHub.App</RootNamespace>
|
<RootNamespace>LubanHub.App</RootNamespace>
|
||||||
<AssemblyName>LubanHub.App</AssemblyName>
|
<AssemblyName>LubanHub.App</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -15,10 +15,17 @@
|
|||||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.6">
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.6">
|
||||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LubanHub.Core\LubanHub.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,16 +2,18 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:views="clr-namespace:LubanHub.App.Views"
|
||||||
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
|
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
|
||||||
x:Class="LubanHub.App.MainWindow"
|
x:Class="LubanHub.App.MainWindow"
|
||||||
Title="LubanHub"
|
Title="LubanHub"
|
||||||
MinWidth="1000" MinHeight="600">
|
MinWidth="1000" MinHeight="600">
|
||||||
|
|
||||||
<Grid>
|
<Grid RowDefinitions="*,Auto">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid Grid.Row="0">
|
||||||
<ColumnDefinition Width="200"/>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="200"/>
|
||||||
</Grid.ColumnDefinitions>
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- 左侧导航栏 -->
|
<!-- 左侧导航栏 -->
|
||||||
<Border Grid.Column="0" Background="{DynamicResource SidebarBackgroundBrush}"
|
<Border Grid.Column="0" Background="{DynamicResource SidebarBackgroundBrush}"
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBox Grid.Column="0"
|
<TextBox Grid.Column="0"
|
||||||
Text="C:\Users\Downloads\LubanHub"
|
Name="DownloadDirectoryTextBox"
|
||||||
Background="{DynamicResource InputBackgroundBrush}"
|
Background="{DynamicResource InputBackgroundBrush}"
|
||||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||||
BorderBrush="{DynamicResource BorderBrush}"
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
@@ -235,8 +237,13 @@
|
|||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
Content="浏览..."
|
Content="浏览..."
|
||||||
Classes="primary"
|
Classes="primary"
|
||||||
Padding="{DynamicResource ButtonPadding}"/>
|
Padding="{DynamicResource ButtonPadding}"
|
||||||
|
Click="OnBrowseDownloadDirectoryClick"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Button Content="🧪 测试其他模块下载"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
Padding="10,5"
|
||||||
|
Click="OnTestModuleDownloadClick"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -259,5 +266,9 @@
|
|||||||
</Panel>
|
</Panel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 底部下载进度区域 -->
|
||||||
|
<views:DownloadProgressView Grid.Row="1" DataContext="{Binding DownloadProgress}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Controls.Selection;
|
using Avalonia.Controls.Selection;
|
||||||
using LubanHub.App.Services;
|
using LubanHub.App.Services;
|
||||||
|
using LubanHub.App.ViewModels;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using LubanHub.Core.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace LubanHub.App;
|
namespace LubanHub.App;
|
||||||
|
|
||||||
@@ -19,11 +23,15 @@ public partial class MainWindow : Window
|
|||||||
private Button? _installButton;
|
private Button? _installButton;
|
||||||
private Button? _settingsButton;
|
private Button? _settingsButton;
|
||||||
private ComboBox? _themeComboBox;
|
private ComboBox? _themeComboBox;
|
||||||
|
private TextBox? _downloadDirectoryTextBox;
|
||||||
|
|
||||||
|
private MainWindowViewModel? _viewModel;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
InitializeComponents();
|
InitializeComponents();
|
||||||
|
InitializeViewModel();
|
||||||
|
|
||||||
// 订阅主题变化事件
|
// 订阅主题变化事件
|
||||||
ThemeManager.ThemeChanged += OnThemeChanged;
|
ThemeManager.ThemeChanged += OnThemeChanged;
|
||||||
@@ -33,6 +41,31 @@ public partial class MainWindow : Window
|
|||||||
UpdateThemeComboBox();
|
UpdateThemeComboBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeViewModel()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_viewModel = new MainWindowViewModel();
|
||||||
|
DataContext = _viewModel;
|
||||||
|
|
||||||
|
// 更新下载目录文本框
|
||||||
|
UpdateDownloadDirectoryTextBox();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 如果ViewModel初始化失败,记录错误但不中断应用启动
|
||||||
|
Console.WriteLine($"初始化ViewModel时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDownloadDirectoryTextBox()
|
||||||
|
{
|
||||||
|
if (_downloadDirectoryTextBox != null && _viewModel != null)
|
||||||
|
{
|
||||||
|
_downloadDirectoryTextBox.Text = _viewModel.DownloadDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeComponents()
|
private void InitializeComponents()
|
||||||
{
|
{
|
||||||
// 获取面板引用
|
// 获取面板引用
|
||||||
@@ -48,7 +81,10 @@ public partial class MainWindow : Window
|
|||||||
_projectButton = this.FindControl<Button>("ProjectButton");
|
_projectButton = this.FindControl<Button>("ProjectButton");
|
||||||
_installButton = this.FindControl<Button>("InstallButton");
|
_installButton = this.FindControl<Button>("InstallButton");
|
||||||
_settingsButton = this.FindControl<Button>("SettingsButton");
|
_settingsButton = this.FindControl<Button>("SettingsButton");
|
||||||
|
|
||||||
|
// 获取其他控件引用
|
||||||
_themeComboBox = this.FindControl<ComboBox>("ThemeComboBox");
|
_themeComboBox = this.FindControl<ComboBox>("ThemeComboBox");
|
||||||
|
_downloadDirectoryTextBox = this.FindControl<TextBox>("DownloadDirectoryTextBox");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowPanel(Grid? targetPanel)
|
private void ShowPanel(Grid? targetPanel)
|
||||||
@@ -130,4 +166,36 @@ public partial class MainWindow : Window
|
|||||||
_themeComboBox.SelectedIndex = ThemeManager.CurrentTheme == ThemeVariant.Dark ? 0 : 1;
|
_themeComboBox.SelectedIndex = ThemeManager.CurrentTheme == ThemeVariant.Dark ? 0 : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnBrowseDownloadDirectoryClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 简单的目录选择实现,实际项目中应该使用文件对话框
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.ChangeDownloadDirectoryCommand.Execute(null);
|
||||||
|
UpdateDownloadDirectoryTextBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"浏览下载目录时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTestModuleDownloadClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.TestModuleDownloadCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"测试模块下载时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
118
src/LubanHub.App/Services/AppDownloadService.cs
Normal file
118
src/LubanHub.App/Services/AppDownloadService.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using LubanHub.Core.Interfaces;
|
||||||
|
using LubanHub.Core.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.App.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用层下载服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IAppDownloadService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 下载文件并自动显示进度
|
||||||
|
/// </summary>
|
||||||
|
Task<string> DownloadFileAsync(string url, string? fileName = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载文件到指定目录并自动显示进度
|
||||||
|
/// </summary>
|
||||||
|
Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前下载目录
|
||||||
|
/// </summary>
|
||||||
|
string GetDownloadDirectory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置下载目录
|
||||||
|
/// </summary>
|
||||||
|
void SetDownloadDirectory(string path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用层下载服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class AppDownloadService : IAppDownloadService
|
||||||
|
{
|
||||||
|
private readonly ICoreDownloadService _coreDownloadService;
|
||||||
|
private readonly IDownloadProgressService _progressService;
|
||||||
|
private readonly ILogger<AppDownloadService> _logger;
|
||||||
|
|
||||||
|
public AppDownloadService(
|
||||||
|
ICoreDownloadService coreDownloadService,
|
||||||
|
IDownloadProgressService progressService,
|
||||||
|
ILogger<AppDownloadService> logger)
|
||||||
|
{
|
||||||
|
_coreDownloadService = coreDownloadService;
|
||||||
|
_progressService = progressService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> DownloadFileAsync(string url, string? fileName = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progress = new Progress<DownloadProgressInfo>(progressInfo =>
|
||||||
|
{
|
||||||
|
_progressService.ReportProgress(progressInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("开始下载文件: {Url}", url);
|
||||||
|
var filePath = await _coreDownloadService.DownloadFileAsync(url, fileName, progress, cancellationToken);
|
||||||
|
|
||||||
|
// 下载完成后延迟隐藏进度条
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(2000, CancellationToken.None); // 2秒后隐藏
|
||||||
|
_progressService.HideProgress();
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("文件下载完成: {FilePath}", filePath);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "下载文件时出错: {Url}", url);
|
||||||
|
_progressService.HideProgress();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progress = new Progress<DownloadProgressInfo>(progressInfo =>
|
||||||
|
{
|
||||||
|
_progressService.ReportProgress(progressInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("开始下载文件到指定目录: {Url} -> {Directory}", url, targetDirectory);
|
||||||
|
var filePath = await _coreDownloadService.DownloadFileAsync(url, targetDirectory, fileName, progress, cancellationToken);
|
||||||
|
|
||||||
|
// 下载完成后延迟隐藏进度条
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(2000, CancellationToken.None); // 2秒后隐藏
|
||||||
|
_progressService.HideProgress();
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("文件下载完成: {FilePath}", filePath);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "下载文件时出错: {Url} -> {Directory}", url, targetDirectory);
|
||||||
|
_progressService.HideProgress();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDownloadDirectory() => _coreDownloadService.GetDownloadDirectory();
|
||||||
|
|
||||||
|
public void SetDownloadDirectory(string path) => _coreDownloadService.SetDownloadDirectory(path);
|
||||||
|
}
|
||||||
28
src/LubanHub.App/Services/DownloadProgressService.cs
Normal file
28
src/LubanHub.App/Services/DownloadProgressService.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using LubanHub.Core.Models;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LubanHub.App.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局下载进度服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class DownloadProgressService : IDownloadProgressService
|
||||||
|
{
|
||||||
|
public event Action<DownloadProgressInfo>? ProgressChanged;
|
||||||
|
|
||||||
|
public void ReportProgress(DownloadProgressInfo progressInfo)
|
||||||
|
{
|
||||||
|
ProgressChanged?.Invoke(progressInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideProgress()
|
||||||
|
{
|
||||||
|
var hideInfo = new DownloadProgressInfo
|
||||||
|
{
|
||||||
|
IsCompleted = true,
|
||||||
|
Progress = 0,
|
||||||
|
FileName = string.Empty
|
||||||
|
};
|
||||||
|
ProgressChanged?.Invoke(hideInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/LubanHub.App/Services/ExampleModuleService.cs
Normal file
55
src/LubanHub.App/Services/ExampleModuleService.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using LubanHub.App.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.App.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 示例模块服务,演示如何在其他模块中使用下载功能
|
||||||
|
/// </summary>
|
||||||
|
public interface IExampleModuleService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 下载Luban示例文件
|
||||||
|
/// </summary>
|
||||||
|
Task DownloadLubanExampleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 示例模块服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class ExampleModuleService : IExampleModuleService
|
||||||
|
{
|
||||||
|
private readonly IAppDownloadService _downloadService;
|
||||||
|
private readonly ILogger<ExampleModuleService> _logger;
|
||||||
|
|
||||||
|
public ExampleModuleService(IAppDownloadService downloadService, ILogger<ExampleModuleService> logger)
|
||||||
|
{
|
||||||
|
_downloadService = downloadService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DownloadLubanExampleAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("示例模块开始下载Luban示例文件");
|
||||||
|
|
||||||
|
// 模拟下载一个示例文件
|
||||||
|
var testUrl = "https://httpbin.org/json";
|
||||||
|
var fileName = "luban_example.json";
|
||||||
|
|
||||||
|
// 调用下载服务,进度会自动显示在UI上
|
||||||
|
var filePath = await _downloadService.DownloadFileAsync(testUrl, fileName, CancellationToken.None);
|
||||||
|
|
||||||
|
_logger.LogInformation("示例模块下载完成: {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "示例模块下载失败");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/LubanHub.App/Services/IDownloadProgressService.cs
Normal file
25
src/LubanHub.App/Services/IDownloadProgressService.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using LubanHub.Core.Models;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LubanHub.App.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局下载进度服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IDownloadProgressService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 下载进度变更事件
|
||||||
|
/// </summary>
|
||||||
|
event Action<DownloadProgressInfo>? ProgressChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告下载进度
|
||||||
|
/// </summary>
|
||||||
|
void ReportProgress(DownloadProgressInfo progressInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 隐藏下载进度
|
||||||
|
/// </summary>
|
||||||
|
void HideProgress();
|
||||||
|
}
|
||||||
104
src/LubanHub.App/ViewModels/DownloadProgressViewModel.cs
Normal file
104
src/LubanHub.App/ViewModels/DownloadProgressViewModel.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using LubanHub.Core.Models;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LubanHub.App.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载进度ViewModel
|
||||||
|
/// </summary>
|
||||||
|
public class DownloadProgressViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private string _fileName = string.Empty;
|
||||||
|
private double _progress;
|
||||||
|
private bool _isVisible;
|
||||||
|
private string _statusText = string.Empty;
|
||||||
|
private string _speedText = string.Empty;
|
||||||
|
|
||||||
|
public string FileName
|
||||||
|
{
|
||||||
|
get => _fileName;
|
||||||
|
set => SetProperty(ref _fileName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Progress
|
||||||
|
{
|
||||||
|
get => _progress;
|
||||||
|
set => SetProperty(ref _progress, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsVisible
|
||||||
|
{
|
||||||
|
get => _isVisible;
|
||||||
|
set => SetProperty(ref _isVisible, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StatusText
|
||||||
|
{
|
||||||
|
get => _statusText;
|
||||||
|
set => SetProperty(ref _statusText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SpeedText
|
||||||
|
{
|
||||||
|
get => _speedText;
|
||||||
|
set => SetProperty(ref _speedText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateProgress(DownloadProgressInfo progressInfo)
|
||||||
|
{
|
||||||
|
FileName = progressInfo.FileName;
|
||||||
|
Progress = progressInfo.Progress;
|
||||||
|
IsVisible = true;
|
||||||
|
|
||||||
|
if (progressInfo.IsCompleted)
|
||||||
|
{
|
||||||
|
StatusText = "下载完成";
|
||||||
|
SpeedText = string.Empty;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(progressInfo.ErrorMessage))
|
||||||
|
{
|
||||||
|
StatusText = $"下载出错: {progressInfo.ErrorMessage}";
|
||||||
|
SpeedText = string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var downloadedMB = progressInfo.DownloadedBytes / 1024.0 / 1024.0;
|
||||||
|
var totalMB = progressInfo.TotalBytes / 1024.0 / 1024.0;
|
||||||
|
|
||||||
|
if (progressInfo.TotalBytes > 0)
|
||||||
|
{
|
||||||
|
StatusText = $"下载中: {downloadedMB:F1}MB / {totalMB:F1}MB";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StatusText = $"下载中: {downloadedMB:F1}MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressInfo.Speed > 0)
|
||||||
|
{
|
||||||
|
var speedKB = progressInfo.Speed / 1024.0;
|
||||||
|
if (speedKB >= 1024)
|
||||||
|
{
|
||||||
|
SpeedText = $"{speedKB / 1024:F1}MB/s";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SpeedText = $"{speedKB:F1}KB/s";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SpeedText = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
IsVisible = false;
|
||||||
|
Progress = 0;
|
||||||
|
StatusText = string.Empty;
|
||||||
|
SpeedText = string.Empty;
|
||||||
|
FileName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/LubanHub.App/ViewModels/MainWindowViewModel.cs
Normal file
154
src/LubanHub.App/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
using LubanHub.App.Services;
|
||||||
|
using LubanHub.Core.Interfaces;
|
||||||
|
using LubanHub.Core.Models;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace LubanHub.App.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主窗口ViewModel
|
||||||
|
/// </summary>
|
||||||
|
public class MainWindowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAppDownloadService _downloadService;
|
||||||
|
private readonly IDownloadProgressService _progressService;
|
||||||
|
private readonly IExampleModuleService _exampleModuleService;
|
||||||
|
private readonly ILogger<MainWindowViewModel> _logger;
|
||||||
|
private string _downloadDirectory = string.Empty;
|
||||||
|
|
||||||
|
public MainWindowViewModel()
|
||||||
|
{
|
||||||
|
// 从依赖注入容器获取服务
|
||||||
|
if (App.ServiceProvider != null)
|
||||||
|
{
|
||||||
|
_downloadService = App.ServiceProvider.GetRequiredService<IAppDownloadService>();
|
||||||
|
_progressService = App.ServiceProvider.GetRequiredService<IDownloadProgressService>();
|
||||||
|
_exampleModuleService = App.ServiceProvider.GetRequiredService<IExampleModuleService>();
|
||||||
|
_logger = App.ServiceProvider.GetRequiredService<ILogger<MainWindowViewModel>>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("ServiceProvider 未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProgress = new DownloadProgressViewModel();
|
||||||
|
|
||||||
|
// 订阅全局下载进度事件
|
||||||
|
_progressService.ProgressChanged += OnProgressChanged;
|
||||||
|
|
||||||
|
// 初始化下载目录
|
||||||
|
_downloadDirectory = _downloadService.GetDownloadDirectory();
|
||||||
|
|
||||||
|
// 初始化命令
|
||||||
|
ChangeDownloadDirectoryCommand = new RelayCommand(async () => await ChangeDownloadDirectoryAsync());
|
||||||
|
TestModuleDownloadCommand = new RelayCommand(async () => await TestModuleDownloadAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadProgressViewModel DownloadProgress { get; }
|
||||||
|
|
||||||
|
public string DownloadDirectory
|
||||||
|
{
|
||||||
|
get => _downloadDirectory;
|
||||||
|
set => SetProperty(ref _downloadDirectory, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ChangeDownloadDirectoryCommand { get; }
|
||||||
|
public ICommand TestModuleDownloadCommand { get; }
|
||||||
|
|
||||||
|
private void OnProgressChanged(DownloadProgressInfo progressInfo)
|
||||||
|
{
|
||||||
|
// 在UI线程中更新进度
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
if (progressInfo.IsCompleted && string.IsNullOrEmpty(progressInfo.FileName))
|
||||||
|
{
|
||||||
|
// 隐藏进度条
|
||||||
|
DownloadProgress.Hide();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DownloadProgress.UpdateProgress(progressInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ChangeDownloadDirectoryAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// TODO: 实现目录选择对话框
|
||||||
|
// 这里先用一个简单的示例
|
||||||
|
var newPath = @"C:\temp\LubanHub"; // 示例路径
|
||||||
|
_downloadService.SetDownloadDirectory(newPath);
|
||||||
|
DownloadDirectory = _downloadService.GetDownloadDirectory();
|
||||||
|
|
||||||
|
_logger.LogInformation("下载目录已更改为: {NewPath}", DownloadDirectory);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "更改下载目录时出错");
|
||||||
|
return Task.FromException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TestModuleDownloadAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("测试其他模块下载功能");
|
||||||
|
await _exampleModuleService.DownloadLubanExampleAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "测试其他模块下载时出错");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 简单的命令实现
|
||||||
|
/// </summary>
|
||||||
|
public class RelayCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly Func<Task> _execute;
|
||||||
|
private readonly Func<bool>? _canExecute;
|
||||||
|
private bool _isExecuting;
|
||||||
|
|
||||||
|
public RelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||||
|
{
|
||||||
|
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||||
|
_canExecute = canExecute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
|
||||||
|
public bool CanExecute(object? parameter)
|
||||||
|
{
|
||||||
|
return !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Execute(object? parameter)
|
||||||
|
{
|
||||||
|
if (!CanExecute(parameter))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isExecuting = true;
|
||||||
|
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _execute();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isExecuting = false;
|
||||||
|
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/LubanHub.App/ViewModels/ViewModelBase.cs
Normal file
29
src/LubanHub.App/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace LubanHub.App.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
OnPropertyChanged(propertyName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/LubanHub.App/Views/DownloadProgressView.axaml
Normal file
46
src/LubanHub.App/Views/DownloadProgressView.axaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="LubanHub.App.Views.DownloadProgressView">
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
Padding="16,8"
|
||||||
|
IsVisible="{Binding IsVisible}">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,4,Auto,4,Auto">
|
||||||
|
|
||||||
|
<!-- 第一行:文件名和速度 -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{Binding FileName}"
|
||||||
|
FontWeight="Medium"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{Binding SpeedText}"
|
||||||
|
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
|
||||||
|
FontSize="12" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 第二行:间隔 -->
|
||||||
|
|
||||||
|
<!-- 第三行:进度条 -->
|
||||||
|
<ProgressBar Grid.Row="2"
|
||||||
|
Value="{Binding Progress}"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="100"
|
||||||
|
Height="4" />
|
||||||
|
|
||||||
|
<!-- 第四行:间隔 -->
|
||||||
|
|
||||||
|
<!-- 第五行:状态文本 -->
|
||||||
|
<TextBlock Grid.Row="4"
|
||||||
|
Text="{Binding StatusText}"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
|
||||||
|
HorizontalAlignment="Left" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
11
src/LubanHub.App/Views/DownloadProgressView.axaml.cs
Normal file
11
src/LubanHub.App/Views/DownloadProgressView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LubanHub.App.Views;
|
||||||
|
|
||||||
|
public partial class DownloadProgressView : UserControl
|
||||||
|
{
|
||||||
|
public DownloadProgressView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/LubanHub.Core/Attributes/RegistServiceAttribute.cs
Normal file
37
src/LubanHub.Core/Attributes/RegistServiceAttribute.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务注册特性,用于标记需要自动注册到DI容器的服务
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||||
|
public class RegistServiceAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 服务生命周期
|
||||||
|
/// </summary>
|
||||||
|
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务接口类型,如果不指定则自动推断
|
||||||
|
/// </summary>
|
||||||
|
public Type? ServiceType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务注册优先级,数值越小优先级越高
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化服务注册特性
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lifetime">服务生命周期</param>
|
||||||
|
/// <param name="serviceType">服务接口类型</param>
|
||||||
|
public RegistServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton, Type? serviceType = null)
|
||||||
|
{
|
||||||
|
Lifetime = lifetime;
|
||||||
|
ServiceType = serviceType;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/LubanHub.Core/Interfaces/ICoreCompressionService.cs
Normal file
41
src/LubanHub.Core/Interfaces/ICoreCompressionService.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解压缩服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICoreCompressionService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 解压ZIP文件
|
||||||
|
/// </summary>
|
||||||
|
Task ExtractZipAsync(string zipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建ZIP文件
|
||||||
|
/// </summary>
|
||||||
|
Task CreateZipAsync(string sourcePath, string zipFilePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解压RAR文件 (如果支持)
|
||||||
|
/// </summary>
|
||||||
|
Task ExtractRarAsync(string rarFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解压7Z文件 (如果支持)
|
||||||
|
/// </summary>
|
||||||
|
Task Extract7ZAsync(string sevenZipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查是否支持指定格式
|
||||||
|
/// </summary>
|
||||||
|
bool IsFormatSupported(string fileExtension);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动检测格式并解压
|
||||||
|
/// </summary>
|
||||||
|
Task ExtractAsync(string archiveFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
37
src/LubanHub.Core/Interfaces/ICoreDownloadService.cs
Normal file
37
src/LubanHub.Core/Interfaces/ICoreDownloadService.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using LubanHub.Core.Models;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICoreDownloadService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取默认下载目录
|
||||||
|
/// </summary>
|
||||||
|
string GetDefaultDownloadDirectory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置下载目录
|
||||||
|
/// </summary>
|
||||||
|
void SetDownloadDirectory(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前下载目录
|
||||||
|
/// </summary>
|
||||||
|
string GetDownloadDirectory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载文件
|
||||||
|
/// </summary>
|
||||||
|
Task<string> DownloadFileAsync(string url, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载文件到指定目录
|
||||||
|
/// </summary>
|
||||||
|
Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
76
src/LubanHub.Core/Interfaces/ICoreFileService.cs
Normal file
76
src/LubanHub.Core/Interfaces/ICoreFileService.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件管理服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICoreFileService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 检查文件是否存在
|
||||||
|
/// </summary>
|
||||||
|
bool FileExists(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查目录是否存在
|
||||||
|
/// </summary>
|
||||||
|
bool DirectoryExists(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建目录
|
||||||
|
/// </summary>
|
||||||
|
void CreateDirectory(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除文件
|
||||||
|
/// </summary>
|
||||||
|
void DeleteFile(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除目录
|
||||||
|
/// </summary>
|
||||||
|
void DeleteDirectory(string path, bool recursive = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制文件
|
||||||
|
/// </summary>
|
||||||
|
void CopyFile(string sourceFile, string destFile, bool overwrite = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移动文件
|
||||||
|
/// </summary>
|
||||||
|
void MoveFile(string sourceFile, string destFile);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取文件文本
|
||||||
|
/// </summary>
|
||||||
|
Task<string> ReadAllTextAsync(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入文件文本
|
||||||
|
/// </summary>
|
||||||
|
Task WriteAllTextAsync(string path, string content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件信息
|
||||||
|
/// </summary>
|
||||||
|
FileInfo GetFileInfo(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目录信息
|
||||||
|
/// </summary>
|
||||||
|
DirectoryInfo GetDirectoryInfo(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目录下的文件列表
|
||||||
|
/// </summary>
|
||||||
|
string[] GetFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目录下的子目录列表
|
||||||
|
/// </summary>
|
||||||
|
string[] GetDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||||
|
}
|
||||||
38
src/LubanHub.Core/Interfaces/ICoreProcessService.cs
Normal file
38
src/LubanHub.Core/Interfaces/ICoreProcessService.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using LubanHub.Core.Models;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程调用服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICoreProcessService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程并等待完成
|
||||||
|
/// </summary>
|
||||||
|
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments = null, string? workingDirectory = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程并实时获取输出
|
||||||
|
/// </summary>
|
||||||
|
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
|
||||||
|
Action<string>? onOutputReceived, Action<string>? onErrorReceived, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动进程但不等待完成
|
||||||
|
/// </summary>
|
||||||
|
System.Diagnostics.Process StartProcess(string fileName, string? arguments = null, string? workingDirectory = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查进程是否正在运行
|
||||||
|
/// </summary>
|
||||||
|
bool IsProcessRunning(string processName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 杀死进程
|
||||||
|
/// </summary>
|
||||||
|
void KillProcess(string processName);
|
||||||
|
}
|
||||||
14
src/LubanHub.Core/LubanHub.Core.csproj
Normal file
14
src/LubanHub.Core/LubanHub.Core.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>LubanHub.Core</RootNamespace>
|
||||||
|
<AssemblyName>LubanHub.Core</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
42
src/LubanHub.Core/Models/DownloadProgressInfo.cs
Normal file
42
src/LubanHub.Core/Models/DownloadProgressInfo.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
namespace LubanHub.Core.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载进度信息
|
||||||
|
/// </summary>
|
||||||
|
public class DownloadProgressInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载进度 (0-100)
|
||||||
|
/// </summary>
|
||||||
|
public double Progress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已下载字节数
|
||||||
|
/// </summary>
|
||||||
|
public long DownloadedBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总字节数
|
||||||
|
/// </summary>
|
||||||
|
public long TotalBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载速度 (bytes/s)
|
||||||
|
/// </summary>
|
||||||
|
public long Speed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否完成
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误信息
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
27
src/LubanHub.Core/Models/ProcessResult.cs
Normal file
27
src/LubanHub.Core/Models/ProcessResult.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace LubanHub.Core.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行结果
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 退出代码
|
||||||
|
/// </summary>
|
||||||
|
public int ExitCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准输出
|
||||||
|
/// </summary>
|
||||||
|
public string StandardOutput { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准错误
|
||||||
|
/// </summary>
|
||||||
|
public string StandardError { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否成功执行
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess => ExitCode == 0;
|
||||||
|
}
|
||||||
80
src/LubanHub.Core/ServiceCollectionExtensions.cs
Normal file
80
src/LubanHub.Core/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using LubanHub.Core.Attributes;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace LubanHub.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Core服务依赖注入扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加Core服务 - 通过反射自动发现并注册带有RegistService特性的服务
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddCoreServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// 注册HttpClient(Core服务的依赖)
|
||||||
|
services.AddSingleton<HttpClient>();
|
||||||
|
|
||||||
|
// 获取当前程序集
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
|
||||||
|
// 发现所有带有RegistService特性的类型
|
||||||
|
var serviceTypes = assembly.GetTypes()
|
||||||
|
.Where(type => type.IsClass && !type.IsAbstract && type.GetCustomAttribute<RegistServiceAttribute>() != null)
|
||||||
|
.Select(type => new
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Attribute = type.GetCustomAttribute<RegistServiceAttribute>()!
|
||||||
|
})
|
||||||
|
.OrderBy(x => x.Attribute.Priority) // 按优先级排序
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 注册服务
|
||||||
|
foreach (var serviceInfo in serviceTypes)
|
||||||
|
{
|
||||||
|
var implementationType = serviceInfo.Type;
|
||||||
|
var attribute = serviceInfo.Attribute;
|
||||||
|
|
||||||
|
// 确定服务接口类型
|
||||||
|
Type serviceType;
|
||||||
|
if (attribute.ServiceType != null)
|
||||||
|
{
|
||||||
|
// 使用特性指定的服务类型
|
||||||
|
serviceType = attribute.ServiceType;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 自动推断:查找实现的第一个接口
|
||||||
|
var interfaceType = implementationType.GetInterfaces().FirstOrDefault();
|
||||||
|
if (interfaceType == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"服务 {implementationType.Name} 没有实现任何接口,且未在RegistService特性中指定ServiceType");
|
||||||
|
}
|
||||||
|
serviceType = interfaceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据生命周期注册服务
|
||||||
|
switch (attribute.Lifetime)
|
||||||
|
{
|
||||||
|
case ServiceLifetime.Singleton:
|
||||||
|
services.AddSingleton(serviceType, implementationType);
|
||||||
|
break;
|
||||||
|
case ServiceLifetime.Scoped:
|
||||||
|
services.AddScoped(serviceType, implementationType);
|
||||||
|
break;
|
||||||
|
case ServiceLifetime.Transient:
|
||||||
|
services.AddTransient(serviceType, implementationType);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(attribute.Lifetime), attribute.Lifetime, "不支持的服务生命周期");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/LubanHub.Core/Services/CoreCompressionService.cs
Normal file
178
src/LubanHub.Core/Services/CoreCompressionService.cs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
using LubanHub.Core.Attributes;
|
||||||
|
using LubanHub.Core.Interfaces;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解压缩服务实现
|
||||||
|
/// </summary>
|
||||||
|
[RegistService(ServiceLifetime.Singleton, typeof(ICoreCompressionService))]
|
||||||
|
public class CoreCompressionService : ICoreCompressionService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CoreCompressionService> _logger;
|
||||||
|
private readonly ICoreFileService _fileService;
|
||||||
|
|
||||||
|
public CoreCompressionService(ILogger<CoreCompressionService> logger, ICoreFileService fileService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_fileService = fileService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExtractZipAsync(string zipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_fileService.FileExists(zipFilePath))
|
||||||
|
throw new FileNotFoundException($"ZIP文件不存在: {zipFilePath}");
|
||||||
|
|
||||||
|
_logger.LogInformation("开始解压ZIP文件: {ZipPath} -> {ExtractPath}", zipFilePath, extractPath);
|
||||||
|
|
||||||
|
// 确保提取目录存在
|
||||||
|
if (!_fileService.DirectoryExists(extractPath))
|
||||||
|
{
|
||||||
|
_fileService.CreateDirectory(extractPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var archive = ZipFile.OpenRead(zipFilePath);
|
||||||
|
var totalEntries = archive.Entries.Count;
|
||||||
|
var extractedEntries = 0;
|
||||||
|
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// 跳过目录条目
|
||||||
|
if (string.IsNullOrEmpty(entry.Name))
|
||||||
|
{
|
||||||
|
extractedEntries++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var destinationPath = Path.Combine(extractPath, entry.FullName);
|
||||||
|
var destinationDir = Path.GetDirectoryName(destinationPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(destinationDir) && !_fileService.DirectoryExists(destinationDir))
|
||||||
|
{
|
||||||
|
_fileService.CreateDirectory(destinationDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "解压文件时出错: {EntryName}", entry.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractedEntries++;
|
||||||
|
var progressPercent = (double)extractedEntries / totalEntries * 100;
|
||||||
|
progress?.Report(progressPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("ZIP文件解压完成: {ZipPath}, 提取了 {Count} 个文件", zipFilePath, extractedEntries);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "解压ZIP文件时出错: {ZipPath}", zipFilePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateZipAsync(string sourcePath, string zipFilePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_fileService.DirectoryExists(sourcePath) && !_fileService.FileExists(sourcePath))
|
||||||
|
throw new DirectoryNotFoundException($"源路径不存在: {sourcePath}");
|
||||||
|
|
||||||
|
_logger.LogInformation("开始创建ZIP文件: {SourcePath} -> {ZipPath}", sourcePath, zipFilePath);
|
||||||
|
|
||||||
|
var zipDir = Path.GetDirectoryName(zipFilePath);
|
||||||
|
if (!string.IsNullOrEmpty(zipDir) && !_fileService.DirectoryExists(zipDir))
|
||||||
|
{
|
||||||
|
_fileService.CreateDirectory(zipDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
if (_fileService.FileExists(sourcePath))
|
||||||
|
{
|
||||||
|
// 压缩单个文件
|
||||||
|
using var archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create);
|
||||||
|
archive.CreateEntryFromFile(sourcePath, Path.GetFileName(sourcePath));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 压缩目录
|
||||||
|
ZipFile.CreateFromDirectory(sourcePath, zipFilePath);
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
progress?.Report(100);
|
||||||
|
_logger.LogInformation("ZIP文件创建完成: {ZipPath}", zipFilePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "创建ZIP文件时出错: {SourcePath} -> {ZipPath}", sourcePath, zipFilePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExtractRarAsync(string rarFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("RAR格式暂不支持,请使用第三方库实现");
|
||||||
|
throw new NotSupportedException("RAR格式暂不支持");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Extract7ZAsync(string sevenZipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("7Z格式暂不支持,请使用第三方库实现");
|
||||||
|
throw new NotSupportedException("7Z格式暂不支持");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsFormatSupported(string fileExtension)
|
||||||
|
{
|
||||||
|
var supportedFormats = new[] { ".zip" };
|
||||||
|
return supportedFormats.Contains(fileExtension.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExtractAsync(string archiveFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var extension = Path.GetExtension(archiveFilePath).ToLowerInvariant();
|
||||||
|
|
||||||
|
switch (extension)
|
||||||
|
{
|
||||||
|
case ".zip":
|
||||||
|
await ExtractZipAsync(archiveFilePath, extractPath, progress, cancellationToken);
|
||||||
|
break;
|
||||||
|
case ".rar":
|
||||||
|
await ExtractRarAsync(archiveFilePath, extractPath, progress, cancellationToken);
|
||||||
|
break;
|
||||||
|
case ".7z":
|
||||||
|
await Extract7ZAsync(archiveFilePath, extractPath, progress, cancellationToken);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"不支持的压缩格式: {extension}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "自动解压文件时出错: {ArchivePath}", archiveFilePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
src/LubanHub.Core/Services/CoreDownloadService.cs
Normal file
200
src/LubanHub.Core/Services/CoreDownloadService.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
using LubanHub.Core.Attributes;
|
||||||
|
using LubanHub.Core.Interfaces;
|
||||||
|
using LubanHub.Core.Models;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载服务实现
|
||||||
|
/// </summary>
|
||||||
|
[RegistService(ServiceLifetime.Singleton, typeof(ICoreDownloadService))]
|
||||||
|
public class CoreDownloadService : ICoreDownloadService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CoreDownloadService> _logger;
|
||||||
|
private readonly ICoreFileService _fileService;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private string _downloadDirectory;
|
||||||
|
|
||||||
|
public CoreDownloadService(ILogger<CoreDownloadService> logger, ICoreFileService fileService, HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_fileService = fileService;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_downloadDirectory = GetDefaultDownloadDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDefaultDownloadDirectory()
|
||||||
|
{
|
||||||
|
// 使用系统用户数据目录的LubanHub子目录
|
||||||
|
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
|
return Path.Combine(appDataPath, "LubanHub", "Downloads");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDownloadDirectory(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
throw new ArgumentException("下载目录路径不能为空", nameof(path));
|
||||||
|
|
||||||
|
_downloadDirectory = path;
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if (!_fileService.DirectoryExists(_downloadDirectory))
|
||||||
|
{
|
||||||
|
_fileService.CreateDirectory(_downloadDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("设置下载目录: {Path}", _downloadDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDownloadDirectory()
|
||||||
|
{
|
||||||
|
// 确保目录存在
|
||||||
|
if (!_fileService.DirectoryExists(_downloadDirectory))
|
||||||
|
{
|
||||||
|
_fileService.CreateDirectory(_downloadDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _downloadDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> DownloadFileAsync(string url, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return DownloadFileAsync(url, GetDownloadDirectory(), fileName, progress, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
throw new ArgumentException("URL不能为空", nameof(url));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(targetDirectory))
|
||||||
|
throw new ArgumentException("目标目录不能为空", nameof(targetDirectory));
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
if (!_fileService.DirectoryExists(targetDirectory))
|
||||||
|
{
|
||||||
|
_fileService.CreateDirectory(targetDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定文件名
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
{
|
||||||
|
fileName = GetFileNameFromUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetFilePath = Path.Combine(targetDirectory, fileName);
|
||||||
|
|
||||||
|
_logger.LogInformation("开始下载文件: {Url} -> {TargetPath}", url, targetFilePath);
|
||||||
|
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var lastBytesRead = 0L;
|
||||||
|
var lastTime = stopwatch.Elapsed;
|
||||||
|
|
||||||
|
var progressInfo = new DownloadProgressInfo
|
||||||
|
{
|
||||||
|
FileName = fileName
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var totalBytes = response.Content.Headers.ContentLength ?? -1;
|
||||||
|
progressInfo.TotalBytes = totalBytes;
|
||||||
|
|
||||||
|
using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
using var fileStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
||||||
|
|
||||||
|
var buffer = new byte[8192];
|
||||||
|
var totalBytesRead = 0L;
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
|
||||||
|
{
|
||||||
|
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
|
||||||
|
totalBytesRead += bytesRead;
|
||||||
|
|
||||||
|
// 计算进度和速度
|
||||||
|
var currentTime = stopwatch.Elapsed;
|
||||||
|
var timeDiff = currentTime - lastTime;
|
||||||
|
|
||||||
|
if (timeDiff.TotalMilliseconds >= 200) // 每200ms更新一次进度
|
||||||
|
{
|
||||||
|
var bytesDiff = totalBytesRead - lastBytesRead;
|
||||||
|
var speed = timeDiff.TotalSeconds > 0 ? (long)(bytesDiff / timeDiff.TotalSeconds) : 0;
|
||||||
|
|
||||||
|
progressInfo.DownloadedBytes = totalBytesRead;
|
||||||
|
progressInfo.Speed = speed;
|
||||||
|
progressInfo.Progress = totalBytes > 0 ? (double)totalBytesRead / totalBytes * 100 : 0;
|
||||||
|
|
||||||
|
progress?.Report(progressInfo);
|
||||||
|
|
||||||
|
lastBytesRead = totalBytesRead;
|
||||||
|
lastTime = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成
|
||||||
|
progressInfo.DownloadedBytes = totalBytesRead;
|
||||||
|
progressInfo.Progress = 100;
|
||||||
|
progressInfo.IsCompleted = true;
|
||||||
|
progressInfo.Speed = 0;
|
||||||
|
progress?.Report(progressInfo);
|
||||||
|
|
||||||
|
_logger.LogInformation("文件下载完成: {TargetPath}, 大小: {Size} 字节, 耗时: {Duration:F2}s",
|
||||||
|
targetFilePath, totalBytesRead, stopwatch.Elapsed.TotalSeconds);
|
||||||
|
|
||||||
|
return targetFilePath;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
progressInfo.ErrorMessage = ex.Message;
|
||||||
|
progress?.Report(progressInfo);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "下载文件时出错: {Url}", url);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFileNameFromUrl(string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = new Uri(url);
|
||||||
|
var fileName = Path.GetFileName(uri.LocalPath);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName) || fileName == "/")
|
||||||
|
{
|
||||||
|
fileName = $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除查询参数
|
||||||
|
var queryIndex = fileName.IndexOf('?');
|
||||||
|
if (queryIndex >= 0)
|
||||||
|
{
|
||||||
|
fileName = fileName.Substring(0, queryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
206
src/LubanHub.Core/Services/CoreFileService.cs
Normal file
206
src/LubanHub.Core/Services/CoreFileService.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
using LubanHub.Core.Attributes;
|
||||||
|
using LubanHub.Core.Interfaces;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件管理服务实现
|
||||||
|
/// </summary>
|
||||||
|
[RegistService(ServiceLifetime.Singleton, typeof(ICoreFileService))]
|
||||||
|
public class CoreFileService : ICoreFileService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CoreFileService> _logger;
|
||||||
|
|
||||||
|
public CoreFileService(ILogger<CoreFileService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FileExists(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return File.Exists(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "检查文件是否存在时出错: {Path}", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DirectoryExists(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.Exists(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "检查目录是否存在时出错: {Path}", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateDirectory(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
_logger.LogDebug("创建目录: {Path}", path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "创建目录时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
_logger.LogDebug("删除文件: {Path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "删除文件时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteDirectory(string path, bool recursive = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
Directory.Delete(path, recursive);
|
||||||
|
_logger.LogDebug("删除目录: {Path}, 递归: {Recursive}", path, recursive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "删除目录时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyFile(string sourceFile, string destFile, bool overwrite = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(sourceFile, destFile, overwrite);
|
||||||
|
_logger.LogDebug("复制文件: {Source} -> {Dest}", sourceFile, destFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "复制文件时出错: {Source} -> {Dest}", sourceFile, destFile);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveFile(string sourceFile, string destFile)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(sourceFile, destFile);
|
||||||
|
_logger.LogDebug("移动文件: {Source} -> {Dest}", sourceFile, destFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "移动文件时出错: {Source} -> {Dest}", sourceFile, destFile);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ReadAllTextAsync(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = await File.ReadAllTextAsync(path);
|
||||||
|
_logger.LogDebug("读取文件: {Path}, 大小: {Size} 字符", path, content.Length);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "读取文件时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteAllTextAsync(string path, string content)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(path, content);
|
||||||
|
_logger.LogDebug("写入文件: {Path}, 大小: {Size} 字符", path, content.Length);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "写入文件时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfo GetFileInfo(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new FileInfo(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取文件信息时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DirectoryInfo GetDirectoryInfo(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new DirectoryInfo(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取目录信息时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.GetFiles(path, searchPattern, searchOption);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取文件列表时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.GetDirectories(path, searchPattern, searchOption);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取目录列表时出错: {Path}", path);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
229
src/LubanHub.Core/Services/CoreProcessService.cs
Normal file
229
src/LubanHub.Core/Services/CoreProcessService.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using LubanHub.Core.Attributes;
|
||||||
|
using LubanHub.Core.Interfaces;
|
||||||
|
using LubanHub.Core.Models;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LubanHub.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程调用服务实现
|
||||||
|
/// </summary>
|
||||||
|
[RegistService(ServiceLifetime.Singleton, typeof(ICoreProcessService))]
|
||||||
|
public class CoreProcessService : ICoreProcessService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CoreProcessService> _logger;
|
||||||
|
|
||||||
|
public CoreProcessService(ILogger<CoreProcessService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments = null, string? workingDirectory = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("执行进程: {FileName} {Arguments}", fileName, arguments);
|
||||||
|
|
||||||
|
var processStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments ?? string.Empty,
|
||||||
|
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
StandardOutputEncoding = Encoding.UTF8,
|
||||||
|
StandardErrorEncoding = Encoding.UTF8
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = new Process { StartInfo = processStartInfo };
|
||||||
|
|
||||||
|
var outputBuilder = new StringBuilder();
|
||||||
|
var errorBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
process.OutputDataReceived += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Data != null)
|
||||||
|
{
|
||||||
|
outputBuilder.AppendLine(args.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.ErrorDataReceived += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Data != null)
|
||||||
|
{
|
||||||
|
errorBuilder.AppendLine(args.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
await process.WaitForExitAsync(cancellationToken);
|
||||||
|
|
||||||
|
var result = new ProcessResult
|
||||||
|
{
|
||||||
|
ExitCode = process.ExitCode,
|
||||||
|
StandardOutput = outputBuilder.ToString(),
|
||||||
|
StandardError = errorBuilder.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogDebug("进程执行完成: {FileName}, 退出代码: {ExitCode}", fileName, result.ExitCode);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "执行进程时出错: {FileName} {Arguments}", fileName, arguments);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
|
||||||
|
Action<string>? onOutputReceived, Action<string>? onErrorReceived, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("执行进程(实时输出): {FileName} {Arguments}", fileName, arguments);
|
||||||
|
|
||||||
|
var processStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments ?? string.Empty,
|
||||||
|
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
StandardOutputEncoding = Encoding.UTF8,
|
||||||
|
StandardErrorEncoding = Encoding.UTF8
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = new Process { StartInfo = processStartInfo };
|
||||||
|
|
||||||
|
var outputBuilder = new StringBuilder();
|
||||||
|
var errorBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
process.OutputDataReceived += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Data != null)
|
||||||
|
{
|
||||||
|
outputBuilder.AppendLine(args.Data);
|
||||||
|
onOutputReceived?.Invoke(args.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.ErrorDataReceived += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Data != null)
|
||||||
|
{
|
||||||
|
errorBuilder.AppendLine(args.Data);
|
||||||
|
onErrorReceived?.Invoke(args.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
await process.WaitForExitAsync(cancellationToken);
|
||||||
|
|
||||||
|
var result = new ProcessResult
|
||||||
|
{
|
||||||
|
ExitCode = process.ExitCode,
|
||||||
|
StandardOutput = outputBuilder.ToString(),
|
||||||
|
StandardError = errorBuilder.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogDebug("进程执行完成(实时输出): {FileName}, 退出代码: {ExitCode}", fileName, result.ExitCode);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "执行进程时出错(实时输出): {FileName} {Arguments}", fileName, arguments);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Process StartProcess(string fileName, string? arguments = null, string? workingDirectory = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("启动进程: {FileName} {Arguments}", fileName, arguments);
|
||||||
|
|
||||||
|
var processStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments ?? string.Empty,
|
||||||
|
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||||
|
UseShellExecute = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = Process.Start(processStartInfo);
|
||||||
|
if (process == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"无法启动进程: {fileName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "启动进程时出错: {FileName} {Arguments}", fileName, arguments);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsProcessRunning(string processName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var processes = Process.GetProcessesByName(processName);
|
||||||
|
return processes.Length > 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "检查进程是否运行时出错: {ProcessName}", processName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void KillProcess(string processName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var processes = Process.GetProcessesByName(processName);
|
||||||
|
foreach (var process in processes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill();
|
||||||
|
_logger.LogDebug("杀死进程: {ProcessName} (PID: {ProcessId})", processName, process.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "杀死进程时出错: {ProcessName} (PID: {ProcessId})", processName, process.Id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "杀死进程时出错: {ProcessName}", processName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user