@@ -69,6 +69,7 @@ public final class ProjectBrowseController {
6969
7070 private static final double RESULTS_INDICATOR_WIDTH = 126 ;
7171 private static final double RESULTS_INDICATOR_SHOW_BUFFER = 10 ;
72+ private static final int CURSEFORGE_BANNER_BATCH_SIZE = 4 ;
7273 private final ModtaleApiClient apiClient ;
7374 private final Executor executor ;
7475 private final Runnable applySettings ;
@@ -126,6 +127,7 @@ public final class ProjectBrowseController {
126127 private int currentPage ;
127128 private int totalPageCount ;
128129 private long totalResultCount ;
130+ private long bannerEnrichmentGeneration ;
129131 private BrowseOptions .BrowseViewOption activeBrowseView = BrowseOptions .BrowseViewOption .defaultOption ();
130132
131133 public ProjectBrowseController (
@@ -812,13 +814,56 @@ private void finishSearch(ProjectPage page, ProjectSearchQuery query) {
812814 updateResultsIndicator (totalResultCount , false );
813815 updatePaginationControls ();
814816 renderProjects ();
817+ if (sourceSelector .source () == ProjectBrowseSource .CURSEFORGE ) {
818+ loadCurseForgeBanners (++bannerEnrichmentGeneration , page .content ());
819+ } else {
820+ bannerEnrichmentGeneration ++;
821+ }
815822 log .accept ("Found " + page .content ().size () + " projects." );
816823 ProjectSearchQuery nextQuery = searchQuery ();
817824 if (!nextQuery .equals (query ) && searchState .shouldSearchForLayout (nextQuery , currentProjects )) {
818825 requestProjects ();
819826 }
820827 }
821828
829+ private void loadCurseForgeBanners (long generation , List <ProjectSummary > projects ) {
830+ List <ProjectSummary > missing = projects .stream ()
831+ .filter (ProjectSummary ::isCurseForge )
832+ .filter (project -> project .bannerUrl () == null || project .bannerUrl ().isBlank ())
833+ .toList ();
834+ loadCurseForgeBannerBatch (generation , missing , 0 );
835+ }
836+
837+ private void loadCurseForgeBannerBatch (long generation , List <ProjectSummary > projects , int start ) {
838+ if (start >= projects .size () || generation != bannerEnrichmentGeneration ) return ;
839+ int end = Math .min (start + CURSEFORGE_BANNER_BATCH_SIZE , projects .size ());
840+ List <CompletableFuture <ProjectSummary >> requests = projects .subList (start , end ).stream ()
841+ .map (project -> CompletableFuture .supplyAsync (
842+ () -> apiClient .enrichCurseForgeBrowseBanner (project ), executor ))
843+ .toList ();
844+ CompletableFuture .allOf (requests .toArray (CompletableFuture []::new ))
845+ .whenComplete ((ignored , error ) -> Platform .runLater (() -> {
846+ if (error != null || generation != bannerEnrichmentGeneration
847+ || sourceSelector .source () != ProjectBrowseSource .CURSEFORGE ) {
848+ return ;
849+ }
850+ Map <String , ProjectSummary > enriched = new LinkedHashMap <>();
851+ for (CompletableFuture <ProjectSummary > request : requests ) {
852+ ProjectSummary project = request .getNow (null );
853+ if (project != null && project .bannerUrl () != null && !project .bannerUrl ().isBlank ()) {
854+ enriched .put (project .id (), project );
855+ }
856+ }
857+ if (!enriched .isEmpty ()) {
858+ currentProjects = currentProjects .stream ()
859+ .map (project -> enriched .getOrDefault (project .id (), project ))
860+ .toList ();
861+ renderProjects ();
862+ }
863+ loadCurseForgeBannerBatch (generation , projects , end );
864+ }));
865+ }
866+
822867 private void renderProjectsForLayoutChange () {
823868 if (currentProjects .isEmpty ()) {
824869 return ;
0 commit comments