diff --git a/ModVersionChecker.sln b/ModVersionChecker.sln new file mode 100644 index 0000000..f4a44f9 --- /dev/null +++ b/ModVersionChecker.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36401.2 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModVersionChecker", "ModVersionChecker\ModVersionChecker.csproj", "{AF2DC7D5-9B7D-42B6-B9AA-092669626033}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AF2DC7D5-9B7D-42B6-B9AA-092669626033}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF2DC7D5-9B7D-42B6-B9AA-092669626033}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF2DC7D5-9B7D-42B6-B9AA-092669626033}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF2DC7D5-9B7D-42B6-B9AA-092669626033}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {12AE20EE-3AC6-4E00-BF52-F5FA5EA2AF25} + EndGlobalSection +EndGlobal diff --git a/ModVersionChecker/Main.cs b/ModVersionChecker/Main.cs new file mode 100644 index 0000000..b78b93d --- /dev/null +++ b/ModVersionChecker/Main.cs @@ -0,0 +1,144 @@ +using Microsoft.Extensions.DependencyInjection; +using ModVersionChecker.forms; +using Microsoft.Extensions.Hosting; +using ModVersionChecker.managers.interfaces; +using ModVersionChecker.managers.filesystem; +using ModVersionChecker.managers.litedb; + +namespace ModVersionChecker +{ + class Program + { + [STAThread] + static void Main() + { + var builder = Host.CreateDefaultBuilder(); + var program = new Program(); + + builder.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }); + + using var host = builder.Build(); + + var lifetime = host.Services.GetRequiredService(); + lifetime.ApplicationStarted.Register(() => + { + Console.WriteLine("Application is shutting down..."); + }); + + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + if (!SystemTray.IsSupported()) + { + MessageBox.Show("System tray not supported", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + var serviceProvider = host.Services; + var configForm = serviceProvider.GetService(); + var versionChecker = serviceProvider.GetService(); + var notifyIconService = serviceProvider.GetRequiredService(); + var configManager = serviceProvider.GetRequiredService(); + var config = configManager.GetConfig(); + + EventHandler openFormHandler = (s, e) => + { + if (configForm == null) return; + + configForm.UpdateListView(); + if (configForm.Visible) + { + configForm.BringToFront(); + return; + } + configForm.ShowDialog(); + }; + + using (var notifyIcon = new NotifyIcon()) + { + notifyIcon.Icon = new Icon("Resources/MVC-Icon.ico"); // Place MVC-Icon.ico in Resources + notifyIcon.Text = "Update Checker"; + notifyIcon.Visible = true; + notifyIconService.SetNotifyIcon(notifyIcon); + + var contextMenu = new ContextMenuStrip(); + contextMenu.Items.Add("Configure", null, openFormHandler); + contextMenu.Items.Add("Exit", null, (s, e) => Application.Exit()); + notifyIcon.ContextMenuStrip = contextMenu; + + notifyIcon.DoubleClick += openFormHandler; + + bool checkOnInitialStart = config.CheckOnStartup; + if (checkOnInitialStart && versionChecker != null) + { + versionChecker.StartVersionChecking(notifyIcon); + versionChecker.OnFinished += (s, e) => { + if (configForm != null) + { + if (configForm.InvokeRequired) + { + configForm.Invoke(() => configForm.UpdateListView()); + } + else + { + configForm.UpdateListView(); + } + } + }; + } + + + if (versionChecker != null) + { + + if (configForm != null) + { + configForm.OnRecheck += (s, e) => + { + if (versionChecker != null) + { + versionChecker.CheckAsync(); + } + }; + } + } + + + // Add to startup + // AddToStartup(); + + Application.Run(); // Keep app running for tray icon + } + + host.RunAsync().GetAwaiter().GetResult(); + } + + static void AddToStartup() + { + using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true)) + { + key?.SetValue("XintanalabsUpdateChecker", $"\"{Application.ExecutablePath}\""); + } + } + } + + public class SystemTray + { + public static bool IsSupported() => System.Windows.Forms.SystemInformation.TerminalServerSession == false; + } +} \ No newline at end of file diff --git a/ModVersionChecker/ModVersionChecker.csproj b/ModVersionChecker/ModVersionChecker.csproj new file mode 100644 index 0000000..1cbc4c4 --- /dev/null +++ b/ModVersionChecker/ModVersionChecker.csproj @@ -0,0 +1,58 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + \ No newline at end of file diff --git a/ModVersionChecker/Resources/MVC-Icon.ico b/ModVersionChecker/Resources/MVC-Icon.ico new file mode 100644 index 0000000..2e6cba6 Binary files /dev/null and b/ModVersionChecker/Resources/MVC-Icon.ico differ diff --git a/ModVersionChecker/Resources/error-icon.ico b/ModVersionChecker/Resources/error-icon.ico new file mode 100644 index 0000000..2a68876 Binary files /dev/null and b/ModVersionChecker/Resources/error-icon.ico differ diff --git a/ModVersionChecker/Resources/ok-icon.ico b/ModVersionChecker/Resources/ok-icon.ico new file mode 100644 index 0000000..46a6dcc Binary files /dev/null and b/ModVersionChecker/Resources/ok-icon.ico differ diff --git a/ModVersionChecker/Resources/up-icon.ico b/ModVersionChecker/Resources/up-icon.ico new file mode 100644 index 0000000..1aedbdc Binary files /dev/null and b/ModVersionChecker/Resources/up-icon.ico differ diff --git a/ModVersionChecker/VersionChecker.cs b/ModVersionChecker/VersionChecker.cs new file mode 100644 index 0000000..68a1aec --- /dev/null +++ b/ModVersionChecker/VersionChecker.cs @@ -0,0 +1,145 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.forms; +using ModVersionChecker.managers.interfaces; +using ModVersionChecker.utils; +using NuGet.Versioning; +using OpenQA.Selenium.BiDi.Script; +using System.Windows.Forms; + + +namespace ModVersionChecker +{ + public class VersionChecker + { + private readonly IConfigManager _configManager; + private readonly IAppsManager _appsManager; + private readonly ISourcesDefManager _sourcesDefManager; + private readonly INotifyIconService _notifyIconService; + private readonly IFlightSimsManager _fsManager; + private List errorMessages = new List(); + private List updateMessages = new List(); + private NotifyIcon? _notifyIcon; + + public event EventHandler? OnFinished; + + public VersionChecker( + IConfigManager configManager, + IAppsManager appsManager, + ISourcesDefManager sourcesDefManager, + INotifyIconService notifyIconService, + IFlightSimsManager fsManager) + { + _configManager = configManager ?? throw new ArgumentNullException(nameof(configManager)); + _appsManager = appsManager ?? throw new ArgumentNullException(nameof(appsManager)); + _sourcesDefManager = sourcesDefManager ?? throw new ArgumentNullException(nameof(sourcesDefManager)); + _notifyIconService = notifyIconService ?? throw new ArgumentNullException(nameof(notifyIconService)); + _fsManager = fsManager ?? throw new ArgumentNullException(nameof(fsManager)); + } + + private void HandleAppError(string message, AppConfig app) + { + errorMessages.Add(message); + _appsManager.UpdateStatus(app, AppStatus.Error); + } + + public void StartVersionChecking(NotifyIcon notifyIcon) + { + var config = _configManager.Load() ?? new GlobalConfig(); + _notifyIcon = notifyIcon ?? throw new ArgumentNullException(nameof(notifyIcon)); + // Run version checks in a background thread + new Thread(async () => + { + while (true) + { + await CheckAsync(); + + Thread.Sleep(config.IntervalMinutes * 60 * 1000); + } + + }) + { IsBackground = true }.Start(); + } + + public async Task CheckAsync() + { + var config = _configManager.Load() ?? new GlobalConfig(); + var apps = _appsManager.Load() ?? new List(); + var sources = _sourcesDefManager.List() ?? new List(); + var fsMods = _fsManager.Load() ?? new List(); + + updateMessages = new List(); + errorMessages = new List(); + + foreach (AppConfig app in apps) + { + if (app.Status != AppStatus.Error && app.LastCheckedAt != 0 && app.LastCheckedAt < TimeUtils.GetUnixTimeMillis(DateTime.Now.AddMinutes(-60))) + continue; + + var status = AppStatus.None; + var sourceId = app.Source; + if (string.IsNullOrWhiteSpace(sourceId)) + { + HandleAppError($"{app.Name} has no source configured.", app); + continue; + } + var source = sources.FirstOrDefault(s => s.Id == sourceId); + if (source == null) + { + HandleAppError($"{app.Name} has an invalid source: {sourceId}", app); + continue; + } + try + { + foreach (var fsVersion in app.MsfsVersions) + { + var fsConfig = _fsManager.GetByShortName(fsVersion); + if (fsConfig == null) + { + HandleAppError($"{app.Name} has no FS mod path configured for version {fsVersion}.", app); + continue; + } + var checker = CheckerFactory.CreateChecker(source.Type); + var current = NuGetVersion.Parse(VersionUtils.GetCurrentVersion(app, fsConfig)); + var latest = NuGetVersion.Parse(await checker.GetLatestVersion(app.Params, source)); + + app.CurrentVersion = current.ToString(); + app.LatestVersion = latest.ToString(); + + if (latest.CompareTo(current) == 1) + { + updateMessages.Add($"{app.Name}: New version {latest} (current: {current})"); + status = AppStatus.UpdateAvailable; + } + } + _appsManager.UpdateStatus(app, status); + + + } + catch (Exception ex) + { + HandleAppError($"Failed for {app.Name}: {ex.Message}", app); + } + } + + if (updateMessages.Count > 0) + { + _notifyIconService.ShowBalloonTip( + 10000, + "Updates Available", + string.Join("\n", updateMessages), + ToolTipIcon.Info + ); + } + if (errorMessages.Count > 0) + { + _notifyIconService.ShowBalloonTip( + 10000, + "Errors", + string.Join("\n", errorMessages), + ToolTipIcon.Error + ); + } + OnFinished?.Invoke(this, "Version check completed."); + } + } +} diff --git a/ModVersionChecker/checkers/ApiChecker.cs b/ModVersionChecker/checkers/ApiChecker.cs new file mode 100644 index 0000000..d27de40 --- /dev/null +++ b/ModVersionChecker/checkers/ApiChecker.cs @@ -0,0 +1,56 @@ +using ModVersionChecker.data.model; +using System; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ModVersionChecker +{ + public class ApiChecker : IVersionChecker + { + private static readonly HttpClient _httpClient = new HttpClient(); + + public async Task GetLatestVersion(Dictionary paramsDict, SourceDef source) + { + if (!paramsDict.TryGetValue("url", out var url) || string.IsNullOrEmpty(url)) + { + throw new ArgumentException("API URL required"); + } + if (!paramsDict.TryGetValue("jsonPath", out var jsonPath) || string.IsNullOrEmpty(jsonPath)) + { + throw new ArgumentException("jsonPath required"); + } + + var response = await _httpClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + { + throw new Exception($"API error: {(int)response.StatusCode} {response.ReasonPhrase}"); + } + + var body = await response.Content.ReadAsStringAsync(); + if (string.IsNullOrEmpty(body)) + { + throw new Exception("Empty API response"); + } + + using var jsonDoc = JsonDocument.Parse(body); + var element = jsonDoc.RootElement; + + foreach (var key in jsonPath.Split('.')) + { + if (!element.TryGetProperty(key, out var nextElement)) + { + throw new Exception($"JSON key '{key}' not found in response"); + } + element = nextElement; + } + + if (element.ValueKind != JsonValueKind.String) + { + throw new Exception($"JSON value for '{jsonPath}' is not a string"); + } + + return element.GetString()!.Trim(); + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/checkers/CheckerFactory.cs b/ModVersionChecker/checkers/CheckerFactory.cs new file mode 100644 index 0000000..0b9fdfd --- /dev/null +++ b/ModVersionChecker/checkers/CheckerFactory.cs @@ -0,0 +1,17 @@ +namespace ModVersionChecker +{ + public static class CheckerFactory + { + public static IVersionChecker CreateChecker(string type) + { + string[] parts = type.Split(':'); + + return parts[0].ToLower() switch + { + "scrape" => new ScrapeChecker(), + "api" => new ApiChecker(), + _ => throw new ArgumentException($"Unknown checker type: {type}") + }; + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/checkers/ScrapeChecker.cs b/ModVersionChecker/checkers/ScrapeChecker.cs new file mode 100644 index 0000000..99adc8c --- /dev/null +++ b/ModVersionChecker/checkers/ScrapeChecker.cs @@ -0,0 +1,94 @@ +using System.Text.RegularExpressions; +using ModVersionChecker.data.model; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; + +namespace ModVersionChecker +{ + public class ScrapeChecker : IVersionChecker + { + public async Task GetLatestVersion(Dictionary paramsDict, SourceDef source) + { + if (!paramsDict.TryGetValue("url", out var url) || string.IsNullOrEmpty(url)) + { + throw new ArgumentException("URL required"); + } + + var mode = GetValueOrDefault(paramsDict, "mode", source); + var response = ""; + if (mode == "selenium") + { + response = await SeleniumFetch(url); + } + else + { + response = await DefaultFetch(url); ; + } + + + string pattern = @">\s+<"; + response = Regex.Replace(response, pattern, "><"); + var regex = GetValueOrDefault(paramsDict, "regex", source); + + var match = System.Text.RegularExpressions.Regex.Match(response, regex); + if (!match.Success || match.Groups.Count < 2) + { + throw new Exception($"No match with regex in response"); + } + return match.Groups[1].Value; + + } + + private string GetValueOrDefault(Dictionary dict, string key, SourceDef source) + { + var value = ""; + if (dict.ContainsKey(key) && !string.IsNullOrEmpty(dict[key])) + { + value = dict[key]; + } + else if (source.Defaults != null && source.Defaults.ContainsKey(key)) + { + value = source.Defaults[key]; + } + return value; + } + + private Task DefaultFetch(string url) + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); + httpClient.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); + httpClient.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.5"); + + return httpClient.GetStringAsync(url); + } + + private async Task SeleniumFetch(string url) + { + var service = ChromeDriverService.CreateDefaultService(); + service.HideCommandPromptWindow = true; + + var options = new ChromeOptions(); + options.AddArgument("--headless"); // Run in headless mode + options.AddArgument("--disable-gpu"); + options.AddArgument("--no-sandbox"); + options.AddArgument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124"); + + using var driver = new ChromeDriver(service, options); + try + { + driver.Navigate().GoToUrl(url); + // Wait for the page to load + await Task.Delay(2000); // Adjust as necessary + // Example: Get the page source + var pageSource = driver.PageSource; + // Close the driver + return pageSource; + } + finally + { + driver.Quit(); + } + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/checkers/VersionChecker.cs b/ModVersionChecker/checkers/VersionChecker.cs new file mode 100644 index 0000000..ead2e3f --- /dev/null +++ b/ModVersionChecker/checkers/VersionChecker.cs @@ -0,0 +1,9 @@ +using ModVersionChecker.data.model; + +namespace ModVersionChecker +{ + public interface IVersionChecker + { + Task GetLatestVersion(Dictionary paramsDict, SourceDef source); + } +} \ No newline at end of file diff --git a/ModVersionChecker/data/apps - Copy.json b/ModVersionChecker/data/apps - Copy.json new file mode 100644 index 0000000..5573c7d --- /dev/null +++ b/ModVersionChecker/data/apps - Copy.json @@ -0,0 +1,55 @@ +[ + { + "name": "PMS50 GTN750", + "msfsVersions": [ "msfs2024" ], + "source": "pms50_gtn750", + "params": { + "url": "https://pms50.com/msfs/" + }, + "currentVersionConfig": { + "package": "pms50-instrument-gtn750" + } + }, + { + "name": "Teikof SKMZ", + "msfsVersions": [ "msfs2024" ], + "source": "sim_market", + "params": { + "url": "https://secure.simmarket.com/teikof-studio-skmz-la-nubia-airport-msfs.phtml" + }, + "currentVersionConfig": { + "package": "teikofstudio-airport-skmz-manizales" + } + }, + { + "name": "SWS", + "msfsVersions": [ + "msfs2024" + ], + "source": "sws", + "params": { + "url": "https://simworksstudios.com/product/kodiak-100-series-ii/", + "regex": "Current Version: (\\d\u002B\\.\\d\u002B\\.\\d\u002B)" + }, + "currentVersionConfig": { + "package": "sws-aircraft-kodiak-wheels", + "version": "" + } + }, + { + "name": "GSX Pro", + "msfsVersions": [ + "msfs2024" + ], + "source": "custom", + "params": { + "url": "https://www.fsdreamteam.com/couatl_liveupdate_notes.html", + "regex": "

Version (\\d\u002B\\.\\d\u002B\\.\\d\u002B) –" + }, + "currentVersionConfig": { + "package": "fsdreamteam-gsx-pro", + "version": "" + } + } + + ] \ No newline at end of file diff --git a/ModVersionChecker/data/apps.json b/ModVersionChecker/data/apps.json new file mode 100644 index 0000000..d366e4b --- /dev/null +++ b/ModVersionChecker/data/apps.json @@ -0,0 +1,86 @@ +[ + { + "id": "8", + "name": "PMS50 GTN750", + "msfsVersions": [ + "msfs2024" + ], + "source": "pms50_gtn750", + "params": { + "url": "https://pms50.com/msfs/", + "regex": "Current version: (\\d\u002B\\.\\d\u002B\\.\\d\u002B)" + }, + "fsFields": { + "msfs2024": { + "package": "pms50-instrument-gtn750" + } + } + }, + { + "id": "2", + "name": "Teikof SKMZ", + "msfsVersions": [ + "msfs2024" + ], + "source": "sim_market", + "params": { + "url": "https://secure.simmarket.com/teikof-studio-skmz-la-nubia-airport-msfs.phtml" + }, + "fsFields": { + "msfs2024": { + "package": "teikofstudio-airport-skmz-manizales" + } + } + }, + { + "id": "3", + "name": "SWS", + "msfsVersions": [ + "msfs2024" + ], + "source": "sws", + "params": { + "url": "https://simworksstudios.com/product/kodiak-100-series-ii/", + "regex": "Current Version: (\\d\u002B\\.\\d\u002B\\.\\d\u002B)" + }, + "fsFields": { + "msfs2024": { + "package": "sws-aircraft-kodiak-wheels" + } + } + }, + { + "id": "4", + "name": "GSX Pro", + "msfsVersions": [ + "msfs2024" + ], + "source": "custom", + "params": { + "url": "https://www.fsdreamteam.com/couatl_liveupdate_notes.html", + "regex": "\u003Cp\u003EVersion (\\d\u002B\\.\\d\u002B\\.\\d\u002B) \u2013" + }, + "fsFields": { + "msfs2024": { + "package": "fsdreamteam-gsx-pro" + } + } + }, + { + "id": "5", + "name": "Aerostar 600", + "msfsVersions": [ + "msfs2024" + ], + "source": "a2a", + "params": { + "url": "https://a2asimulations.com/forum/viewforum.php?f=153", + "regex": "Accu-Sim Aerostar 600 \u2013 v(\\d\u002B\\.\\d\u002B\\.\\d\u002B)" + }, + "fsFields": { + "msfs2024": { + "package": "a2a-aircraft-aerostar600" + } + } + } +] \ No newline at end of file diff --git a/ModVersionChecker/data/checkerTypesDef.json b/ModVersionChecker/data/checkerTypesDef.json new file mode 100644 index 0000000..cf284db --- /dev/null +++ b/ModVersionChecker/data/checkerTypesDef.json @@ -0,0 +1,33 @@ + [ + { + "name": "scrape", + "params": [ + { + "name": "url", + "label": "Url", + "type": "string", + "required": true + }, + { + "label": "Regex", + "name": "regex", + "type": "string" + }, { + "label": "Mode", + "name": "mode", + "type": "string" + } + ] + }, + { + "name": "api", + "params": [ + { + "label": "Url", + "name": "url", + "type": "string", + "required": true + } + ] + } + ] \ No newline at end of file diff --git a/ModVersionChecker/data/config.json b/ModVersionChecker/data/config.json new file mode 100644 index 0000000..49ebcc0 --- /dev/null +++ b/ModVersionChecker/data/config.json @@ -0,0 +1,21 @@ +{ + "intervalMinutes": 60, + "checkOnStartup": false, + "fsModPaths": { + "msfs2024": { + "path": "I:/Microsoft Flight Simulator 2024/Packages/Community/", + "file": "manifest.json", + "fileType": "json", + "key": "package_version", + "fields": [ + { + "name": "package", + "label": "Package Name", + "type": "string", + "control": "directory", + "required": true + } + ] + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/data/model/AppConfig.cs b/ModVersionChecker/data/model/AppConfig.cs new file mode 100644 index 0000000..801c630 --- /dev/null +++ b/ModVersionChecker/data/model/AppConfig.cs @@ -0,0 +1,51 @@ +using ModVersionChecker.data.model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +public class AppConfig +{ + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("msfsVersions")] + public List MsfsVersions { get; set; } = new List { "msfs2024" }; // Default to msfs2024 + + + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; + + [JsonPropertyName("params")] + public Dictionary Params { get; set; } = new Dictionary(); + + [JsonPropertyName("fsFields")] + public Dictionary> FsFields { get; set; } = new Dictionary>(); + + [JsonPropertyName("downloadUrl")] + public string DownloadUrl { get; set; } = string.Empty; + + [JsonPropertyName("currentVersion")] + public string CurrentVersion { get; set; } = string.Empty; + + [JsonPropertyName("latestVersion")] + public string LatestVersion { get; set; } = string.Empty; + + [JsonPropertyName("status")] + public AppStatus Status { get; set; } = AppStatus.None; + + [JsonPropertyName("createdAt")] + public long CreatedAt { get; set; } = 0; + + [JsonPropertyName("updatedAt")] + public long UpdatedAt { get; set; } = 0; + + [JsonPropertyName("lastCheckedAt")] + public long LastCheckedAt { get; set; } = 0; + +} diff --git a/ModVersionChecker/data/model/AppStatus.cs b/ModVersionChecker/data/model/AppStatus.cs new file mode 100644 index 0000000..8e2481d --- /dev/null +++ b/ModVersionChecker/data/model/AppStatus.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.data.model +{ + public enum AppStatus + { + None, + UpdateAvailable, + Error, + } +} diff --git a/ModVersionChecker/data/model/CheckerTypeDef.cs b/ModVersionChecker/data/model/CheckerTypeDef.cs new file mode 100644 index 0000000..3003d98 --- /dev/null +++ b/ModVersionChecker/data/model/CheckerTypeDef.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ModVersionChecker.data.model +{ + public class CheckerTypeDef + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("params")] + public List Params { get; set; } = new List(); + } +} diff --git a/ModVersionChecker/data/model/FieldDef.cs b/ModVersionChecker/data/model/FieldDef.cs new file mode 100644 index 0000000..c91cbff --- /dev/null +++ b/ModVersionChecker/data/model/FieldDef.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ModVersionChecker.data.model +{ + public class FieldDef + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("required")] + public bool Required { get; set; } = false; + + [JsonPropertyName("control")] + public string Control { get; set; } = string.Empty; + } +} diff --git a/ModVersionChecker/data/model/FsModPathConfig.cs b/ModVersionChecker/data/model/FsModPathConfig.cs new file mode 100644 index 0000000..ecf8946 --- /dev/null +++ b/ModVersionChecker/data/model/FsModPathConfig.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ModVersionChecker.data.model +{ + public class FsModPathConfig + { + + public string Id { get; set; } = String.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("shortName")] + public string ShortName { get; set; } = string.Empty; + + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + [JsonPropertyName("file")] + public string File { get; set; } = string.Empty; + + [JsonPropertyName("fileType")] + public string FileType { get; set; } = string.Empty; + + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + [JsonPropertyName("fields")] + public List Fields { get; set; } = new List(); + } +} diff --git a/ModVersionChecker/data/model/GlobalConfig.cs b/ModVersionChecker/data/model/GlobalConfig.cs new file mode 100644 index 0000000..4267932 --- /dev/null +++ b/ModVersionChecker/data/model/GlobalConfig.cs @@ -0,0 +1,21 @@ +using LiteDB; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ModVersionChecker.data.model +{ + public class GlobalConfig + { + public string Id { get; set; } = String.Empty; + + [JsonPropertyName("intervalMinutes")] + public int IntervalMinutes { get; set; } = 60; + + [JsonPropertyName("checkOnStartup")] + public bool CheckOnStartup { get; set; } = true; + } +} diff --git a/ModVersionChecker/data/model/SourceDef.cs b/ModVersionChecker/data/model/SourceDef.cs new file mode 100644 index 0000000..aad8453 --- /dev/null +++ b/ModVersionChecker/data/model/SourceDef.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ModVersionChecker.data.model +{ + public class SourceDef + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("defaults")] + public Dictionary Defaults { get; set; } = new Dictionary(); + } +} diff --git a/ModVersionChecker/data/sourcesDef.json b/ModVersionChecker/data/sourcesDef.json new file mode 100644 index 0000000..39cd3cb --- /dev/null +++ b/ModVersionChecker/data/sourcesDef.json @@ -0,0 +1,47 @@ + [ + { + "id": "custom", + "name": "Custom Source", + "type": "scrape", + "defaults": { + "regex": "", + "url": "" + } + }, + { + "id": "sim_market", + "name": "Sim Market", + "type": "scrape", + "defaults": { + "regex": "(\\d+\\.\\d+\\.\\d+)<\\/span>", + "url": "https://secure.simmarket.com/" + } + }, + { + "id": "pms50_gtn750", + "name": "PMS50 GTN750", + "type": "scrape", + "defaults": { + "url": "https://pms50.com/msfs/", + "regex": "Current version: (\\d+\\.\\d+\\.\\d+)" + } + }, + { + "id": "sws", + "name": "SWS", + "type": "scrape", + "defaults": { + "url": "https://simworksstudios.com/product", + "regex": "Current Version: (\\d+\\.\\d+\\.\\d+)" + } + }, + { + "id": "a2a", + "name": "A2A", + "type": "scrape", + "defaults": { + "url": "https://a2asimulations.com/forum/viewtopic.php?f=153", + "mode": "selenium" + } + } + ] \ No newline at end of file diff --git a/ModVersionChecker/forms/AppDetailsForm.cs b/ModVersionChecker/forms/AppDetailsForm.cs new file mode 100644 index 0000000..637ed78 --- /dev/null +++ b/ModVersionChecker/forms/AppDetailsForm.cs @@ -0,0 +1,477 @@ +using Microsoft.VisualBasic.FileIO; +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; + +namespace ModVersionChecker.forms +{ + public class AppDetailsForm : Form + { + private readonly IConfigManager _configManager; + private readonly IAppsManager _appsManager; + private readonly ISourcesDefManager _sourcesDefManager; + private readonly ICheckerTypesDefManager _checkerTypesDefManager; + private readonly IFlightSimsManager _flightSimsManager; + private readonly GlobalConfig _globalConfig; + private int _currentRow; + //private string? _appId; + private bool _isEditable; + //private List _apps; + private List _sourcesDef; + private List _checkerTypesDef; + + private TextBox _nameField, _downloadUrlField; + private Label _nameLabel, _msfsVersionsLabel, _sourceLabel, _paramsSubtitle, _downloadUrlLabel; + private ComboBox _sourceField; + private Button _saveButton, _closeButton; + private TableLayoutPanel _mainLayout, _paramsPanel, _fsFieldsPanel; + private FlowLayoutPanel _buttonsPanel, _fsPanel; + private readonly Dictionary _paramFields = new Dictionary(); + private readonly Dictionary> _fsFields = new Dictionary>(); + private List _selectedFs = new List(); + private List _fsCheckBoxes = new List(); + private AppConfig? _currentApp; + + private List _flightSims; + + public event EventHandler OnAppChanged; + + public AppDetailsForm( + IConfigManager configManager, + IAppsManager appsManager, + ISourcesDefManager sourcesDefManager, + ICheckerTypesDefManager checkerTypesDefManager, + IFlightSimsManager flightSimsManager + ) + { + + _configManager = configManager ?? throw new ArgumentNullException(nameof(configManager)); + _appsManager = appsManager ?? throw new ArgumentNullException(nameof(appsManager)); + _sourcesDefManager = sourcesDefManager ?? throw new ArgumentNullException(nameof(sourcesDefManager)); + _checkerTypesDefManager = checkerTypesDefManager ?? throw new ArgumentNullException(nameof(checkerTypesDefManager)); + _flightSimsManager = flightSimsManager ?? throw new ArgumentNullException(nameof(flightSimsManager)); + + _flightSims = _flightSimsManager.Load() ?? new List(); + + _globalConfig = _configManager.Load() ?? new GlobalConfig(); + + _sourcesDef = _sourcesDefManager.List() ?? new List(); + _checkerTypesDef = _checkerTypesDefManager.Load() ?? new List(); + + + _selectedFs = _flightSims.Select(sim => sim.ShortName).ToList(); + + _mainLayout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 14, + ColumnStyles = { new ColumnStyle(SizeType.Absolute, 150), new ColumnStyle(SizeType.Percent, 100) } + }; + + // App Name + _nameLabel = new Label { Text = "Name:" }; + _nameField = new TextBox { Text = "", Enabled = _isEditable, Width = 300 }; + // FS Versions + _msfsVersionsLabel = new Label { Text = "FS:" }; + _fsPanel = new FlowLayoutPanel + { + FlowDirection = FlowDirection.LeftToRight, + AutoSize = true, + Dock = DockStyle.Fill + }; + //_msfs2020CheckBox = new CheckBox { Text = "MSFS 2020", Enabled = _isEditable }; + //_msfs2024CheckBox = new CheckBox { Text = "MSFS 2024", Enabled = _isEditable }; + // Source + _sourceLabel = new Label { Text = "Source:" }; + _sourceField = new ComboBox { Enabled = _isEditable, Width = 300, DropDownStyle = ComboBoxStyle.DropDownList }; + _sourceField.Items.AddRange(_sourcesDef.Select(sd => sd.Id).ToArray()); + _sourceField.SelectedIndexChanged += OnSourceFieldIndexChanged; + // Parameters + _paramsSubtitle = new Label { Text = "SourceParameters:", Font = new System.Drawing.Font(Font, System.Drawing.FontStyle.Bold) }; + _paramsPanel = new TableLayoutPanel + { + AutoSize = true, + BackColor = Color.White, + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 2, + ColumnStyles = { new ColumnStyle(SizeType.Absolute, 150), new ColumnStyle(SizeType.Percent, 100) } + }; + // Fs Fields Panel + _fsFieldsPanel = new TableLayoutPanel + { + AutoSize = true, + BackColor = Color.White, + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 2, + ColumnStyles = { new ColumnStyle(SizeType.Absolute, 150), new ColumnStyle(SizeType.Percent, 100) } + }; + + + // App Name + _downloadUrlLabel = new Label { Text = "Download Url:" }; + _downloadUrlField = new TextBox { Text = "", Enabled = _isEditable, Width = 300 }; + + _buttonsPanel = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, AutoSize = true, Dock = DockStyle.Fill }; + _saveButton = new Button { Text = "Save", Width = 100 }; + _closeButton = new Button { Text = "Close", Width = 100 }; + _saveButton.Click += OnSaveButtonClicked; + _closeButton.Click += (s, e) => Close(); + Controls.Add(_mainLayout); + + Size = new System.Drawing.Size(500, 500); + StartPosition = FormStartPosition.CenterParent; + + InitializeForm(); + + } + + public void SetApp(AppConfig? app, bool update = true) + { + _currentApp = app; + _selectedFs = _currentApp?.MsfsVersions ?? new List(); + + if (update) + { + UpdateForm(); + } + } + + public void SetEditable(bool isEditable, bool update = true) + { + _isEditable = isEditable; + if (update) + { + UpdateForm(); + } + } + + public void UpdateForm() + { + + Text = _currentApp == null ? "Add App" : (_isEditable ? "Edit App" : "App Details"); + + _nameField.Text = _currentApp != null ? _currentApp.Name : ""; + _downloadUrlField.Enabled = _nameField.Enabled = _sourceField.Enabled = _isEditable; + _downloadUrlField.Text = _currentApp != null ? _currentApp.DownloadUrl : ""; + + _flightSims.ForEach(fs => + { + if (_currentApp != null && _currentApp.MsfsVersions.Contains(fs.ShortName)) + { + if (!_selectedFs.Contains(fs.ShortName)) + { + _selectedFs.Add(fs.ShortName); + } + } + }); + + for (int i = 0; i < _fsCheckBoxes.Count; i++) + { + var fsKey = _flightSims.FirstOrDefault(f => f.ShortName == _fsCheckBoxes[i].Text)?.ShortName; + if (fsKey != null) + { + _fsCheckBoxes[i].Checked = _currentApp != null && _currentApp.MsfsVersions.Contains(fsKey); + } + } + + _sourceField.SelectedIndex = _sourceField.Items.IndexOf(_currentApp != null ? _currentApp.Source : ""); + + UpdateFsFields(); + + UpdateParamFields(); + } + + private bool isFsSelected(FsModPathConfig fs) + { + return _selectedFs.Contains(fs.ShortName); + } + private void UpdateFsFields() + { + _fsFields.Clear(); + _fsFieldsPanel.Controls.Clear(); + + foreach (var fs in _flightSims) + { + + if (fs == null || !isFsSelected(fs)) + { + continue; + } + + var fsKey = fs.ShortName; + var fieldsDict = new Dictionary(); + _fsFields[fsKey] = fieldsDict; + int currentRow = 0; + + Label horizontalSeparator = new Label + { + Height = 50, + Padding = new Padding(10, 0, 0, 0), + BackColor = Color.GhostWhite, // Line-like separator + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleLeft, + Text = fsKey, // Optional: Add text to the separator + ForeColor = Color.FromArgb(50, 50, 50) // Text color contrasts with background + }; + + _fsFieldsPanel.Controls.Add(horizontalSeparator, 0, currentRow); + _fsFieldsPanel.SetColumnSpan(horizontalSeparator, 2); + currentRow++; + + + foreach (var field in fs.Fields) + { + Control control; + var value = GetFsFieldValue(fsKey, field.Name); + var label = new Label { Text = $"{field.Label} ({(field.Required ? "Required" : "Optional")}):", Width = 100, AutoSize = true }; + var textBox = new TextBox + { + Width = 300, + Enabled = _isEditable, + Text = value + }; + + switch (field.Control.ToLower()) + { + case "directory": + textBox.ReadOnly = true; + control = new TableLayoutPanel + { + AutoSize = true, + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 1, + ColumnStyles = { new ColumnStyle(SizeType.Percent, 80), new ColumnStyle(SizeType.Percent, 20) } + }; + (control as TableLayoutPanel).Controls.Add(textBox, 0, 0); + var browseButton = new Button { Text = "Browse", Width = 80, Enabled = _isEditable }; + browseButton.Click += (s, e) => + { + using (var folderDialog = new FolderBrowserDialog()) + { + folderDialog.Description = $"Select directory for {field.Label}"; + folderDialog.SelectedPath = textBox.Text == "" ? Path.Combine(fs.Path) : textBox.Text; + + if (folderDialog.ShowDialog() == DialogResult.OK) + { + string selectedDirectory = folderDialog.SelectedPath; + string folderName = Path.GetFileName(selectedDirectory); + textBox.Text = folderName; + } + } + }; + (control as TableLayoutPanel).Controls.Add(browseButton, 1, 0); + break; + default: + control = textBox; + break; + } + + fieldsDict[field.Name] = textBox; + + _fsFieldsPanel.Controls.Add(label, 0, currentRow); + _fsFieldsPanel.Controls.Add(control, 1, currentRow); + currentRow++; + } + } + } + + private string GetFsFieldValue(string fsKey, string fieldName) + { + if (_currentApp == null) return ""; + + var fsFields = _currentApp.FsFields.ContainsKey(fsKey) ? _currentApp.FsFields[fsKey] : new Dictionary(); + if (fsFields.ContainsKey(fieldName)) + { + return fsFields[fieldName]; + } + return ""; + } + + private void UpdateParamFields() + { + if (_sourceField?.SelectedItem == null) return; + + var selectedSource = _sourcesDef.FirstOrDefault(sd => sd.Id == _sourceField.SelectedItem.ToString()); + if (selectedSource == null) return; + + var checkerType = _checkerTypesDef.FirstOrDefault(ct => ct.Name == selectedSource.Type); + if (checkerType == null) return; + + _paramFields.Clear(); + _paramsPanel.Controls.Clear(); + + int currentRow = 0; + foreach (var paramDef in checkerType.Params) + { + var label = new Label { Text = $"{paramDef.Label} ({(paramDef.Required ? "Required" : "Optional")}):", Width = 100, AutoSize = true }; + var textBox = new TextBox + { + Width = 300, + Enabled = _isEditable, + Text = GetParamValue(paramDef.Name, selectedSource) + }; + _paramFields[paramDef.Name] = textBox; + + _paramsPanel.Controls.Add(label, 0, currentRow); + _paramsPanel.Controls.Add(textBox, 1, currentRow); + currentRow++; + } + } + + private string GetParamValue(string paramName, SourceDef source) + { + var valueFromSource = source.Defaults != null && source.Defaults.ContainsKey(paramName) ? source.Defaults[paramName] : ""; + + + if (_currentApp == null || _currentApp.Params == null || !_currentApp.Params.ContainsKey(paramName)) + return valueFromSource; + return _currentApp.Params[paramName]; + } + + + private void InitializeForm() + { + + + _currentRow = 0; + _mainLayout.Controls.Add(_nameLabel, 0, _currentRow); + _mainLayout.Controls.Add(_nameField, 1, _currentRow++); + _mainLayout.Controls.Add(_msfsVersionsLabel, 0, _currentRow++); + _mainLayout.Controls.Add(_fsPanel, 1, _currentRow); + _mainLayout.SetColumnSpan(_fsPanel, 2); + _currentRow++; + _mainLayout.Controls.Add(_fsFieldsPanel, 0, _currentRow); + _mainLayout.SetColumnSpan(_fsFieldsPanel, 2); + _currentRow++; + + _mainLayout.Controls.Add(_sourceLabel, 0, _currentRow); + _mainLayout.Controls.Add(_sourceField, 1, _currentRow++); + _mainLayout.Controls.Add(_paramsSubtitle, 0, _currentRow); + _mainLayout.SetColumnSpan(_paramsSubtitle, 2); + _currentRow++; + _mainLayout.Controls.Add(_paramsPanel, 0, _currentRow); + _mainLayout.SetColumnSpan(_paramsPanel, 2); + _currentRow++; + _mainLayout.Controls.Add(_downloadUrlLabel, 0, _currentRow); + _mainLayout.Controls.Add(_downloadUrlField, 1, _currentRow++); + _currentRow++; + + + _mainLayout.Controls.Add(_buttonsPanel, 0, _currentRow++); + + AddFsCheckboxes(); + AddButtons(); + + // UpdateForm(); + } + + private void AddFsCheckboxes() + { + foreach (var fs in _flightSims) + { + var checkBox = new CheckBox + { + Text = fs.ShortName, + Checked = _currentApp != null && _currentApp.MsfsVersions.Contains(fs.ShortName), + }; + checkBox.CheckedChanged += (s, e) => + { + if (checkBox.Checked) + { + if (!_selectedFs.Contains(fs.ShortName)) + { + _selectedFs.Add(fs.ShortName); + } + } + else + { + _selectedFs.Remove(fs.ShortName); + } + UpdateFsFields(); + }; + _fsPanel.Controls.Add(checkBox); + _fsCheckBoxes.Add(checkBox); + } + } + + + private void OnSourceFieldIndexChanged(object? sender, EventArgs e) + { + if (_isEditable && _sourceField.SelectedItem != null) + { + UpdateParamFields(); + } + } + + private void AddButtons() + { + _buttonsPanel.Controls.Clear(); + _buttonsPanel.Controls.Add(_saveButton); + _buttonsPanel.Controls.Add(_closeButton); + } + + private void OnSaveButtonClicked(object? sender, EventArgs e) + { + try { + var paramsDict = _paramFields.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Text.Trim()); + var fsFieldsDict = _fsFields.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.ToDictionary(fkvp => fkvp.Key, fkvp => fkvp.Value.Text.Trim()) + ); + var requiredParams = _checkerTypesDef + .First(ct => ct.Name == _sourcesDef.FirstOrDefault(sd => sd.Id == _sourceField.SelectedItem?.ToString())?.Type) + .Params.Where(p => p.Required) + .Select(p => p.Name); + if (requiredParams.Any(rp => string.IsNullOrWhiteSpace(paramsDict[rp]))) + { + throw new Exception("All required parameters must be filled."); + } + var msfsVersions = _selectedFs; + var isNewApp = (_currentApp == null || string.IsNullOrEmpty(_currentApp.Id)); + var app = new AppConfig + { + Id = isNewApp ? GetUuid() : _currentApp.Id, + Name = _nameField.Text.Trim(), + MsfsVersions = msfsVersions, + Source = _sourceField.SelectedItem?.ToString() ?? "", + Params = paramsDict, + FsFields = fsFieldsDict, + DownloadUrl = _downloadUrlField.Text.Trim(), + CurrentVersion = _currentApp?.CurrentVersion ?? "", + LatestVersion = _currentApp?.LatestVersion ?? "", + Status = _currentApp?.Status ?? AppStatus.None + }; + + if (isNewApp) + { + _appsManager.Insert(app); + } else + { + _appsManager.Update(app); + } + + _currentApp = app; + OnAppChanged?.Invoke(this, "App saved"); + Close(); + } + catch (Exception ex) + { + MessageBox.Show($"Error: {ex.Message}", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private string GetUuid() + { + Guid uuid = Guid.NewGuid(); + return uuid.ToString(); + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/forms/AppDetailsForm.resx b/ModVersionChecker/forms/AppDetailsForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/ModVersionChecker/forms/AppDetailsForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ModVersionChecker/forms/FormFactory.cs b/ModVersionChecker/forms/FormFactory.cs new file mode 100644 index 0000000..29ed561 --- /dev/null +++ b/ModVersionChecker/forms/FormFactory.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; + +namespace ModVersionChecker.forms +{ + public class FormFactory : IFormFactory + { + private readonly IServiceProvider _serviceProvider; + + public FormFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + public AppDetailsForm CreateAppDetailsForm(AppConfig? app, bool isEditable, EventHandler? onAppChanged) + { + var configManager = _serviceProvider.GetRequiredService(); + var appsManager = _serviceProvider.GetRequiredService(); + var sourcesDefManager = _serviceProvider.GetRequiredService(); + var checkerTypesDefManager = _serviceProvider.GetRequiredService(); + var flightSimsManager = _serviceProvider.GetRequiredService(); + var form = new AppDetailsForm(configManager, appsManager, sourcesDefManager, checkerTypesDefManager, flightSimsManager); + form.SetApp(app, false); + form.SetEditable(isEditable); + if (onAppChanged != null) + { + form.OnAppChanged += onAppChanged; + } + return form; + } + + public GlobalConfigForm CreateGlobalConfigForm() + { + var configManager = _serviceProvider.GetRequiredService(); + var form = new GlobalConfigForm(configManager); + return form; + } + + public SourcesConfigForm CreateSourcesConfigForm(EventHandler? onSourcesChanged) + { + var sourcesDefManager = _serviceProvider.GetRequiredService(); + var formFactory = _serviceProvider.GetRequiredService(); + var form = new SourcesConfigForm(formFactory, sourcesDefManager); + if (onSourcesChanged != null) + { + form.OnSourcesChanged += onSourcesChanged; + } + return form; + } + + public SourceDetailForm CreateSourceDetailForm(SourceDef? sourceDef, EventHandler? onSourceChanged) + { + var sourcesDefManager = _serviceProvider.GetRequiredService(); + var checkerTypesDefManager = _serviceProvider.GetRequiredService(); + var formFactory = _serviceProvider.GetRequiredService(); + var form = new SourceDetailForm(formFactory, sourcesDefManager); + form.SourceDef = sourceDef; + + if (onSourceChanged != null) + { + form.UpdateFields(); + form.OnSourceChanged += onSourceChanged; + } + return form; + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/forms/GlobalConfigForm.cs b/ModVersionChecker/forms/GlobalConfigForm.cs new file mode 100644 index 0000000..cdc2004 --- /dev/null +++ b/ModVersionChecker/forms/GlobalConfigForm.cs @@ -0,0 +1,122 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; + +namespace ModVersionChecker.forms +{ + public class GlobalConfigForm : Form + { + private IConfigManager _configManager; + private GlobalConfig _config; + + private Label _millislabel, _checkStartupLabel; + private TrackBar _millisField; + private CheckBox _checkStartupField; + private Button _saveButton, _cancelButton; + private TableLayoutPanel _mainLayout, _configsPanel; + private FlowLayoutPanel _buttonPanel; + + public GlobalConfigForm(IConfigManager configManager) + { + _configManager = configManager; + _config = _configManager.GetConfig(); + InitializeComponent(); + } + private void InitializeComponent() + { + SuspendLayout(); + + ClientSize = new System.Drawing.Size(600, 250); + Name = "GlobalConfigForm"; + Text = "Global Configuration"; + StartPosition = FormStartPosition.CenterParent; + Padding = new Padding(10, 20, 10, 20 ); + _mainLayout = GetMainLayout(); + _configsPanel = GetConfigsPanel(); + + _buttonPanel = GetButtonsPanel(); + + _mainLayout.Controls.Add(_configsPanel, 0, 0); + _mainLayout.Controls.Add(_buttonPanel, 0, 1); + + Controls.Add(_mainLayout); + + + ResumeLayout(false); + + + } + + private FlowLayoutPanel GetButtonsPanel() + { + var buttonsPanel = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, AutoSize = true, Dock = DockStyle.Fill }; + _saveButton = new Button { Text = "Save", AutoSize = true }; + _saveButton.Click += (sender, e) => + { + _config.IntervalMinutes = _millisField.Value; + _config.CheckOnStartup = _checkStartupField.Checked; + _configManager.Save(_config); + DialogResult = DialogResult.OK; + Close(); + }; + _cancelButton = new Button { Text = "Cancel", AutoSize = true }; + _cancelButton.Click += (sender, e) => + { + DialogResult = DialogResult.Cancel; + Close(); + }; + buttonsPanel.Controls.Add(_saveButton); + buttonsPanel.Controls.Add(_cancelButton); + return buttonsPanel; + } + + private TableLayoutPanel GetConfigsPanel() + { + // Initialize the configurations panel + var configsPanel = new TableLayoutPanel + { + AutoSize = true, + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 2, + ColumnStyles = { new ColumnStyle(SizeType.Absolute, 150), new ColumnStyle(SizeType.Percent, 100) } + }; + + _millislabel = new Label { Text = "Millis", Width = 150}; + _millisField = new TrackBar { Minimum = 0, Maximum = 120, Value= _config.IntervalMinutes, Width = 300, TickStyle = TickStyle.None }; + FlowLayoutPanel millisPanel = new FlowLayoutPanel { FlowDirection = FlowDirection.LeftToRight, AutoSize = true }; + Label millisValue = new Label { Text = _millisField.Value.ToString() + " minutes", AutoSize = true, Padding = new Padding(10, 10, 0, 0) }; + millisPanel.Controls.Add(_millisField); + millisPanel.Controls.Add(millisValue); + + _millisField.Scroll += (sender, e) => { millisValue.Text = _millisField.Value.ToString() + " minutes"; }; + + _checkStartupLabel = new Label { Text = "Check on Startup:", Width = 150 }; + _checkStartupField = new CheckBox { Checked = _config.CheckOnStartup }; + + configsPanel.Controls.Add(_millislabel, 0, 0); + configsPanel.Controls.Add(millisPanel, 1, 0); + configsPanel.Controls.Add(_checkStartupLabel, 0, 1); + configsPanel.Controls.Add(_checkStartupField, 1, 1); + + return configsPanel; + } + + private TableLayoutPanel GetMainLayout() + { + // Initialize the main layout panel + var mainLayout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + RowCount = 2, + ColumnCount = 1 + }; + mainLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 150)); // Paths panel height + mainLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 50)); // Button panel height + Controls.Add(mainLayout); + return mainLayout; + } + + // Add methods and properties for global configuration management here + + } +} diff --git a/ModVersionChecker/forms/IFormFactory.cs b/ModVersionChecker/forms/IFormFactory.cs new file mode 100644 index 0000000..78637ee --- /dev/null +++ b/ModVersionChecker/forms/IFormFactory.cs @@ -0,0 +1,14 @@ +using ModVersionChecker.data.model; + +namespace ModVersionChecker.forms +{ + public interface IFormFactory + { + AppDetailsForm CreateAppDetailsForm(AppConfig? app, bool isEditable, EventHandler? onAppChanged); + GlobalConfigForm CreateGlobalConfigForm(); + + SourcesConfigForm CreateSourcesConfigForm(EventHandler? onSourcesChanged); + + SourceDetailForm CreateSourceDetailForm(SourceDef? sourceDef, EventHandler? onSourceChanged); + } +} \ No newline at end of file diff --git a/ModVersionChecker/forms/MainForm.cs b/ModVersionChecker/forms/MainForm.cs new file mode 100644 index 0000000..f1fdef4 --- /dev/null +++ b/ModVersionChecker/forms/MainForm.cs @@ -0,0 +1,310 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using ModVersionChecker.utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Intrinsics.Arm; +using System.Windows.Forms; + + +namespace ModVersionChecker.forms +{ + public class MainForm : Form + { + private readonly IConfigManager _configManager; + private readonly IAppsManager _appsManager; + private readonly IFormFactory _formFactory; + private readonly IAppStatusManager _appStatusManager; + private readonly IFlightSimsManager _fsManager; + private readonly TableLayoutPanel _mainLayout; + + private readonly GlobalConfig _globalConfig; + private List _apps = new List(); + private ListView _listView; + private ImageList _statusImageList = new ImageList(); + + public event EventHandler OnConfigChanged; + public event EventHandler OnRecheck; + private EventHandler onAppChangedHandler; + private MenuStrip _menuStrip; + private List _fsMods; + private readonly Dictionary _fsModPathTextBoxes = new Dictionary(); + + public MainForm(IConfigManager configManager, IAppsManager appsManager, IFormFactory formFactory, IAppStatusManager appStatusManager, IFlightSimsManager fsManager) + { + _configManager = configManager ?? throw new ArgumentNullException(nameof(configManager)); + _appsManager = appsManager ?? throw new ArgumentNullException(nameof(appsManager)); + _formFactory = formFactory ?? throw new ArgumentNullException(nameof(formFactory)); + _appStatusManager = appStatusManager ?? throw new ArgumentNullException(nameof(appStatusManager)); + _fsManager = fsManager ?? throw new ArgumentNullException(nameof(fsManager)); + _fsMods = _fsManager.Load(); + + _statusImageList.Images.Add("none", new Icon("Resources/ok-icon.ico")); + _statusImageList.Images.Add("update", new Icon("Resources/up-icon.ico")); + _statusImageList.Images.Add("error", new Icon("Resources/error-icon.ico")); + + Text = "Update Checker Configuration"; + Size = new Size(600, 800); + StartPosition = FormStartPosition.CenterScreen; + + _globalConfig = configManager.Load() ?? new GlobalConfig(); + + _mainLayout = GetMainLayout(); + + _mainLayout.Controls.Add(GetPathsPanel(), 0, 0); + + _listView = GetListView(); + _listView.SmallImageList = _statusImageList; + + _mainLayout.Controls.Add(_listView , 0, 1); + + _mainLayout.Controls.Add(GetButtonsPanel(), 0, 2); + + onAppChangedHandler = (s2, e) => + { + UpdateListView(); + OnConfigChanged?.Invoke(this, EventArgs.Empty); + }; + + InitializeMenu(); + } + + private TableLayoutPanel GetMainLayout() + { + // Initialize the main layout panel + var mainLayout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + RowCount = 3, + ColumnCount = 1 + }; + mainLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 150)); // Paths panel height + mainLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 70)); // ListView takes remaining space + mainLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 50)); // Button panel height + Controls.Add(mainLayout); + return mainLayout; + } + + private FlowLayoutPanel GetPathsPanel() + { + var pathPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown }; + + foreach (var fsMod in _fsMods) + { + var singlePathPanel = new FlowLayoutPanel { FlowDirection = FlowDirection.LeftToRight, AutoSize = true }; + singlePathPanel.Controls.Add(new Label { Text = $"{fsMod.Name} Path:", Width = 100 }); + var pathField = new TextBox { Text = fsMod.Path ?? "", Width = 300 }; + singlePathPanel.Controls.Add(pathField); + _fsModPathTextBoxes.Add(fsMod.ShortName, pathField); + pathPanel.Controls.Add(singlePathPanel); + } + + //var pathPanel2024 = new FlowLayoutPanel { FlowDirection = FlowDirection.LeftToRight, AutoSize = true }; + //pathPanel2024.Controls.Add(new Label { Text = "MSFS 2024 Path:", Width = 100 }); + //var msfs2024PathField = new TextBox { Text = _globalConfig.FsModPaths.ContainsKey("msfs2024") ? _globalConfig.FsModPaths["msfs2024"].Path : "", Width = 300 }; + //pathPanel2024.Controls.Add(msfs2024PathField); + //pathPanel.Controls.Add(pathPanel2024); + + var savePathsButton = new Button { Text = "Save Paths" }; + savePathsButton.Click += (s, e) => + { + foreach (var fsMod in _fsMods) + { + fsMod.Path = _fsModPathTextBoxes[fsMod.ShortName].Text; + _fsManager.Save(fsMod); + } + _fsMods = _fsManager.Load(); + }; + pathPanel.Controls.Add(savePathsButton); + return pathPanel; + } + + private ListView GetListView() + { + var listView = new ListView + { + Dock = DockStyle.Fill, + View = View.Details, + FullRowSelect = true, + MultiSelect = false, + Visible = true, + Sorting = SortOrder.Ascending + }; + + listView.Columns.Add("Name", 150); + listView.Columns.Add("MSFS Versions", 100); + listView.Columns.Add("Current", 80); + listView.Columns.Add("Latest", 80); + listView.Columns.Add("Last Checked", 150); + + listView.DoubleClick += (s, e) => + { + if (listView.SelectedItems.Count > 0) + { + ListViewItem selectedItem = listView.SelectedItems[0]; + AppConfig? app = selectedItem.Tag as AppConfig; + if (app == null) return; + if (_appStatusManager.GetAppStatus(app.Id) == AppStatus.UpdateAvailable) + { + if (string.IsNullOrEmpty(app.DownloadUrl)) + { + MessageBox.Show("No download URL specified for this app."); + return; + } + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = app.DownloadUrl, + UseShellExecute = true + }); + } else + { + var form = _formFactory.CreateAppDetailsForm(app, true, onAppChangedHandler); + form.FormClosed += (s2, e) => + { + UpdateListView(); + }; + UpdateListView(); + form.ShowDialog(); + form.BringToFront(); + } + + } + }; + + return listView; + } + + private FlowLayoutPanel GetButtonsPanel() { + var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill }; + var addButton = new Button { Text = "Add App" }; + var editButton = new Button { Text = "Edit App", Enabled = false }; + var deleteButton = new Button { Text = "Delete App", Enabled = false }; + var recheckButton = new Button { Text = "Recheck Versions" }; + + addButton.Click += (s, e) => + { + var form = _formFactory.CreateAppDetailsForm(null, true, onAppChangedHandler); // Use factory + form.ShowDialog(); + }; + editButton.Click += (s, e) => + { + if (_listView.SelectedItems.Count > 0) + { + ListViewItem selectedItem = _listView.SelectedItems[0]; + AppConfig app = selectedItem.Tag as AppConfig; + var form = _formFactory.CreateAppDetailsForm(app, true, onAppChangedHandler); // Use factory + form.ShowDialog(); + + } + }; + deleteButton.Click += (s, e) => + { + if (_listView.SelectedItems.Count > 0 && _listView.SelectedItems[0].Tag != null) + { + _appsManager.Delete((_listView.SelectedItems[0].Tag as AppConfig).Id); + UpdateListView(); + OnConfigChanged?.Invoke(this, EventArgs.Empty); + } + }; + _listView.SelectedIndexChanged += (s, e) => + { + editButton.Enabled = deleteButton.Enabled = _listView.SelectedItems.Count > 0; + }; + + // Add recheck logic here + recheckButton.Click += async (s, e) => + { + recheckButton.Enabled = false; + OnRecheck.Invoke(this, "User initiated recheck from ConfigForm"); + recheckButton.Enabled = true; + }; + + buttonPanel.Controls.AddRange(new[] { addButton, editButton, deleteButton, recheckButton }); + return buttonPanel; + } + + public void UpdateListView() + { + _apps = _appsManager.Load(); + _listView.Items.Clear(); + foreach (var app in _apps) + { + var item = new ListViewItem(app.Name); + item.Tag = app; + item.SubItems.Add(string.Join(", ", app.MsfsVersions)); + try + { + var fsMod = _fsMods.FirstOrDefault(fs => fs.ShortName == "msfs2024"); + // Pass the FsModPathConfig object directly, not its Path property + var currentVersion = app.CurrentVersion; + var latestVersion = app.LatestVersion; + var lastChecked = TimeUtils.ToFriendlyTime(app.LastCheckedAt); + item.SubItems.Add(currentVersion); + item.SubItems.Add(latestVersion); + item.SubItems.Add(lastChecked); + } + catch (Exception ex) + { + item.SubItems.Add($"Error: {ex.Message}"); + } + switch (_appStatusManager.GetAppStatus(app.Id)) + { + case AppStatus.UpdateAvailable: + item.ImageKey = "update"; + break; + case AppStatus.Error: + item.ImageKey = "error"; + break; + default: + item.ImageKey = "none"; + break; + } + _listView.Items.Add(item); + } + Console.WriteLine($"UpdateListView item count: {_listView.Items.Count}"); + } + + private void InitializeMenu() + { + _menuStrip = new MenuStrip(); + + // Create top-level menu + var configMenu = new ToolStripMenuItem("Configuration"); + + // Add sub-menu items + var globalConfigItem = new ToolStripMenuItem("Global Settings"); + globalConfigItem.Click += (s, e) => ShowGlobalConfigDialog(); + + var sourcesConfigItem = new ToolStripMenuItem("Sources"); + sourcesConfigItem.Click += (s, e) => ShowSourcesConfigDialog(); + + var FlightSimsConfigItem = new ToolStripMenuItem("Flight Sims"); + FlightSimsConfigItem.Click += (s, e) => MessageBox.Show("Flight Sims configuration dialog would open here."); + + configMenu.DropDownItems.Add(globalConfigItem); + configMenu.DropDownItems.Add(sourcesConfigItem); + configMenu.DropDownItems.Add(FlightSimsConfigItem); + + _menuStrip.Items.Add(configMenu); + + // Add the menu to the form + Controls.Add(_menuStrip); + MainMenuStrip = _menuStrip; + } + + private void ShowGlobalConfigDialog() + { + // Show your global config form/dialog here + var globalConfigForm = _formFactory.CreateGlobalConfigForm(); + globalConfigForm.ShowDialog(); + } + + private void ShowSourcesConfigDialog() + { + EventHandler onSourcesChanged = (s, e) => MessageBox.Show("Sources Changed"); + var form = _formFactory.CreateSourcesConfigForm(onSourcesChanged); + form.ShowDialog(); + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/forms/MainForm.resx b/ModVersionChecker/forms/MainForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/ModVersionChecker/forms/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ModVersionChecker/forms/SourceDetailForm.cs b/ModVersionChecker/forms/SourceDetailForm.cs new file mode 100644 index 0000000..7d04a20 --- /dev/null +++ b/ModVersionChecker/forms/SourceDetailForm.cs @@ -0,0 +1,110 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.forms +{ + // Simple editor form for SourceDef + public class SourceDetailForm : Form + { + private readonly IFormFactory _formFactory; + private readonly ISourcesDefManager _sourceManager; + public SourceDef SourceDef { get; set; } + public Boolean IsEditable => !string.IsNullOrWhiteSpace(SourceDef?.Id); + private TextBox _idField, _nameField, _typeField, _defaultsField; + private Button _okButton, _cancelButton; + + public event EventHandler? OnSourceChanged; + + public SourceDetailForm(IFormFactory formFactory, ISourcesDefManager sourceManager) + { + _formFactory = formFactory ?? throw new ArgumentNullException(nameof(formFactory)); + _sourceManager = sourceManager ?? throw new ArgumentNullException(nameof(sourceManager)); + + InitializeComponent(); + _formFactory = formFactory; + } + + private void InitializeComponent() + { + Text = "Edit SourceDef"; + Size = new Size(400, 300); + StartPosition = FormStartPosition.CenterParent; + Padding = new Padding(10); + + var layout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + RowCount = 5, + ColumnCount = 2, + Padding = new Padding(10) + }; + + layout.Controls.Add(new Label { Text = "Id:", Width = 80 }, 0, 0); + _idField = new TextBox { Text = "", Width = 200 }; + layout.Controls.Add(_idField, 1, 0); + + layout.Controls.Add(new Label { Text = "Name:", Width = 80 }, 0, 1); + _nameField = new TextBox { Text = "", Width = 200 }; + layout.Controls.Add(_nameField, 1, 1); + + layout.Controls.Add(new Label { Text = "Type:", Width = 80 }, 0, 2); + _typeField = new TextBox { Text = "", Width = 200 }; + layout.Controls.Add(_typeField, 1, 2); + + layout.Controls.Add(new Label { Text = "Defaults (key=value, comma separated):", Width = 80 }, 0, 3); + _defaultsField = new TextBox { Text = "", Width = 200 }; + layout.Controls.Add(_defaultsField, 1, 3); + + var buttonPanel = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Fill }; + _okButton = new Button { Text = "OK", DialogResult = DialogResult.OK }; + _cancelButton = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel }; + buttonPanel.Controls.Add(_okButton); + buttonPanel.Controls.Add(_cancelButton); + + layout.Controls.Add(buttonPanel, 0, 4); + layout.SetColumnSpan(buttonPanel, 2); + + Controls.Add(layout); + + _okButton.Click += (s, e) => + { + SourceDef.Id = _idField.Text.Trim(); + SourceDef.Name = _nameField.Text.Trim(); + SourceDef.Type = _typeField.Text.Trim(); + SourceDef.Defaults = ParseDefaults(_defaultsField.Text); + DialogResult = DialogResult.OK; + Close(); + }; + _cancelButton.Click += (s, e) => { DialogResult = DialogResult.Cancel; Close(); }; + } + + public void UpdateFields() + { + if (SourceDef != null) + { + _idField.Text = SourceDef.Id; + _nameField.Text = SourceDef.Name; + _typeField.Text = SourceDef.Type; + _defaultsField.Text = string.Join(", ", SourceDef.Defaults.Select(d => $"{d.Key}={d.Value}")); + } + } + + private Dictionary ParseDefaults(string text) + { + var dict = new Dictionary(); + var pairs = text.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var pair in pairs) + { + var kv = pair.Split(new[] { '=' }, 2); + if (kv.Length == 2) + dict[kv[0].Trim()] = kv[1].Trim(); + } + return dict; + } + } +} diff --git a/ModVersionChecker/forms/SourcesConfigForm.cs b/ModVersionChecker/forms/SourcesConfigForm.cs new file mode 100644 index 0000000..0b23cfa --- /dev/null +++ b/ModVersionChecker/forms/SourcesConfigForm.cs @@ -0,0 +1,136 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace ModVersionChecker.forms +{ + public class SourcesConfigForm : Form + { + private List _sourceDefs; + private ListView _listView; + private Button _addButton, _editButton, _deleteButton, _closeButton; + private TableLayoutPanel _mainLayout; + private readonly ISourcesDefManager _sourcesManager; + private readonly IFormFactory _formFactory; + public event EventHandler? OnSourcesChanged; + public List SourceDefs => _sourceDefs; + + public SourcesConfigForm(IFormFactory formFactory, ISourcesDefManager sourcesManager) + { + _sourcesManager = sourcesManager ?? throw new ArgumentNullException(nameof(sourcesManager)); + _formFactory = formFactory ?? throw new ArgumentNullException(nameof(formFactory)); + _sourceDefs = _sourcesManager.List() ?? new List(); + Padding = new Padding(20); + InitializeComponent(); + } + + private void InitializeComponent() + { + Text = "Source Definitions"; + Size = new Size(800, 400); + StartPosition = FormStartPosition.CenterParent; + + _mainLayout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + RowCount = 2, + ColumnCount = 1, + Padding = new Padding(10) + }; + _mainLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 80)); + _mainLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 20)); + + _listView = new ListView + { + Dock = DockStyle.Fill, + View = View.Details, + FullRowSelect = true, + MultiSelect = false, + GridLines = true + }; + _listView.Columns.Add("Id", 100); + _listView.Columns.Add("Name", 150); + _listView.Columns.Add("Type", 100); + _listView.Columns.Add("Defaults", -2); + + UpdateListView(); + + _mainLayout.Controls.Add(_listView, 0, 0); + + var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight }; + _addButton = new Button { Text = "Add" }; + _editButton = new Button { Text = "Edit", Enabled = false }; + _deleteButton = new Button { Text = "Delete", Enabled = false }; + _closeButton = new Button { Text = "Close", DialogResult = DialogResult.OK }; + + _addButton.Click += (s, e) => AddSourceDef(); + _editButton.Click += (s, e) => EditSourceDef(); + _deleteButton.Click += (s, e) => DeleteSourceDef(); + _closeButton.Click += (s, e) => Close(); + + _listView.SelectedIndexChanged += (s, e) => + { + bool hasSelection = _listView.SelectedItems.Count > 0; + _editButton.Enabled = hasSelection; + _deleteButton.Enabled = hasSelection; + }; + + buttonPanel.Controls.AddRange(new Control[] { _addButton, _editButton, _deleteButton, _closeButton }); + _mainLayout.Controls.Add(buttonPanel, 0, 1); + + Controls.Add(_mainLayout); + } + + private void UpdateListView() + { + _listView.Items.Clear(); + foreach (var src in _sourceDefs) + { + var item = new ListViewItem(src.Id); + item.SubItems.Add(src.Name); + item.SubItems.Add(src.Type); + item.SubItems.Add(string.Join(", ", src.Defaults.Select(d => $"{d.Key}={d.Value}"))); + item.Tag = src; + _listView.Items.Add(item); + } + } + + private void AddSourceDef() + { + EventHandler? handler = (s, e) => MessageBox.Show("Source Changed"); + var editor = _formFactory.CreateSourceDetailForm(null, handler); + if (editor.ShowDialog() == DialogResult.OK) + { + _sourceDefs.Add(editor.SourceDef); + UpdateListView(); + } + } + + private void EditSourceDef() + { + if (_listView.SelectedItems.Count == 0) return; + var src = _listView.SelectedItems[0].Tag as SourceDef; + EventHandler? handler = (s, e) => MessageBox.Show("Source Changed"); + var editor = _formFactory.CreateSourceDetailForm(src, handler); + + if (editor.ShowDialog() == DialogResult.OK) + { + //int idx = _sourceDefs.IndexOf(src); + //_sourceDefs[idx] = editor.SourceDef; + //UpdateListView(); + } + } + + private void DeleteSourceDef() + { + if (_listView.SelectedItems.Count == 0) return; + var src = _listView.SelectedItems[0].Tag as SourceDef; + _sourceDefs.Remove(src); + UpdateListView(); + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/managers/filesystem/AppStatusManager.cs b/ModVersionChecker/managers/filesystem/AppStatusManager.cs new file mode 100644 index 0000000..69b948a --- /dev/null +++ b/ModVersionChecker/managers/filesystem/AppStatusManager.cs @@ -0,0 +1,52 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.filesystem +{ + + public class AppStatusManager : IAppStatusManager + { + private Dictionary _statuses = new Dictionary(); + + public AppStatusManager() { } + public AppStatus? GetAppStatus(string appId) + { + if (!_statuses.ContainsKey(appId)) + { + return null; + } + return _statuses[appId]; + } + public List Load() + { + throw new NotImplementedException(); + } + public void Save(List appStatuses) + { + throw new NotImplementedException(); + } + public void UpdateAppStatus(string appId, AppStatus appStatus) + { + if (_statuses.ContainsKey(appId)) + { + _statuses[appId] = appStatus; + } else + { + _statuses.Add(appId, appStatus); + } + } + public bool DeleteAppStatus(string appId) { + return _statuses.Remove(appId); + } + public void ClearAll() + { + _statuses.Clear(); + } + + } +} diff --git a/ModVersionChecker/managers/filesystem/AppsManager.cs b/ModVersionChecker/managers/filesystem/AppsManager.cs new file mode 100644 index 0000000..8c75406 --- /dev/null +++ b/ModVersionChecker/managers/filesystem/AppsManager.cs @@ -0,0 +1,42 @@ +using ModVersionChecker.managers.interfaces; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace ModVersionChecker.managers.filesystem +{ + public class AppsManager + { + private readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "data", "apps.json"); + + public List Load() + { + if (!File.Exists(FilePath)) + return new List(); + var json = File.ReadAllText(FilePath); + return JsonSerializer.Deserialize>(json) ?? new(); + } + + public void Save(List apps) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(apps, options); + File.WriteAllText(FilePath, json); + } + + public void Upsert(AppConfig app) + { + var apps = Load(); + var index = apps.FindIndex(a => a.Id == app.Id); + if (index >= 0) + { + apps[index] = app; + } + else + { + apps.Add(app); + } + Save(apps); + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/managers/filesystem/CheckerTypesDefManager.cs b/ModVersionChecker/managers/filesystem/CheckerTypesDefManager.cs new file mode 100644 index 0000000..7f51b80 --- /dev/null +++ b/ModVersionChecker/managers/filesystem/CheckerTypesDefManager.cs @@ -0,0 +1,28 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace ModVersionChecker.managers.filesystem +{ + public class CheckerTypesDefManager : ICheckerTypesDefManager + { + private readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "data", "checkerTypesDef.json"); + + public List Load() + { + if (!File.Exists(FilePath)) + return new List(); + var json = File.ReadAllText(FilePath); + return JsonSerializer.Deserialize>(json) ?? new(); + } + + public void Save(List checkerTypesDef) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(checkerTypesDef, options); + File.WriteAllText(FilePath, json); + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/managers/filesystem/ConfigManager.cs b/ModVersionChecker/managers/filesystem/ConfigManager.cs new file mode 100644 index 0000000..b4bba36 --- /dev/null +++ b/ModVersionChecker/managers/filesystem/ConfigManager.cs @@ -0,0 +1,40 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System; +using System.IO; +using System.Text.Json; + +namespace ModVersionChecker.managers.filesystem +{ + public class ConfigManager : IConfigManager + { + + private static readonly string _filePath = Path.Combine(AppContext.BaseDirectory, "data", "config.json"); + private GlobalConfig _config; + + public ConfigManager() + { + _config = Load(); + } + + public GlobalConfig Load() + { + if (!File.Exists(_filePath)) + return new GlobalConfig(); + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize(json) ?? new GlobalConfig(); + } + + public GlobalConfig GetConfig() + { + return _config; + } + + public void Save(GlobalConfig config) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(config, options); + File.WriteAllText(_filePath, json); + } + } +} diff --git a/ModVersionChecker/managers/filesystem/NotifyIconService.cs b/ModVersionChecker/managers/filesystem/NotifyIconService.cs new file mode 100644 index 0000000..fbb70d2 --- /dev/null +++ b/ModVersionChecker/managers/filesystem/NotifyIconService.cs @@ -0,0 +1,22 @@ +using ModVersionChecker.managers.interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.filesystem +{ + public class NotifyIconService : INotifyIconService + { + private NotifyIcon? _notifyIcon; + public void SetNotifyIcon(NotifyIcon icon) + { + _notifyIcon = icon; + } + public void ShowBalloonTip(int millis, string title, string message, ToolTipIcon icon) + { + _notifyIcon?.ShowBalloonTip(millis, title, message, icon); + } + } +} diff --git a/ModVersionChecker/managers/filesystem/SourcesDefManager.cs b/ModVersionChecker/managers/filesystem/SourcesDefManager.cs new file mode 100644 index 0000000..52dfd73 --- /dev/null +++ b/ModVersionChecker/managers/filesystem/SourcesDefManager.cs @@ -0,0 +1,52 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace ModVersionChecker.managers.filesystem +{ + public class SourcesDefManager + { + private readonly string _filePath = Path.Combine(AppContext.BaseDirectory, "data", "sourcesDef.json"); + private List _sourcesDef = new List(); + + public SourcesDefManager() + { + _sourcesDef = Load(); + } + + private List Load() + { + if (!File.Exists(_filePath)) + return new List(); + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize>(json) ?? new(); + } + + public List GetSourcesDef() + { + return _sourcesDef; + } + + public void AddSourceDef(SourceDef sourceDef) + { + _sourcesDef.Add(sourceDef); + Save(_sourcesDef); + } + + public void RemoveSourceDef(string id) + { + _sourcesDef.RemoveAll(s => s.Id == id); + Save(_sourcesDef); + } + + + public void Save(List sourcesDef) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(sourcesDef, options); + File.WriteAllText(_filePath, json); + } + } +} \ No newline at end of file diff --git a/ModVersionChecker/managers/interfaces/IAppStatusManager.cs b/ModVersionChecker/managers/interfaces/IAppStatusManager.cs new file mode 100644 index 0000000..b103070 --- /dev/null +++ b/ModVersionChecker/managers/interfaces/IAppStatusManager.cs @@ -0,0 +1,23 @@ +using ModVersionChecker.data.model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.interfaces +{ + public interface IAppStatusManager + { + List Load(); + void Save(List appStatuses); + AppStatus? GetAppStatus(string appId); + + void UpdateAppStatus(string appId, AppStatus appStatus); + + bool DeleteAppStatus(string appId); + + void ClearAll(); + + } +} diff --git a/ModVersionChecker/managers/interfaces/IAppsManager.cs b/ModVersionChecker/managers/interfaces/IAppsManager.cs new file mode 100644 index 0000000..f101df3 --- /dev/null +++ b/ModVersionChecker/managers/interfaces/IAppsManager.cs @@ -0,0 +1,26 @@ +using ModVersionChecker.data.model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.interfaces +{ + public interface IAppsManager + { + + List Load(); + + void Save(List apps); + + public void Insert(AppConfig app); + + public void Update(AppConfig app); + + void Delete(string id); + + void UpdateStatus(AppConfig app, AppStatus status); + + } +} diff --git a/ModVersionChecker/managers/interfaces/ICheckerTypesDefManager.cs b/ModVersionChecker/managers/interfaces/ICheckerTypesDefManager.cs new file mode 100644 index 0000000..7e93700 --- /dev/null +++ b/ModVersionChecker/managers/interfaces/ICheckerTypesDefManager.cs @@ -0,0 +1,15 @@ +using ModVersionChecker.data.model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.interfaces +{ + public interface ICheckerTypesDefManager + { + List Load(); + void Save(List checkerTypesDef); + } +} diff --git a/ModVersionChecker/managers/interfaces/IConfigManager.cs b/ModVersionChecker/managers/interfaces/IConfigManager.cs new file mode 100644 index 0000000..6efd1e0 --- /dev/null +++ b/ModVersionChecker/managers/interfaces/IConfigManager.cs @@ -0,0 +1,16 @@ +using ModVersionChecker.data.model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.interfaces +{ + public interface IConfigManager + { + GlobalConfig Load(); + void Save(GlobalConfig config); + GlobalConfig GetConfig(); + } +} diff --git a/ModVersionChecker/managers/interfaces/IFlightSimsManager.cs b/ModVersionChecker/managers/interfaces/IFlightSimsManager.cs new file mode 100644 index 0000000..0681204 --- /dev/null +++ b/ModVersionChecker/managers/interfaces/IFlightSimsManager.cs @@ -0,0 +1,17 @@ +using ModVersionChecker.data.model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.interfaces +{ + public interface IFlightSimsManager + { + List Load(); + void Save(FsModPathConfig config); + + FsModPathConfig? GetByShortName(string id); + } +} diff --git a/ModVersionChecker/managers/interfaces/INotifyIconService.cs b/ModVersionChecker/managers/interfaces/INotifyIconService.cs new file mode 100644 index 0000000..a8e35c1 --- /dev/null +++ b/ModVersionChecker/managers/interfaces/INotifyIconService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.interfaces +{ + public interface INotifyIconService + { + void SetNotifyIcon(NotifyIcon icon); + void ShowBalloonTip(int millis, string title, string message, ToolTipIcon icon); + } +} diff --git a/ModVersionChecker/managers/interfaces/ISourcesDefManager.cs b/ModVersionChecker/managers/interfaces/ISourcesDefManager.cs new file mode 100644 index 0000000..9b327b7 --- /dev/null +++ b/ModVersionChecker/managers/interfaces/ISourcesDefManager.cs @@ -0,0 +1,19 @@ +using ModVersionChecker.data.model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.interfaces +{ + public interface ISourcesDefManager + { + List List(); + + SourceDef? GetById(string id); + void AddSourceDef(SourceDef sourceDef); + void RemoveSourceDef(string id); + void Save(SourceDef sourceDef); + } +} diff --git a/ModVersionChecker/managers/litedb/AppConfigLiteDb.cs b/ModVersionChecker/managers/litedb/AppConfigLiteDb.cs new file mode 100644 index 0000000..72965a0 --- /dev/null +++ b/ModVersionChecker/managers/litedb/AppConfigLiteDb.cs @@ -0,0 +1,70 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using ModVersionChecker.utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.litedb +{ + public class AppConfigLiteDb : LiteDb, IAppsManager + { + private string collection = LiteDb.APPS_COLLECTION; + public List Load() + { + var col = _db.GetCollection(collection); + return col.FindAll().ToList(); + } + + public void Insert(AppConfig app) + { + var now = TimeUtils.GetUnixTimeMillis(null); + app.CreatedAt = now; + app.UpdatedAt = now; + var col = _db.GetCollection(collection); + col.Insert(app); + } + + public void Update(AppConfig app) + { + var now = TimeUtils.GetUnixTimeMillis(null); + app.UpdatedAt = now; + var col = _db.GetCollection(collection); + col.Update(app); + } + + //public void Upsert(AppConfig app) + //{ + // var now = TimeUtils.GetUnixTimeMillis(null); + // app.UpdatedAt = now; + // var col = _db.GetCollection(collection); + // if (string.IsNullOrEmpty(app.Id)) + // { + // app.CreatedAt = now; + // col.Insert(app); + // } + // col.Update(app); + //} + + public void Delete(string id) + { + var col = _db.GetCollection(collection); + col.Delete(id); + } + + public void Save(List apps) + { + + } + + public void UpdateStatus(AppConfig app, AppStatus status) + { + app.LastCheckedAt = TimeUtils.GetUnixTimeMillis(null); + app.Status = status; + var col = _db.GetCollection(collection); + col.Update(app); + } + } +} diff --git a/ModVersionChecker/managers/litedb/ConfigLiteDb.cs b/ModVersionChecker/managers/litedb/ConfigLiteDb.cs new file mode 100644 index 0000000..46dd6ec --- /dev/null +++ b/ModVersionChecker/managers/litedb/ConfigLiteDb.cs @@ -0,0 +1,24 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; + +namespace ModVersionChecker.managers.litedb +{ + public class ConfigLiteDb : LiteDb, IConfigManager + { + private string collection = LiteDb.CONFIG_COLLECTION; + public GlobalConfig Load() + { + var col = _db.GetCollection(collection); + return col.FindAll().FirstOrDefault() ?? new GlobalConfig(); + } + public void Save(GlobalConfig config) + { + var col = _db.GetCollection(collection); + col.Upsert(config); + } + public GlobalConfig GetConfig() + { + return Load(); + } + } +} diff --git a/ModVersionChecker/managers/litedb/FlightSimsLiteDb.cs b/ModVersionChecker/managers/litedb/FlightSimsLiteDb.cs new file mode 100644 index 0000000..9c56e1b --- /dev/null +++ b/ModVersionChecker/managers/litedb/FlightSimsLiteDb.cs @@ -0,0 +1,32 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.litedb +{ + internal class FlightSimsLiteDb : LiteDb, IFlightSimsManager + { + private string collection = FLIGHT_SIMS_COLLECTION; + public List Load() + { + var col = _db.GetCollection(collection); + return col.FindAll().ToList(); + } + + public void Save(FsModPathConfig config) + { + var col = _db.GetCollection(collection); + col.Upsert(config); + } + + public FsModPathConfig? GetByShortName(string id) + { + var col = _db.GetCollection(collection); + return col.FindOne(x => x.ShortName == id); + } + } +} diff --git a/ModVersionChecker/managers/litedb/LiteDb.cs b/ModVersionChecker/managers/litedb/LiteDb.cs new file mode 100644 index 0000000..e3f35d5 --- /dev/null +++ b/ModVersionChecker/managers/litedb/LiteDb.cs @@ -0,0 +1,16 @@ +using LiteDB; + +namespace ModVersionChecker.managers.litedb +{ + public class LiteDb + { + public static string DB_PATH = "ModVersionChecker.db"; + public static string APPS_COLLECTION = "apps"; + public static string CHECKER_TYPES_DEF_COLLECTION = "checker_types_def"; + public static string SOURCES_DEF_COLLECTION = "sources_def"; + public static string CONFIG_COLLECTION = "config"; + public static string FLIGHT_SIMS_COLLECTION = "flight_sims"; + + protected LiteDatabase _db = LiteDbSingleton.Instance; + } +} diff --git a/ModVersionChecker/managers/litedb/LiteDbSingleton.cs b/ModVersionChecker/managers/litedb/LiteDbSingleton.cs new file mode 100644 index 0000000..2a6821e --- /dev/null +++ b/ModVersionChecker/managers/litedb/LiteDbSingleton.cs @@ -0,0 +1,12 @@ +using LiteDB; + +public static class LiteDbSingleton +{ + private static readonly LiteDatabase _db = new LiteDatabase(new ConnectionString + { + Filename = "ModVersionChecker.db", + Connection = ConnectionType.Shared + }); + + public static LiteDatabase Instance => _db; +} \ No newline at end of file diff --git a/ModVersionChecker/managers/litedb/SourcesLiteDb.cs b/ModVersionChecker/managers/litedb/SourcesLiteDb.cs new file mode 100644 index 0000000..81ee66a --- /dev/null +++ b/ModVersionChecker/managers/litedb/SourcesLiteDb.cs @@ -0,0 +1,43 @@ +using ModVersionChecker.data.model; +using ModVersionChecker.managers.interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.managers.litedb +{ + public class SourcesLiteDb : LiteDb, ISourcesDefManager + { + private string collection = SOURCES_DEF_COLLECTION; + public List List() + { + var col = _db.GetCollection(collection); + return col.FindAll().ToList(); + } + + public SourceDef? GetById(string id) + { + var col = _db.GetCollection(collection); + return col.FindOne(x => x.Id == id); + } + + public void AddSourceDef(SourceDef sourceDef) + { + var col = _db.GetCollection(collection); + col.Insert(sourceDef); + } + public void RemoveSourceDef(string id) + { + var col = _db.GetCollection(collection); + col.Delete(id); + } + + public void Save(SourceDef sourceDef) + { + var col = _db.GetCollection(collection); + col.Upsert(sourceDef); + } + } +} diff --git a/ModVersionChecker/utils/TimeUtils.cs b/ModVersionChecker/utils/TimeUtils.cs new file mode 100644 index 0000000..da1797a --- /dev/null +++ b/ModVersionChecker/utils/TimeUtils.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ModVersionChecker.utils +{ + public class TimeUtils + { + public static long GetUnixTimeMillis(DateTime? dateTime) + { + DateTime dt = dateTime ?? DateTime.UtcNow; + return (long)(dt - new DateTime(1970, 1, 1)).TotalMilliseconds; + } + + public static DateTime FromUnixTimeMillis(long unixTimeMillis) + { + return new DateTime(1970, 1, 1).AddMilliseconds(unixTimeMillis); + } + + public static string ToFriendlyTime(long millisecods) + { + DateTime dateTime = FromUnixTimeMillis(millisecods); + return ToFriendlyTime(dateTime); + } + + public static string ToFriendlyTime(DateTime dateTime) + { + // Use UTC for consistency with LiteDB if needed + DateTime now = DateTime.UtcNow; + + + TimeSpan span = now - dateTime; + + // Handle future dates (optional) + if (span.TotalSeconds < 0) + return "In the future"; + + if (span.TotalSeconds < 60) + return $"{(int)span.TotalSeconds} seconds ago"; + if (span.TotalMinutes < 60) + return $"{(int)span.TotalMinutes} minute{(span.TotalMinutes < 2 ? "" : "s")} ago"; + if (span.TotalHours < 24) + return $"{(int)span.TotalHours} hour{(span.TotalHours < 2 ? "" : "s")} ago"; + if (span.TotalDays < 30) + return $"{(int)span.TotalDays} day{(span.TotalDays < 2 ? "" : "s")} ago"; + if (span.TotalDays < 365) + return $"{(int)(span.TotalDays / 30)} month{(span.TotalDays / 30 < 2 ? "" : "s")} ago"; + + return $"{(int)(span.TotalDays / 365)} year{(span.TotalDays / 365 < 2 ? "" : "s")} ago"; + } + } +} diff --git a/ModVersionChecker/utils/VersionUtils.cs b/ModVersionChecker/utils/VersionUtils.cs new file mode 100644 index 0000000..5ec1117 --- /dev/null +++ b/ModVersionChecker/utils/VersionUtils.cs @@ -0,0 +1,58 @@ +using ModVersionChecker.data.model; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace ModVersionChecker.utils +{ + public static class VersionUtils + { + public static string GetCurrentVersion(AppConfig app, FsModPathConfig config) + { + var versionConfig = app.FsFields; + var packageName = versionConfig["msfs2024"]["package"]; + var fsPath = config.Path; + var fsFile = config.File; + var fsFileType = config.FileType; + var fsKey = config.Key; + + var filePath = Path.GetFullPath(Path.Combine(fsPath, packageName, fsFile)); + + if (!File.Exists(filePath)) + { + return ""; // Fallback + } + + try + { + var content = File.ReadAllText(filePath).Trim(); + if (string.IsNullOrEmpty(content)) + { + throw new Exception($"Empty file: {filePath}"); + } + + using var jsonDoc = JsonDocument.Parse(content); + var element = jsonDoc.RootElement; + foreach (var key in fsKey.Split('.')) + { + if (!element.TryGetProperty(key, out var nextElement)) + { + throw new Exception($"JSON key '{key}' not found in {filePath}"); + } + element = nextElement; + } + if (element.ValueKind != JsonValueKind.String) + { + throw new Exception($"JSON value for '{fsKey}' is not a string in {filePath}"); + } + + var version = element.GetString()!; + + return version; + } + catch (Exception ex) + { + throw new Exception($"Error reading or processing file '{filePath}': {ex.Message}"); + } + } + } +} \ No newline at end of file