enhance(addon): addon更新功能优化 (#6531)
Fixes #6940 Fixes #5865 Fixes #3841 Fixes #3705
This commit is contained in:
1 parent
0597282463
commit
6f406cede8
29 files changed
+549
-288
No files matched your search
+32
-20
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <[email protected]> and contributors
|
||||
* Copyright (C) 2026 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,47 +15,55 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class AddonCheckUpdatesTask<T extends LocalAddonFile> extends Task<List<LocalAddonFile.AddonUpdate>> {
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final List<Task<LocalAddonFile.AddonUpdate>> dependents;
|
||||
public class AddonCheckUpdatesTask extends Task<AddonUpdates> {
|
||||
private final List<Task<Pair<AddonUpdate, @Nullable AddonUpdate>>> dependents;
|
||||
|
||||
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<T> addons) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<? extends LocalAddonFile> addons) {
|
||||
dependents = addons.stream().map(addon ->
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
LocalAddonFile.AddonUpdate candidate = null;
|
||||
AddonUpdate candidate = null;
|
||||
AddonUpdate candidateRelease = null;
|
||||
for (RemoteAddon.Source source : RemoteAddon.Source.values()) {
|
||||
LocalAddonFile.AddonUpdate update = null;
|
||||
AddonUpdate update = null;
|
||||
AddonUpdate updateRelease = null;
|
||||
try {
|
||||
update = addon.checkUpdates(downloadProvider, gameVersion, source);
|
||||
var u = addon.checkUpdates(downloadProvider, gameVersion, source);
|
||||
if (u != null) {
|
||||
update = u.key();
|
||||
updateRelease = u.value();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Cannot check update for addon %s.", addon.getFileName()), e);
|
||||
}
|
||||
if (update == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate == null || candidate.targetVersion().datePublished().isBefore(update.targetVersion().datePublished())) {
|
||||
if (update == null) continue;
|
||||
if (candidate == null || candidate.targetVersion().datePublished().isBefore(update.targetVersion().datePublished()))
|
||||
candidate = update;
|
||||
}
|
||||
|
||||
if (updateRelease == null) continue;
|
||||
if (candidateRelease == null || candidateRelease.targetVersion().datePublished().isBefore(updateRelease.targetVersion().datePublished()))
|
||||
candidateRelease = updateRelease;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
if (candidate == null) return null; // If there's no candidate for all channels, then no candidate for release channel
|
||||
return Pair.pair(candidate, candidateRelease);
|
||||
}).setName(addon.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
|
||||
).toList();
|
||||
|
||||
@@ -85,8 +93,12 @@ public class AddonCheckUpdatesTask<T extends LocalAddonFile> extends Task<List<L
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(dependents.stream()
|
||||
.map(Task::getResult)
|
||||
.filter(Objects::nonNull).toList());
|
||||
List<AddonUpdate> commonUpdates = new ArrayList<>(), releaseUpdates = new ArrayList<>();
|
||||
dependents.stream().map(Task::getResult).filter(Objects::nonNull)
|
||||
.forEachOrdered(pair -> {
|
||||
commonUpdates.add(pair.key());
|
||||
if (pair.value() != null) releaseUpdates.add(pair.value());
|
||||
});
|
||||
setResult(new AddonUpdates(commonUpdates, releaseUpdates));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class AddonUpdateTask extends Task<Void> {
|
||||
private final Collection<Task<?>> dependents = new ArrayList<>();
|
||||
private final List<LocalAddonFile> failedAddons = new ArrayList<>();
|
||||
|
||||
public AddonUpdateTask(DownloadProvider downloadProvider, Path addonDirectory, List<AddonUpdate> addons) {
|
||||
setStage("addon.check_update.confirm");
|
||||
getProperties().put("total", addons.size());
|
||||
|
||||
for (AddonUpdate addon : addons) {
|
||||
LocalAddonFile local = addon.localAddonFile();
|
||||
RemoteAddon.Version remote = addon.targetVersion();
|
||||
boolean isDisabled = local.isDisabled();
|
||||
String fileName = remote.file().filename();
|
||||
if (isDisabled)
|
||||
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);
|
||||
String newFileName = fileName;
|
||||
Path targetFile = addonDirectory.resolve(newFileName);
|
||||
|
||||
dependents.add(Task
|
||||
.supplyAsync(Schedulers.io(), () -> Files.createTempFile("hmcl-addon-update-", ".tmp"))
|
||||
.thenComposeAsync(tempFile ->
|
||||
new FileDownloadTask(downloadProvider.getDownloadCandidates(remote.file().url()), tempFile)
|
||||
.setName(remote.name())
|
||||
.thenRunAsync(Schedulers.javafx(), () -> local.setOld(true))
|
||||
.thenRunAsync(Schedulers.io(), () -> Files.move(tempFile, targetFile))
|
||||
.whenComplete(Schedulers.javafx(), exception -> {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to delete temp file " + tempFile, e);
|
||||
}
|
||||
if (exception != null) {
|
||||
// restore state if failed
|
||||
local.setOld(false);
|
||||
if (isDisabled)
|
||||
local.markDisabled();
|
||||
} else {
|
||||
local.onUpdated(newFileName);
|
||||
if (!local.keepOldFiles()) {
|
||||
try {
|
||||
local.delete();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete outdated addon: " + local.getFile(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to update addon", exception);
|
||||
failedAddons.add(local);
|
||||
}
|
||||
}).withCounter("addon.check_update.confirm"));
|
||||
}
|
||||
}
|
||||
|
||||
public List<LocalAddonFile> getFailedAddons() {
|
||||
return failedAddons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() {
|
||||
notifyPropertiesChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelyingOnDependents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!isDependentsSucceeded())
|
||||
throw getException();
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.javafx.Subscription;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -823,7 +824,7 @@ public final class FXUtils {
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
public static List<Subscription> bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
int itemCount = children.length;
|
||||
int childSelectedCount = 0;
|
||||
for (BooleanProperty child : children) {
|
||||
@@ -833,49 +834,46 @@ public final class FXUtils {
|
||||
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
|
||||
class Listener implements InvalidationListener {
|
||||
class AllListener {
|
||||
private int childSelectedCount;
|
||||
private boolean updating = false;
|
||||
private final List<Subscription> subscriptions = new ArrayList<>(children.length + 1);
|
||||
|
||||
public Listener(int childSelectedCount) {
|
||||
public AllListener(int childSelectedCount) {
|
||||
this.childSelectedCount = childSelectedCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (updating)
|
||||
return;
|
||||
|
||||
updating = true;
|
||||
try {
|
||||
boolean value = ((BooleanProperty) observable).get();
|
||||
|
||||
if (observable == allEnabled) {
|
||||
{
|
||||
subscriptions.add(Subscription.subscribe(allEnabled, () -> {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
try {
|
||||
boolean value = allEnabled.get();
|
||||
for (BooleanProperty child : children) {
|
||||
child.setValue(value);
|
||||
child.set(value);
|
||||
}
|
||||
childSelectedCount = value ? itemCount : 0;
|
||||
} else {
|
||||
if (value)
|
||||
childSelectedCount++;
|
||||
else
|
||||
childSelectedCount--;
|
||||
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}));
|
||||
for (var child : children) {
|
||||
subscriptions.add(Subscription.subscribe(child, () -> {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
try {
|
||||
if (child.get()) childSelectedCount++;
|
||||
else childSelectedCount--;
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InvalidationListener listener = new Listener(childSelectedCount);
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(listener);
|
||||
allEnabled.addListener(listener);
|
||||
for (BooleanProperty child : children) {
|
||||
child.addListener(weakListener);
|
||||
}
|
||||
return new AllListener(childSelectedCount).subscriptions;
|
||||
}
|
||||
|
||||
public static void setIcon(Stage stage) {
|
||||
|
||||
@@ -31,30 +31,31 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdate;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateTask;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdates;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CSVTable;
|
||||
import org.jackhuang.hmcl.util.javafx.Subscription;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -62,15 +63,17 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane implements DecoratorPage {
|
||||
public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("addon.check_update")));
|
||||
|
||||
private final LocalAddonManager<F> localAddonManager;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final Path localAddonDirectory;
|
||||
private final ObservableList<AddonUpdateObject> objects;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public AddonUpdatesPage(LocalAddonManager<F> localAddonManager, List<LocalAddonFile.AddonUpdate> updates) {
|
||||
this.localAddonManager = localAddonManager;
|
||||
public AddonUpdatesPage(DownloadProvider downloadProvider, Path localAddonDirectory, AddonUpdates updates) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.localAddonDirectory = localAddonDirectory;
|
||||
|
||||
getStyleClass().add("gray-background");
|
||||
|
||||
@@ -96,6 +99,9 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
targetVersionColumn.setPrefWidth(180);
|
||||
setupCellValueFactory(targetVersionColumn, AddonUpdateObject::targetVersionProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> targetVersionTypeColumn = new TableColumn<>(i18n("addon.version_type"));
|
||||
setupCellValueFactory(targetVersionTypeColumn, AddonUpdateObject::targetVersionTypeProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> sourceColumn = new TableColumn<>(i18n("addon.check_update.source"));
|
||||
setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty);
|
||||
|
||||
@@ -115,12 +121,26 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("button.view")));
|
||||
}
|
||||
|
||||
objects = FXCollections.observableList(updates.stream().map(AddonUpdateObject::new).collect(Collectors.toList()));
|
||||
FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new));
|
||||
var common = new Lazy<>(() -> updates.commonUpdates().stream().map(AddonUpdateObject::new).toList());
|
||||
var release = new Lazy<>(() -> updates.releaseUpdates().stream().map(AddonUpdateObject::new).toList());
|
||||
|
||||
objects = FXCollections.observableArrayList();
|
||||
|
||||
BooleanProperty showPreview = new SimpleBooleanProperty();
|
||||
{
|
||||
List<Subscription> subscriptions = new ArrayList<>();
|
||||
FXUtils.onChangeAndOperate(showPreview, preview -> {
|
||||
if (preview) objects.setAll(common.get());
|
||||
else objects.setAll(release.get());
|
||||
subscriptions.forEach(Subscription::unsubscribe);
|
||||
subscriptions.clear();
|
||||
subscriptions.addAll(FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(AddonUpdateObject::enabledProperty).toArray(BooleanProperty[]::new)));
|
||||
});
|
||||
}
|
||||
|
||||
TableView<AddonUpdateObject> table = new TableView<>(objects);
|
||||
table.setEditable(true);
|
||||
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn, changelogColumn);
|
||||
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, targetVersionTypeColumn, sourceColumn, changelogColumn);
|
||||
setMargin(table, new Insets(10, 10, 5, 10));
|
||||
|
||||
setCenter(table);
|
||||
@@ -129,6 +149,9 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
actions.setPadding(new Insets(8));
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXCheckBox showPreviewCheck = new JFXCheckBox(i18n("addon.check_update.preview"));
|
||||
showPreviewCheck.selectedProperty().bindBidirectional(showPreview);
|
||||
|
||||
JFXButton exportListButton = FXUtils.newRaisedButton(i18n("button.export"));
|
||||
exportListButton.setOnAction(e -> exportList());
|
||||
|
||||
@@ -140,7 +163,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
onEscPressed(table, cancelButton::fire);
|
||||
|
||||
actions.getChildren().setAll(exportListButton, nextButton, cancelButton);
|
||||
actions.getChildren().setAll(showPreviewCheck, exportListButton, nextButton, cancelButton);
|
||||
setBottom(actions);
|
||||
}
|
||||
|
||||
@@ -150,7 +173,8 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
|
||||
private void updateFiles() {
|
||||
AddonUpdateTask task = new AddonUpdateTask(
|
||||
localAddonManager.getDirectory(),
|
||||
downloadProvider,
|
||||
localAddonDirectory,
|
||||
objects.stream()
|
||||
.filter(AddonUpdateObject::isEnabled)
|
||||
.map(AddonUpdateObject::getData)
|
||||
@@ -210,31 +234,30 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}
|
||||
|
||||
private static final class AddonUpdateObject {
|
||||
final LocalAddonFile.AddonUpdate data;
|
||||
final AddonUpdate data;
|
||||
final BooleanProperty enabled = new SimpleBooleanProperty();
|
||||
final StringProperty fileName = new SimpleStringProperty();
|
||||
final StringProperty currentVersion = new SimpleStringProperty();
|
||||
final StringProperty targetVersion = new SimpleStringProperty();
|
||||
final StringProperty targetVersionType = new SimpleStringProperty();
|
||||
final StringProperty source = new SimpleStringProperty();
|
||||
String changelog = null;
|
||||
|
||||
public AddonUpdateObject(LocalAddonFile.AddonUpdate data) {
|
||||
public AddonUpdateObject(AddonUpdate data) {
|
||||
this.data = data;
|
||||
|
||||
enabled.set(!data.localAddonFile().isDisabled());
|
||||
fileName.set(data.localAddonFile().getFileName());
|
||||
currentVersion.set(data.currentVersion().version());
|
||||
targetVersion.set(data.targetVersion().version());
|
||||
switch (data.currentVersion().self().getSource()) {
|
||||
case CURSEFORGE:
|
||||
source.set(i18n("addon.curseforge"));
|
||||
break;
|
||||
case MODRINTH:
|
||||
source.set(i18n("addon.modrinth"));
|
||||
}
|
||||
targetVersionType.set(data.targetVersion().versionType().name());
|
||||
source.set(switch (data.currentVersion().source()) {
|
||||
case CURSEFORGE -> i18n("addon.curseforge");
|
||||
case MODRINTH -> i18n("addon.modrinth");
|
||||
});
|
||||
}
|
||||
|
||||
public LocalAddonFile.AddonUpdate getData() {
|
||||
public AddonUpdate getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -246,57 +269,26 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled.set(enabled);
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName.get();
|
||||
}
|
||||
|
||||
public StringProperty fileNameProperty() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName.set(fileName);
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion.get();
|
||||
}
|
||||
|
||||
public StringProperty currentVersionProperty() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
public void setCurrentVersion(String currentVersion) {
|
||||
this.currentVersion.set(currentVersion);
|
||||
}
|
||||
|
||||
public String getTargetVersion() {
|
||||
return targetVersion.get();
|
||||
}
|
||||
|
||||
public StringProperty targetVersionProperty() {
|
||||
return targetVersion;
|
||||
}
|
||||
|
||||
public void setTargetVersion(String targetVersion) {
|
||||
this.targetVersion.set(targetVersion);
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source.get();
|
||||
public StringProperty targetVersionTypeProperty() {
|
||||
return targetVersionType;
|
||||
}
|
||||
|
||||
public StringProperty sourceProperty() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source.set(source);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AddonChangelog extends JFXDialogLayout {
|
||||
@@ -342,13 +334,10 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
|
||||
private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) {
|
||||
spinnerPane.setLoading(true);
|
||||
RemoteAddonRepository repo = object.data.source().getRepository();
|
||||
RemoteAddonRepository repo = object.data.targetVersion().source().getRepository();
|
||||
Task.supplyAsync(() -> {
|
||||
if (object.changelog != null) {
|
||||
return object.changelog;
|
||||
}
|
||||
if (object.changelog != null) return object.changelog;
|
||||
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())
|
||||
@@ -356,7 +345,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
object.changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty");
|
||||
scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo == null ? "" : repo.getBaseUrl()));
|
||||
scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo.getBaseUrl()));
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
spinnerPane.setFailedReason(null);
|
||||
} else {
|
||||
@@ -367,10 +356,9 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}
|
||||
|
||||
private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) {
|
||||
Task.supplyAsync(() -> {
|
||||
RemoteAddonRepository repo = object.data.source().getRepository();
|
||||
return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion());
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Task.supplyAsync(() ->
|
||||
object.data.targetVersion().source().getRepository().getVersionPageUrl(object.data.targetVersion())
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null && StringUtils.isNotBlank(result)) {
|
||||
button.setExternalLink(result);
|
||||
button.setDisable(false);
|
||||
@@ -380,84 +368,4 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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();
|
||||
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;
|
||||
if (isDisabled)
|
||||
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);
|
||||
|
||||
var task = new FileDownloadTask(
|
||||
remote.file().url(),
|
||||
addonDirectory.resolve(fileName)
|
||||
);
|
||||
|
||||
task.setName(remote.name());
|
||||
return task;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
// restore state if failed
|
||||
local.setOld(false);
|
||||
if (isDisabled)
|
||||
local.markDisabled();
|
||||
failedAddons.add(local);
|
||||
} else if (!local.keepOldFiles()) {
|
||||
try {
|
||||
local.delete();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete outdated addon: " + local.getFile(), e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.withCounter("addon.check_update.confirm"));
|
||||
}
|
||||
}
|
||||
|
||||
public List<LocalAddonFile> getFailedAddons() {
|
||||
return failedAddons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() {
|
||||
notifyPropertiesChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelyingOnDependents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!isDependentsSucceeded())
|
||||
throw getException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.update.AddonCheckUpdatesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
@@ -251,19 +252,22 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
GameVersionNumber version = gameInstance.getVersion();
|
||||
return version != GameVersionNumber.unknown()
|
||||
? new AddonCheckUpdatesTask<>(
|
||||
DownloadProviders.getDownloadProvider(), version.toString(), mods)
|
||||
: null;
|
||||
if (version != GameVersionNumber.unknown()) {
|
||||
return new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), version.toString(), mods);
|
||||
} else {
|
||||
LOG.warning("Failed to check for updates, due to unable to get instance game version");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception instanceof CancellationException) return;
|
||||
if (exception != null || result == null) {
|
||||
Controllers.dialog(i18n("addon.check_update.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
} else if (result.isEmpty()) {
|
||||
if (exception != null) LOG.warning("Failed to check for updates", exception);
|
||||
} else if (result.commonUpdates().isEmpty()) {
|
||||
Controllers.dialog(i18n("addon.check_update.empty"));
|
||||
} else {
|
||||
Controllers.navigateForward(new AddonUpdatesPage<>(modManager, result));
|
||||
Controllers.navigateForward(new AddonUpdatesPage(DownloadProviders.getDownloadProvider(), modManager.getDirectory(), result));
|
||||
}
|
||||
})
|
||||
.withStagesHints("update.checking"),
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager;
|
||||
import org.jackhuang.hmcl.addon.update.AddonCheckUpdatesTask;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
@@ -261,18 +262,20 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
GameVersionNumber version = gameInstance.getVersion();
|
||||
return version != GameVersionNumber.unknown()
|
||||
? new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(),
|
||||
version.toString(), resourcePacks)
|
||||
: null;
|
||||
if (version != GameVersionNumber.unknown()) {
|
||||
return new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), version.toString(), resourcePacks);
|
||||
} else {
|
||||
LOG.warning("Failed to check for updates, due to unable to get instance game version");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null || result == null) {
|
||||
Controllers.dialog(i18n("addon.check_update.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
} else if (result.isEmpty()) {
|
||||
} else if (result.commonUpdates().isEmpty()) {
|
||||
Controllers.dialog(i18n("addon.check_update.empty"));
|
||||
} else {
|
||||
Controllers.navigateForward(new AddonUpdatesPage<>(resourcePackManager, result));
|
||||
Controllers.navigateForward(new AddonUpdatesPage(DownloadProviders.getDownloadProvider(), resourcePackManager.getDirectory(), result));
|
||||
}
|
||||
})
|
||||
.withStagesHints("update.checking"),
|
||||
|
||||
@@ -65,8 +65,8 @@ public class DownloadSettingsPage extends StackPane {
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
{
|
||||
var downloadSource = new ComponentList();
|
||||
downloadSource.getStyleClass().add("card-non-transparent");
|
||||
var gameContent = new ComponentList();
|
||||
gameContent.getStyleClass().add("card-non-transparent");
|
||||
{
|
||||
Function<DownloadSource, String> converter = source -> switch (source) {
|
||||
case DEFAULT -> i18n("settings.launcher.download_source.auto");
|
||||
@@ -102,10 +102,10 @@ public class DownloadSettingsPage extends StackPane {
|
||||
defaultAddonSourcePane.setItems("modrinth", "curseforge");
|
||||
defaultAddonSourcePane.valueProperty().bindBidirectional(settings().defaultAddonSourceProperty());
|
||||
|
||||
downloadSource.getContent().setAll(versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
|
||||
gameContent.getContent().setAll(versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
|
||||
}
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.game_content")), gameContent);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -175,6 +175,7 @@ addon.check_update.empty=All files are up-to-date
|
||||
addon.check_update.failed_check=Failed to check for updates.
|
||||
addon.check_update.failed_download=Failed to download some files.
|
||||
addon.check_update.file=File
|
||||
addon.check_update.preview=Show preview versions
|
||||
addon.check_update.source=Source
|
||||
addon.check_update.target_version=Target Version
|
||||
addon.curseforge=CurseForge
|
||||
@@ -192,6 +193,7 @@ addon.sort.last_updated=Last Updated
|
||||
addon.sort.popularity=Popularity
|
||||
addon.sort.relevancy=Relevancy
|
||||
addon.sort.total_downloads=Total Downloads
|
||||
addon.version_type=Type
|
||||
|
||||
archive.author=Author(s)
|
||||
archive.date=Publish Date
|
||||
@@ -1565,6 +1567,7 @@ settings.launcher.font.anti_aliasing.auto=Auto
|
||||
settings.launcher.font.anti_aliasing.gray=Grayscale
|
||||
settings.launcher.font.anti_aliasing.lcd=Sub-pixel
|
||||
settings.launcher.fonts=Fonts
|
||||
settings.launcher.game_content=Game Content
|
||||
settings.launcher.general=General
|
||||
settings.launcher.language=Language
|
||||
settings.launcher.launcher_log.export=Export Launcher Logs
|
||||
|
||||
@@ -176,6 +176,7 @@ addon.check_update.empty=جميع المودات محدّثة
|
||||
addon.check_update.failed_check=فشل التحقق من التحديثات.
|
||||
addon.check_update.failed_download=فشل تنزيل بعض الملفات.
|
||||
addon.check_update.file=الملف
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=المصدر
|
||||
addon.check_update.target_version=الإصدار المستهدف
|
||||
addon.curseforge=CurseForge
|
||||
@@ -193,6 +194,7 @@ addon.sort.last_updated=آخر تحديث
|
||||
addon.sort.popularity=الشعبية
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=إجمالي التنزيلات
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=المؤلف/المؤلفون
|
||||
archive.date=تاريخ النشر
|
||||
@@ -1562,6 +1564,7 @@ settings.launcher.font.anti_aliasing.auto=تلقائي
|
||||
settings.launcher.font.anti_aliasing.gray=تدرج رمادي
|
||||
settings.launcher.font.anti_aliasing.lcd=بكسل فرعي
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=عام
|
||||
settings.launcher.language=اللغة
|
||||
settings.launcher.launcher_log.export=تصدير سجلات المشغّل
|
||||
|
||||
@@ -174,6 +174,7 @@ addon.check_update.empty=Alle Dateien sind auf dem neuesten Stand
|
||||
addon.check_update.failed_check=Fehler beim Suchen nach Aktualisierungen.
|
||||
addon.check_update.failed_download=Fehler beim Herunterladen einiger Dateien.
|
||||
addon.check_update.file=Datei
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Quelle
|
||||
addon.check_update.target_version=Zielversion
|
||||
addon.curseforge=CurseForge
|
||||
@@ -191,6 +192,7 @@ addon.sort.last_updated=Zuletzt aktualisiert
|
||||
addon.sort.popularity=Beliebtheit
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Downloads insgesamt
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Autor(en)
|
||||
archive.date=Veröffentlichungsdatum
|
||||
@@ -1563,6 +1565,7 @@ settings.launcher.font.anti_aliasing.auto=Auto
|
||||
settings.launcher.font.anti_aliasing.gray=Graustufen
|
||||
settings.launcher.font.anti_aliasing.lcd=Sub-Pixel
|
||||
settings.launcher.fonts=Schriftarten
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=Allgemein
|
||||
settings.launcher.language=Sprache
|
||||
settings.launcher.launcher_log.export=Launcher-Protokolle exportieren
|
||||
|
||||
@@ -174,6 +174,7 @@ addon.check_update.empty=Todos los mods están actualizados
|
||||
addon.check_update.failed_check=No se ha podido comprobar si hay actualizaciones.
|
||||
addon.check_update.failed_download=No se han podido descargar algunos de los archivos.
|
||||
addon.check_update.file=Archivo
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Fuente
|
||||
addon.check_update.target_version=Versión de destino
|
||||
addon.curseforge=CurseForge
|
||||
@@ -191,6 +192,7 @@ addon.sort.last_updated=Última actualización
|
||||
addon.sort.popularity=Popularidad
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Descargas totales
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Autor(es)
|
||||
archive.date=Fecha de publicación
|
||||
@@ -1554,6 +1556,7 @@ settings.launcher.font.anti_aliasing.auto=Automático
|
||||
settings.launcher.font.anti_aliasing.gray=Escala de grises
|
||||
settings.launcher.font.anti_aliasing.lcd=Subpíxel
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=General
|
||||
settings.launcher.language=Idioma
|
||||
settings.launcher.launcher_log.export=Exportar registros del launcher
|
||||
|
||||
@@ -163,6 +163,7 @@ addon.check_update.current_version=Current
|
||||
addon.check_update.failed_check=更新のチェックに失敗しました
|
||||
addon.check_update.failed_download=一部のファイルのダウンロードに失敗しました
|
||||
addon.check_update.file=ファイル
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Source
|
||||
addon.check_update.target_version=Target
|
||||
addon.curseforge=CurseForge
|
||||
@@ -180,6 +181,7 @@ addon.sort.last_updated=最終更新
|
||||
addon.sort.popularity=人気
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=合計ダウンロード数
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=作成者
|
||||
archive.date=公開日
|
||||
@@ -1358,6 +1360,7 @@ settings.launcher.font=フォント
|
||||
# settings.launcher.font.anti_aliasing.gray=
|
||||
# settings.launcher.font.anti_aliasing.lcd=
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=全般的
|
||||
settings.launcher.language=言語
|
||||
settings.launcher.launcher_log.export=ランチャーログのエクスポート
|
||||
|
||||
@@ -174,6 +174,7 @@ addon.check_update.empty=無改囊可迭更
|
||||
addon.check_update.failed_check=檢囊迭更未成
|
||||
addon.check_update.failed_download=有引案未成
|
||||
addon.check_update.file=案
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=源
|
||||
addon.check_update.target_version=將至之版
|
||||
addon.curseforge=CurseForge
|
||||
@@ -191,6 +192,7 @@ addon.sort.last_updated=近日迭更
|
||||
addon.sort.popularity=幾熱
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=引數
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=作者
|
||||
archive.date=廣佈之期
|
||||
@@ -1370,6 +1372,7 @@ settings.launcher.font.anti_aliasing.auto=自調
|
||||
settings.launcher.font.anti_aliasing.gray=灰階
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=貫用
|
||||
settings.launcher.language=文
|
||||
settings.launcher.launcher_log.export=錄出啟者之誌
|
||||
|
||||
@@ -170,6 +170,7 @@ addon.check_update.empty=Все файлы актуальны
|
||||
addon.check_update.failed_check=Не удалось проверить обновления.
|
||||
addon.check_update.failed_download=Не удалось скачать некоторые файлы.
|
||||
addon.check_update.file=Файл
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Источник
|
||||
addon.check_update.target_version=Целевая версия
|
||||
addon.curseforge=CurseForge
|
||||
@@ -187,6 +188,7 @@ addon.sort.last_updated=Обновлён
|
||||
addon.sort.popularity=Популярность
|
||||
addon.sort.relevancy=Релевантность
|
||||
addon.sort.total_downloads=Скачиваний
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Автор(ы)
|
||||
archive.date=Дата публикации
|
||||
@@ -1494,6 +1496,7 @@ settings.launcher.font.anti_aliasing.auto=Автоматическое
|
||||
settings.launcher.font.anti_aliasing.gray=Оттенки серого
|
||||
settings.launcher.font.anti_aliasing.lcd=Субпиксельное
|
||||
settings.launcher.fonts=Шрифты
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=Общие
|
||||
settings.launcher.language=Язык
|
||||
settings.launcher.launcher_log.export=Экспорт логов лаунчера
|
||||
|
||||
@@ -176,6 +176,7 @@ addon.check_update.empty=Усі моди оновлені
|
||||
addon.check_update.failed_check=Не вдалося перевірити оновлення.
|
||||
addon.check_update.failed_download=Не вдалося завантажити деякі файли.
|
||||
addon.check_update.file=Файл
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Джерело
|
||||
addon.check_update.target_version=Цільова версія
|
||||
addon.curseforge=CurseForge
|
||||
@@ -193,6 +194,7 @@ addon.sort.last_updated=Останнє оновлення
|
||||
addon.sort.popularity=Популярність
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Всього завантажень
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Автор(и)
|
||||
archive.date=Дата публікації
|
||||
@@ -1517,6 +1519,7 @@ settings.launcher.font.anti_aliasing.auto=Авто
|
||||
settings.launcher.font.anti_aliasing.gray=Відтінки сірого
|
||||
settings.launcher.font.anti_aliasing.lcd=Субпіксельне
|
||||
settings.launcher.fonts=Шрифти
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=Загальні
|
||||
settings.launcher.language=Мова
|
||||
settings.launcher.launcher_log.export=Експортувати журнали лаунчера
|
||||
|
||||
@@ -175,6 +175,7 @@ addon.check_update.empty=没有需要更新的文件
|
||||
addon.check_update.failed_check=检查更新失败
|
||||
addon.check_update.failed_download=部分文件下载失败
|
||||
addon.check_update.file=文件
|
||||
addon.check_update.preview=显示测试版本
|
||||
addon.check_update.source=来源
|
||||
addon.check_update.target_version=目标版本
|
||||
addon.curseforge=CurseForge
|
||||
@@ -192,6 +193,7 @@ addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=热度
|
||||
addon.sort.relevancy=相关度
|
||||
addon.sort.total_downloads=下载量
|
||||
addon.version_type=版本类型
|
||||
|
||||
archive.author=作者
|
||||
archive.date=发布日期
|
||||
@@ -1377,6 +1379,7 @@ settings.launcher.font.anti_aliasing.auto=自动
|
||||
settings.launcher.font.anti_aliasing.gray=灰度
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
settings.launcher.fonts=字体
|
||||
settings.launcher.game_content=游戏内容
|
||||
settings.launcher.general=通用
|
||||
settings.launcher.language=语言
|
||||
settings.launcher.launcher_log.export=导出启动器日志
|
||||
|
||||
@@ -173,6 +173,7 @@ addon.check_update.empty=沒有需要更新的檔案
|
||||
addon.check_update.failed_check=檢查更新失敗
|
||||
addon.check_update.failed_download=部分檔案下載失敗
|
||||
addon.check_update.file=檔案
|
||||
addon.check_update.preview=顯示 Alpha/Beta 版本
|
||||
addon.check_update.source=來源
|
||||
addon.check_update.target_version=目標版本
|
||||
addon.curseforge=CurseForge
|
||||
@@ -190,6 +191,7 @@ addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=熱度
|
||||
addon.sort.relevancy=相關度
|
||||
addon.sort.total_downloads=下載量
|
||||
addon.version_type=版本類型
|
||||
|
||||
archive.author=作者
|
||||
archive.date=發布日期
|
||||
@@ -1371,6 +1373,7 @@ settings.launcher.font.anti_aliasing.auto=自動
|
||||
settings.launcher.font.anti_aliasing.gray=灰階
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
settings.launcher.fonts=字體
|
||||
settings.launcher.game_content=遊戲內容
|
||||
settings.launcher.general=一般
|
||||
settings.launcher.language=語言
|
||||
settings.launcher.launcher_log.export=匯出啟動器日誌
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon;
|
||||
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdate;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateConditions;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -26,8 +29,10 @@ import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// Sub-classes should implement `Comparable`
|
||||
@@ -53,20 +58,43 @@ public abstract class LocalAddonFile {
|
||||
|
||||
public abstract void delete() throws IOException;
|
||||
|
||||
/// @return the update target version restrictions for this addon file, or null if not upgradable.
|
||||
@Nullable
|
||||
public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
protected AddonUpdateConditions getTargetVersionRestrictions() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
public record AddonUpdate(
|
||||
RemoteAddon.Source source,
|
||||
RemoteAddon.Type repoType,
|
||||
LocalAddonFile localAddonFile,
|
||||
RemoteAddon.Version currentVersion,
|
||||
RemoteAddon.Version targetVersion,
|
||||
boolean useRemoteFileName
|
||||
) {
|
||||
/// @return A pair of addon update info, the first for all versions and the second for release versions only (might be null).
|
||||
@Nullable
|
||||
public Pair<AddonUpdate, @Nullable AddonUpdate> checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
var restrictions = getTargetVersionRestrictions();
|
||||
if (restrictions == null) return null;
|
||||
|
||||
RemoteAddonRepository repo = source.getRepository();
|
||||
Optional<RemoteAddon.Version> currentVersion = repo.getRemoteVersionByLocalFile(getFile());
|
||||
if (currentVersion.isEmpty()) return null;
|
||||
var current = currentVersion.orElseThrow();
|
||||
|
||||
var stream = repo.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId())
|
||||
.filter(version -> version.gameVersions().contains(gameVersion));
|
||||
if (current.gameVersions().contains(gameVersion)) // Otherwise it means we are upgrading from another game version
|
||||
stream = stream.filter(version -> version.datePublished().isAfter(current.datePublished()));
|
||||
for (var p : restrictions.restrictions())
|
||||
stream = stream.filter(p);
|
||||
|
||||
List<RemoteAddon.Version> remoteVersions = stream.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()).toList();
|
||||
if (remoteVersions.isEmpty()) return null;
|
||||
var release = remoteVersions.stream()
|
||||
.filter(v -> v.versionType() == RemoteAddon.VersionType.Release)
|
||||
.findFirst().orElse(null);
|
||||
|
||||
return Pair.pair(
|
||||
new AddonUpdate(this, current, remoteVersions.get(0)), // All channels
|
||||
release != null ? new AddonUpdate(this, current, release) : null // Release channel
|
||||
);
|
||||
}
|
||||
|
||||
public void onUpdated(String newFileNameWithExt) {
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -143,6 +144,7 @@ public record RemoteAddon(String slug, String author, String title, String descr
|
||||
|
||||
private final RemoteAddonRepository repo;
|
||||
|
||||
@NotNull
|
||||
public RemoteAddonRepository getRepository() {
|
||||
return repo;
|
||||
}
|
||||
@@ -167,11 +169,7 @@ public record RemoteAddon(String slug, String author, String title, String descr
|
||||
Stream<Version> loadVersions(RemoteAddonRepository repo, DownloadProvider downloadProvider) throws IOException;
|
||||
}
|
||||
|
||||
public interface IVersion {
|
||||
Source getSource();
|
||||
}
|
||||
|
||||
public record Version(IVersion self, String versionId, String projectId, String name, String version,
|
||||
public record Version(RemoteAddon.Source source, String versionId, String projectId, String name, String version,
|
||||
Instant datePublished, VersionType versionType, File file, List<Dependency> dependencies,
|
||||
List<String> gameVersions, List<AddonLoader> loaders) {
|
||||
}
|
||||
|
||||
@@ -19,17 +19,17 @@ package org.jackhuang.hmcl.addon.mod;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateConditions;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -195,19 +195,8 @@ public final class LocalModFile extends LocalAddonFile implements Comparable<Loc
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
RemoteAddonRepository repository = source.getRepository();
|
||||
if (repository == null) return null;
|
||||
Optional<RemoteAddon.Version> currentVersion = repository.getRemoteVersionByLocalFile(file);
|
||||
if (currentVersion.isEmpty()) return null;
|
||||
List<RemoteAddon.Version> remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId())
|
||||
.filter(version -> version.gameVersions().contains(gameVersion))
|
||||
.filter(version -> version.loaders().stream().anyMatch(it -> it.type() == getModLoaderType()))
|
||||
.filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0)
|
||||
.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);
|
||||
protected AddonUpdateConditions getTargetVersionRestrictions() {
|
||||
return new AddonUpdateConditions(v -> v.loaders().stream().map(AddonLoader::type).collect(Collectors.toSet()).contains(getModLoaderType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-7
@@ -554,7 +554,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
int releaseType, int fileStatus, List<LatestFileHash> hashes, Instant fileDate,
|
||||
int fileLength, int downloadCount, String downloadUrl, List<String> gameVersions,
|
||||
List<Dependency> dependencies, int alternateFileId, boolean isServerPack,
|
||||
long fileFingerprint) implements RemoteAddon.IVersion {
|
||||
long fileFingerprint) {
|
||||
|
||||
@Override
|
||||
public String downloadUrl() {
|
||||
@@ -566,11 +566,6 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon.Source getSource() {
|
||||
return RemoteAddon.Source.CURSEFORGE;
|
||||
}
|
||||
|
||||
public RemoteAddon.Version toVersion() {
|
||||
RemoteAddon.VersionType versionType = switch (releaseType()) {
|
||||
case 1 -> RemoteAddon.VersionType.Release;
|
||||
@@ -596,7 +591,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
}
|
||||
|
||||
return new RemoteAddon.Version(
|
||||
this,
|
||||
RemoteAddon.Source.CURSEFORGE,
|
||||
Integer.toString(id()),
|
||||
Integer.toString(modId()),
|
||||
displayName(),
|
||||
|
||||
+8
-17
@@ -443,7 +443,7 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
@SerializedName("author_id") String authorId,
|
||||
@SerializedName("date_published") Instant datePublished, int downloads,
|
||||
@SerializedName("changelog_url") String changelogUrl,
|
||||
List<ProjectVersionFile> files) implements RemoteAddon.IVersion {
|
||||
List<ProjectVersionFile> files) {
|
||||
private static final Map<String, RemoteAddon.@Nullable DependencyType> DEPENDENCY_TYPE = mapOf(
|
||||
pair("required", RemoteAddon.DependencyType.REQUIRED),
|
||||
pair("optional", RemoteAddon.DependencyType.OPTIONAL),
|
||||
@@ -451,29 +451,20 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
pair("incompatible", RemoteAddon.DependencyType.INCOMPATIBLE)
|
||||
);
|
||||
|
||||
@Override
|
||||
public RemoteAddon.Source getSource() {
|
||||
return RemoteAddon.Source.MODRINTH;
|
||||
}
|
||||
|
||||
public Optional<RemoteAddon.Version> toVersion() {
|
||||
RemoteAddon.VersionType type;
|
||||
if ("release".equals(versionType)) {
|
||||
type = RemoteAddon.VersionType.Release;
|
||||
} else if ("beta".equals(versionType)) {
|
||||
type = RemoteAddon.VersionType.Beta;
|
||||
} else if ("alpha".equals(versionType)) {
|
||||
type = RemoteAddon.VersionType.Alpha;
|
||||
} else {
|
||||
type = RemoteAddon.VersionType.Release;
|
||||
}
|
||||
RemoteAddon.VersionType type = switch (versionType) {
|
||||
case "release" -> RemoteAddon.VersionType.Release;
|
||||
case "beta" -> RemoteAddon.VersionType.Beta;
|
||||
case "alpha" -> RemoteAddon.VersionType.Alpha;
|
||||
default -> RemoteAddon.VersionType.Release;
|
||||
};
|
||||
|
||||
if (files.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new RemoteAddon.Version(
|
||||
this,
|
||||
RemoteAddon.Source.MODRINTH,
|
||||
id,
|
||||
projectId,
|
||||
name,
|
||||
|
||||
@@ -72,5 +72,4 @@ final class ResourcePackFolder extends ResourcePackFile {
|
||||
public void delete() throws IOException {
|
||||
FileUtils.deleteDirectory(file);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -509,6 +509,39 @@ public final class ResourcePackManager extends LocalAddonManager<ResourcePackFil
|
||||
}
|
||||
}
|
||||
|
||||
public boolean rename(String oldName, String newName) {
|
||||
if (newName.equals(oldName)) return false;
|
||||
|
||||
String oldNameNewFormat = "file/" + oldName;
|
||||
String newNameNewFormat = "file/" + newName;
|
||||
boolean modified = false;
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
Map<String, String> options = loadOptions();
|
||||
List<String> optPacks = new ArrayList<>(deserializePackList(options.get("resourcePacks")));
|
||||
List<String> optIncompatiblePacks = new ArrayList<>(deserializePackList(options.get("incompatibleResourcePacks")));
|
||||
for (var l : List.of(optPacks, optIncompatiblePacks)) {
|
||||
var it = l.listIterator();
|
||||
while (it.hasNext()) {
|
||||
var next = it.next();
|
||||
if (oldName.equals(next) || oldNameNewFormat.equals(next)) {
|
||||
it.set(supportsNewOptionsFormat ? newNameNewFormat : newName);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
options.put("resourcePacks", serializePackList(optPacks));
|
||||
options.put("incompatibleResourcePacks", serializePackList(optIncompatiblePacks));
|
||||
saveOptions(options);
|
||||
}
|
||||
return modified;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Pair<ResourcePackFile, Boolean>> arePacksEnabled(Stream<ResourcePackFile> resourcePacks) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
+10
-18
@@ -18,10 +18,8 @@
|
||||
package org.jackhuang.hmcl.addon.resourcepack;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.meta.PackMcMeta;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateConditions;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -29,13 +27,11 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
final class ResourcePackZipFile extends ResourcePackFile {
|
||||
|
||||
private final PackMcMeta.PackInfo info;
|
||||
|
||||
public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException {
|
||||
@@ -85,18 +81,14 @@ final class ResourcePackZipFile extends ResourcePackFile {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
RemoteAddonRepository repository = source.getRepository();
|
||||
if (repository == null) return null;
|
||||
Optional<RemoteAddon.Version> currentVersion = repository.getRemoteVersionByLocalFile(file);
|
||||
if (currentVersion.isEmpty()) return null;
|
||||
List<RemoteAddon.Version> remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId())
|
||||
.filter(version -> version.gameVersions().contains(gameVersion))
|
||||
.filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0)
|
||||
.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);
|
||||
protected AddonUpdateConditions getTargetVersionRestrictions() {
|
||||
return new AddonUpdateConditions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdated(String newFileNameWithExt) {
|
||||
super.onUpdated(newFileNameWithExt);
|
||||
manager.rename(getFileNameWithExtension(), newFileNameWithExt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
public record AddonUpdate(
|
||||
LocalAddonFile localAddonFile,
|
||||
RemoteAddon.Version currentVersion,
|
||||
RemoteAddon.Version targetVersion
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
///
|
||||
/// @param restrictions conditions that the remote version must satisfy to become a valid target
|
||||
@NotNullByDefault
|
||||
public record AddonUpdateConditions(@Unmodifiable List<Predicate<RemoteAddon.Version>> restrictions) {
|
||||
public AddonUpdateConditions {
|
||||
restrictions = List.copyOf(restrictions);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public AddonUpdateConditions(Predicate<RemoteAddon.Version>... restrictions) {
|
||||
this(List.of(restrictions));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@NotNullByDefault
|
||||
public record AddonUpdates(@Unmodifiable List<AddonUpdate> commonUpdates, @Unmodifiable List<AddonUpdate> releaseUpdates) {
|
||||
|
||||
public AddonUpdates {
|
||||
commonUpdates = List.copyOf(commonUpdates);
|
||||
releaseUpdates = List.copyOf(releaseUpdates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/// Alternate for JavaFX version below 21
|
||||
@FunctionalInterface
|
||||
public interface Subscription {
|
||||
|
||||
Subscription EMPTY = () -> {};
|
||||
|
||||
static Subscription subscribe(Observable observable, Runnable invalidationSubscriber) {
|
||||
Objects.requireNonNull(invalidationSubscriber, "invalidationSubscriber cannot be null");
|
||||
InvalidationListener listener = obs -> invalidationSubscriber.run();
|
||||
|
||||
observable.addListener(listener);
|
||||
|
||||
return () -> observable.removeListener(listener);
|
||||
}
|
||||
|
||||
static Subscription combine(Subscription... subscriptions) {
|
||||
List<Subscription> list = List.of(subscriptions);
|
||||
|
||||
return () -> list.forEach(Subscription::unsubscribe);
|
||||
}
|
||||
|
||||
void unsubscribe();
|
||||
|
||||
default Subscription and(Subscription other) {
|
||||
Objects.requireNonNull(other, "other cannot be null");
|
||||
|
||||
return () -> {
|
||||
unsubscribe();
|
||||
other.unsubscribe();
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user