From 94e6ef651e6f8e903f562955bdd25497831d7c9e Mon Sep 17 00:00:00 2001 From: Jose Conde Date: Thu, 4 Sep 2025 10:14:30 +0200 Subject: [PATCH] Add project files. --- ModVersionChecker.sln | 25 + ModVersionChecker/Main.cs | 144 ++++++ ModVersionChecker/ModVersionChecker.csproj | 58 +++ ModVersionChecker/Resources/MVC-Icon.ico | Bin 0 -> 19518 bytes ModVersionChecker/Resources/error-icon.ico | Bin 0 -> 106343 bytes ModVersionChecker/Resources/ok-icon.ico | Bin 0 -> 105630 bytes ModVersionChecker/Resources/up-icon.ico | Bin 0 -> 104539 bytes ModVersionChecker/VersionChecker.cs | 145 ++++++ ModVersionChecker/checkers/ApiChecker.cs | 56 ++ ModVersionChecker/checkers/CheckerFactory.cs | 17 + ModVersionChecker/checkers/ScrapeChecker.cs | 94 ++++ ModVersionChecker/checkers/VersionChecker.cs | 9 + ModVersionChecker/data/apps - Copy.json | 55 ++ ModVersionChecker/data/apps.json | 86 ++++ ModVersionChecker/data/checkerTypesDef.json | 33 ++ ModVersionChecker/data/config.json | 21 + ModVersionChecker/data/model/AppConfig.cs | 51 ++ ModVersionChecker/data/model/AppStatus.cs | 15 + .../data/model/CheckerTypeDef.cs | 18 + ModVersionChecker/data/model/FieldDef.cs | 27 + .../data/model/FsModPathConfig.cs | 36 ++ ModVersionChecker/data/model/GlobalConfig.cs | 21 + ModVersionChecker/data/model/SourceDef.cs | 24 + ModVersionChecker/data/sourcesDef.json | 47 ++ ModVersionChecker/forms/AppDetailsForm.cs | 477 ++++++++++++++++++ ModVersionChecker/forms/AppDetailsForm.resx | 120 +++++ ModVersionChecker/forms/FormFactory.cs | 68 +++ ModVersionChecker/forms/GlobalConfigForm.cs | 122 +++++ ModVersionChecker/forms/IFormFactory.cs | 14 + ModVersionChecker/forms/MainForm.cs | 310 ++++++++++++ ModVersionChecker/forms/MainForm.resx | 120 +++++ ModVersionChecker/forms/SourceDetailForm.cs | 110 ++++ ModVersionChecker/forms/SourcesConfigForm.cs | 136 +++++ .../managers/filesystem/AppStatusManager.cs | 52 ++ .../managers/filesystem/AppsManager.cs | 42 ++ .../filesystem/CheckerTypesDefManager.cs | 28 + .../managers/filesystem/ConfigManager.cs | 40 ++ .../managers/filesystem/NotifyIconService.cs | 22 + .../managers/filesystem/SourcesDefManager.cs | 52 ++ .../managers/interfaces/IAppStatusManager.cs | 23 + .../managers/interfaces/IAppsManager.cs | 26 + .../interfaces/ICheckerTypesDefManager.cs | 15 + .../managers/interfaces/IConfigManager.cs | 16 + .../managers/interfaces/IFlightSimsManager.cs | 17 + .../managers/interfaces/INotifyIconService.cs | 14 + .../managers/interfaces/ISourcesDefManager.cs | 19 + .../managers/litedb/AppConfigLiteDb.cs | 70 +++ .../managers/litedb/ConfigLiteDb.cs | 24 + .../managers/litedb/FlightSimsLiteDb.cs | 32 ++ ModVersionChecker/managers/litedb/LiteDb.cs | 16 + .../managers/litedb/LiteDbSingleton.cs | 12 + .../managers/litedb/SourcesLiteDb.cs | 43 ++ ModVersionChecker/utils/TimeUtils.cs | 54 ++ ModVersionChecker/utils/VersionUtils.cs | 58 +++ 54 files changed, 3134 insertions(+) create mode 100644 ModVersionChecker.sln create mode 100644 ModVersionChecker/Main.cs create mode 100644 ModVersionChecker/ModVersionChecker.csproj create mode 100644 ModVersionChecker/Resources/MVC-Icon.ico create mode 100644 ModVersionChecker/Resources/error-icon.ico create mode 100644 ModVersionChecker/Resources/ok-icon.ico create mode 100644 ModVersionChecker/Resources/up-icon.ico create mode 100644 ModVersionChecker/VersionChecker.cs create mode 100644 ModVersionChecker/checkers/ApiChecker.cs create mode 100644 ModVersionChecker/checkers/CheckerFactory.cs create mode 100644 ModVersionChecker/checkers/ScrapeChecker.cs create mode 100644 ModVersionChecker/checkers/VersionChecker.cs create mode 100644 ModVersionChecker/data/apps - Copy.json create mode 100644 ModVersionChecker/data/apps.json create mode 100644 ModVersionChecker/data/checkerTypesDef.json create mode 100644 ModVersionChecker/data/config.json create mode 100644 ModVersionChecker/data/model/AppConfig.cs create mode 100644 ModVersionChecker/data/model/AppStatus.cs create mode 100644 ModVersionChecker/data/model/CheckerTypeDef.cs create mode 100644 ModVersionChecker/data/model/FieldDef.cs create mode 100644 ModVersionChecker/data/model/FsModPathConfig.cs create mode 100644 ModVersionChecker/data/model/GlobalConfig.cs create mode 100644 ModVersionChecker/data/model/SourceDef.cs create mode 100644 ModVersionChecker/data/sourcesDef.json create mode 100644 ModVersionChecker/forms/AppDetailsForm.cs create mode 100644 ModVersionChecker/forms/AppDetailsForm.resx create mode 100644 ModVersionChecker/forms/FormFactory.cs create mode 100644 ModVersionChecker/forms/GlobalConfigForm.cs create mode 100644 ModVersionChecker/forms/IFormFactory.cs create mode 100644 ModVersionChecker/forms/MainForm.cs create mode 100644 ModVersionChecker/forms/MainForm.resx create mode 100644 ModVersionChecker/forms/SourceDetailForm.cs create mode 100644 ModVersionChecker/forms/SourcesConfigForm.cs create mode 100644 ModVersionChecker/managers/filesystem/AppStatusManager.cs create mode 100644 ModVersionChecker/managers/filesystem/AppsManager.cs create mode 100644 ModVersionChecker/managers/filesystem/CheckerTypesDefManager.cs create mode 100644 ModVersionChecker/managers/filesystem/ConfigManager.cs create mode 100644 ModVersionChecker/managers/filesystem/NotifyIconService.cs create mode 100644 ModVersionChecker/managers/filesystem/SourcesDefManager.cs create mode 100644 ModVersionChecker/managers/interfaces/IAppStatusManager.cs create mode 100644 ModVersionChecker/managers/interfaces/IAppsManager.cs create mode 100644 ModVersionChecker/managers/interfaces/ICheckerTypesDefManager.cs create mode 100644 ModVersionChecker/managers/interfaces/IConfigManager.cs create mode 100644 ModVersionChecker/managers/interfaces/IFlightSimsManager.cs create mode 100644 ModVersionChecker/managers/interfaces/INotifyIconService.cs create mode 100644 ModVersionChecker/managers/interfaces/ISourcesDefManager.cs create mode 100644 ModVersionChecker/managers/litedb/AppConfigLiteDb.cs create mode 100644 ModVersionChecker/managers/litedb/ConfigLiteDb.cs create mode 100644 ModVersionChecker/managers/litedb/FlightSimsLiteDb.cs create mode 100644 ModVersionChecker/managers/litedb/LiteDb.cs create mode 100644 ModVersionChecker/managers/litedb/LiteDbSingleton.cs create mode 100644 ModVersionChecker/managers/litedb/SourcesLiteDb.cs create mode 100644 ModVersionChecker/utils/TimeUtils.cs create mode 100644 ModVersionChecker/utils/VersionUtils.cs 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 0000000000000000000000000000000000000000..2e6cba65d4ba5deab3c287a1875a039f53ea7868 GIT binary patch literal 19518 zcmeI2XK)qC6@?E<2(1uFD~Ys{fB^|u5Qr7YCN04P8)K3eOjh=jT<*`?beWN{?%mP2abv7nwF>Lkug9oSqcCsYJRCcA4B_G7$e%wyHf`F36)RTY<;#~? zy?Qn7-o17-7$Vj|- z^9I$bRYSvu4NK4L(`^Bv32WKtXZ=L7cX8!&6+ilFJC@PnKA_%Hf+Fz2@_DL zP$85qT^c=l^uXA$V=-XB0JLh=3d4pCL$++$P_kr6tXsDZhYue{rAn1Bbm&lg{P+>Q zd-ukmL4#1CLItF!r=wrLeh3T<#JF+e5EmDRTD5ASTeof)F=7PTv}uDfWy)aX%9U8Q zY#BOt?u<{LKH%O>^Wn-T^n#)>q=A;!H%gy9_k$hn$$|7Vw zq$#=@o=Q7iRjre8F}2E!9sF2Pcfy=mEfN?066wW)7_-oQ`BJTM#J>6IqV~9HnJ5w$ z{#@0sk~^Czhtv{h?3*uZP1|2Y=E6TFi_BoJhbOSqQQ=H{@`uaXUzqI{k-6~u$|5s3 z>dv3-C~!%9^2^KOBxV~aGAF;7tl_~CcYcbq!kPKxACR?~%$Byp$sZ{*-c!BuGi8kx^l)`lj?Xo&a~^V)|FiAwmH(Ap!=s+A zo<(KBJl82txyqMovadqC^}kK z9p#+xk}3QE?zn?LvhP+ro_~1+H51!Kw8Ck;O2|eBr|vCN_|}@j*9)KA|BKEdY~^(B zLlu3Uy0^@YZ+;QT7SH?)KF1!|1zLoEbvpac-(ndG->n||zm50|8Z2Vxe)gYl9sWPV zIsj8W@|y`}Eyn+kh@F=i|M@>~zc_5<<0g;%I(*JB%$>ej#4Q{`IzpB zUtA!K5sbM(B;lWhUta9@47%_0#Q!7lJDN;Na;s0k-|b%^?4<8X59hx;xmTM=t6_%W zc$xE;zpRV3Ig+2@fnS(5*~Fu=$oSndSJ;EU{H0%Ww;IeMCy$an@c$)&~A?!1}Hk`%miylF8zdOIO1gpMn348^4Sj zUkLo}%tWRMA^kJ(r*f}6+$oO7KMHrb{gFa2{w{daNce~S1pM9qxwuzlZZv_%|Krc{9?6YH5r~hHUz_C7go_c% z7!tC|7d`}X@})Y2a3e?}A&Y#vm+D2n=1AK=REm-&-wY0ylE23vH!mw}T1k|`A53DH zgbg97xsdLqkmMM1)~xky?K*XpbH}n#`1W-bkhfa2{qp1N->7)|O_2GCm9_lV!F5S| z@l(iwjrQh>?Uz2WN)-zOX`+hMr9@N8=!;)oIsnP`I{f&rAoFjODi)^DL`A75R3goN z^u<>XtWx%>oNu3yUrP=T=q}Zu*G{G;m9y7&iG1;kE6yeC^*EpIbPU7%jlHsbAqy2T zFXH9u^WD6ciW`!1>`hBXR`%u3V4=ca#m2=KDH<9U9K?RLAK=DUSJJWWfbM|qfbM|qfbM|qfbM|q rfbM|qfbM|qfbM|qfbM|qfbM|qfbM|qfbM|qfbM|qfbPKmVF&&Pr9YY4 literal 0 HcmV?d00001 diff --git a/ModVersionChecker/Resources/error-icon.ico b/ModVersionChecker/Resources/error-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2a688764a0a4611add84c2f8c93a9e82e6753ebd GIT binary patch literal 106343 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 Ur`3CPe>;zPFfW!VS0af20m$KWQvd(} literal 0 HcmV?d00001 diff --git a/ModVersionChecker/Resources/ok-icon.ico b/ModVersionChecker/Resources/ok-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..46a6dccf72eca2900704a3d596c59ecd2ddbb70b GIT binary patch literal 105630 zcmeHQ2UrzH7hOQ43w8`5U{LH}45FdhL8BNeR=`-IU{|mK55-Qgiv`i&E+BTn0#A&f zF^Yi*M5ur(aZF{W(;%Jk?z#3NNX^R?@}2?gF@8zOAUtU=r6-8 z@87SNn({ZwRJEFob@u_d3MiuKku&yDn+@`9x_22g=f>kyC-1F^qhg-qq&+&G_{8|2 zPyED(-F?YZ=ji#BlFsHa$(!!Y9e;OIz+ZFSbtW3e zTBJ>IaWRVxFNy6ek!$+p{RvbZ;cSWfMYj+ckGoghY8I?B-F9BqD^5;crib#6U9A4ss$_<~pUX=eeu*Ok=Rw#C!+ zRAyjI(X-5vKV+xxZ+PfNnN5aDcFI}tO1q&}p668mlwalkR9?JZY?|_va88g2u5O4RDLRZg{Jqz2}QYWheWMDJ|_LUhQ>x!{N>nr=&*~hi;?~?Qb#O zsLPC$hr{wm#xfo`SKaD&T6{-y(3sV;>(y9y=CQ&-ml(^gCq(IWI_|OT;%}H?d|2GX z?vUYw4)ZUXXE*aUI-|JtoN~kZOx8XX-2n~{WvydV2Bnu~CkM)dE^%dfT8S^z9cR`YUDN$Sz=#2^V!Cx>jMS^w zxUN#-qh;@B(SMitPuW$YQ+%%%bVypLqnP(JdDyw4Cw0AU zoY9Jy7}uhKuH8E?KP1Jh(7AI5twd&(qb519Rh@bgz22 zBq~0LT6;v!r|zld#iF5;bLShc8^l=mN?DcelKvX;kDLu(FS1!AtU&iA`2ZuFJS#a9g_lE1FHLFIq+24Oy^xYav zi9v3-&zz%60v3vz%I|3)8av?Nx)YAZ`Rm20+0SaPahhqFJjGk%*yW(OCyhK~Y|0uN zI<#MSRBRtU({Ih>u76w{D5E>9uVdgwb1x<1V(V8y+q=-=k|t=$7=K zWOOH%sJW<5Gw+z<+9N79#$;%daLJgJc59miH(N6+uQ>6^^b8q?9YaDaPs-#Z?kVps z>U>vt#u7^-1RONu_fn=?AXZjAB;H4pgy<=oLQTRIL&F^3IEO z!s3(bwwqX|!ENVezlX;Ly~sbmJhNqNq5g(J&syZ3&Q7Tlw?Ubicp>(>-_S?nu4J#h zGvV}geYXh7H$xnPyWiWTR-chw;JVr`@Vcu)$H=l`o6*12^$RLq*fex*nOgq(6G;Z6 z)e3YsNOC5Ae>ZTsGV`RlV&)4=pOB}?U7k+(H8E^eN{{uv*qh!O!a|6#$DvHn6JobEO%pm4s^3Ir(;|dDHHd%C=y4*Zb z%V5M&oecNj7arS(grrBxL+Q?hAzi|4&$ebqeLv9orguHf3n*Aokup{HMX=0@Y>P*-Ky74`gwGZzkP7qoO8 z8GYi}Z%sp1UfbWv|GsOz-UpTq@$?`LnY}5~MFJ3nD@`BRb%@Rq!tfE5k@W_Pc6I$9W5E))= zzi+tZgjP(?^tMU$(o4mL3Wg#P}{V?#4M*oCr?Y4y;Gw)7W`t6b}rFX>*;~g z*t&I=6zN3{iGAEqE}&_RR)NzJ`{?XvtQ&;(OJ1I8ea|{Orq&{*u1&A(uQhst<##2s zzT2f4!)#b;p75RWAm&*AgX>(z=p?oFJQ)_>EZE*{*{bfDK{Oz@+}>Hgt!JWRhq8kg zmXzwcEloRfGS^qm-Tk`5aHA)tNpWfMcKs|B^CY|ds1_HE;@mRb+!5I+O@dp_U)5*t zAMG6bCONiX(pJnK);p=^wqdum6^1vfQTM2Cp+tR}>WGY;ZY3R8VF>IP?3ScPs~MNt zcVi^srfGfl*%%zoZ`~yBhgw_D6@}!?yf=FP;H=QGg#n$Ek;Jxq8lb(6byjA#sr%LT>InKY? z<)X`$7iB-zSrV3(an*d-!F3D%x3X~f@wCvy$0|F{ifg-?J#LbG{87UHJYtI_7FpvP zx3+8j&}>73O4l(OJxWdNZmKXDhvqM6IPIiq#8&5chY2Z(?J^8g!z1O&E@@kKk+|&t zYw`7CQ)MkQ&StwDpSo6Km|x7DJPphIdY3xO#r}L;vQrdj`=ocs@Wd$=a@(;;seeR1 zBW_Hwd!9mSrejn|VLz4V!}o9K7}ThhZhvsy2G5mq^^YuTs4o*K@+q{p^?#6f;Ch^Q zlhUoCGsy0+1qxW_)_n50*5tmJ#W3nv?ts9Ni(Fu|dZcBZ?A#XR0+yLqBPzp%N+ zdKxFSPTM^FaqP{hhqMk)5$knLkJR>%yBogRz^lDvPN?WuVyRwvCbRoCfG?@O`MMm%<0ku@ahO2ien-FNQCZk`#b zm6_doa;LfS{;n}9BY#kBDL2SpR$LkqG11**+FsKLv&`o+lN36So|L>oT~&60^6A1d zwE`8_>`oo$AJQ^$ygdKh_|hw5!^SHG?T_hDw!q-T+O0!U<~Q#1KrMHi&vSKkEHIsK zk6_wmnD`hMml~yODRmL+t92bXTgA~JW%2!y10rwaubk@Zp5KeQS)(?IqaJ#9Nt`{` zr}#yKHW%-ExSBe7Ifg3*XRW#y96QINOI)7CqT`|_G7%Y9M_Db2zSxjS@`Q{T=ibR={r!#j&NQx2z z?u~X2X!FD^?d*o1?dBQw|J7q(`D*OjaLtQ-bDno7u~??|yzeUW43m}DM-J3ixO!LV zyu<15Eb=egwY8ITj}Mr?XGn;B-1qH<_D()DzU=!QFH-u>a0wh2Rdd_ioD5UjB<)W7 zpWohD;&6F?Lr<0ClSU=aQP(@pbXRuXHHMjDlcS%I?!M8~O*80lobu^83Y&vBIhWLN zRCLR}v@UDU;e-q8z5IWT+|YXWTC1HyLIP*rJ2*4cO=ZXI)g6DDDC;D1`}z?lhi-vo z;cLGuniq4}Si_-1>4pdHNdt?VcPTT@YLbr5g9H6et^t_GZz?_=K4Go+wT9F3uBQ8l&&j4eL1pl(61sl~vcgyR8; z8Uq_z?Gkr%)40|w_u2We9t)f0`OA#Xy1KjX;O90i_uQX+*jq=-zWMLkVQp@@n(TDi zc5aFM1WymwduhwB=!kA_PgdS>=euqZA)@Eq+#i>1XqGYBwPbFTzeoC0J!^}eJ5)3# z-2ZE-PyXJ%*Ore9$P8>A(ed()7jv$Nn>@N3(EP^^X#)d0xLY(PQYDPR*UfhCy=_rx^dR@ma|T#nul?<}FTb898Bn z|8B+U8;7pEYB_bd%puWnmv&>^JeVta>D_zoV3KU@;^wb#5ixXZe80(zOK3|9+d3opWQ(V*ejTcK?($@Pg9gtnT@7=M zADKV9w=QXB-A0RLGi3FG9!)%ws_|rLa(e&LU(^coP5PZr%~HD+8aw|$PSK#7Hf|o4 zCp^B{@+`jpfY>V&>nV%;W80?d*VyT@+N}1z!OtvmqI9QB|FzG=(}m`9)l;ME&OYKi z@09oMXR<4&-wt^CW5;WI0u?k?t{FdCEhW=8y-QMOgG;R(Z)DBVN#D}>(UML2#c6X( zXI(X)(rmctDn9IYrn=MVvHTd(9 zj`p{MnPZ2S`PB2SJ0)!Yx=8bE$%MWh*VQCjL*kq-XwCBuDL&~_$JT#yOj^w1?7)6Y zu2{>Iz^pGaS^z}u)^ z=YIJk)*qj*b8DENO&81bA37RHnbN5J;+D*?1%qQ!kA<=(N_StdABZjpK&mt!(nwbof*ZFAcxan7>IYtBW6PEXj@ za?J>Xmn+UwnKOSm6RA`%&6GiU!}w zGh7nYkeS>#=I(8iLzAP$FSM*TZ)%u$#HYc%jtlLh8%wfx`PeYNr(y!AJSk*7{r_6f>XJrO8N+bHKHfg5Y@rlsf6q*hVQ9ivlJ zFDV&Ehkt)m{kFa4tRtR#?axnZ(z?d3j6saPoypJF3>vkqJta&>-dEpjv5xVwJoVcF zGZv(n@3ovVVfiY9Co{zpii@)^jW1tz?3Z5^Ff}SrzNE-=aGQZw>p3pHHRM(y>K=Mz)iEP=cZE;=K*5rY=axQ0jWcqTh_fN#1FD)LiF1J}}?s z@`9;W1C;x<6*cMJp{6V2ao=~U{FF=6=5C!~<}`kXd{=!B^-B_uvgVR+Uv5!7=3dL$ zq}s6jglUv0t$^h7@;)E*M`Zes#a%nrN;BS>wWiGWi2e)aa&}&rse9uQDaY148QxN( zK4YGnSA&kn>gZi+;g$cq`HhCm>~USW4`r8OU)rsevg&}Kg!&IQM)X&qrN|&g)$n@Jmpk-*N6km(mRhbj znP`?eX`?Bfgflih`gK3p)nQ=^ii?mz$K*IifB7fFoUg@{qas5H1O@~K1O@~K1O@~K z1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K z1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K1O@~K z1O~nc1KKl;89?}}o(z!fWd!?IKigOId)VHEuz=97`msYG?Psk&b5RwV51o(yQe^mWytrpo8w$~u^Ap{d79OP3xmC%Zy zQmu*lk19`g#e6@SaEidSJvY9Cu6_3(rJ!1>MfOC$hwU-G|3*Mx=h~bbe@5552=r?c z!e3Qqfb0m{*C7le!1g%=uDxFu|DDRrCe*F!`d4T9$$qeXLqc!D2|^)(Yj3Rk$|~~M z5Z6)eB*B&-OQ_B?Ryo~auL{Uhh;=@Htp^*!-Y*EH6>)@I-^ijgd`5*J><6ll7m&T! z`FXVnPvt-{0XA zus3%eKwc(cFriUZ(7!s%NH${UdyM($_iwU2?8@~C#NiK^5BRRz@yBz%z4>XBhYvg_aIaw=h$XZj2+s)qMSmdvVS7zN9|F$v z`TIH85No*)wLNQZ+>5hd+|Q032(gZsM=&7JFAWQSUz-8q9k$1F+P9tUz#sT~Jl3}F z8^YeGhqXU@-yO=cBVfJxwbifo?Gfi#>rEoy`8{hBHbj5LK8{U)j*xGE)OnFGjv(v@ zzLG~0*X&x)h2l8d!Mht=`?B->r}leporq%{fcb!%kGbI>q0d)Rzgjm#Jj3=42z?2c z3AqHWJz#s_L*{$f^mTokTQB0+3t%4L?+MZ=ZyQ02AnXgjkcSetuzfuO&UQiweEXok z!#3}0d)OSlf;B$E_wfOg#k|5Ti*|1kMi6Qdgt_1g_yBk%3?v*T@NEx!@a@7c!%xFk z*zp5nJHKD^%d=_L&)76V+?z~jPLTfs>Q~z;iBEREccnP?_x%3N_F=9)-W7+v0d7Br z|M1rm@2ZD;9d+Rufn~Kd)@N3nxP#_Znl@ZQ)*xGnL9vvp5GHi6GQd;s?3^Ug1aG}}k{<2u#=n6I}GvIyLJQI@@roByUb zf2_d$SWCJSntX)%)&3UZ4eLG&f;R!Sex3c$uiw^BG1sFE-%pT-v)j1@Gr|$VeFE3M zsP}DkbIY>+!u12>T_X%8XjXfJegpx;n<}9_;V9uTfxm8p9bac(*a5afALQSI`T2JO z`etK@MIp}c&k`^uaP5kE_(I$T!;n&Zm zQ66B^2yYVt3Gn4lV4?cnO1vo(ObN>fd>_D=i#Z;B4f6rtR>+GZI1uU*{_)NR7x349tPNl%%=dPL1|Py1m9J0ON#L&`u@=NQz_#(O5Mv16 z2Odz~7{Wi+ARi)QbuUZ&VJ(IE+MB>%55P`Xb73sUI|Co|UIxk&%m|wZT>HVN(6_PH ze3$KU{dHpo=7tLdCxS2re1anrf7rKK5LOT{zrt3ScQH52B-AGSW3Bf-P11dJ2pBse z2wYpEjSm?E;EViq1LmzDLbvykMJV%T28ctf3BM-<6ENOmt>;d7wZDJ!-EY4_bzq*J zOn5{ba?%zOwpM8{%zFqq2bCvj0AhaZ$AWS5DvhBeh)q}O* zH-xhU{+R;&;eCAoV-kOzf_vdtRs{HP^)c8N(v&!ay*}&vcT`^uLKDL81pb~5X9Dj# z7qD{^*Jt2E>4eS%;auPo{en0|pZ&yID&J1^;Tb;0m$3xA+rb^@S)20525uQ{9Ob$8 zM;tyCKUjwUqf%Rb^LHE<76z={~43KSb z-q@UgX9WCl96o?E-}f0CP!{D7vVMYiG6ByB@T~K*o@;(7PO4kIWEZ@TVno1NfWN21 zSr5Qp=d(8Eh8X|(a{%i5lhBu-THUyV0cV4-H`lJ%54`VO0Q>WO1wOE! z(25{DBalL;AiLmP0M7}w5Yh=;`@gPVbFXvbu>E5Ke;h#i7lN=SlwSM82kH~n5cqpS z*!4rk0Q7nOy5Rxk;hh#?Pbh^x0FlTxSf}+OY$I^(hyDv6z}Y{)Z*%)U8)w&o+_E@V zJWUu!5YC6C*9RJqeTEV64jOAugjmmGp9ec~*ZJHs+&IPy{#o)v%EMZqjg+b{i1l(D zz_VhE)mRJiZH@IH#saRb`Ek})Fb{CA;l3*bdx2=_Wq|C1wIKF|X9@gwg<)InxX!gN z8^=BTzK?6^1gtM=Nw4;TWMm(#1>^|d6V4IP=ULk##Mu72^&sYbJOf0?_mKq3!X8R4`V+1yh;*uTl-dJ72cEU{p+t0Zn_5^nba|!tO&BC{YUq@yY&Lf** zEpU{;Kik22Fq5#4P=_F_^{TL)-zGZQ_Gdx_f!p^nCTt>LttY(e@iqdh_%*UG)&ll~ zWCDBuXFIbAjR|qQx>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^> zATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^>ATS^> zATS^>@KFp%guiOQz&)Ohi{6oDDqk;SJSt_Byd#~u#8E(3OJl}(p!B1!Ku8S#c$tf?Qv<@dtNk;w6X==XGbu`BnFc)c#fc&(WcU^=u zhw<_xN1>PLzXg21^xq1I7)F=Nu$W;SxB`-V^xxkC^V0>43{-qYNC~4*nPEnqEW{PK zJZ<^v29b`0uK%4Ddq_lF1~Vi`|Lqcm5{dY2X{wlHtmG1v1f=V)OM6g0`v)6(unB~e zw&bQsAf0!l{}z}ERuY)f89V`Blm44=DqopViG(j;Wrih^ccn?8O7)X~l?WpNE6Go3 z9TMa3KYp6ur}<+*+20OQUdcbwm7AAHDmO2Y`2F3s5c_Hjv|t#*008xcOberdiDdJ*8~4+;2oVo3#94)7h-RfL+-tjEX52Av4dJ&VBA9dYQ-{&oa@ zPr#1wpZR7y=>ogM=lOPr?ku>!t%L6>8h#AV(smE&fpMNY*0Da%>dg)D4QX75Kb$A1 zNt-?&A`i)r`5ki{JI=pNcklqc!2uheL-8s(7LeTVd8}CW{7_8ar~Eh)PX z;anQZlFagip@eM0+jNJ$SzWpMV;*4h@jd;f1Zhz1oiad9!Z^a~#(A!e81FFF!!Ove zpMM=`aCCrB`<;#d%LPbA%yIUF_thP`K}YV`&*cF8uww$o0;&G4I>|STz|L`y8@A=@ z!|DpX`1$a8mJjwCcwzeienYk!L2AD*PO_O3k_oIHto^w9z}Imdx?Lm$5HRK=?{)nE z`QQ=ZHNv<5vYWrLd6Es^hVDUNeE>SLwr1nd1AIVtd^fr=;Q|49xQ22pUu+(DVe|bd zF4cQwlFy3p7lD=k1;x2MKwnLkNQxU0ZWB-*zQFoCp9iE-mkU8^*T*Cq`~V!l4_KQb z1hB4aLtxd%e1SFMWCHs;Qg0eJzzsM+z3mB7I~G7Wk`H|W;{jxb|3Y`YSBi5Yl#jV% zGl4Dpy0L&A2T%re>2Ru$x+uv9Kj=!p??ap;v>?3EW^_%T5KUlx272|&J($G-Inmi@%4S$l%_JI3+4~3 zbzo=of!FO3&sRAJT@jmnD6a z2;KyC9Arb51B?x9-@w{ornKwyA@Y$v=m)h4rwPy*_GjlraDlZT@}c`Yg7SydUscMI zUShlvl`pS%edoS6IS`Uxgm1KrZ8D#c~y2#_-E!bdVog z@G_(pUP?^gOF=$;W2E8}80?ohL=_T?coJ7sOrPZND8G-@u%P08R`W8J5uNg45{YF+ z_yq!dC&Gx}KzQ|?2z}u@5k!aZComu|ATS^>ATS^>AVmyR)$?G;P0%6)5h}eODn<0Z zR+I|a@jOOM!1Fjfm(h4_VJXWbIl(*oEQx)80^wqUR6kSV$qwG}JP@+u**KnqOYQqm zBpa7^uIz~8xi<7jCrI7<{v^}eWQY6!-bsKCCkYDDoa$huG)K zc*gjzzgI^xfcJ)kUkR_19eDt&1LOzr-cWbKzh(xsO=v_oN8tJgD=XxN-?P5Y=Cd}q zPS+a$TMAIy@EOPsIa%LkWkeqP{te;)dkrD3A#Xq7U+D|fF5VG+-&ZEth7&LrvGTGyAPrx_yNH7c|4LtYm+eU|dBQpZ zIEM`&H&+M5GYB~Hm&!kqtv(@!09m>6BhAj=rzox>jS7%l=nI&i(HB_1VQm1rz>kpc zE{*a-CXx+f>I4ESE97SRhwNBq_mx)pD|Ep6Vn>DC*dt?(gX~WUKM|y|4|6#s88r!5 z1F`;-L-D=@sh7RN2Jj^-0@k8HmEfIc0~wNg5@8^rO7PB=pJNApwvZMW5Eu{`5Eu{` zcrONCzIw#|TGZzZW2kD>x2Ph-%TSaq!%I{Ymf$5S3JdVk6ot93Ld){B7;j}!zUWm* zZ%>t{>FuoYkSe55tXmNpQN@(bokkT?SXl5fq=r0@rqHrHlu(68zY0a_FViB0m!Y^K z%&5rEttg*caeslw%W@@D1?oXnA01-Trc1CQyn0WIp1ujc=R<@De*yzllmXgTV4w7_ zNRK@Y!I`iC@vkWar)`{7;hg!aJ-g$y&7Sw+Y#@X1#h*94rfux+*>h4nbNFK42dBE> zYp^$~H+!~#5F9{`>0iPRs9v<~MBtuHa?ivd1J1wkZ+%TZw+vJ#_VKT4o7J7=0_R={ zggT#72CC1lqJ7AKwh-cbHn0cIcDQH%i2sl9S-*Kj^$jJ!_GlYzc7=`UK zUoxb2aCQv;XJugfB+m6et1rAs4{B!$0rm&r3y=lAf$uTA>9)_7Pi@p7gb<)R+b1Cd z&eXrkdqUI>&R)|9tR27wKD*?}(0Q6VL^HubRXw(+4fB@Z72w$yj zmKthPf#6Sg<1=8k?3W6u?JrfeP_MwiXJ_E$Il~+NpGWyO31WhXKqnsf33!}%l+bAh z{$X30A|0D(63pnhkO|-GkIJ(1HP)6`JL7&lb8YuN6;Qbb zge!zmghqrr1kC;M1l9Mfxf^CcLSz2*F=KW!8{`-b>PffxF z0@e`w-}gRlIl51eu!r!`-~6O|xm8L{{N-6F&O;fdlu$s(^=WDFfVA;Edl5a NI*A`4ogjIM{{uazzApd( literal 0 HcmV?d00001 diff --git a/ModVersionChecker/Resources/up-icon.ico b/ModVersionChecker/Resources/up-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1aedbdce046bc5962972071767eac51ad6fcbd92 GIT binary patch literal 104539 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 bwLakQmjMwL6Z5jz9`do}8jzDUrKtZ0KJ#97 literal 0 HcmV?d00001 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