Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersi
continue;
}

if (candidate == null || candidate.targetVersion().datePublished().isBefore(update.targetVersion().datePublished())) {
if (candidate == null || candidate.latestAvailableVersion().datePublished().isBefore(update.latestAvailableVersion().datePublished())) {
candidate = update;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
Expand All @@ -27,10 +28,7 @@
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.addon.LocalAddonFile;
import org.jackhuang.hmcl.addon.LocalAddonManager;
import org.jackhuang.hmcl.addon.RemoteAddon;
Expand All @@ -43,6 +41,7 @@
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CSVTable;
Expand All @@ -55,6 +54,8 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -99,7 +100,7 @@ public AddonUpdatesPage(LocalAddonManager<F> localAddonManager, List<LocalAddonF
TableColumn<AddonUpdateObject, String> sourceColumn = new TableColumn<>(i18n("addon.check_update.source"));
setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty);

TableColumn<AddonUpdateObject, String> changelogColumn = new TableColumn<>(i18n("addon.changelog"));
TableColumn<AddonUpdateObject, String> changelogColumn = new TableColumn<>(i18n("addon.select_target_version"));
{
var oldCellFactory = changelogColumn.getCellFactory();
changelogColumn.setCellFactory(param -> {
Expand Down Expand Up @@ -153,7 +154,6 @@ private void updateFiles() {
localAddonManager.getDirectory(),
objects.stream()
.filter(AddonUpdateObject::isEnabled)
.map(AddonUpdateObject::getData)
.toList()
);
Controllers.taskDialog(
Expand Down Expand Up @@ -186,10 +186,10 @@ private void exportList() {
csvTable.set(3, 0, "Update Source");

for (int i = 0; i < objects.size(); i++) {
csvTable.set(0, i + 1, objects.get(i).fileName.get());
csvTable.set(1, i + 1, objects.get(i).currentVersion.get());
csvTable.set(2, i + 1, objects.get(i).targetVersion.get());
csvTable.set(3, i + 1, objects.get(i).source.get());
csvTable.set(0, i + 1, objects.get(i).getFileName());
csvTable.set(1, i + 1, objects.get(i).getCurrentVersion());
csvTable.set(2, i + 1, objects.get(i).getTargetVersion());
csvTable.set(3, i + 1, objects.get(i).getSource());
}

csvTable.write(path);
Expand All @@ -211,26 +211,25 @@ public ReadOnlyObjectWrapper<State> stateProperty() {

private static final class AddonUpdateObject {
final LocalAddonFile.AddonUpdate data;
final ObjectProperty<RemoteAddon.Version> targetVersionObject = new SimpleObjectProperty<>();
final BooleanProperty enabled = new SimpleBooleanProperty();
final StringProperty fileName = new SimpleStringProperty();
final StringProperty currentVersion = new SimpleStringProperty();
final StringProperty targetVersion = new SimpleStringProperty();
final StringProperty source = new SimpleStringProperty();
String changelog = null;

public AddonUpdateObject(LocalAddonFile.AddonUpdate data) {
this.data = data;
this.targetVersionObject.set(data.latestAvailableVersion());

enabled.set(!data.localAddonFile().isDisabled());
fileName.set(data.localAddonFile().getFileName());
currentVersion.set(data.currentVersion().version());
targetVersion.set(data.targetVersion().version());
targetVersion.set(targetVersionObject.get().version());
FXUtils.onChange(targetVersionObject, (value) -> targetVersion.set(value.version()));
switch (data.currentVersion().self().getSource()) {
case CURSEFORGE:
source.set(i18n("addon.curseforge"));
break;
case MODRINTH:
source.set(i18n("addon.modrinth"));
case CURSEFORGE -> source.set(i18n("addon.curseforge"));
case MODRINTH -> source.set(i18n("addon.modrinth"));
}
}

Expand Down Expand Up @@ -300,34 +299,64 @@ public void setSource(String source) {
}

private static final class AddonChangelog extends JFXDialogLayout {
private final Map<String, String> changelogCache = new ConcurrentHashMap<>();
/// ID of the most recently started changelog request. Only the result of this request may update the UI.
@FXThread
private int changelogRequestId = 0;
/// ID of the most recently started version page request. Only the result of this request may update the UI.
@FXThread
private int versionPageRequestId = 0;

public AddonChangelog(AddonUpdateObject object) {
RemoteAddon.Version targetVersion = object.data.targetVersion();
List<RemoteAddon.Version> availableVersions = object.data.availableVersions();

this.setHeading(new HBox(new Label(i18n("addon.changelog") + " - " + targetVersion.name())));
Label headingLabel = new Label(i18n("addon.select_target_version"));
this.setHeading(new HBox(8, headingLabel));

VBox box = new VBox(8);
box.setPadding(new Insets(8));

// Version selector ComboBox
JFXComboBox<RemoteAddon.Version> versionComboBox = new JFXComboBox<>();
versionComboBox.getItems().setAll(availableVersions);
versionComboBox.getSelectionModel().select(object.targetVersionObject.get());
FXUtils.onChange(versionComboBox.getSelectionModel().selectedItemProperty(), object.targetVersionObject::set);
versionComboBox.setConverter(FXUtils.stringConverter(RemoteAddon.Version::name));
HBox.setHgrow(versionComboBox, Priority.ALWAYS);

Label selectVersionLabel = new Label(i18n("addon.changelog.view_other_log"));

HBox versionSelector = new HBox(8, selectVersionLabel, versionComboBox);
versionSelector.setAlignment(Pos.CENTER_LEFT);
versionSelector.setPadding(new Insets(0, 0, 4, 0));
box.getChildren().add(versionSelector);

SpinnerPane spinnerPane = new SpinnerPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
FXUtils.setOverflowHidden(scrollPane, 8);

loadChangelog(object, spinnerPane, scrollPane);
spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, scrollPane));
loadChangelog(object, versionComboBox.getSelectionModel().getSelectedItem(), spinnerPane, scrollPane);
spinnerPane.setOnFailedAction(e -> loadChangelog(object, versionComboBox.getSelectionModel().getSelectedItem(), spinnerPane, scrollPane));

JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url"));
versionPageBtn.setDisable(true);
loadVersionPageUrl(object, versionPageBtn, versionComboBox.getSelectionModel().getSelectedItem());

versionComboBox.getSelectionModel().selectedItemProperty().addListener((obs, oldVersion, newVersion) -> {
if (newVersion != null) {
loadChangelog(object, newVersion, spinnerPane, scrollPane);
loadVersionPageUrl(object, versionPageBtn, newVersion);
}
});

spinnerPane.setContent(scrollPane);
box.getChildren().add(spinnerPane);
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);

this.setBody(box);

JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url"));
versionPageBtn.setDisable(true);
loadVersionPageUrl(object, versionPageBtn);

JFXButton closeButton = new JFXButton(i18n("button.ok"));
closeButton.getStyleClass().add("dialog-accept");
closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
Expand All @@ -340,23 +369,34 @@ public AddonChangelog(AddonUpdateObject object) {
onEscPressed(this, closeButton::fire);
}

private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) {
private void loadChangelog(AddonUpdateObject object, RemoteAddon.Version version, SpinnerPane spinnerPane, ScrollPane scrollPane) {
if (version == null) return;
// will anyone actually change selected version 10000000+ times?
int requestId = ++changelogRequestId;
spinnerPane.setLoading(true);
RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType());
Task.supplyAsync(() -> {
if (object.changelog != null) {
return object.changelog;
String cached = changelogCache.get(version.versionId());
if (cached != null) {
return cached;
}
RemoteAddon.Version version = object.data.targetVersion();
if (repo == null) return null;
return StringUtils.convertToHtml(
repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.projectId(), version.versionId()),
"238222".equals(object.data.targetVersion().projectId())
"238222".equals(version.projectId())
);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (requestId != changelogRequestId) {
// A newer request has been started, discard this stale result
// NOTE: comparing version might not be enough: selecting A -> B -> A makes two
// requests for the same version, and the older one may finish last.
return;
}

if (exception == null) {
object.changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty");
scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo == null ? "" : repo.getBaseUrl()));
String html = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty");
changelogCache.put(version.versionId(), html);
scrollPane.setContent(FXUtils.renderAddonChangelog(html, repo == null ? "" : repo.getBaseUrl()));
Comment thread
ZZZank marked this conversation as resolved.
FXUtils.smoothScrolling(scrollPane);
spinnerPane.setFailedReason(null);
} else {
Expand All @@ -366,15 +406,23 @@ private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, Sc
}).start();
}

private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) {
private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button, RemoteAddon.Version version) {
button.setDisable(true);
int requestId = ++versionPageRequestId;
Task.supplyAsync(() -> {
RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType());
return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion());
return repo == null ? null : repo.getVersionPageUrl(version);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (requestId != versionPageRequestId) {
// A newer request has been started, discard this stale result
return;
}

if (exception == null && StringUtils.isNotBlank(result)) {
button.setExternalLink(result);
button.setDisable(false);
} else {
button.setDisable(true);
LOG.warning("Failed to load addon version page url", exception);
}
}).start();
Expand All @@ -385,21 +433,21 @@ public static class AddonUpdateTask extends Task<Void> {
private final Collection<Task<?>> dependents;
private final List<LocalAddonFile> failedAddons = new ArrayList<>();

AddonUpdateTask(Path addonDirectory, List<LocalAddonFile.AddonUpdate> addons) {
AddonUpdateTask(Path addonDirectory, List<AddonUpdateObject> addons) {
setStage("addon.check_update.confirm");
getProperties().put("total", addons.size());

this.dependents = new ArrayList<>();
for (LocalAddonFile.AddonUpdate addon : addons) {
LocalAddonFile local = addon.localAddonFile();
RemoteAddon.Version remote = addon.targetVersion();
for (var addon : addons) {
LocalAddonFile local = addon.data.localAddonFile();
RemoteAddon.Version remote = addon.targetVersionObject.get();
boolean isDisabled = local.isDisabled();
String originalFileName = local.getFile().getFileName().toString();

dependents.add(Task
.runAsync(Schedulers.javafx(), () -> local.setOld(true))
.thenComposeAsync(() -> {
String fileName = addon.useRemoteFileName() ? remote.file().filename() : originalFileName;
String fileName = addon.data.useRemoteFileName() ? remote.file().filename() : originalFileName;
if (isDisabled)
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);

Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ account.username=Username
addon.category=Category
addon.changelog=Changelog
addon.changelog.empty=Currently no changelog
addon.changelog.view_other_log=Select version
addon.select_target_version=Select target version
addon.channel.alpha=Alpha
addon.channel.beta=Beta
addon.channel.release=Release
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ addon.broken_dependency.desc=Diese Abhängigkeit existierte vorher, ist aber nic
addon.category=Kategorie
addon.changelog=Änderungsprotokoll
addon.changelog.empty=Derzeit kein Änderungsprotokoll
addon.changelog.view_other_log=Version auswählen
addon.select_target_version=Zielversion auswählen
addon.channel.alpha=Alpha
addon.channel.beta=Beta
addon.channel.release=Veröffentlichung
Expand Down
4 changes: 3 additions & 1 deletion HMCL/src/main/resources/assets/lang/I18N_lzh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ account.skin.invalid_skin=外觀之案有謬
account.username=戶名

addon.category=類
addon.changelog=迭更誌
addon.changelog=改囊之誌
addon.changelog.view_other_log=擇版
addon.select_target_version=擇標的之版
addon.channel.alpha=預版
addon.channel.beta=試版
addon.channel.release=當版
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_uk.properties
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ account.username=Ім'я користувача
addon.category=Категорія
addon.changelog=Список змін
addon.changelog.empty=Наразі немає списку змін
addon.changelog.view_other_log=Оберіть версію
addon.select_target_version=Оберіть цільову версію
addon.channel.alpha=Альфа
addon.channel.beta=Бета
addon.channel.release=Реліз
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ account.username=使用者名稱
addon.category=類別
addon.changelog=更新日誌
addon.changelog.empty=暫無更新日誌
addon.changelog.view_other_log=選擇版本
addon.select_target_version=選擇目標版本
addon.channel.alpha=Alpha
addon.channel.beta=Beta
addon.channel.release=Release
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ account.username=用户名
addon.category=类别
addon.changelog=更新日志
addon.changelog.empty=暂无更新日志
addon.changelog.view_other_log=选择版本
addon.select_target_version=选择目标版本
addon.channel.alpha=快照版本
addon.channel.beta=测试版本
addon.channel.release=稳定版本
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ public record AddonUpdate(
RemoteAddon.Type repoType,
LocalAddonFile localAddonFile,
RemoteAddon.Version currentVersion,
RemoteAddon.Version targetVersion,
List<RemoteAddon.Version> availableVersions,
boolean useRemoteFileName
) {
public RemoteAddon.Version latestAvailableVersion() {
return availableVersions.get(0);
}
}

public static class Description {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe
.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed())
.toList();
if (remoteVersions.isEmpty()) return null;
return new AddonUpdate(source, RemoteAddon.Type.MOD, this, currentVersion.get(), remoteVersions.get(0), true);
return new AddonUpdate(source, RemoteAddon.Type.MOD, this, currentVersion.get(), remoteVersions, true);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe
.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed())
.toList();
if (remoteVersions.isEmpty()) return null;
return new AddonUpdate(source, RemoteAddon.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions.get(0), false);
return new AddonUpdate(source, RemoteAddon.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions, false);
}
}