mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-08-03 11:26:38 -04:00
Merge remote-tracking branch 'upstream/main' into x-bmclapi-hash
This commit is contained in:
commit
d7e73f69fa
@ -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<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
|
||||
|
||||
public static void shutdown() {
|
||||
for (Map.Entry<String, Path> 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<Image> 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<Image> newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
var image = new SimpleObjectProperty<Image>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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> state = new ReadOnlyObjectWrapper<>();
|
||||
@ -229,6 +230,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
}
|
||||
|
||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||
private final JFXListView<RemoteMod> 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<RemoteMod> 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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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"));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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=新增模組
|
||||
|
@ -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=添加模组
|
||||
|
@ -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"),
|
||||
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.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<Path> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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<T> extends Task<T> {
|
||||
protected static final int DEFAULT_RETRY = 3;
|
||||
|
||||
protected final List<URI> 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<T> extends Task<T> {
|
||||
setExecutor(download());
|
||||
}
|
||||
|
||||
public void setCaching(boolean caching) {
|
||||
this.caching = caching;
|
||||
}
|
||||
|
||||
public void setCacheRepository(CacheRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
@ -78,6 +78,7 @@ public class FileDownloadTask extends FetchTask<Void> {
|
||||
|
||||
private final Path file;
|
||||
private final IntegrityCheck integrityCheck;
|
||||
private boolean caching;
|
||||
private Path candidate;
|
||||
private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>();
|
||||
|
||||
@ -126,7 +127,7 @@ public class FileDownloadTask extends FetchTask<Void> {
|
||||
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
||||
*/
|
||||
public FileDownloadTask(List<URI> 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<Void> {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void setCaching(boolean caching) {
|
||||
this.caching = caching;
|
||||
}
|
||||
|
||||
public FileDownloadTask setCandidate(Path candidate) {
|
||||
this.candidate = candidate;
|
||||
return this;
|
||||
|
@ -36,8 +36,6 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
*/
|
||||
public final class GetTask extends FetchTask<String> {
|
||||
|
||||
private static final int DEFAULT_RETRY = 3;
|
||||
|
||||
private final Charset charset;
|
||||
|
||||
public GetTask(URI url) {
|
||||
|
@ -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<CacheResult, IOException> cacheSupplier) throws IOException {
|
||||
private Path cacheData(URLConnection connection, ExceptionalSupplier<CacheResult, IOException> 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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user