优化下载页面图标缓存功能 (#5211)
This commit is contained in:
@@ -56,29 +56,20 @@ import org.jackhuang.hmcl.ui.construct.FloatListCell;
|
|||||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
import org.jackhuang.hmcl.util.*;
|
||||||
import org.jackhuang.hmcl.util.Holder;
|
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
|
||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CancellationException;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.CompletionException;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
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 {
|
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
|
||||||
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||||
@@ -247,6 +238,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
|
|
||||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||||
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||||
|
private final RemoteImageLoader iconLoader = new RemoteImageLoader() {
|
||||||
|
@Override
|
||||||
|
protected @NotNull Task<Image> createLoadTask(@NotNull URI uri) {
|
||||||
|
return FXUtils.getRemoteImageTask(uri, 80, 80, true, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||||
super(control);
|
super(control);
|
||||||
@@ -377,6 +374,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0);
|
IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0);
|
||||||
IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1);
|
IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1);
|
||||||
EventHandler<ActionEvent> searchAction = e -> {
|
EventHandler<ActionEvent> searchAction = e -> {
|
||||||
|
iconLoader.clearInvalidCache();
|
||||||
if (currentFilterID.get() != -1 && currentFilterID.get() != filterID.get()) {
|
if (currentFilterID.get() != -1 && currentFilterID.get() != filterID.get()) {
|
||||||
control.pageOffset.set(0);
|
control.pageOffset.set(0);
|
||||||
}
|
}
|
||||||
@@ -536,7 +534,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
|
|
||||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||||
var iconCache = new WeakHashMap<String, WeakReference<CompletableFuture<Image>>>();
|
|
||||||
listView.setCellFactory(x -> new FloatListCell<>(listView) {
|
listView.setCellFactory(x -> new FloatListCell<>(listView) {
|
||||||
private final TwoLineListItem content = new TwoLineListItem();
|
private final TwoLineListItem content = new TwoLineListItem();
|
||||||
private final ImageView imageView = new ImageView();
|
private final ImageView imageView = new ImageView();
|
||||||
@@ -564,66 +562,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
if (getSkinnable().shouldDisplayCategory(category))
|
if (getSkinnable().shouldDisplayCategory(category))
|
||||||
content.addTag(getSkinnable().getLocalizedCategory(category));
|
content.addTag(getSkinnable().getLocalizedCategory(category));
|
||||||
}
|
}
|
||||||
loadIcon(dataItem);
|
iconLoader.load(imageView.imageProperty(), dataItem.getIconUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadIcon(RemoteMod mod) {
|
|
||||||
if (StringUtils.isBlank(mod.getIconUrl())) {
|
|
||||||
imageView.setImage(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WeakReference<CompletableFuture<Image>> cacheRef = iconCache.get(mod.getIconUrl());
|
|
||||||
CompletableFuture<Image> cache;
|
|
||||||
if (cacheRef != null && (cache = cacheRef.get()) != null) {
|
|
||||||
loadIcon(cache, mod.getIconUrl());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
URI iconUrl = NetworkUtils.toURIOrNull(mod.getIconUrl());
|
|
||||||
if (iconUrl == null) {
|
|
||||||
imageView.setImage(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<Image> future = new CompletableFuture<>();
|
|
||||||
WeakReference<CompletableFuture<Image>> futureRef = new WeakReference<>(future);
|
|
||||||
iconCache.put(mod.getIconUrl(), futureRef);
|
|
||||||
|
|
||||||
FXUtils.getRemoteImageTask(iconUrl, 80, 80, true, true)
|
|
||||||
.whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
|
|
||||||
if (exception == null) {
|
|
||||||
future.complete(result);
|
|
||||||
} else {
|
|
||||||
LOG.warning("Failed to load image from " + iconUrl, exception);
|
|
||||||
future.completeExceptionally(exception);
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
loadIcon(future, mod.getIconUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadIcon(@NotNull CompletableFuture<Image> future,
|
|
||||||
@NotNull String iconUrl) {
|
|
||||||
Image image;
|
|
||||||
try {
|
|
||||||
image = future.getNow(null);
|
|
||||||
} catch (CancellationException | CompletionException ignored) {
|
|
||||||
imageView.setImage(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image != null) {
|
|
||||||
imageView.setImage(image);
|
|
||||||
} else {
|
|
||||||
imageView.setImage(null);
|
|
||||||
future.thenAcceptAsync(result -> {
|
|
||||||
RemoteMod item = getItem();
|
|
||||||
if (item != null && iconUrl.equals(item.getIconUrl())) {
|
|
||||||
this.imageView.setImage(result);
|
|
||||||
}
|
|
||||||
}, Schedulers.javafx());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2026 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.util;
|
||||||
|
|
||||||
|
import javafx.beans.value.WritableValue;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
|
/// @author Glavo
|
||||||
|
public abstract class RemoteImageLoader {
|
||||||
|
private final Map<URI, WeakReference<Image>> cache = new HashMap<>();
|
||||||
|
private final Map<URI, List<WeakReference<WritableValue<Image>>>> pendingRequests = new HashMap<>();
|
||||||
|
private final WeakHashMap<WritableValue<Image>, URI> reverseLookup = new WeakHashMap<>();
|
||||||
|
|
||||||
|
public RemoteImageLoader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @Nullable Image getPlaceholder() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract @NotNull Task<Image> createLoadTask(@NotNull URI uri);
|
||||||
|
|
||||||
|
@FXThread
|
||||||
|
public void load(@NotNull WritableValue<Image> writableValue, String url) {
|
||||||
|
URI uri = NetworkUtils.toURIOrNull(url);
|
||||||
|
if (uri == null) {
|
||||||
|
reverseLookup.remove(writableValue);
|
||||||
|
writableValue.setValue(getPlaceholder());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeakReference<Image> reference = cache.get(uri);
|
||||||
|
if (reference != null) {
|
||||||
|
Image image = reference.get();
|
||||||
|
if (image != null) {
|
||||||
|
reverseLookup.remove(writableValue);
|
||||||
|
writableValue.setValue(image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cache.remove(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
List<WeakReference<WritableValue<Image>>> list = pendingRequests.get(uri);
|
||||||
|
if (list != null) {
|
||||||
|
list.add(new WeakReference<>(writableValue));
|
||||||
|
reverseLookup.put(writableValue, uri);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
list = new ArrayList<>(1);
|
||||||
|
list.add(new WeakReference<>(writableValue));
|
||||||
|
pendingRequests.put(uri, list);
|
||||||
|
reverseLookup.put(writableValue, uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createLoadTask(uri).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
|
Image image;
|
||||||
|
if (exception == null) {
|
||||||
|
image = result;
|
||||||
|
} else {
|
||||||
|
LOG.warning("Failed to load image from " + uri, exception);
|
||||||
|
image = getPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.put(uri, new WeakReference<>(image));
|
||||||
|
List<WeakReference<WritableValue<Image>>> list = pendingRequests.remove(uri);
|
||||||
|
if (list != null) {
|
||||||
|
for (WeakReference<WritableValue<Image>> ref : list) {
|
||||||
|
WritableValue<Image> target = ref.get();
|
||||||
|
if (target != null && uri.equals(reverseLookup.get(target))) {
|
||||||
|
reverseLookup.remove(target);
|
||||||
|
target.setValue(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXThread
|
||||||
|
public void unload(@NotNull WritableValue<Image> writableValue) {
|
||||||
|
reverseLookup.remove(writableValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXThread
|
||||||
|
public void clearInvalidCache() {
|
||||||
|
cache.entrySet().removeIf(entry -> entry.getValue().get() == null);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user