diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java index dc54158d23e..2155a6ae1b9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java @@ -138,6 +138,11 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th return getBackedRemoteModRepository().getAddonById(downloadProvider, id); } + @Override + public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException { + return getBackedRemoteModRepository().resolveDependency(downloadProvider, id); + } + @Override public RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException { return getBackedRemoteModRepository().getAddonFile(projectId, fileId); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java index 5b86e08a86f..9083f4fc0c3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java @@ -401,21 +401,15 @@ private static final class DependencyAddonItem extends LineButton { }); setNode(IDX_LEADING, pane); - if (addon != RemoteAddon.BROKEN) { - ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug()); - content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title()); - content.setSubtitle(addon.description()); - for (String category : addon.categories()) { - if (page.shouldDisplayCategory(category)) - content.addTag(page.getLocalizedCategory(category, null)); - } - if (StringUtils.isNotBlank(addon.iconUrl())) { - imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.iconUrl(), 80, 80, true, true)); - } - } else { - content.setTitle(i18n("addon.broken_dependency.title")); - content.setSubtitle(i18n("addon.broken_dependency.desc")); - imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@4x.png")); + ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug()); + content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title()); + content.setSubtitle(addon.description()); + for (String category : addon.categories()) { + if (page.shouldDisplayCategory(category)) + content.addTag(page.getLocalizedCategory(category, null)); + } + if (StringUtils.isNotBlank(addon.iconUrl())) { + imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.iconUrl(), 80, 80, true, true)); } } } @@ -549,6 +543,7 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s scrollPane.setContent(dependenciesList); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); + FXUtils.onChangeAndOperate(scrollPane.widthProperty(), d -> FXUtils.setLimitWidth(dependenciesList, d.doubleValue())); FXUtils.smoothScrolling(scrollPane); FXUtils.setOverflowHidden(scrollPane, 8); spinnerPane.setContent(scrollPane); @@ -619,17 +614,19 @@ private void loadDependencies(RemoteAddon.Version version, DownloadPage selfPage return; } DependencyAddonItem dependencyAddonItem = new DependencyAddonItem(selfPage.page, dep, selfPage.instanceReference); + FXUtils.onChangeAndOperate(dependenciesList.widthProperty(), d -> FXUtils.setLimitWidth(dependencyAddonItem, d.doubleValue())); dependencies.get(dependency.getType()).value().add(dependencyAddonItem); }) .setSignificance(Task.TaskSignificance.MINOR)); } return Task.allOf(queue).thenSupplyAsync(() -> - dependencies.values().stream().flatMap(types -> - Stream.concat( - Stream.of(types.key()), - types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER))) - ).toList() + dependencies.values().stream().flatMap(types -> { + if (types.value().isEmpty()) return Stream.of(); + return Stream.concat( + Stream.of(types.key()), + types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER))); + }).toList() ); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ca60b1b0a01..0db4eca29c0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -172,8 +172,6 @@ account.skin.upload.failed=Failed to upload skin. account.skin.invalid_skin=Invalid skin file. account.username=Username -addon.broken_dependency.title=Broken dependency -addon.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source. addon.category=Category addon.changelog=Changelog addon.changelog.empty=Currently no changelog diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 05b429a1074..e8d49a658a9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -167,8 +167,6 @@ account.skin.upload.failed=فشل رفع المظهر. account.skin.invalid_skin=ملف المظهر غير صالح. account.username=اسم المستخدم -addon.broken_dependency.title=تبعية معطوبة -addon.broken_dependency.desc=كانت هذه التبعية موجودة مسبقاً، لكنها لم تعد كذلك. جرّب استخدام مصدر تنزيل آخر. addon.category=الفئة addon.channel.alpha=ألفا addon.channel.beta=بيتا diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index c1ab2c80d98..3b3f2ab604c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -147,8 +147,6 @@ account.skin.upload.failed=No se pudo subir el aspecto. account.skin.invalid_skin=Archivo de aspecto inválido. account.username=Nombre de usuario -addon.broken_dependency.title=Dependencia rota -addon.broken_dependency.desc=Esta dependencia existía antes, pero ahora no existe. Intente utilizar otra fuente de descarga. addon.category=Categoría addon.channel.alpha=Alpha addon.channel.beta=Beta diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index fef9114fe92..25a1d65b2f6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -149,8 +149,6 @@ account.skin.upload.failed=匯之外觀而未成 account.skin.invalid_skin=外觀之案有謬 account.username=戶名 -addon.broken_dependency.title=所依之壞者 -addon.broken_dependency.desc=夫改囊素存於改囊庫,今闕矣,宜易他源。 addon.category=類 addon.changelog=迭更誌 addon.channel.alpha=預版 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 9c603092dfe..569194edfa0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -146,8 +146,6 @@ account.skin.upload.failed=Не удалось загрузить скин. account.skin.invalid_skin=Недопустимый файл скина. account.username=Имя пользователя -addon.broken_dependency.title=Сломанная зависимость -addon.broken_dependency.desc=Эта зависимость существовала раньше, но теперь ее нет. Попробуйте использовать другой источник скачивания. addon.category=Категория addon.channel.alpha=Альфа addon.channel.beta=Бета diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index ee0dd985b8c..70f621ee477 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -147,8 +147,6 @@ account.skin.upload.failed=Не вдалося завантажити скін. account.skin.invalid_skin=Недійсний файл скіна. account.username=Ім'я користувача -addon.broken_dependency.title=Зламана залежність -addon.broken_dependency.desc=Ця залежність існувала раніше, але тепер її немає. Спробуйте використовувати інше джерело завантаження. addon.category=Категорія addon.channel.alpha=Альфа addon.channel.beta=Бета diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5eeafafcb3b..b3f3314028f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -170,8 +170,6 @@ account.skin.upload.failed=外觀上傳失敗 account.skin.invalid_skin=無法識別的外觀檔案 account.username=使用者名稱 -addon.broken_dependency.title=損壞的相依內容 -addon.broken_dependency.desc=該相依內容曾經存在於附加內容下載源中,但現在已被刪除,請嘗試其他下載源。 addon.category=類別 addon.changelog=更新日誌 addon.changelog.empty=暫無更新日誌 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 6fe08134e54..532632df6f2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -172,8 +172,6 @@ account.skin.upload.failed=皮肤上传失败 account.skin.invalid_skin=无法识别的皮肤文件 account.username=用户名 -addon.broken_dependency.title=损坏的前置内容 -addon.broken_dependency.desc=该前置内容曾经在该附加内容下载源上存在过,但现在被删除了。换个下载源试试吧。 addon.category=类别 addon.changelog=更新日志 addon.changelog.empty=暂无更新日志 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java index 9c850c4b9f8..500f230c172 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java @@ -88,9 +88,9 @@ SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Null RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) throws IOException; - default RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException { - return getAddonById(downloadProvider, id); - } + /// @return the dependency resolved, or {@link RemoteAddon#BROKEN} when the dependency is not found + /// @throws IOException if an I/O error occurs, except for when the desired dependency is not found + RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException; RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java index e33849960bf..4aba07e91e6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java @@ -26,14 +26,13 @@ import org.jackhuang.hmcl.util.MurmurHash2; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.io.HttpRequest; -import org.jackhuang.hmcl.util.io.JarUtils; -import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.io.*; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -176,7 +175,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } if (response == null) { - throw exception != null ? exception : new IOException("No candidates found"); + throw exception != null ? exception : new NoCandidatesException(); } // https://github.com/HMCL-dev/HMCL/issues/1549 @@ -254,6 +253,19 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th } } + @Override + public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException { + try { + return getAddonById(downloadProvider, id); + } catch (IOException e) { + if (e instanceof NoCandidatesException) throw e; + if (e instanceof FileNotFoundException + || e instanceof ResponseCodeException rce && rce.getResponseCode() == 404) + return RemoteAddon.BROKEN; + throw e; + } + } + @Override public RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException { SEMAPHORE.acquireUninterruptibly(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java index b8de101b0e3..c42ecad84b5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.io.NoCandidatesException; import org.jackhuang.hmcl.util.io.ResponseCodeException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -201,7 +202,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } } - throw exception != null ? exception : new IOException("No candidates found"); + throw exception != null ? exception : new NoCandidatesException(); } finally { SEMAPHORE.release(); } @@ -255,7 +256,7 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th } } - throw exception != null ? exception : new IOException("No candidates found"); + throw exception != null ? exception : new NoCandidatesException(); } finally { SEMAPHORE.release(); } @@ -265,13 +266,19 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException { try { return getAddonById(downloadProvider, id); - } catch (ResponseCodeException e) { - if (e.getResponseCode() == 502 || e.getResponseCode() == 404) { + } catch (IOException e) { + if (e instanceof NoCandidatesException) throw e; + List l = new ArrayList<>(List.of(e)); + l.addAll(List.of(e.getSuppressed())); + if (l.stream().allMatch(t -> { + var cause = t.getCause(); + return cause == null + || cause instanceof FileNotFoundException + || cause instanceof ResponseCodeException rce && rce.getResponseCode() == 404; + })) { // Which means the file does not exist return RemoteAddon.BROKEN; } throw e; - } catch (FileNotFoundException e) { - return RemoteAddon.BROKEN; } } @@ -307,7 +314,7 @@ public Stream getRemoteVersionsById(DownloadProvider downlo } } - throw exception != null ? exception : new IOException("No candidates found"); + throw exception != null ? exception : new NoCandidatesException(); } finally { SEMAPHORE.release(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NoCandidatesException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NoCandidatesException.java new file mode 100644 index 00000000000..ed59bce50fc --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NoCandidatesException.java @@ -0,0 +1,27 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.util.io; + +import java.io.IOException; + +public class NoCandidatesException extends IOException { + + public NoCandidatesException() { + super("No candidates found"); + } +}