11package com .cope .meteoraddons .gui .screens ;
22
3+ import com .cope .meteoraddons .MeteorAddonsAddon ;
34import com .cope .meteoraddons .addons .Addon ;
5+ import com .cope .meteoraddons .addons .InstalledAddon ;
46import com .cope .meteoraddons .addons .OnlineAddon ;
57import com .cope .meteoraddons .config .IconSizeConfig ;
68import com .cope .meteoraddons .models .AddonMetadata ;
9+ import com .cope .meteoraddons .models .UpdateInfo ;
710import com .cope .meteoraddons .systems .AddonManager ;
11+ import com .cope .meteoraddons .util .GitHubReleaseAPI ;
12+ import com .cope .meteoraddons .util .HashUtil ;
813import com .cope .meteoraddons .util .IconCache ;
914import meteordevelopment .meteorclient .gui .GuiTheme ;
15+ import meteordevelopment .meteorclient .gui .GuiThemes ;
1016import meteordevelopment .meteorclient .gui .WindowScreen ;
1117import meteordevelopment .meteorclient .gui .widgets .containers .WHorizontalList ;
1218import meteordevelopment .meteorclient .gui .widgets .containers .WSection ;
1925
2026import com .cope .meteoraddons .util .TimeUtil ;
2127
28+ import java .nio .file .Path ;
29+ import java .util .Collections ;
2230import java .util .List ;
31+ import java .util .Optional ;
2332
2433import static meteordevelopment .meteorclient .MeteorClient .mc ;
2534import static meteordevelopment .meteorclient .utils .Utils .getWindowWidth ;
@@ -115,7 +124,7 @@ public void initWidgets() {
115124 // Actions (Buttons)
116125 WHorizontalList actions = add (theme .horizontalList ()).right ().widget ();
117126
118- // Download/Install Button
127+ // Download/Install Button (for non-installed addons)
119128 if (!addon .isInstalled () && addon instanceof OnlineAddon ) {
120129 WButton downloadButton = actions .add (theme .button ("Download" )).widget ();
121130 downloadButton .action = () -> {
@@ -133,6 +142,26 @@ public void initWidgets() {
133142 };
134143 }
135144
145+ // Check for Updates Button (for installed addons)
146+ if (addon .isInstalled () && addon instanceof InstalledAddon installedAddon ) {
147+ WButton checkUpdateBtn = actions .add (theme .button ("Check for Updates" )).widget ();
148+ checkUpdateBtn .action = () -> {
149+ checkUpdateBtn .set ("Checking..." );
150+ MeteorExecutor .execute (() -> {
151+ Optional <UpdateInfo > update = checkForUpdate (installedAddon );
152+ mc .execute (() -> {
153+ if (update .isPresent ()) {
154+ checkUpdateBtn .set ("Update Found!" );
155+ // Show updates screen with this single update
156+ mc .setScreen (new UpdatesAvailableScreen (GuiThemes .get (), Collections .singletonList (update .get ())));
157+ } else {
158+ checkUpdateBtn .set ("Up to date" );
159+ }
160+ });
161+ });
162+ };
163+ }
164+
136165 // Link Buttons
137166 if (addon .getGithubUrl ().isPresent ()) {
138167 WButton btn = actions .add (theme .button ("GitHub" )).widget ();
@@ -189,4 +218,123 @@ private boolean addFeatureList(WSection section, String label, List<String> item
189218
190219 return true ;
191220 }
221+
222+ /**
223+ * Check for updates for a specific installed addon.
224+ */
225+ private Optional <UpdateInfo > checkForUpdate (InstalledAddon installed ) {
226+ MeteorAddonsAddon .LOG .info ("Checking for updates for: {}" , installed .getName ());
227+
228+ // Get GitHub URL from the installed addon
229+ Optional <String > githubUrlOpt = installed .getGithubUrl ();
230+ if (githubUrlOpt .isEmpty ()) {
231+ MeteorAddonsAddon .LOG .warn ("No GitHub URL for {}" , installed .getName ());
232+ return Optional .empty ();
233+ }
234+
235+ String githubUrl = githubUrlOpt .get ();
236+ MeteorAddonsAddon .LOG .info ("GitHub URL: {}" , githubUrl );
237+
238+ // Parse owner/repo
239+ Optional <String []> parsed = GitHubReleaseAPI .parseGitHubUrl (githubUrl );
240+ if (parsed .isEmpty ()) {
241+ MeteorAddonsAddon .LOG .warn ("Failed to parse GitHub URL: {}" , githubUrl );
242+ return Optional .empty ();
243+ }
244+
245+ String [] ownerRepo = parsed .get ();
246+ MeteorAddonsAddon .LOG .info ("Parsed: owner={}, repo={}" , ownerRepo [0 ], ownerRepo [1 ]);
247+
248+ // Get local JAR path
249+ Path localJarPath = getJarPath (installed );
250+ if (localJarPath == null ) {
251+ MeteorAddonsAddon .LOG .warn ("Could not determine JAR path for {}" , installed .getName ());
252+ return Optional .empty ();
253+ }
254+ MeteorAddonsAddon .LOG .info ("Local JAR path: {}" , localJarPath );
255+
256+ // Compute local hash
257+ String localHash = HashUtil .computeSha256 (localJarPath );
258+ if (localHash == null ) {
259+ MeteorAddonsAddon .LOG .warn ("Failed to compute local hash for {}" , installed .getName ());
260+ return Optional .empty ();
261+ }
262+ MeteorAddonsAddon .LOG .info ("Local SHA256: {}" , localHash );
263+
264+ // Fetch release info from GitHub
265+ Optional <GitHubReleaseAPI .ReleaseInfo > releaseOpt = GitHubReleaseAPI .getLatestRelease (ownerRepo [0 ], ownerRepo [1 ]);
266+ if (releaseOpt .isEmpty ()) {
267+ MeteorAddonsAddon .LOG .warn ("No release found for {}/{}" , ownerRepo [0 ], ownerRepo [1 ]);
268+ return Optional .empty ();
269+ }
270+
271+ GitHubReleaseAPI .ReleaseInfo release = releaseOpt .get ();
272+ MeteorAddonsAddon .LOG .info ("Latest release: {} ({})" , release .getName (), release .getTagName ());
273+
274+ // Find JAR asset
275+ Optional <GitHubReleaseAPI .AssetInfo > assetOpt = GitHubReleaseAPI .findJarAsset (release );
276+ if (assetOpt .isEmpty ()) {
277+ MeteorAddonsAddon .LOG .warn ("No JAR asset found in release for {}" , installed .getName ());
278+ return Optional .empty ();
279+ }
280+
281+ GitHubReleaseAPI .AssetInfo asset = assetOpt .get ();
282+ String remoteHash = asset .getSha256 ();
283+ MeteorAddonsAddon .LOG .info ("Remote JAR: {}" , asset .getFileName ());
284+ MeteorAddonsAddon .LOG .info ("Remote SHA256: {}" , remoteHash );
285+
286+ if (remoteHash == null || remoteHash .isEmpty ()) {
287+ MeteorAddonsAddon .LOG .warn ("No SHA256 digest available for {} (GitHub may not have computed it yet)" , installed .getName ());
288+ return Optional .empty ();
289+ }
290+
291+ // Compare hashes
292+ if (!HashUtil .hashesMatch (localHash , remoteHash )) {
293+ MeteorAddonsAddon .LOG .info ("UPDATE AVAILABLE for {}: hashes differ" , installed .getName ());
294+
295+ return Optional .of (new UpdateInfo (
296+ installed ,
297+ installed .getName (),
298+ installed .getVersion (),
299+ release .getVersion (),
300+ release .getChangelog (),
301+ asset .getDownloadUrl (),
302+ remoteHash ,
303+ localHash ,
304+ localJarPath
305+ ));
306+ } else {
307+ MeteorAddonsAddon .LOG .info ("{} is up to date (hashes match)" , installed .getName ());
308+ return Optional .empty ();
309+ }
310+ }
311+
312+ /**
313+ * Get the JAR file path for an installed addon.
314+ */
315+ private Path getJarPath (InstalledAddon addon ) {
316+ try {
317+ List <Path > rootPaths = addon .getModContainer ().getRootPaths ();
318+ if (!rootPaths .isEmpty ()) {
319+ Path rootPath = rootPaths .get (0 );
320+ String pathStr = rootPath .toUri ().toString ();
321+ MeteorAddonsAddon .LOG .debug ("Root path URI: {}" , pathStr );
322+
323+ if (pathStr .startsWith ("jar:file:" )) {
324+ // Extract JAR path from jar:file:/path/to/mod.jar!/
325+ int exclamation = pathStr .indexOf ('!' );
326+ if (exclamation > 0 ) {
327+ String jarUriStr = pathStr .substring (4 , exclamation ); // Get "file:/path/to/mod.jar"
328+ java .net .URI jarUri = new java .net .URI (jarUriStr );
329+ return Path .of (jarUri );
330+ }
331+ } else if (pathStr .endsWith (".jar" )) {
332+ return rootPath ;
333+ }
334+ }
335+ } catch (Exception e ) {
336+ MeteorAddonsAddon .LOG .warn ("Failed to get JAR path for {}: {}" , addon .getName (), e .getMessage ());
337+ }
338+ return null ;
339+ }
192340}
0 commit comments