Keep dev-3.0 content, discard master changes
92
.github/ISSUE_TEMPLATE/BugReport.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Bug 反馈
|
||||
description: 在使用 CRSim 的过程中遇到了 Bug。
|
||||
title: (将此栏【替换】为你的标题)
|
||||
labels: ["Bug", "待查看", "优先级:无"]
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您进行 Bug 反馈。请在上面的文本框⬆️起一个能够清晰描述您的问题的标题,便于开发者解决您的问题。
|
||||
|
||||
> [!important]
|
||||
> 重要:如果要提出**多个 Bug**,请为每一个 Bug 开一个单独的 issue。
|
||||
>
|
||||
> 如果您不知道如何有效、精准地表述,我们建议您先阅读《提问的智慧》([链接](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md))。
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 检查清单
|
||||
description: 在开始反馈这个问题之前,请先检查:
|
||||
options:
|
||||
- label: 我已更新到最新版  或  测试,并看过 [最新提交](https://github.com/denglihong2007/CRSim/commits/dev/),确认这一 Bug 还没有修复。我也已在 [Issues](https://github.com/denglihong2007/CRSim/issues?q=type%3ABug) 中检索,确认这一 Bug 未被提交过。
|
||||
required: true
|
||||
- label: 我已经仔细阅读过选项里的内容,并且知道这个选项不用勾选。
|
||||
- label: 我已知晓并同意,此处仅用于汇报程序中存在的问题(关于其他非程序本身的问题应当在 Discussion 板块提出)。
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Bug 信息
|
||||
|
||||
描述您遇到的 Bug。您可以附上截图、录屏、堆栈跟踪、日志等材料,便于开发者追踪问题。
|
||||
|
||||
> [!note]
|
||||
> 上传附件时**请优先使用 GitHub 的附件系统上传附件**,将需要上传的附件粘贴或拖动到撰写区域即可上传。多个文件可打包为 zip 格式后上传。**尽量避免使用需要登陆或安装客户端才能接收附件的网盘(如\*度网盘、\*克网盘、1\*3网盘等)上传附件**,以避免开发者因不具有对应网盘客户端或账户而延误问题诊断。
|
||||
- type: textarea
|
||||
id: excepted
|
||||
attributes:
|
||||
label: 期望的行为
|
||||
description: 详细的描述你期望发生的行为,突出与目前(可能不正确的)行为的不同。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: 实际结果
|
||||
description: 实际发生的行为。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: |
|
||||
详细描述要怎么操作才能再次触发这个 Bug。
|
||||
placeholder: |
|
||||
1. 首先……
|
||||
2. 然后……
|
||||
3. ……
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: stacktrace
|
||||
attributes:
|
||||
label: 堆栈跟踪(可选)
|
||||
description: 如果在遇到这个 Bug 时发生了崩溃(弹出崩溃提示),或者产生了错误日志,请将产生的堆栈跟踪信息粘贴到此处,便于开发者定位 Bug。
|
||||
render: shell
|
||||
- type: input
|
||||
id: app_version
|
||||
attributes:
|
||||
label: 应用版本
|
||||
description: 您当前使用的 CRSim 版本号,可以在【设置】->【关于 CRSim】中查看。
|
||||
placeholder: 如:2.3.102.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: 操作系统版本
|
||||
description: |
|
||||
您当前使用的 Windows 操作系统版本,可以运行 `winver.exe` 查看。
|
||||
如果您不能肯定此 Bug 与操作系统无关,请填写此项。
|
||||
placeholder: 如:10.0.22000.0
|
||||
- type: checkboxes
|
||||
id: revision
|
||||
attributes:
|
||||
label: 最后一步
|
||||
description: 回顾您的回答
|
||||
options:
|
||||
- label: 我认为上述的描述已经足以详细,以允许开发人员能复现该问题。如果我的 issue 没有按照上述的要求填写,可能会被无条件关闭。
|
||||
required: true
|
||||
55
.github/ISSUE_TEMPLATE/FeatureRequest.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: 功能请求
|
||||
description: 提出一项新的功能。
|
||||
title: (将此栏【替换】为你的标题)
|
||||
labels: ["新功能", "待查看", "优先级:无"]
|
||||
type: 特性
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您贡献您的想法。请在上面的文本框⬆️起一个能够清晰概括此功能的标题,便于开发者了解您的想法。
|
||||
|
||||
> [!important]
|
||||
> 重要:如果您想要提出 **多项功能**,请为每一个功能开一个单独的 issue。
|
||||
>
|
||||
> 由于开发者时间精力有限,加上已经积压了大量的功能请求,提交的功能请求可能需要比较久的时间才能实现。
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 检查清单
|
||||
description: 在开始反馈这个问题之前,请先检查:
|
||||
options:
|
||||
- label: 我已将本应用更新到最新测试版 ,并看过 [最新提交](https://github.com/denglihong2007/CRSim/commits/dev/),确认我想要的功能还没有实现。我也已在 [Issues](https://github.com/denglihong2007/CRSim/issues?q=-label%3ABug) 确认我想要的功能未被提交过。
|
||||
required: true
|
||||
- label: 我认为我想要的功能是用户广泛需要的。
|
||||
required: true
|
||||
- label: 我已经仔细阅读过选项里的内容,并且知道这个选项不用勾选。
|
||||
- label: 我已知晓由于开发者时间精力有限,加上已经积压了大量的功能请求,我提交的功能请求可能需要比较久的时间才能实现。
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!note]
|
||||
> 上传附件时**请优先使用 GitHub 的附件系统上传附件**,将需要上传的附件粘贴或拖动到撰写区域即可上传。多个文件可打包为 zip 格式后上传。**尽量避免使用需要登陆或安装客户端才能接收附件的网盘(如\*度网盘、\*克网盘、1\*3网盘等)上传附件**,以避免开发者因不具有对应网盘客户端或账户而延误问题诊断。
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: 背景与动机
|
||||
description: 为什么你需要这个功能?这个功能对你和其他用户有多重要?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: 想要实现或优化的功能
|
||||
description: 详细地描述一下你想要的功能,描述的越具体,采纳的可能性越高。
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: revision
|
||||
attributes:
|
||||
label: 最后一步
|
||||
description: 回顾您的回答
|
||||
options:
|
||||
- label: 我认为上述的描述已经足以详细,以便开发人员理解您想要的功能。如果我的 issue 没有按照上述的要求填写,可能会被无条件关闭。
|
||||
required: true
|
||||
@@ -1,8 +1,7 @@
|
||||
using CRSim.Core.Models;
|
||||
using static System.Collections.Specialized.BitVector32;
|
||||
using Section = CRSim.Core.Models.Section;
|
||||
|
||||
namespace CRSim.Core.Services
|
||||
namespace CRSim.Core.Abstractions
|
||||
{
|
||||
public interface IDatabaseService
|
||||
{
|
||||
@@ -13,7 +12,7 @@ namespace CRSim.Core.Services
|
||||
void AddStationByName(string name);
|
||||
void AddStation(Station station);
|
||||
Task ImportStationFrom7D(string path);
|
||||
void AddTrainNumber(string number);
|
||||
void AddTrainNumber(TrainNumber trainNumber);
|
||||
void DeleteStation(Station station);
|
||||
void DeleteTrainNumber(TrainNumber trainNumber);
|
||||
void UpdateStation(string stationName,Station station);
|
||||
7
CRSim.Core/Abstractions/IHasTimeService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace CRSim.Core.Abstractions
|
||||
{
|
||||
public interface IHasTimeService
|
||||
{
|
||||
ITimeService TimeService { get; set; }
|
||||
}
|
||||
}
|
||||
13
CRSim.Core/Abstractions/INetworkService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using CRSim.Core.Models;
|
||||
using CRSim.Core.Models.Plugin;
|
||||
|
||||
namespace CRSim.Core.Abstractions
|
||||
{
|
||||
public interface INetworkService
|
||||
{
|
||||
Task<List<TrainStop>?> GetTimeTableAsync(string number);
|
||||
Task<List<TrainStop>?> GetTrainNumbersAsync(string name);
|
||||
List<PluginManifest>? GetOnlinePlugins(string uri);
|
||||
Task<UpdateInfo?> GetUpdateAsync(string uri);
|
||||
}
|
||||
}
|
||||
31
CRSim.Core/Abstractions/IPluginService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using CRSim.Core.Models.Plugin;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace CRSim.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 插件服务。用于管理应用各插件的加载和设置。
|
||||
/// </summary>
|
||||
public interface IPluginService
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件包文件扩展名。
|
||||
/// </summary>
|
||||
public static readonly string PluginPackageExtension = ".crsp";
|
||||
|
||||
internal static List<PluginInfo> LoadedPluginsInternal { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 已加载的插件信息列表。
|
||||
/// </summary>
|
||||
public static List<PluginInfo> LoadedPlugins => LoadedPluginsInternal;
|
||||
|
||||
internal static List<PluginInfo> OnlinePluginsInternal { get; } = [];
|
||||
|
||||
public static List<PluginInfo> OnlinePlugins => OnlinePluginsInternal;
|
||||
|
||||
Task InstallPluginOnlineAsync(PluginInfo plugin);
|
||||
Task InstallPluginLocalAsync(string filePath);
|
||||
Task PackPluginAsync(PluginInfo plugin, string filePath);
|
||||
Task LoadOnlinePluginsAsync();
|
||||
}
|
||||
@@ -5,10 +5,11 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CRSim.Core.Services
|
||||
namespace CRSim.Core.Abstractions
|
||||
{
|
||||
public interface ISettingsService
|
||||
{
|
||||
void LoadSettings();
|
||||
void SaveSettings();
|
||||
Settings GetSettings();
|
||||
}
|
||||
17
CRSim.Core/Abstractions/ITimeService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace CRSim.Core.Abstractions
|
||||
{
|
||||
public interface ITimeService
|
||||
{
|
||||
event EventHandler OneSecondElapsed;
|
||||
|
||||
event EventHandler RefreshSecondsElapsed;
|
||||
|
||||
void Start();
|
||||
|
||||
DateTime GetDateTimeNow();
|
||||
|
||||
DateTime SimulateTime { get; set; } // 初始模拟时间
|
||||
|
||||
double Speed { get; set; } // 模拟倍速
|
||||
}
|
||||
}
|
||||
21
CRSim.Core/Abstractions/PluginBase.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using CRSim.Core.Models.Plugin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace CRSim.Core.Abstractions;
|
||||
public abstract class PluginBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前插件的信息
|
||||
/// </summary>
|
||||
public PluginInfo Info { get; internal set; } = null!;
|
||||
|
||||
public abstract Type ViewModel { get; }
|
||||
public abstract Type View { get; }
|
||||
/// <summary>
|
||||
/// 初始化插件。一般在这个方法中完成插件的各项服务的注册。
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="services"></param>
|
||||
public abstract void Initialize(HostBuilderContext context, IServiceCollection services);
|
||||
}
|
||||
3
CRSim.Core/Attributes/PluginEntrance.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace CRSim.Core.Attributes;
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PluginEntrance : Attribute;
|
||||
@@ -1,10 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<PackageId>CRSim.Core</PackageId>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Downloader" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
24
CRSim.Core/Enums/PluginLoadStatus.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace CRSim.Core.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 表示插件加载状态。
|
||||
/// </summary>
|
||||
public enum PluginLoadStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件未加载
|
||||
/// </summary>
|
||||
NotLoaded,
|
||||
/// <summary>
|
||||
/// 插件成功加载。
|
||||
/// </summary>
|
||||
Loaded,
|
||||
/// <summary>
|
||||
/// 插件已禁用。
|
||||
/// </summary>
|
||||
Disabled,
|
||||
/// <summary>
|
||||
/// 插件加载时出现错误。
|
||||
/// </summary>
|
||||
Error,
|
||||
}
|
||||
8
CRSim.Core/Models/CommandLineOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace CRSim.Core.Models
|
||||
{
|
||||
public class CommandLineOptions
|
||||
{
|
||||
public string? ExternalPluginPath { get; set; }
|
||||
public bool Debug { get; set; }
|
||||
}
|
||||
}
|
||||
20
CRSim.Core/Models/JsonSerializationContext.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using CRSim.Core.Models.Plugin;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CRSim.Core.Models
|
||||
{
|
||||
[JsonSerializable(typeof(Json))]
|
||||
[JsonSerializable(typeof(UpdateInfo))]
|
||||
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public partial class JsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(List<PluginManifest>))]
|
||||
[JsonSerializable(typeof(StyleInfo))]
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
public partial class JsonContextWithCamelCase : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
86
CRSim.Core/Models/Plugin/PluginInfo.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.Core.Enums;
|
||||
|
||||
namespace CRSim.Core.Models.Plugin;
|
||||
public partial class PluginInfo : ObservableRecipient
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int _downloadProgress = 0;
|
||||
|
||||
public bool IsAvailableOnMarket => IPluginService.OnlinePlugins.Any(x => x.Manifest.Id == Manifest.Id);
|
||||
|
||||
public bool IsUpdateAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsAvailableOnMarket)
|
||||
{
|
||||
if(IPluginService.OnlinePlugins.Where(x => x.Manifest.Id == Manifest.Id).FirstOrDefault() is PluginInfo onlinePlugin)
|
||||
{
|
||||
return onlinePlugin.Manifest.Version != Manifest.Version;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private PluginManifest _manifest = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _restartRequired = false;
|
||||
|
||||
public PluginLoadStatus LoadStatus { get; internal set; } = PluginLoadStatus.NotLoaded;
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return !File.Exists(Path.Combine(PluginFolderPath, ".disabled"));
|
||||
}
|
||||
set
|
||||
{
|
||||
var path = Path.Combine(PluginFolderPath, ".disabled");
|
||||
RestartRequired = true;
|
||||
if (value)
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(path, "");
|
||||
}
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string PluginFolderPath { get; internal set; } = "";
|
||||
public string RealIconPath { get; set; } = "";
|
||||
public bool IsUninstalling
|
||||
{
|
||||
get
|
||||
{
|
||||
return File.Exists(Path.Combine(PluginFolderPath, ".uninstall"));
|
||||
}
|
||||
set
|
||||
{
|
||||
var path = Path.Combine(PluginFolderPath, ".uninstall");
|
||||
if (value)
|
||||
{
|
||||
RestartRequired = true;
|
||||
File.WriteAllText(path, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
RestartRequired = false;
|
||||
File.Delete(path);
|
||||
}
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public Exception? Exception { get; internal set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private StyleInfo? styleInfo;
|
||||
}
|
||||
53
CRSim.Core/Models/Plugin/PluginManifest.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace CRSim.Core.Models.Plugin;
|
||||
|
||||
/// <summary>
|
||||
/// 插件元数据
|
||||
/// </summary>
|
||||
public class PluginManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// 入口程序集。加载插件时,将在此入口程序集中搜索插件类。
|
||||
/// </summary>
|
||||
/// <example>MyPlugin.dll</example>
|
||||
public string EntranceAssembly { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件显示名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件ID。
|
||||
/// </summary>
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件自述。
|
||||
/// </summary>
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 项目 Url
|
||||
/// </summary>
|
||||
public string Url { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public string Version { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件目标 CRSim 版本
|
||||
/// </summary>
|
||||
public string ApiVersion { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
public string Author { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 插件类型
|
||||
/// </summary>
|
||||
public string Type { get; set; } = "";
|
||||
}
|
||||
@@ -5,8 +5,6 @@ namespace CRSim.Core.Models
|
||||
{
|
||||
public class Settings
|
||||
{
|
||||
public TimeSpan TimeOffset { get; set; } = TimeSpan.Zero;
|
||||
|
||||
public TimeSpan DepartureCheckInAdvanceDuration { get; set; } = TimeSpan.FromMinutes(20);
|
||||
|
||||
public TimeSpan PassingCheckInAdvanceDuration { get; set; } = TimeSpan.FromMinutes(10);
|
||||
@@ -21,7 +19,7 @@ namespace CRSim.Core.Models
|
||||
/// </summary>
|
||||
public TimeSpan StopDisplayFromArrivalDuration { get; set; } = TimeSpan.FromMinutes(10);
|
||||
public TimeSpan StopCheckInAdvanceDuration { get; set; } = TimeSpan.FromMinutes(2);
|
||||
|
||||
public string ApiUri { get; set; } = "http://47.122.74.193:25565";
|
||||
public int MaxPages { get; set; } = 3;
|
||||
public int SwitchPageSeconds { get; set; } = 20;
|
||||
public string UserKey { get; set; } = "";
|
||||
|
||||
9
CRSim.Core/Models/Simulator/TicketCheck.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CRSim.Core.Models
|
||||
{
|
||||
public class TicketCheck
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; }
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,10 @@ namespace CRSim.Core.Models
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
public TimeSpan? DepartureTime { get; set; }
|
||||
|
||||
public List<string>? TicketChecks { get; set; }
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
public TimeSpan? Status { get; set; } = TimeSpan.Zero;
|
||||
|
||||
public string? WaitingArea { get; set; }
|
||||
public List<Guid>? TicketCheckIds { get; set; }
|
||||
|
||||
public string? Origin { get; set; }
|
||||
|
||||
10
CRSim.Core/Models/Simulator/WaitingArea.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace CRSim.Core.Models
|
||||
{
|
||||
public class WaitingArea
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public ObservableCollection<TicketCheck> TicketChecks { get; set; } = [];
|
||||
}
|
||||
}
|
||||
11
CRSim.Core/Models/StyleInfo.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CRSim.Core.Models.Plugin;
|
||||
|
||||
namespace CRSim.Core.Models
|
||||
{
|
||||
public class StyleInfo
|
||||
{
|
||||
public string[] Parameters { get; set; } = [];
|
||||
public string Type { get; set; } = "";
|
||||
public string Region { get; set; } = "";
|
||||
}
|
||||
}
|
||||
170
CRSim.Core/Models/UpdateInfo.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CRSim.Core.Models
|
||||
{
|
||||
public class UpdateInfo
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
[JsonPropertyName("assets_url")]
|
||||
public string? AssetsUrl { get; set; }
|
||||
[JsonPropertyName("upload_url")]
|
||||
public string? UploadUrl { get; set; }
|
||||
[JsonPropertyName("html_url")]
|
||||
public string? HtmlUrl { get; set; }
|
||||
[JsonPropertyName("id")]
|
||||
public long? Id { get; set; }
|
||||
[JsonPropertyName("author")]
|
||||
public AuthorEntity? Author { get; set; }
|
||||
[JsonPropertyName("node_id")]
|
||||
public string? NodeId { get; set; }
|
||||
[JsonPropertyName("tag_name")]
|
||||
public string? TagName { get; set; }
|
||||
[JsonPropertyName("target_commitish")]
|
||||
public string? TargetCommitish { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
[JsonPropertyName("draft")]
|
||||
public bool? Draft { get; set; }
|
||||
[JsonPropertyName("immutable")]
|
||||
public bool? Immutable { get; set; }
|
||||
[JsonPropertyName("prerelease")]
|
||||
public bool? Prerelease { get; set; }
|
||||
[JsonPropertyName("created_at")]
|
||||
public string? CreatedAt { get; set; }
|
||||
[JsonPropertyName("updated_at")]
|
||||
public string? UpdatedAt { get; set; }
|
||||
[JsonPropertyName("published_at")]
|
||||
public string? PublishedAt { get; set; }
|
||||
[JsonPropertyName("assets")]
|
||||
public List<AssetsEntity?>? Assets { get; set; }
|
||||
[JsonPropertyName("tarball_url")]
|
||||
public string? TarballUrl { get; set; }
|
||||
[JsonPropertyName("zipball_url")]
|
||||
public string? ZipballUrl { get; set; }
|
||||
[JsonPropertyName("body")]
|
||||
public string? Body { get; set; }
|
||||
[JsonPropertyName("mentions_count")]
|
||||
public long? MentionsCount { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class AuthorEntity
|
||||
{
|
||||
[JsonPropertyName("login")]
|
||||
public string? Login { get; set; }
|
||||
[JsonPropertyName("id")]
|
||||
public long? Id { get; set; }
|
||||
[JsonPropertyName("node_id")]
|
||||
public string? NodeId { get; set; }
|
||||
[JsonPropertyName("avatar_url")]
|
||||
public string? AvatarUrl { get; set; }
|
||||
[JsonPropertyName("gravatar_id")]
|
||||
public string? GravatarId { get; set; }
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
[JsonPropertyName("html_url")]
|
||||
public string? HtmlUrl { get; set; }
|
||||
[JsonPropertyName("followers_url")]
|
||||
public string? FollowersUrl { get; set; }
|
||||
[JsonPropertyName("following_url")]
|
||||
public string? FollowingUrl { get; set; }
|
||||
[JsonPropertyName("gists_url")]
|
||||
public string? GistsUrl { get; set; }
|
||||
[JsonPropertyName("starred_url")]
|
||||
public string? StarredUrl { get; set; }
|
||||
[JsonPropertyName("subscriptions_url")]
|
||||
public string? SubscriptionsUrl { get; set; }
|
||||
[JsonPropertyName("organizations_url")]
|
||||
public string? OrganizationsUrl { get; set; }
|
||||
[JsonPropertyName("repos_url")]
|
||||
public string? ReposUrl { get; set; }
|
||||
[JsonPropertyName("events_url")]
|
||||
public string? EventsUrl { get; set; }
|
||||
[JsonPropertyName("received_events_url")]
|
||||
public string? ReceivedEventsUrl { get; set; }
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
[JsonPropertyName("user_view_type")]
|
||||
public string? UserViewType { get; set; }
|
||||
[JsonPropertyName("site_admin")]
|
||||
public bool? SiteAdmin { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class AssetsEntity
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
[JsonPropertyName("id")]
|
||||
public long? Id { get; set; }
|
||||
[JsonPropertyName("node_id")]
|
||||
public string? NodeId { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
[JsonPropertyName("label")]
|
||||
public string? Label { get; set; }
|
||||
[JsonPropertyName("uploader")]
|
||||
public UploaderEntity? Uploader { get; set; }
|
||||
[JsonPropertyName("content_type")]
|
||||
public string? ContentType { get; set; }
|
||||
[JsonPropertyName("state")]
|
||||
public string? State { get; set; }
|
||||
[JsonPropertyName("size")]
|
||||
public long? Size { get; set; }
|
||||
[JsonPropertyName("digest")]
|
||||
public string? Digest { get; set; }
|
||||
[JsonPropertyName("download_count")]
|
||||
public long? DownloadCount { get; set; }
|
||||
[JsonPropertyName("created_at")]
|
||||
public string? CreatedAt { get; set; }
|
||||
[JsonPropertyName("updated_at")]
|
||||
public string? UpdatedAt { get; set; }
|
||||
[JsonPropertyName("browser_download_url")]
|
||||
public string? BrowserDownloadUrl { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class UploaderEntity
|
||||
{
|
||||
[JsonPropertyName("login")]
|
||||
public string? Login { get; set; }
|
||||
[JsonPropertyName("id")]
|
||||
public long? Id { get; set; }
|
||||
[JsonPropertyName("node_id")]
|
||||
public string? NodeId { get; set; }
|
||||
[JsonPropertyName("avatar_url")]
|
||||
public string? AvatarUrl { get; set; }
|
||||
[JsonPropertyName("gravatar_id")]
|
||||
public string? GravatarId { get; set; }
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
[JsonPropertyName("html_url")]
|
||||
public string? HtmlUrl { get; set; }
|
||||
[JsonPropertyName("followers_url")]
|
||||
public string? FollowersUrl { get; set; }
|
||||
[JsonPropertyName("following_url")]
|
||||
public string? FollowingUrl { get; set; }
|
||||
[JsonPropertyName("gists_url")]
|
||||
public string? GistsUrl { get; set; }
|
||||
[JsonPropertyName("starred_url")]
|
||||
public string? StarredUrl { get; set; }
|
||||
[JsonPropertyName("subscriptions_url")]
|
||||
public string? SubscriptionsUrl { get; set; }
|
||||
[JsonPropertyName("organizations_url")]
|
||||
public string? OrganizationsUrl { get; set; }
|
||||
[JsonPropertyName("repos_url")]
|
||||
public string? ReposUrl { get; set; }
|
||||
[JsonPropertyName("events_url")]
|
||||
public string? EventsUrl { get; set; }
|
||||
[JsonPropertyName("received_events_url")]
|
||||
public string? ReceivedEventsUrl { get; set; }
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
[JsonPropertyName("user_view_type")]
|
||||
public string? UserViewType { get; set; }
|
||||
[JsonPropertyName("site_admin")]
|
||||
public bool? SiteAdmin { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CRSim.Core.Models
|
||||
{
|
||||
public class WaitingArea
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<string> TicketChecks { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using CRSim.Core.Models;
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.Core.Models;
|
||||
using CRSim.Core.Utils;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -6,23 +8,9 @@ namespace CRSim.Core.Services
|
||||
{
|
||||
public class DatabaseService : IDatabaseService
|
||||
{
|
||||
private readonly string _jsonFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CRSim");
|
||||
private List<Station> _stations;
|
||||
private List<TrainNumber> _trainNumbers;
|
||||
|
||||
private readonly JsonSerializerOptions options = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
public DatabaseService()
|
||||
{
|
||||
if (!Directory.Exists(_jsonFilePath)) Directory.CreateDirectory(_jsonFilePath);
|
||||
_jsonFilePath += "\\data.json";
|
||||
ImportData(_jsonFilePath);
|
||||
}
|
||||
|
||||
public List<Station> GetAllStations()
|
||||
{
|
||||
return _stations;
|
||||
@@ -32,21 +20,10 @@ namespace CRSim.Core.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(jsonFilePath).Replace("StationStop", "TrainStop");// 修复旧版本的数据
|
||||
var data = JsonSerializer.Deserialize<Json>(json);
|
||||
var json = File.ReadAllText(jsonFilePath).Replace("StationStop", "TrainStop");
|
||||
var data = JsonSerializer.Deserialize<Json>(json,JsonContext.Default.Json);
|
||||
_stations = data.Stations;
|
||||
_trainNumbers = data.TrainNumbers;
|
||||
|
||||
foreach (var station in _stations)
|
||||
{
|
||||
foreach(var trainStop in station.TrainStops)
|
||||
{
|
||||
if (trainStop.WaitingArea == null)
|
||||
{
|
||||
trainStop.WaitingArea = station.WaitingAreas.Where(x => x.TicketChecks.Contains(trainStop.TicketChecks[0])).FirstOrDefault().Name;
|
||||
}
|
||||
}
|
||||
}// 修复旧版本的数据
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -109,8 +86,8 @@ namespace CRSim.Core.Services
|
||||
{
|
||||
Stations = _stations,
|
||||
TrainNumbers = _trainNumbers
|
||||
}, options);
|
||||
await File.WriteAllTextAsync(_jsonFilePath, json);
|
||||
},JsonContext.Default.Json);
|
||||
await File.WriteAllTextAsync(AppPaths.ConfigFilePath, json);
|
||||
}
|
||||
|
||||
public async Task ExportData(string p)
|
||||
@@ -119,7 +96,7 @@ namespace CRSim.Core.Services
|
||||
{
|
||||
Stations = _stations,
|
||||
TrainNumbers = _trainNumbers
|
||||
}, options);
|
||||
}, JsonContext.Default.Json);
|
||||
await File.WriteAllTextAsync(p, json);
|
||||
}
|
||||
|
||||
@@ -128,12 +105,9 @@ namespace CRSim.Core.Services
|
||||
return _trainNumbers;
|
||||
}
|
||||
|
||||
public void AddTrainNumber(string number)
|
||||
public void AddTrainNumber(TrainNumber trainNumber)
|
||||
{
|
||||
_trainNumbers.Add(new TrainNumber()
|
||||
{
|
||||
Number = number
|
||||
});
|
||||
_trainNumbers.Add(trainNumber);
|
||||
}
|
||||
|
||||
public async Task ImportStationFrom7D(string path)
|
||||
@@ -169,9 +143,9 @@ namespace CRSim.Core.Services
|
||||
|
||||
foreach (var ticketCheckName in ticketCheck.Split('|'))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ticketCheckName) && !waitingArea.TicketChecks.Contains(ticketCheckName))
|
||||
if (!string.IsNullOrWhiteSpace(ticketCheckName) && !waitingArea.TicketChecks.Any(x => x.Name == ticketCheckName))
|
||||
{
|
||||
waitingArea.TicketChecks.Add(ticketCheckName);
|
||||
waitingArea.TicketChecks.Add(new TicketCheck { Name = ticketCheckName });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,10 +173,9 @@ namespace CRSim.Core.Services
|
||||
Number = data[0],
|
||||
Terminal = data[3],
|
||||
Origin = data[2],
|
||||
TicketChecks = [.. ticketCheck.Split('|')],
|
||||
TicketCheckIds = [.. waitingArea.TicketChecks.Select(x=>x.Id)],
|
||||
ArrivalTime = arrivalTime,
|
||||
DepartureTime = departureTime,
|
||||
WaitingArea = waitingAreaName,
|
||||
Platform = platform,
|
||||
Length = data[0].StartsWith('G') || data[0].StartsWith('D') || data[0].StartsWith('C') ? Math.Abs(data[0].GetHashCode()) % 3 == 0 ? 8 : 16 : 18,
|
||||
Landmark = data[8] + "色" ?? null,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using CRSim.Core.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CRSim.Core.Services
|
||||
{
|
||||
public interface INetworkService
|
||||
{
|
||||
Task<List<TrainStop>?> GetTimeTableAsync(string number);
|
||||
Task<List<TrainStop>> GetTrainNumbersAsync(string name);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Timers;
|
||||
|
||||
namespace CRSim.Core.Services
|
||||
{
|
||||
public interface ITimeService
|
||||
{
|
||||
event EventHandler OneSecondElapsed; // 1秒触发的事件
|
||||
event EventHandler RefreshSecondsElapsed; // 20秒触发的事件
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
DateTime GetDateTimeNow();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
using CRSim.Core.Models;
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.Core.Models;
|
||||
using CRSim.Core.Models.Plugin;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CRSim.Core.Services
|
||||
{
|
||||
public class NetworkService(IDatabaseService databaseService) : INetworkService
|
||||
public class NetworkService() : INetworkService
|
||||
{
|
||||
private readonly IDatabaseService _databaseService = databaseService;
|
||||
|
||||
public async Task<List<TrainStop>?> GetTimeTableAsync(string number)
|
||||
{
|
||||
try
|
||||
@@ -61,10 +61,10 @@ namespace CRSim.Core.Services
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static TimeSpan? ParseTime(string timeStr)
|
||||
{
|
||||
timeStr = string.Concat(timeStr.AsSpan(0, 2), ":", timeStr.AsSpan(2, 2));
|
||||
@@ -77,58 +77,94 @@ namespace CRSim.Core.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<List<TrainStop>> GetTrainNumbersAsync(string name)
|
||||
public async Task<List<TrainStop>?> GetTrainNumbersAsync(string name)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
var stations = (await client.GetStringAsync("https://kyfw.12306.cn/otn/resources/js/framework/station_name.js")).Split("|||");
|
||||
string tel = "";
|
||||
foreach (string station in stations)
|
||||
try
|
||||
{
|
||||
if (station.StartsWith("';")) continue;
|
||||
if (station.Split("|")[1] == name)
|
||||
var client = new HttpClient();
|
||||
var stations = (await client.GetStringAsync("https://kyfw.12306.cn/otn/resources/js/framework/station_name.js")).Split("|||");
|
||||
string tel = "";
|
||||
foreach (string station in stations)
|
||||
{
|
||||
tel = station.Split("|")[2];
|
||||
if (station.StartsWith("';")) continue;
|
||||
if (station.Split("|")[1] == name)
|
||||
{
|
||||
tel = station.Split("|")[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tel == "")
|
||||
{
|
||||
return [];
|
||||
}
|
||||
var baseAddress = new Uri("https://mobile.12306.cn/wxxcx/wechat/bigScreen/queryTrainByStation");
|
||||
var cookieContainer = new CookieContainer();
|
||||
cookieContainer.Add(baseAddress, new Cookie("BIGipServerweixin_xiaochengxu", "2028077578.5670.0000"));
|
||||
var handler = new HttpClientHandler() { CookieContainer = cookieContainer, UseCookies = true };
|
||||
if (tel == "")
|
||||
{
|
||||
return [];
|
||||
}
|
||||
var baseAddress = new Uri("https://mobile.12306.cn/wxxcx/wechat/bigScreen/queryTrainByStation");
|
||||
var cookieContainer = new CookieContainer();
|
||||
cookieContainer.Add(baseAddress, new Cookie("BIGipServerweixin_xiaochengxu", "2028077578.5670.0000"));
|
||||
var handler = new HttpClientHandler() { CookieContainer = cookieContainer, UseCookies = true };
|
||||
|
||||
HttpContent content = new FormUrlEncodedContent(new Dictionary<string, string>()
|
||||
{
|
||||
{"train_station_code",tel },
|
||||
{"train_start_date",DateTime.Now.ToString("yyyyMMdd")}
|
||||
});
|
||||
client = new HttpClient(handler) { BaseAddress = baseAddress };
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090c11)XWEB/11581");
|
||||
var response = await client.PostAsync(baseAddress, content);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
if (json.Contains("操作失败,请稍后重试")) return [];
|
||||
var list = JsonDocument.Parse(json).RootElement.GetProperty("data").EnumerateArray();
|
||||
if (!list.Any()) return [];
|
||||
List<TrainStop> trainStops = [];
|
||||
foreach (var item in list)
|
||||
{
|
||||
var arriveTimeStr = item.GetProperty("arrive_time").GetString();
|
||||
var startTimeStr = item.GetProperty("start_time").GetString();
|
||||
TimeSpan? arriveTime = null;
|
||||
TimeSpan? startTime = null;
|
||||
if (arriveTimeStr != "----")
|
||||
HttpContent content = new FormUrlEncodedContent(new Dictionary<string, string>()
|
||||
{
|
||||
arriveTime = TimeSpan.Parse(arriveTimeStr);
|
||||
}
|
||||
if (startTimeStr != arriveTimeStr)
|
||||
{"train_station_code",tel },
|
||||
{"train_start_date",DateTime.Now.ToString("yyyyMMdd")}
|
||||
});
|
||||
client = new HttpClient(handler) { BaseAddress = baseAddress };
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090c11)XWEB/11581");
|
||||
var response = await client.PostAsync(baseAddress, content);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
if (json.Contains("操作失败,请稍后重试")) return [];
|
||||
var list = JsonDocument.Parse(json).RootElement.GetProperty("data").EnumerateArray();
|
||||
if (!list.Any()) return [];
|
||||
List<TrainStop> trainStops = [];
|
||||
foreach (var item in list)
|
||||
{
|
||||
startTime = TimeSpan.Parse(startTimeStr);
|
||||
var arriveTimeStr = item.GetProperty("arrive_time").GetString();
|
||||
var startTimeStr = item.GetProperty("start_time").GetString();
|
||||
TimeSpan? arriveTime = null;
|
||||
TimeSpan? startTime = null;
|
||||
if (arriveTimeStr != "----")
|
||||
{
|
||||
arriveTime = TimeSpan.Parse(arriveTimeStr);
|
||||
}
|
||||
if (startTimeStr != arriveTimeStr)
|
||||
{
|
||||
startTime = TimeSpan.Parse(startTimeStr);
|
||||
}
|
||||
trainStops.Add(new TrainStop() { Number = item.GetProperty("station_train_code").ToString(), Terminal = item.GetProperty("end_station_name").ToString(), Origin = item.GetProperty("start_station_name").ToString(), ArrivalTime = arriveTime, DepartureTime = startTime });
|
||||
}
|
||||
trainStops.Add(new TrainStop() { Number = item.GetProperty("station_train_code").ToString(), Terminal = item.GetProperty("end_station_name").ToString(), Origin = item.GetProperty("start_station_name").ToString(), ArrivalTime = arriveTime, DepartureTime = startTime });
|
||||
return trainStops;
|
||||
}
|
||||
return trainStops;
|
||||
catch
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<PluginManifest>? GetOnlinePlugins(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new HttpClient();
|
||||
var response = client.GetStringAsync(url).Result;
|
||||
return JsonSerializer.Deserialize(response, JsonContextWithCamelCase.Default.ListPluginManifest);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<UpdateInfo?> GetUpdateAsync(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("CRSim");
|
||||
var response = await client.GetStringAsync(uri);
|
||||
return JsonSerializer.Deserialize(response, JsonContext.Default.UpdateInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
267
CRSim.Core/Services/PluginService.cs
Normal file
@@ -0,0 +1,267 @@
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.Core.Attributes;
|
||||
using CRSim.Core.Enums;
|
||||
using CRSim.Core.Models;
|
||||
using CRSim.Core.Models.Plugin;
|
||||
using CRSim.Core.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Windows.System;
|
||||
using Downloader;
|
||||
|
||||
namespace CRSim.Core.Services;
|
||||
|
||||
public class PluginService : IPluginService
|
||||
{
|
||||
public static readonly string PluginManifestFileName = "manifest.json";
|
||||
public static readonly string StyleInfoFileName = "style.json";
|
||||
|
||||
private string IndexUrl => $"{_settings.ApiUri}/GetFile?fileName=plugins.json";
|
||||
|
||||
public static void InitializePlugins(HostBuilderContext context, IServiceCollection services,string externalPluginPath)
|
||||
{
|
||||
Console.WriteLine("开始加载插件");
|
||||
if (!Directory.Exists(AppPaths.PluginsRootPath))
|
||||
{
|
||||
Directory.CreateDirectory(AppPaths.PluginsRootPath);
|
||||
}
|
||||
|
||||
var pluginDirs = Directory.EnumerateDirectories(AppPaths.PluginsRootPath);
|
||||
if(!string.IsNullOrEmpty(externalPluginPath))
|
||||
{
|
||||
pluginDirs = pluginDirs.Append(externalPluginPath);
|
||||
}
|
||||
|
||||
foreach (var pluginDir in pluginDirs)
|
||||
{
|
||||
Console.WriteLine($"当前插件搜寻目录: {pluginDir}");
|
||||
if (string.IsNullOrWhiteSpace(pluginDir))
|
||||
continue;
|
||||
var manifestPath = Path.Combine(pluginDir, PluginManifestFileName);
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var manifestYaml = File.ReadAllText(manifestPath);
|
||||
PluginManifest manifest = new();
|
||||
try
|
||||
{
|
||||
manifest = JsonSerializer.Deserialize(manifestYaml,JsonContextWithCamelCase.Default.PluginManifest);
|
||||
Console.WriteLine("成功解析JSON文件: " + manifestPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("JSON解析失败");
|
||||
Console.WriteLine(ex.ToString());
|
||||
continue;
|
||||
}
|
||||
Console.WriteLine("找到插件: " + manifest.Id);
|
||||
var info = new PluginInfo
|
||||
{
|
||||
Manifest = manifest,
|
||||
PluginFolderPath = Path.GetFullPath(pluginDir),
|
||||
RealIconPath = Path.Combine(Path.GetFullPath(pluginDir), "icon.png"),
|
||||
StyleInfo = manifest.Type == "ScreenStyle" ? JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(Path.GetFullPath(pluginDir), StyleInfoFileName)),JsonContextWithCamelCase.Default.StyleInfo) : null,
|
||||
};
|
||||
if (info.IsUninstalling)
|
||||
{
|
||||
Directory.Delete(pluginDir, true);
|
||||
continue;
|
||||
}
|
||||
if (!info.IsEnabled)
|
||||
{
|
||||
info.LoadStatus = PluginLoadStatus.Disabled;
|
||||
}
|
||||
IPluginService.LoadedPluginsInternal.Add(info);
|
||||
}
|
||||
|
||||
foreach (var info in IPluginService.LoadedPluginsInternal)
|
||||
{
|
||||
if(info.LoadStatus == PluginLoadStatus.Disabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var manifest = info.Manifest;
|
||||
var pluginDir = info.PluginFolderPath;
|
||||
try
|
||||
{
|
||||
Console.WriteLine("尝试加载插件: " + manifest.Id);
|
||||
var fullPath = Path.GetFullPath(Path.Combine(pluginDir, manifest.EntranceAssembly));
|
||||
var loadContext = new PluginLoadContext(fullPath);
|
||||
var assembly = loadContext.LoadFromAssemblyPath(fullPath);
|
||||
var entrance = assembly.ExportedTypes.FirstOrDefault(x =>
|
||||
x.BaseType == typeof(PluginBase) ||
|
||||
x.GetCustomAttributes().FirstOrDefault(a => a.GetType() == typeof(PluginEntrance)) != null);
|
||||
|
||||
if (entrance == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Activator.CreateInstance(entrance) is not PluginBase entranceObj)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entranceObj.Info = info;
|
||||
entranceObj.Initialize(context, services);
|
||||
services.AddSingleton(typeof(PluginBase), entranceObj);
|
||||
services.AddSingleton(entrance, entranceObj);
|
||||
info.LoadStatus = PluginLoadStatus.Loaded;
|
||||
Console.WriteLine($"加载插件成功: {pluginDir} ({manifest.Version})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"加载插件失败: {ex}");
|
||||
info.Exception = ex;
|
||||
info.LoadStatus = PluginLoadStatus.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
private readonly Models.Settings _settings;
|
||||
private readonly INetworkService _networkService;
|
||||
public PluginService(INetworkService networkService,ISettingsService settingsService)
|
||||
{
|
||||
_networkService = networkService;
|
||||
_settings = settingsService.GetSettings();
|
||||
}
|
||||
public async Task LoadOnlinePluginsAsync()
|
||||
{
|
||||
var pluginManifests = _networkService.GetOnlinePlugins(IndexUrl);
|
||||
IPluginService.OnlinePluginsInternal.Clear();
|
||||
foreach (var Manifest in pluginManifests ?? [])
|
||||
{
|
||||
var localInfo = IPluginService.LoadedPluginsInternal.FirstOrDefault(x => x.Manifest.Id == Manifest.Id);
|
||||
var info = new PluginInfo
|
||||
{
|
||||
Manifest = Manifest,
|
||||
RealIconPath = $"{_settings.ApiUri}/GetFile?fileName=icons/{Manifest.Id}.png",
|
||||
LoadStatus = localInfo?.LoadStatus ?? PluginLoadStatus.NotLoaded,
|
||||
PluginFolderPath = localInfo?.PluginFolderPath ?? string.Empty,
|
||||
};
|
||||
IPluginService.OnlinePluginsInternal.Add(info);
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task InstallPluginOnlineAsync(PluginInfo plugin)
|
||||
{
|
||||
var id = plugin.Manifest.Id;
|
||||
var packageUrl = $"{_settings.ApiUri}/GetFile?fileName=plugins/{id}{IPluginService.PluginPackageExtension}";
|
||||
var tempDir = Path.Combine(AppPaths.TempPath, "Plugins", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var packagePath = Path.Combine(tempDir, $"{id}{IPluginService.PluginPackageExtension}");
|
||||
DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
var downloader = new DownloadService();
|
||||
plugin.DownloadProgress = 0;
|
||||
try
|
||||
{
|
||||
downloader.DownloadProgressChanged += (s, e) =>
|
||||
{
|
||||
dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
plugin.DownloadProgress = (int)e.ProgressPercentage;
|
||||
Console.WriteLine(e.ProgressPercentage);
|
||||
});
|
||||
};
|
||||
await downloader.DownloadFileTaskAsync(packageUrl, packagePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
plugin.DownloadProgress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 解压到插件目录
|
||||
var destDir = Path.Combine(AppPaths.PluginsRootPath, id);
|
||||
if (Directory.Exists(destDir))
|
||||
Directory.Delete(destDir, true);
|
||||
Directory.CreateDirectory(destDir);
|
||||
System.IO.Compression.ZipFile.ExtractToDirectory(packagePath, destDir);
|
||||
|
||||
// 清理临时文件
|
||||
try { Directory.Delete(tempDir, true); } catch { }
|
||||
|
||||
plugin.DownloadProgress = 0;
|
||||
plugin.RestartRequired = true;
|
||||
}
|
||||
|
||||
public async Task InstallPluginLocalAsync(string filePath)
|
||||
{
|
||||
var tempDir = Path.Combine(AppPaths.TempPath, "Plugins", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
System.IO.Compression.ZipFile.ExtractToDirectory(filePath, tempDir);
|
||||
|
||||
var manifestPath = Path.Combine(tempDir, PluginManifestFileName);
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
throw new FileNotFoundException("插件包中缺少 manifest.json 文件。");
|
||||
}
|
||||
var manifestYaml = File.ReadAllText(manifestPath);
|
||||
if( JsonSerializer.Deserialize(manifestYaml, JsonContextWithCamelCase.Default.PluginManifest) is not PluginManifest manifest || manifest.Id == "")
|
||||
{
|
||||
throw new InvalidDataException("插件包中的 manifest.json 文件格式不正确。");
|
||||
}
|
||||
|
||||
// 解压到插件目录
|
||||
var destDir = Path.Combine(AppPaths.PluginsRootPath, manifest.Id);
|
||||
if (Directory.Exists(destDir))
|
||||
Directory.Delete(destDir, true);
|
||||
System.IO.Compression.ZipFile.ExtractToDirectory(filePath, destDir);
|
||||
|
||||
var info = new PluginInfo
|
||||
{
|
||||
Manifest = manifest,
|
||||
PluginFolderPath = destDir,
|
||||
RealIconPath = Path.Combine(destDir, "icon.png"),
|
||||
StyleInfo = manifest.Type == "ScreenStyle" ? JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(destDir, StyleInfoFileName)), JsonContextWithCamelCase.Default.StyleInfo) : null,
|
||||
RestartRequired = true
|
||||
};
|
||||
// 清理临时文件
|
||||
try { Directory.Delete(tempDir, true); } catch { }
|
||||
IPluginService.LoadedPluginsInternal.Add(info);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task PackPluginAsync(PluginInfo plugin, string filePath)
|
||||
{
|
||||
var pluginDir = plugin.PluginFolderPath;
|
||||
if (string.IsNullOrWhiteSpace(pluginDir) || !Directory.Exists(pluginDir))
|
||||
throw new DirectoryNotFoundException("插件目录不存在。");
|
||||
|
||||
// 临时目录用于打包
|
||||
var tempDir = Path.Combine(AppPaths.TempPath, "Pack", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
// 复制所有文件到临时目录,排除指定dll
|
||||
foreach (var file in Directory.EnumerateFiles(pluginDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var fileName = Path.GetFileName(file);
|
||||
if (fileName.Equals("Microsoft.Windows.SDK.NET.dll", StringComparison.OrdinalIgnoreCase) ||
|
||||
fileName.Equals("WinRT.Runtime.dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var relativePath = Path.GetRelativePath(pluginDir, file);
|
||||
var destPath = Path.Combine(tempDir, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
|
||||
File.Copy(file, destPath, true);
|
||||
}
|
||||
|
||||
// 如果目标文件已存在,先删除
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
|
||||
// 打包为zip
|
||||
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, filePath);
|
||||
|
||||
// 清理临时目录
|
||||
try { Directory.Delete(tempDir, true); } catch { }
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using CRSim.Core.Models;
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.Core.Models;
|
||||
using Microsoft.Win32;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -8,14 +9,10 @@ namespace CRSim.Core.Services
|
||||
{
|
||||
private Settings _settings;
|
||||
private RegistryKey _key = Registry.CurrentUser.OpenSubKey(@"Software\CRSim\Settings",true);
|
||||
public SettingsService()
|
||||
{
|
||||
LoadSettings();
|
||||
}
|
||||
public void SaveSettings()
|
||||
{
|
||||
_key.SetValue("TimeOffset", (int)_settings.TimeOffset.TotalMinutes);
|
||||
_key.SetValue("SwitchPageSeconds", _settings.SwitchPageSeconds);
|
||||
_key.SetValue("ApiUri", _settings.ApiUri);
|
||||
_key.SetValue("MaxPages", _settings.MaxPages);
|
||||
_key.SetValue("StopCheckInAdvanceDuration", (int)_settings.StopCheckInAdvanceDuration.TotalMinutes);
|
||||
_key.SetValue("StopDisplayUntilDepartureDuration", (int)_settings.StopDisplayUntilDepartureDuration.TotalMinutes);
|
||||
@@ -26,7 +23,7 @@ namespace CRSim.Core.Services
|
||||
_key.SetValue("LoadTodayOnly", _settings.LoadTodayOnly);
|
||||
_key.SetValue("ReopenUnclosedScreensOnLoad", _settings.ReopenUnclosedScreensOnLoad);
|
||||
}
|
||||
private void LoadSettings()
|
||||
public void LoadSettings()
|
||||
{
|
||||
if (_key==null)
|
||||
{
|
||||
@@ -38,8 +35,8 @@ namespace CRSim.Core.Services
|
||||
else
|
||||
{
|
||||
_settings = new Settings();
|
||||
if (_key.GetValue("TimeOffset") != null) _settings.TimeOffset = TimeSpan.FromMinutes((int)_key.GetValue("TimeOffset"));
|
||||
if (_key.GetValue("SwitchPageSeconds") != null) _settings.SwitchPageSeconds = (int)_key.GetValue("SwitchPageSeconds");
|
||||
if (_key.GetValue("ApiUri") != null) _settings.ApiUri = (string)_key.GetValue("ApiUri");
|
||||
if (_key.GetValue("MaxPages") != null) _settings.MaxPages = (int)_key.GetValue("MaxPages");
|
||||
if (_key.GetValue("StopCheckInAdvanceDuration") != null) _settings.StopCheckInAdvanceDuration = TimeSpan.FromMinutes((int)_key.GetValue("StopCheckInAdvanceDuration"));
|
||||
if (_key.GetValue("StopDisplayUntilDepartureDuration") != null) _settings.StopDisplayUntilDepartureDuration = TimeSpan.FromMinutes((int)_key.GetValue("StopDisplayUntilDepartureDuration"));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.Core.Models;
|
||||
using System.Timers;
|
||||
|
||||
@@ -6,49 +6,76 @@ namespace CRSim.Core.Services
|
||||
{
|
||||
public class TimeService : ITimeService
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
private readonly System.Timers.Timer _oneSecondTimer;
|
||||
private readonly System.Timers.Timer _twentySecondsTimer;
|
||||
public DateTime SimulateTime { get; set; }
|
||||
|
||||
public event EventHandler OneSecondElapsed;
|
||||
public event EventHandler RefreshSecondsElapsed;
|
||||
private double _speed = 1.0;
|
||||
public double Speed
|
||||
{
|
||||
get => _speed;
|
||||
set
|
||||
{
|
||||
UpdateSimulatedOffset(); // 切换前先结算旧速率下的时间
|
||||
_speed = value;
|
||||
_lastSpeedChangeTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
private DateTime _lastSpeedChangeTime; // 上一次倍速切换时的真实时间
|
||||
private TimeSpan _simulatedOffset = TimeSpan.Zero; // 累计模拟时间偏移(倍速放大之后的)
|
||||
|
||||
private readonly System.Timers.Timer _oneSecondTimer;
|
||||
private readonly System.Timers.Timer _twentySecondsTimer;
|
||||
|
||||
public event EventHandler OneSecondElapsed;
|
||||
public event EventHandler RefreshSecondsElapsed;
|
||||
|
||||
private Settings _settings;
|
||||
|
||||
public TimeService(ISettingsService settingsService)
|
||||
{
|
||||
_settings = settingsService.GetSettings();
|
||||
|
||||
_oneSecondTimer = new(1000);
|
||||
_oneSecondTimer = new(1000);
|
||||
_oneSecondTimer.Elapsed += OnOneSecondElapsed;
|
||||
|
||||
_twentySecondsTimer = new(_settings.SwitchPageSeconds*1000);
|
||||
_twentySecondsTimer = new(_settings.SwitchPageSeconds * 1000);
|
||||
_twentySecondsTimer.Elapsed += OnTwentySecondsElapsed;
|
||||
|
||||
_lastSpeedChangeTime = DateTime.Now;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_oneSecondTimer.Start();
|
||||
_twentySecondsTimer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_oneSecondTimer.Stop();
|
||||
_twentySecondsTimer.Stop();
|
||||
_oneSecondTimer.Start();
|
||||
_twentySecondsTimer.Start();
|
||||
_lastSpeedChangeTime = DateTime.Now; // 启动时刷新时间
|
||||
}
|
||||
|
||||
private void OnOneSecondElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
OneSecondElapsed?.Invoke(this, EventArgs.Empty);
|
||||
OneSecondElapsed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void OnTwentySecondsElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
RefreshSecondsElapsed?.Invoke(this, EventArgs.Empty);
|
||||
RefreshSecondsElapsed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public DateTime GetDateTimeNow()
|
||||
{
|
||||
return DateTime.Now+ _settings.TimeOffset;
|
||||
var now = DateTime.Now;
|
||||
var elapsed = now - _lastSpeedChangeTime;
|
||||
return SimulateTime + _simulatedOffset + TimeSpan.FromTicks((long)(elapsed.Ticks * Speed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当倍速改变时,先结算之前段的累计偏移
|
||||
/// </summary>
|
||||
private void UpdateSimulatedOffset()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var elapsed = now - _lastSpeedChangeTime;
|
||||
_simulatedOffset += TimeSpan.FromTicks((long)(elapsed.Ticks * Speed));
|
||||
}
|
||||
}
|
||||
}
|
||||
16
CRSim.Core/Utils/AppPaths.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace CRSim.Core.Utils
|
||||
{
|
||||
public static class AppPaths
|
||||
{
|
||||
public static string AppDataPath =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CRSim");
|
||||
public static string TempPath =>
|
||||
Path.Combine(Path.GetTempPath(), "CRSim");
|
||||
|
||||
public static string ConfigFilePath =>
|
||||
Path.Combine(AppDataPath, "data.json");
|
||||
|
||||
public static string PluginsRootPath =>
|
||||
Path.Combine(AppDataPath, "Plugins");
|
||||
}
|
||||
}
|
||||
44
CRSim.Core/Utils/CommandLineParser.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using CRSim.Core.Models;
|
||||
namespace CRSim.Core.Utils
|
||||
{
|
||||
public static class CommandLineParser
|
||||
{
|
||||
public static CommandLineOptions Parse(string[] args)
|
||||
{
|
||||
var options = new CommandLineOptions();
|
||||
|
||||
for (int i = 1; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
|
||||
if (!arg.StartsWith("-"))
|
||||
continue;
|
||||
|
||||
string key = arg.TrimStart('-', '/').ToLower();
|
||||
|
||||
// 是否为最后一个参数 或 下一个参数也是一个key(说明当前是布尔/无值参数)
|
||||
bool isLastArg = (i + 1 >= args.Length);
|
||||
bool nextIsFlag = !isLastArg && args[i + 1].StartsWith("-");
|
||||
|
||||
string? value = null;
|
||||
if (!isLastArg && !nextIsFlag)
|
||||
{
|
||||
value = args[i + 1];
|
||||
i++; // skip value
|
||||
}
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case "epp":
|
||||
options.ExternalPluginPath = value;
|
||||
break;
|
||||
case "debug":
|
||||
options.Debug = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
CRSim.Core/Utils/FileHashHelper.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CRSim.Core.Utils
|
||||
{
|
||||
public class FileHashHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算文件 SHA256 值
|
||||
/// </summary>
|
||||
public static string ComputeSHA256(string filePath)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
using var stream = File.OpenRead(filePath);
|
||||
var hashBytes = sha256.ComputeHash(stream);
|
||||
return Convert.ToHexStringLower(hashBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证文件 SHA256
|
||||
/// </summary>
|
||||
public static bool VerifySHA256(string filePath, string expectedHash)
|
||||
{
|
||||
var fileHash = ComputeSHA256(filePath);
|
||||
return string.Equals(fileHash, expectedHash, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CRSim.Core/Utils/PluginLoadContext.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace CRSim.Core.Utils
|
||||
{
|
||||
internal class PluginLoadContext(string pluginPath) : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver = new(pluginPath);
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
if (assemblyPath != null)
|
||||
{
|
||||
return LoadFromAssemblyPath(assemblyPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
CRSim.ScreenSimulator/Abstractions/IScreenViewModel.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.Core.Models;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace CRSim.ScreenSimulator.Abstractions
|
||||
{
|
||||
public interface IScreenViewModel
|
||||
{
|
||||
ITimeService TimeService { get; }
|
||||
Dispatcher UIDispatcher { get; set; }
|
||||
void LoadData(Station station, TicketCheck? ticketCheck, string platform);
|
||||
string? Text { get; set; }
|
||||
Uri Video { get; set; }
|
||||
int Location { get; set; }
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 20 KiB |
BIN
CRSim.ScreenSimulator/Assets/CRSimIcon.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 805 B |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 385 KiB |
@@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace CRSim.Shared.Converters
|
||||
namespace CRSim.ScreenSimulator.Converters
|
||||
{
|
||||
public class DateTimeToStringConverter : IValueConverter
|
||||
{
|
||||
@@ -13,7 +13,7 @@ namespace CRSim.Shared.Converters
|
||||
if (value is DateTime dateTime && dateTime != new DateTime())
|
||||
{
|
||||
var CultureInfo = new CultureInfo(Culture);
|
||||
return dateTime.ToString(Format,CultureInfo);
|
||||
return dateTime.ToString(Format, CultureInfo);
|
||||
}
|
||||
return "--:--";
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace CRSim.Shared.Converters
|
||||
namespace CRSim.ScreenSimulator.Converters
|
||||
{
|
||||
public class EmptyToVisibilityConverter : IValueConverter
|
||||
{
|
||||
@@ -12,7 +12,7 @@ namespace CRSim.Shared.Converters
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(str) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
if(value is int i)
|
||||
if (value is int i)
|
||||
{
|
||||
return i == 0 ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace CRSim.Shared.Converters
|
||||
namespace CRSim.ScreenSimulator.Converters
|
||||
{
|
||||
public class ListToStringConverter : IValueConverter
|
||||
{
|
||||
@@ -1,29 +1,26 @@
|
||||
using CRSim.Core.Services;
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.ScreenSimulator.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace CRSim.ScreenSimulator.Converters
|
||||
{
|
||||
public class MetroDateTimeToStringConverter : IValueConverter
|
||||
public class MetroDateTimeToStringConverter : IValueConverter, IHasTimeService
|
||||
{
|
||||
private ITimeService _timeService;
|
||||
public ITimeService TimeService { get; set; }
|
||||
public string WaitingText { get; set; } = "请耐心等待";
|
||||
public string ComingText { get; set; } = "即将进站";
|
||||
public string ArrivedText { get; set; } = "列车进站";
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
var serviceProvider = (IServiceProvider)Application.Current.Resources["ServiceProvider"];
|
||||
_timeService = serviceProvider.GetRequiredService<ITimeService>();
|
||||
if (value is TrainInfo trainInfo && parameter is string para)
|
||||
{
|
||||
if(trainInfo.TrainNumber == null)
|
||||
{
|
||||
return WaitingText;
|
||||
}
|
||||
var timeDifference = (trainInfo.ArrivalTime ?? DateTime.MinValue) - _timeService.GetDateTimeNow();
|
||||
var timeDifference = (trainInfo.ArrivalTime ?? DateTime.MinValue) - TimeService.GetDateTimeNow();
|
||||
bool isArrivalTimeNull = trainInfo.ArrivalTime == null;
|
||||
var checkInAdvanceDuration = TimeSpan.FromMinutes(1);
|
||||
if (timeDifference < checkInAdvanceDuration)
|
||||
@@ -32,7 +29,7 @@ namespace CRSim.ScreenSimulator.Converters
|
||||
{
|
||||
if (!isArrivalTimeNull)
|
||||
{
|
||||
if(trainInfo.ArrivalTime - _timeService.GetDateTimeNow() > TimeSpan.Zero)
|
||||
if(trainInfo.ArrivalTime - TimeService.GetDateTimeNow() > TimeSpan.Zero)
|
||||
{
|
||||
return ComingText;
|
||||
}
|
||||
@@ -43,7 +40,7 @@ namespace CRSim.ScreenSimulator.Converters
|
||||
else
|
||||
{
|
||||
if (para == "StackPanel")
|
||||
return Math.Round((trainInfo.ArrivalTime - _timeService.GetDateTimeNow()).Value.TotalMinutes).ToString();
|
||||
return Math.Round((trainInfo.ArrivalTime - TimeService.GetDateTimeNow()).Value.TotalMinutes).ToString();
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using CRSim.Core.Services;
|
||||
using CRSim.Core.Abstractions;
|
||||
using CRSim.ScreenSimulator.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Globalization;
|
||||
@@ -7,16 +7,14 @@ using System.Windows.Data;
|
||||
|
||||
namespace CRSim.ScreenSimulator.Converters
|
||||
{
|
||||
public class MetroDateTimeToVisibilityConverter : IValueConverter
|
||||
public class MetroDateTimeToVisibilityConverter : IValueConverter, IHasTimeService
|
||||
{
|
||||
private ITimeService _timeService;
|
||||
public ITimeService TimeService { get; set; }
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
var serviceProvider = (IServiceProvider)Application.Current.Resources["ServiceProvider"];
|
||||
_timeService = serviceProvider.GetRequiredService<ITimeService>();
|
||||
if (value is TrainInfo trainInfo && parameter is string para)
|
||||
{
|
||||
var timeDifference = (trainInfo.ArrivalTime ?? DateTime.MinValue) - _timeService.GetDateTimeNow();
|
||||
var timeDifference = (trainInfo.ArrivalTime ?? DateTime.MinValue) - TimeService.GetDateTimeNow();
|
||||
var checkInAdvanceDuration = TimeSpan.FromMinutes(1);
|
||||
if (timeDifference < checkInAdvanceDuration)
|
||||
{
|
||||
|
||||
@@ -10,17 +10,17 @@ namespace CRSim.ScreenSimulator.Converters
|
||||
if (value == null)
|
||||
return null;
|
||||
string trainNumber = value.ToString();
|
||||
string imageName = "pack://application:,,,/CRSim.ScreenSimulator;component/Assets/ChengduShuangliuAirport/logo-GTJT.png";
|
||||
string imageName = "pack://application:,,,/CRSim.ScreenSimulator;component/Assets/logo-GTJT.png";
|
||||
if (!string.IsNullOrEmpty(trainNumber) && (trainNumber.StartsWith('G') || trainNumber.StartsWith('C') || trainNumber.StartsWith('D')))
|
||||
{
|
||||
int probability = trainNumber.GetHashCode() % 2;
|
||||
if (probability == 1)
|
||||
{
|
||||
imageName = "pack://application:,,,/CRSim.ScreenSimulator;component/Assets/ChengduShuangliuAirport/logo-CR.png";
|
||||
imageName = "pack://application:,,,/CRSim.ScreenSimulator;component/Assets/logo-CR.png";
|
||||
}
|
||||
else
|
||||
{
|
||||
imageName = "pack://application:,,,/CRSim.ScreenSimulator;component/Assets/ChengduShuangliuAirport/logo-CRH.png";
|
||||
imageName = "pack://application:,,,/CRSim.ScreenSimulator;component/Assets/logo-CRH.png";
|
||||
}
|
||||
}
|
||||
var image = new System.Windows.Media.Imaging.BitmapImage();
|
||||
|
||||