Add project files.

This commit is contained in:
Jose Conde
2025-09-04 10:14:30 +02:00
parent a7a404148c
commit 94e6ef651e
54 changed files with 3134 additions and 0 deletions

25
ModVersionChecker.sln Normal file
View File

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

144
ModVersionChecker/Main.cs Normal file
View File

@@ -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<IConfigManager, ConfigLiteDb>();
services.AddSingleton<IAppsManager, AppConfigLiteDb>();
services.AddSingleton<ISourcesDefManager, SourcesLiteDb>();
services.AddSingleton<ICheckerTypesDefManager, CheckerTypesDefManager>();
services.AddSingleton<IFlightSimsManager, FlightSimsLiteDb>();
services.AddSingleton<IFormFactory, FormFactory>();
services.AddSingleton<IAppStatusManager, AppStatusManager>();
services.AddSingleton<INotifyIconService, NotifyIconService>();
services.AddTransient<MainForm>();
services.AddTransient<AppDetailsForm>();
services.AddTransient<VersionChecker>();
});
using var host = builder.Build();
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
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<MainForm>();
var versionChecker = serviceProvider.GetService<VersionChecker>();
var notifyIconService = serviceProvider.GetRequiredService<INotifyIconService>();
var configManager = serviceProvider.GetRequiredService<IConfigManager>();
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;
}
}

View File

@@ -0,0 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack.CssSelectors" Version="1.0.2" />
<PackageReference Include="LiteDB" Version="5.0.21" />
<PackageReference Include="LiteDB.Async" Version="0.1.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageReference Include="NuGet.Versioning" Version="6.14.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.35.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="139.0.7258.6800" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.Json" Version="9.0.8" />
</ItemGroup>
<ItemGroup>
<None Update="data\apps - Copy.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\apps.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\checkerTypesDef.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\sourcesDef.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\error-icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\MVC-Icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\ok-icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\up-icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="database\" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -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<string> errorMessages = new List<string>();
private List<string> updateMessages = new List<string>();
private NotifyIcon? _notifyIcon;
public event EventHandler<string>? 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<AppConfig>();
var sources = _sourcesDefManager.List() ?? new List<SourceDef>();
var fsMods = _fsManager.Load() ?? new List<FsModPathConfig>();
updateMessages = new List<string>();
errorMessages = new List<string>();
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.");
}
}
}

View File

@@ -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<string> GetLatestVersion(Dictionary<string, string> 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();
}
}
}

View File

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

View File

@@ -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<string> GetLatestVersion(Dictionary<string, string> 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<string, string> 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<string> 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<string> 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();
}
}
}
}

View File

@@ -0,0 +1,9 @@
using ModVersionChecker.data.model;
namespace ModVersionChecker
{
public interface IVersionChecker
{
Task<string> GetLatestVersion(Dictionary<string, string> paramsDict, SourceDef source);
}
}

View File

@@ -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": "<p>Version (\\d\u002B\\.\\d\u002B\\.\\d\u002B) "
},
"currentVersionConfig": {
"package": "fsdreamteam-gsx-pro",
"version": ""
}
}
]

View File

@@ -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"
}
}
}
]

View File

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

View File

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

View File

@@ -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<string> MsfsVersions { get; set; } = new List<string> { "msfs2024" }; // Default to msfs2024
[JsonPropertyName("source")]
public string Source { get; set; } = string.Empty;
[JsonPropertyName("params")]
public Dictionary<string, string> Params { get; set; } = new Dictionary<string, string>();
[JsonPropertyName("fsFields")]
public Dictionary<string, Dictionary<string, string>> FsFields { get; set; } = new Dictionary<string, Dictionary<string, string>>();
[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;
}

View File

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

View File

@@ -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<FieldDef> Params { get; set; } = new List<FieldDef>();
}
}

View File

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

View File

@@ -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<FieldDef> Fields { get; set; } = new List<FieldDef>();
}
}

View File

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

View File

@@ -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<string, string> Defaults { get; set; } = new Dictionary<string, string>();
}
}

View File

@@ -0,0 +1,47 @@
[
{
"id": "custom",
"name": "Custom Source",
"type": "scrape",
"defaults": {
"regex": "",
"url": ""
}
},
{
"id": "sim_market",
"name": "Sim Market",
"type": "scrape",
"defaults": {
"regex": "<span class=\"details-card__item-text\">(\\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"
}
}
]

View File

@@ -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<AppConfig> _apps;
private List<SourceDef> _sourcesDef;
private List<CheckerTypeDef> _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<string, TextBox> _paramFields = new Dictionary<string, TextBox>();
private readonly Dictionary<string, Dictionary<string, TextBox>> _fsFields = new Dictionary<string, Dictionary<string, TextBox>>();
private List<string> _selectedFs = new List<string>();
private List<CheckBox> _fsCheckBoxes = new List<CheckBox>();
private AppConfig? _currentApp;
private List<FsModPathConfig> _flightSims;
public event EventHandler<string> 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<FsModPathConfig>();
_globalConfig = _configManager.Load() ?? new GlobalConfig();
_sourcesDef = _sourcesDefManager.List() ?? new List<SourceDef>();
_checkerTypesDef = _checkerTypesDefManager.Load() ?? new List<CheckerTypeDef>();
_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<string>();
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<string, TextBox>();
_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<string, string>();
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();
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -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<string>? onAppChanged)
{
var configManager = _serviceProvider.GetRequiredService<IConfigManager>();
var appsManager = _serviceProvider.GetRequiredService<IAppsManager>();
var sourcesDefManager = _serviceProvider.GetRequiredService<ISourcesDefManager>();
var checkerTypesDefManager = _serviceProvider.GetRequiredService<ICheckerTypesDefManager>();
var flightSimsManager = _serviceProvider.GetRequiredService<IFlightSimsManager>();
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<IConfigManager>();
var form = new GlobalConfigForm(configManager);
return form;
}
public SourcesConfigForm CreateSourcesConfigForm(EventHandler<string>? onSourcesChanged)
{
var sourcesDefManager = _serviceProvider.GetRequiredService<ISourcesDefManager>();
var formFactory = _serviceProvider.GetRequiredService<IFormFactory>();
var form = new SourcesConfigForm(formFactory, sourcesDefManager);
if (onSourcesChanged != null)
{
form.OnSourcesChanged += onSourcesChanged;
}
return form;
}
public SourceDetailForm CreateSourceDetailForm(SourceDef? sourceDef, EventHandler<string>? onSourceChanged)
{
var sourcesDefManager = _serviceProvider.GetRequiredService<ISourcesDefManager>();
var checkerTypesDefManager = _serviceProvider.GetRequiredService<ICheckerTypesDefManager>();
var formFactory = _serviceProvider.GetRequiredService<IFormFactory>();
var form = new SourceDetailForm(formFactory, sourcesDefManager);
form.SourceDef = sourceDef;
if (onSourceChanged != null)
{
form.UpdateFields();
form.OnSourceChanged += onSourceChanged;
}
return form;
}
}
}

View File

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

View File

@@ -0,0 +1,14 @@
using ModVersionChecker.data.model;
namespace ModVersionChecker.forms
{
public interface IFormFactory
{
AppDetailsForm CreateAppDetailsForm(AppConfig? app, bool isEditable, EventHandler<string>? onAppChanged);
GlobalConfigForm CreateGlobalConfigForm();
SourcesConfigForm CreateSourcesConfigForm(EventHandler<string>? onSourcesChanged);
SourceDetailForm CreateSourceDetailForm(SourceDef? sourceDef, EventHandler<string>? onSourceChanged);
}
}

View File

@@ -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<AppConfig> _apps = new List<AppConfig>();
private ListView _listView;
private ImageList _statusImageList = new ImageList();
public event EventHandler<EventArgs> OnConfigChanged;
public event EventHandler<string> OnRecheck;
private EventHandler<string> onAppChangedHandler;
private MenuStrip _menuStrip;
private List<FsModPathConfig> _fsMods;
private readonly Dictionary<string, TextBox> _fsModPathTextBoxes = new Dictionary<string, TextBox>();
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<string> onSourcesChanged = (s, e) => MessageBox.Show("Sources Changed");
var form = _formFactory.CreateSourcesConfigForm(onSourcesChanged);
form.ShowDialog();
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -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<string>? 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<string, string> ParseDefaults(string text)
{
var dict = new Dictionary<string, string>();
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;
}
}
}

View File

@@ -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<SourceDef> _sourceDefs;
private ListView _listView;
private Button _addButton, _editButton, _deleteButton, _closeButton;
private TableLayoutPanel _mainLayout;
private readonly ISourcesDefManager _sourcesManager;
private readonly IFormFactory _formFactory;
public event EventHandler<string>? OnSourcesChanged;
public List<SourceDef> 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<SourceDef>();
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<string>? 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<string>? 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();
}
}
}

View File

@@ -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<string, AppStatus> _statuses = new Dictionary<string, AppStatus>();
public AppStatusManager() { }
public AppStatus? GetAppStatus(string appId)
{
if (!_statuses.ContainsKey(appId))
{
return null;
}
return _statuses[appId];
}
public List<AppStatus> Load()
{
throw new NotImplementedException();
}
public void Save(List<AppStatus> 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();
}
}
}

View File

@@ -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<AppConfig> Load()
{
if (!File.Exists(FilePath))
return new List<AppConfig>();
var json = File.ReadAllText(FilePath);
return JsonSerializer.Deserialize<List<AppConfig>>(json) ?? new();
}
public void Save(List<AppConfig> 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);
}
}
}

View File

@@ -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<CheckerTypeDef> Load()
{
if (!File.Exists(FilePath))
return new List<CheckerTypeDef>();
var json = File.ReadAllText(FilePath);
return JsonSerializer.Deserialize<List<CheckerTypeDef>>(json) ?? new();
}
public void Save(List<CheckerTypeDef> checkerTypesDef)
{
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(checkerTypesDef, options);
File.WriteAllText(FilePath, json);
}
}
}

View File

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

View File

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

View File

@@ -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<SourceDef> _sourcesDef = new List<SourceDef>();
public SourcesDefManager()
{
_sourcesDef = Load();
}
private List<SourceDef> Load()
{
if (!File.Exists(_filePath))
return new List<SourceDef>();
var json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<List<SourceDef>>(json) ?? new();
}
public List<SourceDef> 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<SourceDef> sourcesDef)
{
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(sourcesDef, options);
File.WriteAllText(_filePath, json);
}
}
}

View File

@@ -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<AppStatus> Load();
void Save(List<AppStatus> appStatuses);
AppStatus? GetAppStatus(string appId);
void UpdateAppStatus(string appId, AppStatus appStatus);
bool DeleteAppStatus(string appId);
void ClearAll();
}
}

View File

@@ -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<AppConfig> Load();
void Save(List<AppConfig> apps);
public void Insert(AppConfig app);
public void Update(AppConfig app);
void Delete(string id);
void UpdateStatus(AppConfig app, AppStatus status);
}
}

View File

@@ -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<CheckerTypeDef> Load();
void Save(List<CheckerTypeDef> checkerTypesDef);
}
}

View File

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

View File

@@ -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<FsModPathConfig> Load();
void Save(FsModPathConfig config);
FsModPathConfig? GetByShortName(string id);
}
}

View File

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

View File

@@ -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<SourceDef> List();
SourceDef? GetById(string id);
void AddSourceDef(SourceDef sourceDef);
void RemoveSourceDef(string id);
void Save(SourceDef sourceDef);
}
}

View File

@@ -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<AppConfig> Load()
{
var col = _db.GetCollection<AppConfig>(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<AppConfig>(collection);
col.Insert(app);
}
public void Update(AppConfig app)
{
var now = TimeUtils.GetUnixTimeMillis(null);
app.UpdatedAt = now;
var col = _db.GetCollection<AppConfig>(collection);
col.Update(app);
}
//public void Upsert(AppConfig app)
//{
// var now = TimeUtils.GetUnixTimeMillis(null);
// app.UpdatedAt = now;
// var col = _db.GetCollection<AppConfig>(collection);
// if (string.IsNullOrEmpty(app.Id))
// {
// app.CreatedAt = now;
// col.Insert(app);
// }
// col.Update(app);
//}
public void Delete(string id)
{
var col = _db.GetCollection<AppConfig>(collection);
col.Delete(id);
}
public void Save(List<AppConfig> apps)
{
}
public void UpdateStatus(AppConfig app, AppStatus status)
{
app.LastCheckedAt = TimeUtils.GetUnixTimeMillis(null);
app.Status = status;
var col = _db.GetCollection<AppConfig>(collection);
col.Update(app);
}
}
}

View File

@@ -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<GlobalConfig>(collection);
return col.FindAll().FirstOrDefault() ?? new GlobalConfig();
}
public void Save(GlobalConfig config)
{
var col = _db.GetCollection<GlobalConfig>(collection);
col.Upsert(config);
}
public GlobalConfig GetConfig()
{
return Load();
}
}
}

View File

@@ -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<FsModPathConfig> Load()
{
var col = _db.GetCollection<FsModPathConfig>(collection);
return col.FindAll().ToList();
}
public void Save(FsModPathConfig config)
{
var col = _db.GetCollection<FsModPathConfig>(collection);
col.Upsert(config);
}
public FsModPathConfig? GetByShortName(string id)
{
var col = _db.GetCollection<FsModPathConfig>(collection);
return col.FindOne(x => x.ShortName == id);
}
}
}

View File

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

View File

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

View File

@@ -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<SourceDef> List()
{
var col = _db.GetCollection<SourceDef>(collection);
return col.FindAll().ToList();
}
public SourceDef? GetById(string id)
{
var col = _db.GetCollection<SourceDef>(collection);
return col.FindOne(x => x.Id == id);
}
public void AddSourceDef(SourceDef sourceDef)
{
var col = _db.GetCollection<SourceDef>(collection);
col.Insert(sourceDef);
}
public void RemoveSourceDef(string id)
{
var col = _db.GetCollection<SourceDef>(collection);
col.Delete(id);
}
public void Save(SourceDef sourceDef)
{
var col = _db.GetCollection<SourceDef>(collection);
col.Upsert(sourceDef);
}
}
}

View File

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

View File

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