diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 2d12de7d4..075a79a29 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -29,6 +29,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.*; import javafx.collections.ObservableMap; import javafx.event.Event; @@ -44,10 +45,7 @@ import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.*; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; +import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; @@ -57,9 +55,8 @@ import javafx.stage.Stage; import javafx.util.Callback; import javafx.util.Duration; import javafx.util.StringConverter; -import org.glavo.png.PNGType; -import org.glavo.png.PNGWriter; -import org.glavo.png.javafx.PNGJavaFXUtils; +import org.jackhuang.hmcl.task.CacheFileTask; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.util.*; @@ -82,13 +79,18 @@ import javax.imageio.stream.ImageInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import java.awt.image.BufferedImage; import java.io.*; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.ref.WeakReference; import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.List; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -169,18 +171,8 @@ public final class FXUtils { ); private static final Map builtinImageCache = new ConcurrentHashMap<>(); - private static final Map remoteImageCache = new ConcurrentHashMap<>(); public static void shutdown() { - for (Map.Entry entry : remoteImageCache.entrySet()) { - try { - Files.deleteIfExists(entry.getValue()); - } catch (IOException e) { - LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e); - } - remoteImageCache.remove(entry.getKey()); - } - builtinImageCache.clear(); } @@ -903,78 +895,55 @@ public final class FXUtils { } } - /** - * Load image from the internet. It will cache the data of images for the further usage. - * The cached data will be deleted when HMCL is closed or hidden. - * - * @param url the url of image. The image resource should be a file on the internet. - * @return the image resource within the jar. - */ - public static Image newRemoteImage(String url) { - return newRemoteImage(url, 0, 0, false, false, false); + public static Task getRemoteImageTask(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { + return new CacheFileTask(URI.create(url)) + .thenApplyAsync(file -> { + try (var channel = FileChannel.open(file, StandardOpenOption.READ)) { + var header = new byte[12]; + var buffer = ByteBuffer.wrap(header); + + //noinspection StatementWithEmptyBody + while (channel.read(buffer) > 0) { + } + + channel.position(0L); + if (!buffer.hasRemaining()) { + // WebP File + if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' && + header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') { + + WebPImageReaderSpi spi = new WebPImageReaderSpi(); + ImageReader reader = spi.createReaderInstance(null); + BufferedImage bufferedImage; + try (ImageInputStream imageInput = ImageIO.createImageInputStream(Channels.newInputStream(channel))) { + reader.setInput(imageInput, true, true); + bufferedImage = reader.read(0, reader.getDefaultReadParam()); + } finally { + reader.dispose(); + } + return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth); + } + } + + Image image = new Image(Channels.newInputStream(channel), requestedWidth, requestedHeight, preserveRatio, smooth); + if (image.isError()) + throw image.getException(); + return image; + } + }); } - /** - * Load image from the internet. It will cache the data of images for the further usage. - * The cached data will be deleted when HMCL is closed or hidden. - * - * @param url the url of image. The image resource should be a file on the internet. - * @param requestedWidth the image's bounding box width - * @param requestedHeight the image's bounding box height - * @param preserveRatio indicates whether to preserve the aspect ratio of - * the original image when scaling to fit the image within the - * specified bounding box - * @param smooth indicates whether to use a better quality filtering - * algorithm or a faster one when scaling this image to fit within - * the specified bounding box - * @return the image resource within the jar. - */ - public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) { - Path currentPath = remoteImageCache.get(url); - if (currentPath != null) { - if (Files.isReadable(currentPath)) { - try (InputStream inputStream = Files.newInputStream(currentPath)) { - return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth); - } catch (IOException e) { - LOG.warning("An exception encountered while reading data from cached image file.", e); - } - } - - // The file is unavailable or unreadable. - remoteImageCache.remove(url); - - try { - Files.deleteIfExists(currentPath); - } catch (IOException e) { - LOG.warning("An exception encountered while deleting broken cached image file.", e); - } - } - - Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading); - image.progressProperty().addListener((observable, oldValue, newValue) -> { - if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) { - Task.runAsync(() -> { - Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache"); - try ( // Make sure the file is released from JVM before we put the path into remoteImageCache. - OutputStream outputStream = Files.newOutputStream(newPath); - PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL) - ) { - writer.write(PNGJavaFXUtils.asArgbImage(image)); - } catch (IOException e) { - try { - Files.delete(newPath); - } catch (IOException e2) { - e2.addSuppressed(e); - throw e2; - } - throw e; + public static ObservableValue newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { + var image = new SimpleObjectProperty(); + getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + image.set(result); + } else { + LOG.warning("An exception encountered while loading remote image: " + url, exception); } - if (remoteImageCache.putIfAbsent(url, newPath) != null) { - Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder. - } - }).start(); - } - }); + }) + .start(); return image; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 8f8556113..01a9b2942 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -190,10 +190,12 @@ public final class HTMLRenderer { } } - Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false); - if (image.isError()) { - LOG.warning("Failed to load image: " + uri, image.getException()); - } else { + try { + Image image = FXUtils.getRemoteImageTask(uri.toString(), width, height, true, true) + .run(); + if (image == null) + throw new AssertionError("Image loading task returned null"); + ImageView imageView = new ImageView(image); if (hyperlink != null) { URI target = resolveLink(hyperlink); @@ -204,6 +206,8 @@ public final class HTMLRenderer { } children.add(imageView); return; + } catch (Throwable e) { + LOG.warning("Failed to load image: " + uri, e); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index d2da008cc..525209e1e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -68,6 +68,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable { protected final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); @@ -229,6 +230,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } private static class ModDownloadListPageSkin extends SkinBase { + private final JFXListView listView = new JFXListView<>(); protected ModDownloadListPageSkin(DownloadListPage control) { super(control); @@ -449,6 +451,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP boolean disableNext = disableAll || pageOffset == pageCount - 1; nextPageButton.setDisable(disableNext); lastPageButton.setDisable(disableNext); + + listView.scrollTo(0); }; FXUtils.onChange(control.pageCount, pageCountN -> { @@ -504,7 +508,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } }); - JFXListView listView = new JFXListView<>(); spinnerPane.setContent(listView); Bindings.bindContent(listView.getItems(), getSkinnable().items); FXUtils.onClicked(listView, () -> { @@ -540,7 +543,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP .collect(Collectors.toList())); if (StringUtils.isNotBlank(dataItem.getIconUrl())) { - imageView.setImage(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true, true)); + LOG.debug("Icon: " + dataItem.getIconUrl()); + imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true)); } } }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 6de0971de..2a8e5d1ab 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -220,7 +220,7 @@ public class DownloadPage extends Control implements DecoratorPage { { ImageView imageView = new ImageView(); if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) { - imageView.setImage(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true)); + imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true)); } descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40)); @@ -359,7 +359,7 @@ public class DownloadPage extends Control implements DecoratorPage { .collect(Collectors.toList())); if (StringUtils.isNotBlank(addon.getIconUrl())) { - imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true)); + imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true)); } } else { content.setTitle(i18n("mods.broken_dependency.title")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java index 1163c0e8c..80c068d44 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java @@ -120,5 +120,43 @@ public final class SwingFXUtils { pw.setPixels(0, 0, bw, bh, pf, data, offset, scan); return wimg; } + + public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { + if (requestedWidth <= 0. || requestedHeight <= 0.) { + return toFXImage(bimg, null); + } + + int width = (int) requestedWidth; + int height = (int) requestedHeight; + + // Calculate actual dimensions if preserveRatio is true + if (preserveRatio) { + double originalWidth = bimg.getWidth(); + double originalHeight = bimg.getHeight(); + double scaleX = requestedWidth / originalWidth; + double scaleY = requestedHeight / originalHeight; + double scale = Math.min(scaleX, scaleY); + + width = (int) (originalWidth * scale); + height = (int) (originalHeight * scale); + } + + // Create scaled BufferedImage + BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D g2d = scaledImage.createGraphics(); + try { + if (smooth) { + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } + + g2d.drawImage(bimg, 0, 0, width, height, null); + } finally { + g2d.dispose(); + } + + return toFXImage(scaledImage, null); + } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index c65575b19..a237e15b8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -930,17 +930,20 @@ modpack.wizard.step.initialization.server=Click here for more information on how modrinth.category.adventure=Adventure modrinth.category.atmosphere=Atmosphere modrinth.category.audio=Audio +modrinth.category.babric=Babric modrinth.category.blocks=Blocks modrinth.category.bloom=Bloom +modrinth.category.bta-babric=BTA (Babric) modrinth.category.bukkit=Bukkit modrinth.category.bungeecord=BungeeCord modrinth.category.canvas=Canvas modrinth.category.cartoon=Cartoon modrinth.category.challenging=Challenging modrinth.category.colored-lighting=Colored Lighting -modrinth.category.core-shaders=Core Shaders modrinth.category.combat=Combat +modrinth.category.core-shaders=Core Shaders modrinth.category.cursed=Cursed +modrinth.category.datapack=Datapack modrinth.category.decoration=Decoration modrinth.category.economy=Economy modrinth.category.entities=Entities @@ -948,6 +951,7 @@ modrinth.category.environment=Environment modrinth.category.equipment=Equipment modrinth.category.fabric=Fabric modrinth.category.fantasy=Fantasy +modrinth.category.folia=Folia modrinth.category.foliage=Foliage modrinth.category.fonts=Fonts modrinth.category.food=Food @@ -957,7 +961,9 @@ modrinth.category.gui=GUI modrinth.category.high=High modrinth.category.iris=Iris modrinth.category.items=Items +modrinth.category.java-agent=Java Agent modrinth.category.kitchen-sink=Kitchen-Sink +modrinth.category.legacy-fabric=Legacy Fabric modrinth.category.library=Library modrinth.category.lightweight=Lightweight modrinth.category.liteloader=LiteLoader @@ -975,8 +981,10 @@ modrinth.category.models=Models modrinth.category.modloader=Modloader modrinth.category.multiplayer=Multiplayer modrinth.category.neoforge=NeoForge +modrinth.category.nilloader=NilLoader modrinth.category.optifine=OptiFine modrinth.category.optimization=Optimization +modrinth.category.ornithe=Ornithe modrinth.category.paper=Paper modrinth.category.path-tracing=Path Tracing modrinth.category.pbr=PBR @@ -1005,8 +1013,6 @@ modrinth.category.vanilla-like=Vanilla-like modrinth.category.velocity=Velocity modrinth.category.waterfall=Waterfall modrinth.category.worldgen=Worldgen -modrinth.category.datapack=Datapack -modrinth.category.folia=Folia modrinth.category.8x-=8x- modrinth.category.16x=16x modrinth.category.32x=32x diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 4c81f36f7..7e66815c2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -935,17 +935,20 @@ modpack.wizard.step.initialization.server=Haga clic aquí para ver más tutorial modrinth.category.adventure=Aventura modrinth.category.atmosphere=Atmósfera modrinth.category.audio=Audio +modrinth.category.babric=Babric modrinth.category.blocks=Bloqueos modrinth.category.bloom=Resplandor +modrinth.category.bta-babric=BTA (Babric) modrinth.category.bukkit=Bukkit modrinth.category.bungeecord=BungeeCord modrinth.category.canvas=Canvas modrinth.category.cartoon=Caricatura modrinth.category.challenging=Desafío modrinth.category.colored-lighting=Iluminación de color -modrinth.category.core-shaders=Sombreadores de núcleo modrinth.category.combat=Combate +modrinth.category.core-shaders=Sombreadores de núcleo modrinth.category.cursed=Maldición +modrinth.category.datapack=Paquete de datos modrinth.category.decoration=Decoración modrinth.category.economy=Economía modrinth.category.entities=Entidades @@ -953,6 +956,7 @@ modrinth.category.environment=Medio ambiente modrinth.category.equipment=Equipo modrinth.category.fabric=Fabric modrinth.category.fantasy=Fantasía +modrinth.category.folia=Folia modrinth.category.foliage=Vegetación modrinth.category.fonts=Fonts modrinth.category.food=Alimentos @@ -962,7 +966,9 @@ modrinth.category.gui=GUI modrinth.category.high=Alto modrinth.category.iris=Iris modrinth.category.items=Objetos +modrinth.category.java-agent=Java Agent modrinth.category.kitchen-sink=Fregadero de cocina +modrinth.category.legacy-fabric=Legacy Fabric modrinth.category.library=Biblioteca modrinth.category.lightweight=Peso ligero modrinth.category.liteloader=LiteLoader @@ -980,8 +986,10 @@ modrinth.category.models=Modelos modrinth.category.modloader=Cargador de mods modrinth.category.multiplayer=Multijugador modrinth.category.neoforge=NeoForge +modrinth.category.nilloader=NilLoader modrinth.category.optifine=OptiFine modrinth.category.optimization=Optimización +modrinth.category.ornithe=Ornithe modrinth.category.paper=Paper modrinth.category.path-tracing=Trazado de rutas modrinth.category.pbr=PBR @@ -1010,8 +1018,6 @@ modrinth.category.vanilla-like=Similar a la vainilla modrinth.category.velocity=Velocity modrinth.category.waterfall=Waterfall modrinth.category.worldgen=Generación de mundos -modrinth.category.datapack=Paquete de datos -modrinth.category.folia=Folia modrinth.category.8x-=8x- modrinth.category.16x=16x modrinth.category.32x=32x diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 4494d6cf4..3b80f79d7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -934,17 +934,20 @@ modpack.wizard.step.initialization.server=Нажмите здесь для по modrinth.category.adventure=Приключение modrinth.category.atmosphere=Атмосфера modrinth.category.audio=Аудио +modrinth.category.babric=Babric modrinth.category.blocks=Блоки modrinth.category.bloom=Свечение +modrinth.category.bta-babric=BTA (Babric) modrinth.category.bukkit=Bukkit modrinth.category.bungeecord=BungeeCord modrinth.category.canvas=Canvas modrinth.category.cartoon=Мультяшный modrinth.category.challenging=Вызов modrinth.category.colored-lighting=Цветное освещение -modrinth.category.core-shaders=Ядро шейдеров modrinth.category.combat=Бой +modrinth.category.core-shaders=Ядро шейдеров modrinth.category.cursed=Cursed +modrinth.category.datapack=Набор данных modrinth.category.decoration=Украшения modrinth.category.economy=Экономика modrinth.category.entities=Сущности @@ -952,6 +955,7 @@ modrinth.category.environment=Окружающая среда modrinth.category.equipment=Оборудование modrinth.category.fabric=Fabric modrinth.category.fantasy=Фэнтези +modrinth.category.folia=Folia modrinth.category.foliage=Растительность modrinth.category.fonts=Шрифты modrinth.category.food=Еда @@ -961,7 +965,9 @@ modrinth.category.gui=ГИП modrinth.category.high=Высокий modrinth.category.iris=Iris modrinth.category.items=Предметы +modrinth.category.java-agent=Java Agent modrinth.category.kitchen-sink=Kitchen-Sink +modrinth.category.legacy-fabric=Legacy Fabric modrinth.category.library=Библиотека modrinth.category.lightweight=Легкий modrinth.category.liteloader=LiteLoader @@ -979,8 +985,10 @@ modrinth.category.models=Модели modrinth.category.modloader=Мод-загрузчик modrinth.category.multiplayer=Сетевая игра modrinth.category.neoforge=NeoForge +modrinth.category.nilloader=NilLoader modrinth.category.optifine=OptiFine modrinth.category.optimization=Оптимизация +modrinth.category.ornithe=Ornithe modrinth.category.paper=Paper modrinth.category.path-tracing=Трассировка пути modrinth.category.pbr=PBR @@ -1009,8 +1017,6 @@ modrinth.category.vanilla-like=Ванильное modrinth.category.velocity=Velocity modrinth.category.waterfall=Waterfall modrinth.category.worldgen=Генерация мира -modrinth.category.datapack=Набор данных -modrinth.category.folia=Folia modrinth.category.8x-=8x- modrinth.category.16x=16x modrinth.category.32x=32x diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e035c7f63..90c3b63fd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -744,17 +744,20 @@ modpack.wizard.step.initialization.server=點選此處查看有關伺服器自 modrinth.category.adventure=冒險 modrinth.category.atmosphere=氛圍 modrinth.category.audio=聲音 +modrinth.category.babric=Babric modrinth.category.blocks=方塊 modrinth.category.bloom=泛光 +modrinth.category.bta-babric=BTA (Babric) modrinth.category.bukkit=Bukkit modrinth.category.bungeecord=BungeeCord modrinth.category.canvas=Canvas modrinth.category.cartoon=卡通 modrinth.category.challenging=高難度 modrinth.category.colored-lighting=彩色光照 -modrinth.category.core-shaders=核心著色器 modrinth.category.combat=戰鬥 +modrinth.category.core-shaders=核心著色器 modrinth.category.cursed=Cursed +modrinth.category.datapack=資料包 modrinth.category.decoration=裝飾 modrinth.category.economy=經濟 modrinth.category.entities=實體 @@ -762,6 +765,7 @@ modrinth.category.environment=環境 modrinth.category.equipment=裝備 modrinth.category.fabric=Fabric modrinth.category.fantasy=幻想 +modrinth.category.folia=Folia modrinth.category.foliage=植被 modrinth.category.fonts=字體 modrinth.category.food=食物 @@ -771,7 +775,9 @@ modrinth.category.gui=GUI modrinth.category.high=高 modrinth.category.iris=Iris modrinth.category.items=物品 +modrinth.category.java-agent=Java Agent modrinth.category.kitchen-sink=大雜燴 +modrinth.category.legacy-fabric=Legacy Fabric modrinth.category.library=支援庫 modrinth.category.lightweight=輕量 modrinth.category.liteloader=LiteLoader @@ -789,8 +795,10 @@ modrinth.category.models=模型 modrinth.category.modloader=ModLoader modrinth.category.multiplayer=多人 modrinth.category.neoforge=NeoForge +modrinth.category.nilloader=NilLoader modrinth.category.optifine=OptiFine modrinth.category.optimization=最佳化 +modrinth.category.ornithe=Ornithe modrinth.category.paper=Paper modrinth.category.path-tracing=路径追踪 modrinth.category.pbr=PBR @@ -819,8 +827,6 @@ modrinth.category.vanilla-like=類原生 modrinth.category.velocity=Velocity modrinth.category.waterfall=Waterfall modrinth.category.worldgen=世界生成 -modrinth.category.datapack=資料包 -modrinth.category.folia=Folia mods=模組 mods.add=新增模組 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 8b0a454ab..de1c55bcf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -754,17 +754,20 @@ modpack.wizard.step.initialization.server=点击此处查看有关服务器自 modrinth.category.adventure=冒险 modrinth.category.atmosphere=氛围 modrinth.category.audio=声音 +modrinth.category.babric=Babric modrinth.category.blocks=方块 modrinth.category.bloom=泛光 +modrinth.category.bta-babric=BTA (Babric) modrinth.category.bukkit=Bukkit modrinth.category.bungeecord=BungeeCord modrinth.category.canvas=Canvas modrinth.category.cartoon=卡通 modrinth.category.challenging=高难度 modrinth.category.colored-lighting=彩色光照 -modrinth.category.core-shaders=核心着色器 modrinth.category.combat=战斗 +modrinth.category.core-shaders=核心着色器 modrinth.category.cursed=Cursed +modrinth.category.datapack=数据包 modrinth.category.decoration=装饰 modrinth.category.economy=经济 modrinth.category.entities=实体 @@ -772,6 +775,7 @@ modrinth.category.environment=环境 modrinth.category.equipment=装备 modrinth.category.fabric=Fabric modrinth.category.fantasy=幻想 +modrinth.category.folia=Folia modrinth.category.foliage=植被 modrinth.category.fonts=字体 modrinth.category.food=食物 @@ -781,7 +785,9 @@ modrinth.category.gui=GUI modrinth.category.high=高 modrinth.category.iris=Iris modrinth.category.items=物品 +modrinth.category.java-agent=Java Agent modrinth.category.kitchen-sink=大杂烩 +modrinth.category.legacy-fabric=Legacy Fabric modrinth.category.library=支持库 modrinth.category.lightweight=轻量 modrinth.category.liteloader=LiteLoader @@ -799,8 +805,10 @@ modrinth.category.models=模型 modrinth.category.modloader=Modloader modrinth.category.multiplayer=多人 modrinth.category.neoforge=NeoForge +modrinth.category.nilloader=NilLoader modrinth.category.optifine=OptiFine modrinth.category.optimization=优化 +modrinth.category.ornithe=Ornithe modrinth.category.paper=Paper modrinth.category.path-tracing=路径追踪 modrinth.category.pbr=PBR @@ -829,8 +837,6 @@ modrinth.category.vanilla-like=类原生 modrinth.category.velocity=Velocity modrinth.category.waterfall=Waterfall modrinth.category.worldgen=世界生成 -modrinth.category.datapack=数据包 -modrinth.category.folia=Folia mods=模组 mods.add=添加模组 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java index 474d5c757..432606593 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java @@ -71,8 +71,7 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider { pair("http://files.minecraftforge.net/maven", apiRoot + "/maven"), pair("https://files.minecraftforge.net/maven", apiRoot + "/maven"), pair("https://maven.minecraftforge.net", apiRoot + "/maven"), - pair("https://maven.neoforged.net/releases/net/neoforged/forge", apiRoot + "/maven/net/neoforged/forge"), - pair("https://maven.neoforged.net/releases/net/neoforged/neoforge", apiRoot + "/maven/net/neoforged/neoforge"), + pair("https://maven.neoforged.net/releases/", apiRoot + "/maven/"), pair("http://dl.liteloader.com/versions/versions.json", apiRoot + "/maven/com/mumfrey/liteloader/versions.json"), pair("http://dl.liteloader.com/versions", apiRoot + "/maven"), pair("https://meta.fabricmc.net", apiRoot + "/fabric-meta"), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java new file mode 100644 index 000000000..6f67c7c7a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java @@ -0,0 +1,107 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 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.task; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * Download a file to cache repository. + * + * @author Glavo + */ +public final class CacheFileTask extends FetchTask { + + public CacheFileTask(@NotNull URI uri) { + super(List.of(uri), DEFAULT_RETRY); + } + + public CacheFileTask(@NotNull URI uri, int retry) { + super(List.of(uri), retry); + } + + @Override + protected EnumCheckETag shouldCheckETag() { + // Check cache + for (URI uri : uris) { + try { + setResult(repository.getCachedRemoteFile(uri)); + return EnumCheckETag.CACHED; + } catch (IOException ignored) { + } + } + return EnumCheckETag.CHECK_E_TAG; + } + + @Override + protected void useCachedResult(Path cache) { + setResult(cache); + } + + @Override + protected Context getContext(URLConnection connection, boolean checkETag) throws IOException { + assert checkETag; + + Path temp = Files.createTempFile("hmcl-download-", null); + OutputStream fileOutput = Files.newOutputStream(temp); + + return new Context() { + @Override + public void write(byte[] buffer, int offset, int len) throws IOException { + fileOutput.write(buffer, offset, len); + } + + @Override + public void close() throws IOException { + try { + fileOutput.close(); + } catch (IOException e) { + LOG.warning("Failed to close file: " + temp, e); + } + + if (!isSuccess()) { + try { + Files.deleteIfExists(temp); + } catch (IOException e) { + LOG.warning("Failed to delete file: " + temp, e); + } + return; + } + + try { + setResult(repository.cacheRemoteFile(connection, temp)); + } finally { + try { + Files.deleteIfExists(temp); + } catch (IOException e) { + LOG.warning("Failed to delete file: " + temp, e); + } + } + } + }; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java index 5012c0311..21d59d105 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java @@ -46,9 +46,10 @@ import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public abstract class FetchTask extends Task { + protected static final int DEFAULT_RETRY = 3; + protected final List uris; protected final int retry; - protected boolean caching; protected CacheRepository repository = CacheRepository.getInstance(); public FetchTask(@NotNull List<@NotNull URI> uris, int retry) { @@ -63,10 +64,6 @@ public abstract class FetchTask extends Task { setExecutor(download()); } - public void setCaching(boolean caching) { - this.caching = caching; - } - public void setCacheRepository(CacheRepository repository) { this.repository = repository; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java index 5e30a55b3..12ab51bae 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java @@ -78,6 +78,7 @@ public class FileDownloadTask extends FetchTask { private final Path file; private final IntegrityCheck integrityCheck; + private boolean caching; private Path candidate; private final ArrayList integrityCheckHandlers = new ArrayList<>(); @@ -126,7 +127,7 @@ public class FileDownloadTask extends FetchTask { * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed */ public FileDownloadTask(List uris, Path path, IntegrityCheck integrityCheck) { - this(uris, path, integrityCheck, 3); + this(uris, path, integrityCheck, DEFAULT_RETRY); } /** @@ -149,6 +150,10 @@ public class FileDownloadTask extends FetchTask { return file; } + public void setCaching(boolean caching) { + this.caching = caching; + } + public FileDownloadTask setCandidate(Path candidate) { this.candidate = candidate; return this; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java index e8edb0bfa..dba5d75f7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java @@ -36,8 +36,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; */ public final class GetTask extends FetchTask { - private static final int DEFAULT_RETRY = 3; - private final Charset charset; public GetTask(URI url) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java index e891ed771..0f33160fe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java @@ -203,20 +203,20 @@ public class CacheRepository { // conn.setRequestProperty("If-Modified-Since", eTagItem.getRemoteLastModified()); } - public void cacheRemoteFile(URLConnection connection, Path downloaded) throws IOException { - cacheData(connection, () -> { + public Path cacheRemoteFile(URLConnection connection, Path downloaded) throws IOException { + return cacheData(connection, () -> { String hash = DigestUtils.digestToString(SHA1, downloaded); Path cached = cacheFile(downloaded, SHA1, hash); return new CacheResult(hash, cached); }); } - public void cacheText(URLConnection connection, String text) throws IOException { - cacheBytes(connection, text.getBytes(UTF_8)); + public Path cacheText(URLConnection connection, String text) throws IOException { + return cacheBytes(connection, text.getBytes(UTF_8)); } - public void cacheBytes(URLConnection connection, byte[] bytes) throws IOException { - cacheData(connection, () -> { + public Path cacheBytes(URLConnection connection, byte[] bytes) throws IOException { + return cacheData(connection, () -> { String hash = DigestUtils.digestToString(SHA1, bytes); Path cached = getFile(SHA1, hash); FileUtils.writeBytes(cached, bytes); @@ -224,9 +224,9 @@ public class CacheRepository { }); } - private void cacheData(URLConnection connection, ExceptionalSupplier cacheSupplier) throws IOException { + private Path cacheData(URLConnection connection, ExceptionalSupplier cacheSupplier) throws IOException { String eTag = connection.getHeaderField("ETag"); - if (StringUtils.isBlank(eTag)) return; + if (StringUtils.isBlank(eTag)) return null; URI uri; try { uri = NetworkUtils.dropQuery(connection.getURL().toURI()); @@ -243,6 +243,7 @@ public class CacheRepository { } finally { lock.writeLock().unlock(); } + return cacheResult.cachedFile; } private static final class CacheResult {