diff --git a/CRSim.Core/Abstractions/INetworkService.cs b/CRSim.Core/Abstractions/INetworkService.cs index b6674f5..2613533 100644 --- a/CRSim.Core/Abstractions/INetworkService.cs +++ b/CRSim.Core/Abstractions/INetworkService.cs @@ -1,15 +1,12 @@ using CRSim.Core.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using CRSim.Core.Models.Plugin; namespace CRSim.Core.Abstractions { public interface INetworkService { Task?> GetTimeTableAsync(string number); - Task> GetTrainNumbersAsync(string name); + Task?> GetTrainNumbersAsync(string name); + List? GetOnlinePlugins(string uri); } } diff --git a/CRSim.Core/Abstractions/IPluginService.cs b/CRSim.Core/Abstractions/IPluginService.cs index 2a268f0..3331853 100644 --- a/CRSim.Core/Abstractions/IPluginService.cs +++ b/CRSim.Core/Abstractions/IPluginService.cs @@ -11,12 +11,19 @@ public interface IPluginService /// /// 插件包文件扩展名。 /// - public static readonly string PluginPackageExtension = ".cp"; + public static readonly string PluginPackageExtension = ".crsp"; internal static ObservableCollection LoadedPluginsInternal { get; } = []; /// /// 已加载的插件信息列表。 /// - public static IReadOnlyList LoadedPlugins => LoadedPluginsInternal; + public static ObservableCollection LoadedPlugins => LoadedPluginsInternal; + + internal static ObservableCollection OnlinePluginsInternal { get; } = []; + + public static ObservableCollection OnlinePlugins => OnlinePluginsInternal; + + Task InstallPluginAsync(PluginInfo plugin); + void LoadOnlinePlugins(); } \ No newline at end of file diff --git a/CRSim.Core/CRSim.Core.csproj b/CRSim.Core/CRSim.Core.csproj index d93cb21..b87a12d 100644 --- a/CRSim.Core/CRSim.Core.csproj +++ b/CRSim.Core/CRSim.Core.csproj @@ -5,12 +5,14 @@ 10.0.17763.0 enable enable + partial AnyCPU;x64 + diff --git a/CRSim.Core/Models/Plugin/PluginInfo.cs b/CRSim.Core/Models/Plugin/PluginInfo.cs index 1e0dd86..dbfd2f7 100644 --- a/CRSim.Core/Models/Plugin/PluginInfo.cs +++ b/CRSim.Core/Models/Plugin/PluginInfo.cs @@ -1,16 +1,41 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CRSim.Core.Abstractions; using CRSim.Core.Enums; +using Downloader; namespace CRSim.Core.Models.Plugin; public partial class PluginInfo : ObservableRecipient { - //private DownloadProgress? _downloadProgress; - //private bool _isAvailableOnMarket = false; + public DownloadService? DownloadService; + + [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 @@ -32,6 +57,7 @@ public partial class PluginInfo : ObservableRecipient OnPropertyChanged(); } } + public string PluginFolderPath { get; internal set; } = ""; public string RealIconPath { get; set; } = ""; public bool IsUninstalling @@ -45,17 +71,18 @@ public partial class PluginInfo : ObservableRecipient 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; } - //private bool _isUpdateAvailable = false; [ObservableProperty] private StyleInfo? styleInfo; diff --git a/CRSim.Core/Models/Plugin/PluginManifest.cs b/CRSim.Core/Models/Plugin/PluginManifest.cs index 527b42a..476afc1 100644 --- a/CRSim.Core/Models/Plugin/PluginManifest.cs +++ b/CRSim.Core/Models/Plugin/PluginManifest.cs @@ -1,4 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; +using System.Text.Json.Serialization; namespace CRSim.Core.Models.Plugin; @@ -11,50 +12,54 @@ public class PluginManifest /// 入口程序集。加载插件时,将在此入口程序集中搜索插件类。 /// /// MyPlugin.dll + [JsonPropertyName("entranceAssembly")] public string EntranceAssembly { get; set; } = ""; /// /// 插件显示名称。 /// + [JsonPropertyName("name")] public string Name { get; set; } = ""; /// /// 插件ID。 /// + [JsonPropertyName("id")] public string Id { get; set; } = ""; - /// - /// 插件图标路径。默认为icon.png。 - /// - public string Icon { get; set; } = "icon.png"; - /// /// 插件自述。 /// + [JsonPropertyName("description")] public string Description { get; set; } = ""; /// /// 项目 Url /// + [JsonPropertyName("url")] public string? Url { get; set; } /// /// 插件版本 /// + [JsonPropertyName("version")] public string Version { get; set; } = ""; /// /// 插件目标 CRSim 版本 /// + [JsonPropertyName("apiVersion")] public string ApiVersion { get; set; } = ""; /// /// 插件作者 /// + [JsonPropertyName("author")] public string Author { get; set; } = ""; /// /// 插件类型 /// + [JsonPropertyName("type")] public string Type { get; set; } = ""; } \ No newline at end of file diff --git a/CRSim.Core/Models/Settings.cs b/CRSim.Core/Models/Settings.cs index d79e3c1..0af445a 100644 --- a/CRSim.Core/Models/Settings.cs +++ b/CRSim.Core/Models/Settings.cs @@ -21,7 +21,7 @@ namespace CRSim.Core.Models /// 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; } = ""; diff --git a/CRSim.Core/Services/NetworkService.cs b/CRSim.Core/Services/NetworkService.cs index 779fc8f..7172ec5 100644 --- a/CRSim.Core/Services/NetworkService.cs +++ b/CRSim.Core/Services/NetworkService.cs @@ -1,14 +1,13 @@ 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?> GetTimeTableAsync(string number) { try @@ -62,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)); @@ -78,58 +77,79 @@ namespace CRSim.Core.Services return null; } - public async Task> GetTrainNumbersAsync(string name) + public async Task?> 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() - { - {"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 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() { - 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 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? GetOnlinePlugins(string url) + { + try + { + var client = new HttpClient(); + var response = client.GetStringAsync(url).Result; + return JsonSerializer.Deserialize>(response); + } + catch + { + } + return null; } } } diff --git a/CRSim.Core/Services/PluginService.cs b/CRSim.Core/Services/PluginService.cs index 22f1afe..f475654 100644 --- a/CRSim.Core/Services/PluginService.cs +++ b/CRSim.Core/Services/PluginService.cs @@ -4,9 +4,11 @@ using CRSim.Core.Enums; using CRSim.Core.Models; using CRSim.Core.Models.Plugin; using CRSim.Core.Utils; +using Downloader; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System.Reflection; +using Windows.System; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -14,7 +16,9 @@ namespace CRSim.Core.Services; public class PluginService : IPluginService { - public static readonly string PluginManifestFileName = "manifest.yml"; + public static readonly string PluginManifestFileName = "manifest.yml"; + + private string IndexUrl => $"{_settings.ApiUri}/GetFile?fileName=plugins.json"; public static void InitializePlugins(HostBuilderContext context, IServiceCollection services,string externalPluginPath) { @@ -52,7 +56,7 @@ public class PluginService : IPluginService { Manifest = manifest, PluginFolderPath = Path.GetFullPath(pluginDir), - RealIconPath = Path.Combine(Path.GetFullPath(pluginDir), manifest.Icon), + RealIconPath = Path.Combine(Path.GetFullPath(pluginDir), "icon.png"), StyleInfo = manifest.Type == "ScreenStyle" ? deserializer.Deserialize(File.ReadAllText(Path.Combine(Path.GetFullPath(pluginDir), "style.yml"))) : null, }; if (info.IsUninstalling) @@ -60,15 +64,19 @@ public class PluginService : IPluginService Directory.Delete(pluginDir, true); continue; } - IPluginService.LoadedPluginsInternal.Add(info); 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 @@ -103,5 +111,71 @@ public class PluginService : IPluginService } } } + private readonly Models.Settings _settings; + private readonly INetworkService _networkService; + public PluginService(INetworkService networkService,ISettingsService settingsService) + { + _networkService = networkService; + _settings = settingsService.GetSettings(); + LoadOnlinePlugins(); + } + public void LoadOnlinePlugins() + { + 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); + } + } + public async Task InstallPluginAsync(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(); + plugin.DownloadService = new DownloadService(); + plugin.DownloadService.DownloadProgressChanged += (s, e) => + { + dispatcherQueue.TryEnqueue(() => + { + plugin.DownloadProgress = (int)(e.ProgressPercentage); + Console.WriteLine(e.ProgressPercentage); + }); + }; + try + { + await plugin.DownloadService.DownloadFileTaskAsync(packageUrl,packagePath); + } + catch(Exception e) + { + Console.WriteLine(e.Message); + 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; + } } \ No newline at end of file diff --git a/CRSim.Core/Services/SettingsService.cs b/CRSim.Core/Services/SettingsService.cs index 5e779bc..29b00d0 100644 --- a/CRSim.Core/Services/SettingsService.cs +++ b/CRSim.Core/Services/SettingsService.cs @@ -17,6 +17,7 @@ namespace CRSim.Core.Services { _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); @@ -41,6 +42,7 @@ namespace CRSim.Core.Services _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")); diff --git a/CRSim.Core/Utils/AppPaths.cs b/CRSim.Core/Utils/AppPaths.cs index ded3755..1ff84c0 100644 --- a/CRSim.Core/Utils/AppPaths.cs +++ b/CRSim.Core/Utils/AppPaths.cs @@ -2,13 +2,15 @@ { public static class AppPaths { - public static string AppDataFolder => + 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(AppDataFolder, "data.json"); + Path.Combine(AppDataPath, "data.json"); public static string PluginsRootPath => - Path.Combine(AppDataFolder, "Plugins"); + Path.Combine(AppDataPath, "Plugins"); } } diff --git a/CRSim.ExamplePlugin/CRSim.ExamplePlugin.csproj b/CRSim.ExamplePlugin/CRSim.ExamplePlugin.csproj index 32d140e..741e289 100644 --- a/CRSim.ExamplePlugin/CRSim.ExamplePlugin.csproj +++ b/CRSim.ExamplePlugin/CRSim.ExamplePlugin.csproj @@ -9,6 +9,7 @@ AnyCPU;x64 enable enable + partial diff --git a/CRSim.ScreenSimulator/CRSim.ScreenSimulator.csproj b/CRSim.ScreenSimulator/CRSim.ScreenSimulator.csproj index 4b408e5..3f5ef60 100644 --- a/CRSim.ScreenSimulator/CRSim.ScreenSimulator.csproj +++ b/CRSim.ScreenSimulator/CRSim.ScreenSimulator.csproj @@ -4,7 +4,8 @@ net9.0-windows10.0.19041.0 10.0.17763.0 enable - true + partial + true AnyCPU;x64 enable diff --git a/CRSim/CRSim.csproj b/CRSim/CRSim.csproj index 9248cfd..82ce86a 100644 --- a/CRSim/CRSim.csproj +++ b/CRSim/CRSim.csproj @@ -15,7 +15,7 @@ preview partial true - 2.3.101.1 + 2.3.102.0