feat: #203 添加插件市场

This commit is contained in:
denglihong2007
2025-07-20 16:13:56 +08:00
parent 46ec868134
commit ee80792fcc
11 changed files with 323 additions and 20 deletions

View File

@@ -14,9 +14,9 @@
{
LaunchDebugConsole();
}
if (!Directory.Exists(AppPaths.AppDataFolder))
if (!Directory.Exists(AppPaths.AppDataPath))
{
Directory.CreateDirectory(AppPaths.AppDataFolder);
Directory.CreateDirectory(AppPaths.AppDataPath);
}
AppHost = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
@@ -34,6 +34,7 @@
services.AddTransient<StationManagementPageViewModel>();
services.AddTransient<ScreenSimulatorPageViewModel>();
services.AddTransient<SettingsPageViewModel>();
services.AddTransient<PluginManagementPageViewModel>();
PluginService.InitializePlugins(context, services, parsedOptions.ExternalPluginPath);
})
.Build();
@@ -45,7 +46,6 @@
AppHost.Services.GetRequiredService<ITimeService>().Start();
MainWindow = AppHost.Services.GetService<MainWindow>();
MainWindow?.Activate();
var win = AppHost.Services.GetService<StyleManager>();
}
[System.Runtime.InteropServices.DllImport("kernel32.dll")]

View File

@@ -13,6 +13,8 @@
<Nullable>enable</Nullable>
<WindowsPackageType>None</WindowsPackageType>
<LangVersion>preview</LangVersion>
<TrimMode>partial</TrimMode>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<Version>2.3.101.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -82,6 +84,9 @@
<Page Update="Views\ScreenSimulatorPage.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Views\PluginManagementPage.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Views\SettingsPage.xaml">
<SubType>Designer</SubType>
</Page>

View File

@@ -6,7 +6,9 @@ namespace CRSim.Converters
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value == null ? Visibility.Collapsed : Visibility.Visible;
bool invert = (parameter as string)?.ToLower() == "invert";
bool isNull = value == null;
return invert ? isNull ? Visibility.Visible : Visibility.Collapsed : isNull ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)

View File

@@ -0,0 +1,34 @@
using CRSim.Core.Enums;
using CRSim.Core.Models.Plugin;
using Microsoft.UI.Xaml.Data;
namespace CRSim.Converters
{
public partial class PluginLoadStatusToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is PluginInfo plugin && parameter is string target)
{
PluginLoadStatus status = plugin.LoadStatus;
if ((plugin.IsUninstalling && target== "Loaded") || (plugin.RestartRequired && target == "NotLoaded"))
{
return Visibility.Collapsed;
}
if ((status == PluginLoadStatus.Loaded && target == "Loaded") ||
(status == PluginLoadStatus.Error && target == "Error") ||
(status == PluginLoadStatus.NotLoaded && target == "NotLoaded") ||
(status == PluginLoadStatus.Disabled && target == "Disabled"))
{
return Visibility.Visible;
}
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -27,6 +27,11 @@
IsBackButtonVisible="Collapsed" IsBackEnabled="False">
<NavigationView.MenuItems>
<NavigationViewItem Icon="Home" Content="主页" Tag="DashboardPage" />
<NavigationViewItem Content="插件" Tag="PluginManagementPage">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xEA86;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItemHeader Content="数据管理"/>
<NavigationViewItem Content="车站" Tag="StationManagementPage">
<NavigationViewItem.Icon>

View File

@@ -0,0 +1,92 @@
using CRSim.Core.Abstractions;
using CRSim.Core.Models;
using CRSim.Core.Models.Plugin;
using CRSim.Core.Services;
using System.Diagnostics;
namespace CRSim.ViewModels;
public partial class PluginManagementPageViewModel : ObservableObject
{
[ObservableProperty]
public partial string PageTitle { get; set; } = "插件管理";
[ObservableProperty]
public partial PluginInfo? SelectedPlugin { get; set; }
public ObservableCollection<PluginInfo> Plugins = [];
private IPluginService _pluginService;
public PluginManagementPageViewModel(IPluginService pluginService)
{
_pluginService = pluginService;
}
[RelayCommand]
public void PluginSelected(object args)
{
if(args is SelectionChangedEventArgs selectionChangedEventArgs && selectionChangedEventArgs.AddedItems.Count == 1)
{
SelectedPlugin = (PluginInfo)selectionChangedEventArgs.AddedItems.FirstOrDefault();
}
}
[RelayCommand]
public async Task InstallPlugin()
{
await _pluginService.InstallPluginAsync(SelectedPlugin);
OnPropertyChanged(nameof(SelectedPlugin));
}
[RelayCommand]
public void UninstallPlugin()
{
SelectedPlugin.IsUninstalling = true;
OnPropertyChanged(nameof(SelectedPlugin));
}
[RelayCommand]
public void CancelUninstallPlugin()
{
SelectedPlugin.IsUninstalling = false;
OnPropertyChanged(nameof(SelectedPlugin));
}
[RelayCommand]
public static void Restart()
{
string exePath = Process.GetCurrentProcess().MainModule.FileName;
Process.Start(new ProcessStartInfo(exePath)
{
UseShellExecute = true
});
Application.Current.Exit();
}
[RelayCommand]
public void SourceSelected(SelectorBarItem selectorBarItem)
{
SelectedPlugin = null;
Plugins = (selectorBarItem?.Text == "本地" || selectorBarItem is null)
? IPluginService.OnlinePlugins : IPluginService.LoadedPlugins;
OnPropertyChanged(nameof(Plugins));
}
[RelayCommand]
public static void OpenPluginFolder()
{
Process.Start(new ProcessStartInfo
{
FileName = AppPaths.PluginsRootPath,
UseShellExecute = true
});
}
[RelayCommand]
public void Refresh()
{
_pluginService.LoadOnlinePlugins();
}
}

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using System.Diagnostics.Metrics;
using System.Reflection;
namespace CRSim.ViewModels
{
@@ -9,7 +10,11 @@ namespace CRSim.ViewModels
[ObservableProperty]
public partial string AppVersion { get; set; } = "";
public ObservableCollection<InfoItem> Apis { get; } =
[
new InfoItem { Title = "官方插件源", Detail = "http://47.122.74.193:25565" },
new InfoItem { Title = "镜像站插件源", Detail = "https://crsim.com.cn/api" },
];
private Settings _settings;
private readonly ISettingsService _settingsService;
private readonly IDatabaseService _databaseService;
@@ -39,6 +44,9 @@ namespace CRSim.ViewModels
[ObservableProperty]
public partial string SwitchPageSeconds { get; set; }
[ObservableProperty]
public partial InfoItem ApiUri { get; set; }
[ObservableProperty]
public partial string UserKey { get; set; }
@@ -67,6 +75,7 @@ namespace CRSim.ViewModels
StopDisplayUntilDepartureDuration = _settings.StopDisplayUntilDepartureDuration.TotalMinutes.ToString();
StopDisplayFromArrivalDuration = _settings.StopDisplayFromArrivalDuration.TotalMinutes.ToString();
StopCheckInAdvanceDuration = _settings.StopCheckInAdvanceDuration.TotalMinutes.ToString();
ApiUri = Apis.Where(x => x.Detail == _settings.ApiUri).FirstOrDefault();
MaxPages = _settings.MaxPages.ToString();
SwitchPageSeconds = _settings.SwitchPageSeconds.ToString();
UserKey = _settings.UserKey;
@@ -88,6 +97,7 @@ namespace CRSim.ViewModels
_settings.UserKey = UserKey;
_settings.LoadTodayOnly = LoadTodayOnly;
_settings.ReopenUnclosedScreensOnLoad = ReopenUnclosedScreensOnLoad;
_settings.ApiUri = ApiUri.Detail;
_settingsService.SaveSettings();
}
private void UpdateSettings(string input, bool allowNegative, Action<int> updateAction)

View File

@@ -573,24 +573,17 @@ public partial class StationManagementPageViewModel : ObservableObject
[RelayCommand]
public async Task ImportFromInternet()
{
try
if (!await CheckCanImport()) return;
var stops = await _networkService.GetTrainNumbersAsync(SelectedStation.Name);
if (stops is not null && stops.Count != 0)
{
if (!await CheckCanImport()) return;
var stops = await _networkService.GetTrainNumbersAsync(SelectedStation.Name);
if (stops.Count != 0)
TrainStops.Clear();
foreach (TrainStop t in stops)
{
TrainStops.Clear();
foreach (TrainStop t in stops)
{
TrainStops.Add(RandomTrainStopProperties(t));
}
}
else
{
throw new Exception();
TrainStops.Add(RandomTrainStopProperties(t));
}
}
catch
else
{
await _dialogService.ShowMessageAsync("获取失败", "服务器繁忙或车站不存在。");
}

View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="CRSim.Views.PluginManagementPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CRSim.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="using:CRSim.Converters"
xmlns:converters1="using:CommunityToolkit.WinUI.Converters"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:winui="using:CommunityToolkit.WinUI"
xmlns:viewmodels="using:CRSim.ViewModels"
xmlns:models="using:CRSim.Models"
xmlns:coremodels="using:CRSim.Core.Models"
xmlns:pluginmodels="using:CRSim.Core.Models.Plugin"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=viewmodels:PluginManagementPageViewModel}">
<Page.Resources>
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
<converters:IntToVisibilityConverter x:Key="IntToVisibilityConverter"/>
<converters:PluginLoadStatusToVisibilityConverter x:Key="PluginLoadStatusToVisibilityConverter"/>
<converters1:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Page.Resources>
<Grid x:Name="ContentPagePane" Margin="36,40,0,36">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{x:Bind ViewModel.PageTitle}" Margin="0,0,0,36"
Style="{StaticResource TitleTextBlockStyle}"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<SelectorBar x:Name="SelectorBar" Grid.Column="0">
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="SelectionChanged">
<interactivity:InvokeCommandAction Command="{x:Bind ViewModel.SourceSelectedCommand}" CommandParameter="{x:Bind SelectorBar.SelectedItem,Mode=OneWay}"/>
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<SelectorBarItem Text="在线" Icon="{winui:FontIcon Glyph=&#xEDE4;}" IsSelected="True"/>
<SelectorBarItem Text="本地" Icon="{winui:FontIcon Glyph=&#xEDA2;}" />
</SelectorBar>
<Button Grid.Column="2" Content="{winui:FontIcon Glyph=&#xE72C;}" Style="{StaticResource EllipsisButton}"
Command="{x:Bind ViewModel.RefreshCommand}"/>
<Button Grid.Column="3" Content="{winui:FontIcon Glyph=&#xE8B7;}" Style="{StaticResource EllipsisButton}"
Command="{x:Bind ViewModel.OpenPluginFolderCommand}"/>
</Grid>
<Border Style="{StaticResource ListViewBorder}" Grid.Row="1">
<ListView ItemsSource="{x:Bind ViewModel.Plugins,Mode=OneWay}" Width="290" SelectionMode="Single">
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="SelectionChanged">
<interactivity:InvokeCommandAction Command="{x:Bind ViewModel.PluginSelectedCommand}" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate x:DataType="pluginmodels:PluginInfo">
<StackPanel Orientation="Horizontal" Padding="0,6">
<Border CornerRadius="4">
<Image Width="72" Height="72" Source="{x:Bind RealIconPath}" Margin="0,0,8,0"/>
</Border>
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind Manifest.Name}" Margin="0,0,0,4"/>
<TextBlock Text="{x:Bind Manifest.Description}" TextWrapping="Wrap" MaxWidth="185"
Style="{StaticResource CaptionTextBlockStyle}" Foreground="{ThemeResource SystemControlBackgroundBaseMediumBrush}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Border>
</Grid>
<ScrollViewer Grid.Column="1" Padding="16,0,36,0">
<Grid>
<TextBlock Text="请在左侧选择一个插件" Visibility="{x:Bind ViewModel.SelectedPlugin,Mode=OneWay,Converter={StaticResource NullToVisibilityConverter},ConverterParameter=invert}"
VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="{ThemeResource SystemControlBackgroundBaseMediumBrush}"/>
<StackPanel Orientation="Vertical" Visibility="{x:Bind ViewModel.SelectedPlugin,Mode=OneWay,Converter={StaticResource NullToVisibilityConverter}}">
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Height="124" Source="{x:Bind ViewModel.SelectedPlugin.RealIconPath,Mode=OneWay}" HorizontalAlignment="Left"/>
<StackPanel Grid.Column="1" Orientation="Vertical" Margin="12,0,0,0">
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind ViewModel.SelectedPlugin.Manifest.Name,Mode=OneWay}" />
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind ViewModel.SelectedPlugin.Manifest.Version, Mode=OneWay}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" VerticalAlignment="Bottom" Margin="4,0,0,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
<HyperlinkButton Content="项目主页" NavigateUri="{x:Bind ViewModel.SelectedPlugin.Manifest.Url,Mode=OneWay}" Style="{StaticResource TextBlockButtonStyle}"/>
<TextBlock Text="{x:Bind ViewModel.SelectedPlugin.Manifest.Author, Mode=OneWay}" VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
<Button Style="{StaticResource AccentButtonStyle}" Command="{x:Bind ViewModel.RestartCommand}" Visibility="{x:Bind ViewModel.SelectedPlugin.RestartRequired,Mode=OneWay,Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<FontIcon Glyph="&#xE777;" Margin="0,0,8,0"/>
<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}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<FontIcon Glyph="&#xE896;" Margin="0,0,8,0"/>
<TextBlock Text="下载" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Command="{x:Bind ViewModel.UninstallPluginCommand}" Visibility="{x:Bind ViewModel.SelectedPlugin,Mode=OneWay,Converter={StaticResource PluginLoadStatusToVisibilityConverter},ConverterParameter=Loaded}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<FontIcon Glyph="&#xE74D;" Margin="0,0,8,0"/>
<TextBlock Text="卸载" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Command="{x:Bind ViewModel.CancelUninstallPluginCommand}" Visibility="{x:Bind ViewModel.SelectedPlugin.IsUninstalling,Mode=OneWay,Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<FontIcon Glyph="&#xE7A7;" Margin="0,0,8,0"/>
<TextBlock Text="撤销" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<ProgressBar Value="{x:Bind ViewModel.SelectedPlugin.DownloadProgress,Mode=OneWay}" Visibility="{x:Bind ViewModel.SelectedPlugin.DownloadProgress,Mode=OneWay,Converter={StaticResource IntToVisibilityConverter},ConverterParameter=false}"/>
</StackPanel>
</Grid>
<TextBlock Text="{x:Bind ViewModel.SelectedPlugin.Manifest.Description,Mode=OneWay}"/>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,14 @@
using System.Diagnostics;
namespace CRSim.Views;
public sealed partial class PluginManagementPage : Page
{
public PluginManagementPageViewModel ViewModel { get; } = App.AppHost.Services.GetService<PluginManagementPageViewModel>();
public PluginManagementPage()
{
InitializeComponent();
DataContext = ViewModel;
}
}

View File

@@ -41,6 +41,10 @@
<controls:SettingsCard Header="密钥" Description="用于解锁特定功能" HeaderIcon="{winui:FontIcon Glyph=&#xE8D7;}">
<TextBox Width="168" Text="{x:Bind ViewModel.UserKey, Mode=TwoWay}"/>
</controls:SettingsCard>
<controls:SettingsCard Header="插件市场API" Description="插件市场资源源,重启应用生效" HeaderIcon="{winui:FontIcon Glyph=&#xE943;}">
<ComboBox ItemsSource="{x:Bind ViewModel.Apis}" SelectedItem="{x:Bind ViewModel.ApiUri,Mode=TwoWay}"
DisplayMemberPath="Title" Width="168"/>
</controls:SettingsCard>
</StackPanel>
<TextBlock Text="引导屏模拟" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Margin="0,0,0,12"/>
<StackPanel Spacing="4" Margin="0,0,0,24">