Fix #3065: 修复 CurseForge 搜索 API 对翻页总量计算不正确的 Bug (#3066)

* Fix #3065

* Fix

* Fix.
This commit is contained in:
Burning_TNT
2024-07-20 05:03:08 +08:00
committed by GitHub
parent 7c7c36f8aa
commit 61096142d5
3 changed files with 82 additions and 44 deletions

View File

@@ -43,7 +43,6 @@ import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.WeakListenerHolder;
@@ -52,6 +51,7 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.AggregatedObservableList; import org.jackhuang.hmcl.util.AggregatedObservableList;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.I18n;
@@ -64,10 +64,10 @@ import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable { public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(); protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -87,7 +87,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
protected final ListProperty<String> downloadSources = new SimpleListProperty<>(this, "downloadSources", FXCollections.observableArrayList()); protected final ListProperty<String> downloadSources = new SimpleListProperty<>(this, "downloadSources", FXCollections.observableArrayList());
protected final StringProperty downloadSource = new SimpleStringProperty(); protected final StringProperty downloadSource = new SimpleStringProperty();
private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private TaskExecutor executor; private int searchID = 0;
protected RemoteModRepository repository; protected RemoteModRepository repository;
private Runnable retrySearch; private Runnable retrySearch;
@@ -163,11 +163,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
setLoading(true); setLoading(true);
setFailed(false); setFailed(false);
if (executor != null && !executor.isCancelled()) { int currentSearchID = searchID = searchID + 1;
executor.cancel(); Task.supplyAsync(() -> {
}
executor = Task.supplyAsync(() -> {
Profile.ProfileVersion version = this.version.get(); Profile.ProfileVersion version = this.version.get();
if (StringUtils.isBlank(version.getVersion())) { if (StringUtils.isBlank(version.getVersion())) {
return userGameVersion; return userGameVersion;
@@ -176,9 +173,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
? version.getProfile().getRepository().getGameVersion(version.getVersion()).orElse("") ? version.getProfile().getRepository().getGameVersion(version.getVersion()).orElse("")
: ""; : "";
} }
}).thenApplyAsync(gameVersion -> }).thenApplyAsync(gameVersion -> repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)).whenComplete(Schedulers.javafx(), (result, exception) -> {
repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC) if (searchID != currentSearchID) {
).whenComplete(Schedulers.javafx(), (result, exception) -> { return;
}
setLoading(false); setLoading(false);
if (exception == null) { if (exception == null) {
items.setAll(result.getResults().collect(Collectors.toList())); items.setAll(result.getResults().collect(Collectors.toList()));
@@ -196,7 +195,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
return i18n("curse.category." + category); return i18n("curse.category." + category);
} }
protected String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) { private String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) {
return StringUtils.repeats(' ', category.indent * 4) + return StringUtils.repeats(' ', category.indent * 4) +
(category.getCategory() == null (category.getCategory() == null
? i18n("curse.category.0") ? i18n("curse.category.0")
@@ -344,13 +343,14 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
sortComboBox.getSelectionModel().select(0); sortComboBox.getSelectionModel().select(0);
searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane); searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
StringProperty previousSearchFilter = new SimpleStringProperty(this, "Previous Seach Filter", ""); IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0);
IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1);
EventHandler<ActionEvent> searchAction = e -> { EventHandler<ActionEvent> searchAction = e -> {
if (!previousSearchFilter.get().equals(nameField.getText())) { if (currentFilterID.get() != filterID.get()) {
control.pageOffset.set(0); control.pageOffset.set(0);
} }
currentFilterID.set(filterID.get());
previousSearchFilter.set(nameField.getText());
getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(), getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(),
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem()) Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
.map(CategoryIndented::getCategory) .map(CategoryIndented::getCategory)
@@ -360,60 +360,93 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
sortComboBox.getSelectionModel().getSelectedItem()); sortComboBox.getSelectionModel().getSelectedItem());
}; };
control.listenerHolder.add(FXUtils.observeWeak(
() -> filterID.set(filterID.get() + 1),
control.downloadSource,
gameVersionField.getSelectionModel().selectedItemProperty(),
categoryComboBox.getSelectionModel().selectedItemProperty(),
nameField.textProperty(),
sortComboBox.getSelectionModel().selectedItemProperty()
));
HBox actionsBox = new HBox(8); HBox actionsBox = new HBox(8);
GridPane.setColumnSpan(actionsBox, 4); GridPane.setColumnSpan(actionsBox, 4);
actionsBox.setAlignment(Pos.CENTER); actionsBox.setAlignment(Pos.CENTER);
{ {
AggregatedObservableList<Node> actions = new AggregatedObservableList<>(); AggregatedObservableList<Node> actions = new AggregatedObservableList<>();
Holder<Runnable> changeButton = new Holder<>();
JFXButton firstPageButton = FXUtils.newBorderButton(i18n("search.first_page")); JFXButton firstPageButton = FXUtils.newBorderButton(i18n("search.first_page"));
firstPageButton.setOnAction(event -> { firstPageButton.setOnAction(event -> {
control.pageOffset.set(0); control.pageOffset.set(0);
changeButton.value.run();
searchAction.handle(event); searchAction.handle(event);
}); });
firstPageButton.setDisable(true);
control.pageCount.addListener((observable, oldValue, newValue) -> firstPageButton.setDisable(control.pageCount.get() == -1));
JFXButton previousPageButton = FXUtils.newBorderButton(i18n("search.previous_page")); JFXButton previousPageButton = FXUtils.newBorderButton(i18n("search.previous_page"));
previousPageButton.setOnAction(event -> { previousPageButton.setOnAction(event -> {
if (control.pageOffset.get() > 0) { int pageOffset = control.pageOffset.get();
control.pageOffset.set(control.pageOffset.get() - 1); if (pageOffset > 0) {
control.pageOffset.set(pageOffset - 1);
changeButton.value.run();
searchAction.handle(event); searchAction.handle(event);
} }
}); });
previousPageButton.setDisable(true);
control.pageOffset.addListener((observable, oldValue, newValue) -> previousPageButton.setDisable(
control.pageCount.get() == -1 || control.pageOffset.get() == 0
));
Label pageOffset = new Label(i18n("search.page_n", 0, "-")); Label pageDescription = new Label();
control.pageOffset.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n( pageDescription.textProperty().bind(Bindings.createStringBinding(() -> {
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString() int pageCount = control.pageCount.get();
))); return i18n("search.page_n", control.pageOffset.get() + 1, pageCount == -1 ? "-" : String.valueOf(pageCount));
control.pageCount.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n( }, control.pageOffset, control.pageCount));
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString()
)));
JFXButton nextPageButton = FXUtils.newBorderButton(i18n("search.next_page")); JFXButton nextPageButton = FXUtils.newBorderButton(i18n("search.next_page"));
nextPageButton.setOnAction(event -> { nextPageButton.setOnAction(event -> {
control.pageOffset.set(control.pageOffset.get() + 1); int nv = control.pageOffset.get() + 1;
searchAction.handle(event); if (nv < control.pageCount.get()) {
control.pageOffset.set(nv);
changeButton.value.run();
searchAction.handle(event);
}
}); });
nextPageButton.setDisable(true);
control.pageOffset.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
));
control.pageCount.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
));
JFXButton lastPageButton = FXUtils.newBorderButton(i18n("search.last_page")); JFXButton lastPageButton = FXUtils.newBorderButton(i18n("search.last_page"));
lastPageButton.setOnAction(event -> { lastPageButton.setOnAction(event -> {
control.pageOffset.set(control.pageCount.get() - 1); control.pageOffset.set(control.pageCount.get() - 1);
changeButton.value.run();
searchAction.handle(event); searchAction.handle(event);
}); });
firstPageButton.setDisable(true);
previousPageButton.setDisable(true);
lastPageButton.setDisable(true); lastPageButton.setDisable(true);
control.pageCount.addListener((observable, oldValue, newValue) -> lastPageButton.setDisable(control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1)); nextPageButton.setDisable(true);
changeButton.value = () -> {
int pageOffset = control.pageOffset.get();
int pageCount = control.pageCount.get();
boolean disablePrevious = pageOffset == 0;
firstPageButton.setDisable(disablePrevious);
previousPageButton.setDisable(disablePrevious);
boolean disableNext = pageOffset == pageCount - 1;
nextPageButton.setDisable(disableNext);
lastPageButton.setDisable(disableNext || pageCount == -1);
};
FXUtils.onChange(control.pageCount, pageCountN -> {
int pageCount = pageCountN.intValue();
if (pageCount != -1) {
if (control.pageOffset.get() + 1 >= pageCount) {
control.pageOffset.set(pageCount - 1);
}
}
changeButton.value.run();
});
Pane placeholder = new Pane(); Pane placeholder = new Pane();
HBox.setHgrow(placeholder, Priority.SOMETIMES); HBox.setHgrow(placeholder, Priority.SOMETIMES);
@@ -421,13 +454,14 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
JFXButton searchButton = FXUtils.newRaisedButton(i18n("search")); JFXButton searchButton = FXUtils.newRaisedButton(i18n("search"));
searchButton.setOnAction(searchAction); searchButton.setOnAction(searchAction);
actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageOffset, nextPageButton, lastPageButton, placeholder, searchButton)); actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageDescription, nextPageButton, lastPageButton, placeholder, searchButton));
actions.appendList(control.actions); actions.appendList(control.actions);
Bindings.bindContent(actionsBox.getChildren(), actions.getAggregatedList()); Bindings.bindContent(actionsBox.getChildren(), actions.getAggregatedList());
} }
searchPane.addRow(rowIndex++, actionsBox); searchPane.addRow(rowIndex++, actionsBox);
FXUtils.onChange(control.downloadSource, v -> searchAction.handle(null));
nameField.setOnAction(searchAction); nameField.setOnAction(searchAction);
gameVersionField.setOnAction(searchAction); gameVersionField.setOnAction(searchAction);
categoryComboBox.setOnAction(searchAction); categoryComboBox.setOnAction(searchAction);

View File

@@ -94,6 +94,10 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
return "asc"; return "asc";
} }
private int calculateTotalPages(Response<List<CurseAddon>> response, int pageSize) {
return (int) Math.ceil((double) Math.min(response.pagination.totalCount, 10000) / pageSize);
}
@Override @Override
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException { public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
int categoryId = 0; int categoryId = 0;
@@ -112,7 +116,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
.getJson(new TypeToken<Response<List<CurseAddon>>>() { .getJson(new TypeToken<Response<List<CurseAddon>>>() {
}.getType()); }.getType());
if (searchFilter.isEmpty()) { if (searchFilter.isEmpty()) {
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), (int)Math.ceil((double)response.pagination.totalCount / pageSize)); return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
} }
// https://github.com/HMCL-dev/HMCL/issues/1549 // https://github.com/HMCL-dev/HMCL/issues/1549
@@ -135,7 +139,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
} }
return pair(remoteMod, diff); return pair(remoteMod, diff);
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), response.pagination.totalCount); }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
} }
@Override @Override

View File

@@ -95,7 +95,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query)) Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
.getJson(new TypeToken<Response<ProjectSearchResult>>() { .getJson(new TypeToken<Response<ProjectSearchResult>>() {
}.getType()); }.getType());
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int)Math.ceil((double)response.totalHits / pageSize)); return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize));
} }
@Override @Override