Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2727: Optimize mod version download page #2808

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
78 changes: 38 additions & 40 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.*;
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
import org.jackhuang.hmcl.ui.versions.HMCLLocalizedDownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
Expand All @@ -69,7 +72,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<DownloadListPage> modTab = new TabHeader.Tab<>("modTab");
private final TabHeader.Tab<DownloadListPage> modpackTab = new TabHeader.Tab<>("modpackTab");
private final TabHeader.Tab<DownloadListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TabHeader.Tab<DownloadListPage> customizationTab = new TabHeader.Tab<>("customizationTab");
private final TabHeader.Tab<DownloadListPage> worldTab = new TabHeader.Tab<>("worldTab");
private final TransitionPane transitionPane = new TransitionPane();
private final DownloadNavigator versionPageNavigator = new DownloadNavigator();
Expand All @@ -80,17 +82,16 @@ public DownloadPage() {
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected)));
modpackTab.setNodeSupplier(loadVersionFor(() -> {
ModpackDownloadListPage page = new ModpackDownloadListPage(Versions::downloadModpackImpl, false);
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack(Versions::downloadModpackImpl, false);

JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
installLocalModpackButton.setOnAction(e -> Versions.importModpack());

page.getActions().add(installLocalModpackButton);
return page;
}));
modTab.setNodeSupplier(loadVersionFor(() -> new ModDownloadListPage((profile, version, file) -> download(profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> new ResourcePackDownloadListPage((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
customizationTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.CUSTOMIZATIONS)));
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file, shouldProvideRenameUI) -> download(profile, version, file, "mods", shouldProvideRenameUI), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file, shouldProvideRenameUI) -> download(profile, version, file, "resourcepacks", shouldProvideRenameUI), true)));
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);

Expand Down Expand Up @@ -129,12 +130,6 @@ public DownloadPage() {
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
item.setOnAction(e -> tab.select(resourcePackTab));
})
// .addNavigationDrawerItem(item -> {
// item.setTitle(i18n("download.curseforge.customization"));
// item.setLeftGraphic(wrap(SVG::script));
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
// item.setOnAction(e -> selectTabIfCurseForgeAvailable(customizationTab));
// })
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("world"));
item.setLeftGraphic(wrap(SVG.EARTH));
Expand Down Expand Up @@ -165,37 +160,43 @@ private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSuppl
};
}

private static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
private static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName, boolean shouldProvideRenameUI) {
if (version == null) version = profile.getSelectedVersion();

Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
Path runDirectory = (profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory()).toPath().resolve(subdirectoryName);

Controllers.prompt(i18n("archive.name"), (result, resolve, reject) -> {
if (!OperatingSystem.isNameValid(result)) {
reject.accept(i18n("install.new_game.malformed"));
return;
}
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);

Controllers.taskDialog(Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
String filename = file.getFile().getFilename();
if (shouldProvideRenameUI) {
Controllers.prompt(i18n("archive.name"), (result, resolve, reject) -> {
if (!OperatingSystem.isNameValid(result)) {
reject.accept(i18n("install.new_game.malformed"));
return;
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);

resolve.run();
}, file.getFile().getFilename());
download0(file, runDirectory.resolve(result));
resolve.run();
}, filename);
} else {
download0(file, runDirectory.resolve(filename));
}
}

private static void download0(RemoteMod.Version file, Path dest) {
Controllers.taskDialog(Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
}

private void loadVersions(Profile profile) {
Expand All @@ -212,9 +213,6 @@ private void loadVersions(Profile profile) {
if (resourcePackTab.isInitialized()) {
resourcePackTab.getNode().loadVersion(profile, null);
}
if (customizationTab.isInitialized()) {
customizationTab.getNode().loadVersion(profile, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
}
Expand Down
41 changes: 28 additions & 13 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -172,11 +173,11 @@ public void setFailed(boolean failed) {
this.failed.set(failed);
}

public void download(RemoteMod.Version file) {
public void download(RemoteMod.Version file, boolean shouldProvideRenameUI) {
if (this.callback == null) {
saveAs(file);
} else {
this.callback.download(version.getProfile(), version.getVersion(), file);
this.callback.download(version.getProfile(), version.getVersion(), file, shouldProvideRenameUI);
}
}

Expand Down Expand Up @@ -376,7 +377,6 @@ private static final class ModItem extends StackPane {

{
StackPane graphicPane = new StackPane();
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));

TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
Expand All @@ -385,11 +385,16 @@ private static final class ModItem extends StackPane {

switch (dataItem.getVersionType()) {
case Alpha:
content.getTags().add(i18n("version.game.snapshot"));
graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Beta:
content.getTags().add(i18n("version.game.snapshot"));
graphicPane.getChildren().setAll(SVG.BETA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Release:
content.getTags().add(i18n("version.game.release"));
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
}

Expand Down Expand Up @@ -452,29 +457,39 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) {

this.setBody(box);

JFXButton downloadButton = new JFXButton(i18n("download"));
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
boolean isModPack = selfPage.page.repository.getType() == RemoteModRepository.Type.MODPACK;
if (OperatingSystem.isNameValid(version.getFile().getFilename())) {
JFXButton downloadButton = new JFXButton(i18n("button.install"));
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.setOnAction(e -> {
if (isModPack) {
fireEvent(new DialogCloseEvent());
}
selfPage.download(version, false);
});
getActions().add(downloadButton);
}

JFXButton renameAndInstallButton = new JFXButton(i18n("button.rename_and_install"));
renameAndInstallButton.getStyleClass().add("dialog-accept");
renameAndInstallButton.setOnAction(e -> {
if (isModPack) {
fireEvent(new DialogCloseEvent());
}
selfPage.download(version);
selfPage.download(version, true);
});

JFXButton saveAsButton = new JFXButton(i18n("button.save_as"));
saveAsButton.getStyleClass().add("dialog-accept");
saveAsButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
fireEvent(new DialogCloseEvent());
}
selfPage.saveAs(version);
});

JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));

this.setActions(downloadButton, saveAsButton, cancelButton);
this.getActions().addAll(renameAndInstallButton, saveAsButton, cancelButton);

this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7));
this.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.7));
Expand Down Expand Up @@ -518,6 +533,6 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage,
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());

public interface DownloadCallback {
void download(Profile profile, @Nullable String version, RemoteMod.Version file);
void download(Profile profile, @Nullable String version, RemoteMod.Version file, boolean shouldProvideRenameUI);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,23 @@
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

public class ResourcePackDownloadListPage extends DownloadListPage {
public ResourcePackDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) {
public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MOD, CurseForgeRemoteModRepository.MODS, ModrinthRemoteModRepository.MODS);
}

public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MODPACK, CurseForgeRemoteModRepository.MODPACKS, ModrinthRemoteModRepository.MODPACKS);
}

public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.RESOURCE_PACK, CurseForgeRemoteModRepository.RESOURCE_PACKS, ModrinthRemoteModRepository.RESOURCE_PACKS);
}

private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteModRepository.Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
super(null, callback, versionSelection);

repository = new Repository();
repository = new Repository(type, curseForge, modrinth);

supportChinese.set(true);
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
Expand All @@ -42,14 +54,23 @@ public ResourcePackDownloadListPage(DownloadPage.DownloadCallback callback, bool
downloadSource.set("mods.modrinth");
}

private class Repository extends LocalizedRemoteModRepository {
public class Repository extends LocalizedRemoteModRepository {
private final RemoteModRepository.Type type;
private final CurseForgeRemoteModRepository curseForge;
private final ModrinthRemoteModRepository modrinth;

public Repository(Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
this.type = type;
this.curseForge = curseForge;
this.modrinth = modrinth;
}

@Override
protected RemoteModRepository getBackedRemoteModRepository() {
if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.RESOURCE_PACKS;
return modrinth;
} else {
return CurseForgeRemoteModRepository.RESOURCE_PACKS;
return curseForge;
}
}

Expand All @@ -64,19 +85,17 @@ protected SortType getBackedRemoteModRepositorySortOrder() {

@Override
public Type getType() {
return Type.MOD;
return type;
}
}

@Override
protected String getLocalizedCategory(String category) {
String key;
if ("mods.modrinth".equals(downloadSource.get())) {
key = "modrinth.category." + category;
} else {
key = "curse.category." + category;
if (category.isEmpty()) {
return "";
}

String key = ("mods.modrinth".equals(downloadSource.get()) ? "modrinth" : "curse") + ".category." + category;
try {
return I18n.getResourceBundle().getString(key);
} catch (MissingResourceException e) {
Expand Down