Keep dev-3.0 content, discard master changes

This commit is contained in:
denglihong2007
2025-08-30 17:24:41 +08:00
371 changed files with 6377 additions and 14981 deletions

92
.github/ISSUE_TEMPLATE/BugReport.yml vendored Normal file
View 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://img.shields.io/github/v/release/denglihong2007/CRSim?include_prereleases&style=flat-square&label=) 或 ![最新的版本号](https://img.shields.io/github/v/release/denglihong2007/CRSim?style=flat-square&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

View 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://img.shields.io/github/v/release/denglihong2007/CRSim?include_prereleases&style=flat-square&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

View File

@@ -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);

View File

@@ -0,0 +1,7 @@
namespace CRSim.Core.Abstractions
{
public interface IHasTimeService
{
ITimeService TimeService { get; set; }
}
}

View 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);
}
}

View 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();
}

View File

@@ -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();
}

View 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; } // 模拟倍速
}
}

View 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);
}

View File

@@ -0,0 +1,3 @@
namespace CRSim.Core.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class PluginEntrance : Attribute;

View File

@@ -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>

View 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,
}

View File

@@ -0,0 +1,8 @@
namespace CRSim.Core.Models
{
public class CommandLineOptions
{
public string? ExternalPluginPath { get; set; }
public bool Debug { get; set; }
}
}

View 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
{
}
}

View 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;
}

View 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; } = "";
}

View File

@@ -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; } = "";

View 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;
}
}

View File

@@ -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; }

View 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; } = [];
}
}

View 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; } = "";
}
}

View 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; }
}
}

View File

@@ -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; } = [];
}
}

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View 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;
}
}

View File

@@ -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"));

View File

@@ -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));
}
}
}

View 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");
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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; }
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 385 KiB

View File

@@ -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 "--:--";
}

View File

@@ -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;
}

View File

@@ -1,7 +1,7 @@
using System.Globalization;
using System.Windows.Data;
namespace CRSim.Shared.Converters
namespace CRSim.ScreenSimulator.Converters
{
public class ListToStringConverter : IValueConverter
{

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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();

Some files were not shown because too many files have changed in this diff Show More