Feature: Display third-party download links with description.

This commit is contained in:
burningtnt 2025-09-20 13:49:04 +08:00
parent fb2c844b57
commit 8f5e2084ba
No known key found for this signature in database
GPG Key ID: 18A43F21F9ACE8C4
7 changed files with 73 additions and 35 deletions

View File

@ -24,6 +24,7 @@ import org.jackhuang.hmcl.terracotta.provider.GeneralProvider;
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OSVersion;
@ -45,7 +46,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -54,6 +54,9 @@ public final class TerracottaMetadata {
private TerracottaMetadata() {
}
public record Link(@SerializedName("desc") LocalizedText description, String link) {
}
private record Config(
@SerializedName("version_legacy") String legacy,
@SerializedName("version_recent") List<String> recent,
@ -61,7 +64,7 @@ public final class TerracottaMetadata {
@SerializedName("classifiers") Map<String, String> classifiers,
@SerializedName("downloads") List<String> downloads,
@SerializedName("links") List<String> links
@SerializedName("links") List<Link> links
) {
private TerracottaNative of(String classifier) {
List<URI> links = new ArrayList<>(this.downloads.size());
@ -87,7 +90,7 @@ public final class TerracottaMetadata {
public static final ITerracottaProvider PROVIDER;
public static final String PACKAGE_NAME;
private static final List<String> PACKAGE_LINKS;
private static final List<Link> PACKAGE_LINKS;
private static final Pattern LEGACY;
private static final List<String> RECENT;
@ -110,12 +113,14 @@ public final class TerracottaMetadata {
PACKAGE_NAME = context != null ? String.format("terracotta-%s-%s-%s-pkg.tar.gz", config.latest, context.system, context.arch) : null;
if (context != null) {
List<String> packageLinks = new ArrayList<>(config.links.size());
for (String link : config.links) {
packageLinks.add(link.replace("${version}", LATEST)
.replace("${system}", context.system)
.replace("${arch}", context.arch)
);
List<Link> packageLinks = new ArrayList<>(config.links.size());
for (Link link : config.links) {
packageLinks.add(new Link(
link.description,
link.link.replace("${version}", LATEST)
.replace("${system}", context.system)
.replace("${arch}", context.arch)
));
}
PACKAGE_LINKS = Collections.unmodifiableList(packageLinks);
@ -127,8 +132,8 @@ public final class TerracottaMetadata {
private record ProviderContext(ITerracottaProvider provider, String system, String arch) {
}
public static String getPackageLink() {
return PACKAGE_LINKS.get(ThreadLocalRandom.current().nextInt(0, PACKAGE_LINKS.size()));
public static List<Link> getPackageLinks() {
return PACKAGE_LINKS;
}
@Nullable

View File

@ -73,8 +73,7 @@ final class ComponentListCell extends StackPane {
@SuppressWarnings("unchecked")
private void updateLayout() {
if (content instanceof ComponentList) {
ComponentList list = (ComponentList) content;
if (content instanceof ComponentList list) {
content.getStyleClass().remove("options-list");
content.getStyleClass().add("options-sublist");
@ -130,7 +129,10 @@ final class ComponentListCell extends StackPane {
groupNode.getChildren().add(headerRippler);
VBox container = new VBox();
container.setPadding(new Insets(8, 16, 10, 16));
boolean hasPadding = !content.getProperties().containsKey("ComponentSubList.noPadding");
if (hasPadding) {
container.setPadding(new Insets(8, 16, 10, 16));
}
FXUtils.setLimitHeight(container, 0);
FXUtils.setOverflowHidden(container);
container.getChildren().setAll(content);
@ -149,7 +151,7 @@ final class ComponentListCell extends StackPane {
}
Platform.runLater(() -> {
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + 8 + 10) * (expanded ? 1 : -1);
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + (hasPadding ? 8 + 10 : 0)) * (expanded ? 1 : -1);
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(list.getWidth());
double contentHeight = expanded ? newAnimatedHeight : 0;

View File

@ -59,10 +59,12 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Files;
@ -182,14 +184,7 @@ public class TerracottaControllerPage extends StackPane {
}
});
LineButton local = LineButton.of();
local.setLeftImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
local.setTitle(i18n("terracotta.from_local.title"));
local.setSubtitle(i18n("terracotta.from_local.desc"));
local.setRightIcon(SVG.OPEN_IN_NEW);
FXUtils.onClicked(local, () -> FXUtils.openLink(TerracottaMetadata.getPackageLink()));
nodesProperty.setAll(body, download, local);
nodesProperty.setAll(body, download, getThirdPartyDownloadNodes());
} else if (state instanceof TerracottaState.Preparing) {
statusProperty.set(i18n("terracotta.status.preparing"));
progressProperty.bind(((TerracottaState.Preparing) state).progressProperty());
@ -432,13 +427,7 @@ public class TerracottaControllerPage extends StackPane {
});
if (fatal.getType() == TerracottaState.Fatal.Type.NETWORK) {
LineButton local = LineButton.of();
local.setLeftImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
local.setTitle(i18n("terracotta.from_local.title"));
local.setSubtitle(i18n("terracotta.from_local.desc"));
local.setRightIcon(SVG.OPEN_IN_NEW);
FXUtils.onClicked(local, () -> FXUtils.openLink(TerracottaMetadata.getPackageLink()));
nodesProperty.setAll(retry, local);
nodesProperty.setAll(retry, getThirdPartyDownloadNodes());
} else {
nodesProperty.setAll(retry);
}
@ -486,6 +475,35 @@ public class TerracottaControllerPage extends StackPane {
getChildren().setAll(scrollPane);
}
private ComponentList getThirdPartyDownloadNodes() {
ComponentSublist locals = new ComponentSublist();
locals.getProperties().put("ComponentSubList.noPadding", true);
LineButton header = LineButton.of(false);
header.setLeftImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
header.setTitle(i18n("terracotta.from_local.title"));
header.setSubtitle(i18n("terracotta.from_local.desc"));
locals.setHeaderLeft(header);
for (TerracottaMetadata.Link link : TerracottaMetadata.getPackageLinks()) {
HBox node = new HBox();
node.setAlignment(Pos.CENTER_LEFT);
node.setPadding(new Insets(10, 16, 10, 16));
Label description = new Label(link.description().getText(I18n.getLocale().getCandidateLocales()));
HBox placeholder = new HBox();
HBox.setHgrow(placeholder, Priority.ALWAYS);
Node icon = SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), 16);
node.getChildren().setAll(description, placeholder, icon);
RipplerContainer container = new RipplerContainer(node);
container.setOnMouseClicked(ev -> FXUtils.openLink(link.link()));
container.getProperties().put("ComponentList.noPadding", true);
locals.getContent().add(container);
}
return locals;
}
private static final class LineButton extends RipplerContainer {
private final WeakListenerHolder holder = new WeakListenerHolder();
@ -494,8 +512,14 @@ public class TerracottaControllerPage extends StackPane {
private final ObjectProperty<Node> right = new SimpleObjectProperty<>();
public static LineButton of() {
return of(true);
}
public static LineButton of(boolean padding) {
HBox container = new HBox();
container.setPadding(new Insets(10, 16, 10, 16));
if (padding) {
container.setPadding(new Insets(10, 16, 10, 16));
}
container.setAlignment(Pos.CENTER_LEFT);
container.setCursor(Cursor.HAND);
container.setSpacing(16);

View File

@ -1419,7 +1419,7 @@ terracotta.status=Lobby
terracotta.back=Exit
terracotta.network_warning=Terracotta | Multiplayer is based on P2P. The final experience depends greatly on your network.
terracotta.sudo_installing=HMCL must verify your password before installing Multiplayer Core
terracotta.from_local.title=Open third-party download link for Multiplayer Core
terracotta.from_local.title=Third-party download links for Multiplayer Core
terracotta.from_local.desc=In some regions, the default download channel for Multiplayer Core may be unstable.
terracotta.from_local.file_name_mismatch=You should download the Multiplayer Core package named %1$s instead of %2$s
terracotta.status.bootstrap=Gathering information

View File

@ -1207,7 +1207,7 @@ terracotta.status=聯機大廳
terracotta.back=退出
terracotta.network_warning=多人連線基於 p2p最終線上體驗和您的網路情況有較大關係。
terracotta.sudo_installing=HMCL 需要驗證您的密碼才能安裝線上核心
terracotta.from_local.title=開啟線上核心第三方下載鏈接
terracotta.from_local.title=線上核心第三方下載鏈接
terracotta.from_local.desc=在部分地區,線上核心預設下載管道可能不穩定
terracotta.from_local.file_name_mismatch=您應該下載名為 %1$s 的線上核心包,而非 %2$s
terracotta.status.bootstrap=正在收集信息

View File

@ -1218,7 +1218,7 @@ terracotta.status=联机大厅
terracotta.back=退出
terracotta.network_warning=多人联机基于 p2p最终联机体验和您的网络情况有较大关系。
terracotta.sudo_installing=HMCL 需要验证您的密码才能安装联机核心
terracotta.from_local.title=打开联机核心第三方下载链接
terracotta.from_local.title=联机核心第三方下载链接
terracotta.from_local.desc=在部分地区,联机核心默认下载渠道可能不稳定
terracotta.from_local.file_name_mismatch=您应当下载名为 %1$s 的联机核心包,而非 %2$s
terracotta.status.bootstrap=正在收集信息

View File

@ -25,6 +25,13 @@
"https://cp.zkitefly.eu.org/https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}"
],
"links": [
"https://github.com/burningtnt/Terracotta/releases/tag/v${version}"
{
"desc": {
"default": "GitHub Release",
"zh": "GitHub 发布页",
"zh-Hant": "GitHub 發布頁"
},
"link": "https://github.com/burningtnt/Terracotta/releases/tag/v${version}"
}
]
}