diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index c7a99881db..0f3ce1915a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -44,6 +44,7 @@ public enum SVG { CHECK("M9.55 18 3.85 12.3 5.275 10.875 9.55 15.15 18.725 5.975 20.15 7.4 9.55 18Z"), CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"), CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"), + CLEAN("M3 23v-7q0-2.075 1.463-3.537T8 11h1V3q0-.825.588-1.412T11 1h2q.825 0 1.413.588T15 3v8h1q2.075 0 3.538 1.463T21 16v7zm2-2h2v-3q0-.425.288-.712T8 17t.713.288T9 18v3h2v-3q0-.425.288-.712T12 17t.713.288T13 18v3h2v-3q0-.425.288-.712T16 17t.713.288T17 18v3h2v-5q0-1.25-.875-2.125T16 13H8q-1.25 0-2.125.875T5 16zm8-10V3h-2v8zm0 0h-2z"), CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"), CONTENT_CUT("M19 21l-7-7-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22q-1.65 0-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12 7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6q0-1.65 1.175-2.825T6 2q1.65 0 2.825 1.175T10 6q0 .425-.075.85t-.275.8L22 20v1H19Zm-4-10-2-2 6-6h3v1l-7 7ZM7.4125 7.4125Q8 6.825 8 6t-.5875-1.4125Q6.825 4 6 4t-1.4125.5875Q4 5.175 4 6t.5875 1.4125T6 8t1.4125-.5875ZM12.35 12.35q.15-.15.15-.35t-.15-.35-.35-.15-.35.15-.15.35.15.35.35.15.35-.15ZM7.4125 19.4125Q8 18.825 8 18t-.5875-1.4125Q6.825 16 6 16t-1.4125.5875Q4 17.175 4 18t.5875 1.4125Q5.175 20 6 20t1.4125-.5875Z"), CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java index 2bed442a5f..7cd2c51692 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java @@ -64,6 +64,7 @@ public String getDisplayName() { } private final HBox actions; + private final EnhancedTextFlow textFlow; private @Nullable ButtonBase cancelButton; @@ -87,7 +88,7 @@ public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull StackPane content = new StackPane(); content.getStyleClass().add("jfx-layout-body"); - EnhancedTextFlow textFlow = new EnhancedTextFlow(text); + textFlow = new EnhancedTextFlow(text); textFlow.setStyle("-fx-font-size: 14px;"); if (textFlow.computePrefHeight(400.0) <= 350.0) content.getChildren().setAll(textFlow); @@ -115,6 +116,10 @@ public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull }); } + public void setText(String text) { + textFlow.setText(text); + } + public void addButton(Node btn) { btn.addEventHandler(ActionEvent.ACTION, e -> fireEvent(new DialogCloseEvent())); actions.getChildren().add(btn); @@ -130,7 +135,13 @@ public ButtonBase getCancelButton() { private static final class EnhancedTextFlow extends TextFlow { EnhancedTextFlow(String text) { - this.getChildren().setAll(FXUtils.parseSegment(text, Controllers::onHyperlinkAction)); + setText(text); + } + + public void setText(String newText) { + this.getChildren().setAll( + FXUtils.parseSegment(newText, Controllers::onHyperlinkAction) + ); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index cd8aede005..5eab5b0128 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -34,7 +34,10 @@ import javafx.scene.control.SkinBase; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -import javafx.scene.layout.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.util.Duration; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -187,6 +190,10 @@ public void refreshList() { Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start(); } + public void clean() { + Versions.cleanGameFiles(Profiles.getSelectedProfile()); + } + @Override protected Skin createDefaultSkin() { return new GameListSkin(this); @@ -237,7 +244,7 @@ public GameListSkin(GameList skinnable) { searchBar.getChildren().setAll(searchField, closeSearchBar); - toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))); + toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)), createToolbarButton2(i18n("game.clean"), SVG.CLEAN, skinnable::clean)); toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index ae72ab4854..e19e90616d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -18,7 +18,9 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXSpinner; import javafx.application.Platform; +import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; @@ -42,6 +44,7 @@ import org.jackhuang.hmcl.ui.export.ExportWizardProvider; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -49,10 +52,14 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -194,6 +201,125 @@ public static void updateGameAssets(Profile profile, String version) { executor.start(); } + public static void cleanGameFiles(Profile profile) { + var dialogBuilder = new MessageDialogPane.Builder(i18n("game.clean.content", i18n("game.clean.loading")), i18n("message.question"), MessageDialogPane.MessageType.QUESTION); + var spinner = new JFXSpinner(); + spinner.getStyleClass().add("small-spinner"); + + StackPane buttonPane = new StackPane(); + + JFXButton okButton = new JFXButton(i18n("button.yes")); + okButton.getStyleClass().add("dialog-accept"); + + dialogBuilder.addAction(buttonPane); + dialogBuilder.addCancel(null); + + var dialog = dialogBuilder.build(); + + Task.supplyAsync(() -> { + var repository = profile.getRepository(); + var versions = repository.getVersions(); + + Set activeAssets = versions.parallelStream() + .flatMap(version -> { + try { + var index = repository.getAssetIndex(version.getId(), version.getAssetIndex().getId()); + return index.getObjects().values().stream().map(AssetObject::getLocation); + } catch (IOException ignored) { + return Stream.empty(); + } + }) + .collect(Collectors.toSet()); + + Set activeLibraries = versions.parallelStream() + .flatMap(version -> version.getLibraries().stream()) + .map(Library::getPath) + .collect(Collectors.toSet()); + + List unusedFiles = new ArrayList<>(); + + unusedFiles.addAll(findUnlistedFiles(repository.getBaseDirectory().resolve("assets").resolve("objects"), activeAssets)); + unusedFiles.addAll(findUnlistedFiles(repository.getBaseDirectory().resolve("libraries"), activeLibraries)); + + List unusedFolders = new ArrayList<>(); + + for (String path : List.of("logs", "crash-reports", "modernfix", "mods/.connector", "CustomSkinLoader/caches", ".fabric")) { + unusedFolders.add(repository.getBaseDirectory().resolve(path)); + versions.forEach(v -> { + unusedFolders.add(repository.getVersionRoot(v.getId()).resolve(path)); + }); + } + + versions.forEach(v -> { + try (var walker = Files.walk(repository.getVersionRoot(v.getId()), 1)) { + unusedFolders.addAll(walker + .filter(it -> { + var name = it.getFileName().toString(); + return Files.isDirectory(it) && (name.startsWith("natives-") || name.endsWith("-natives")); + }).toList()); + } catch (IOException ignored) { + } + }); + + for (Path dir : unusedFolders) { + if (Files.exists(dir)) { + try (var s = Files.walk(dir)) { + s.filter(Files::isRegularFile).forEach(unusedFiles::add); + } catch (IOException ignored) { + } + } + } + + return unusedFiles; + }).thenApplyAsync((list) -> { + long totalSize = list.stream() + .mapToLong(path -> { + try { + return Files.size(path); + } catch (IOException e) { + return 0L; + } + }) + .sum(); + + FXUtils.runInFX(() -> { + dialog.setText(i18n("game.clean.content", I18n.formatSize(totalSize))); + buttonPane.getChildren().setAll(okButton); + okButton.setOnAction(event -> { + buttonPane.getChildren().setAll(spinner); + Task.runAsync(() -> list.forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + })).thenRunAsync(Schedulers.javafx(), () -> { + dialog.fireEvent(new DialogCloseEvent()); + }).start(); + }); + }); + return null; + }).start(); + + buttonPane.getChildren().setAll(spinner); + + Controllers.dialog(dialog); + } + + private static List findUnlistedFiles(Path root, Set activePaths) { + if (!Files.exists(root)) return List.of(); + try (var stream = Files.walk(root)) { + return stream + .filter(Files::isRegularFile) + .filter(path -> { + String relative = root.relativize(path).toString().replace("\\", "/"); + return !activePaths.contains(relative); + }) + .toList(); + } catch (IOException e) { + return List.of(); + } + } + public static void cleanVersion(Profile profile, String id) { try { profile.getRepository().clean(id); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java index 286910be07..5c4db97ffc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java @@ -77,6 +77,10 @@ public static String formatSpeed(long bytes) { return getTranslator().formatSpeed(bytes); } + public static String formatSize(long bytes) { + return getTranslator().formatSize(bytes); + } + public static String getDisplayVersion(RemoteVersion version) { return getTranslator().getDisplayVersion(version); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java index 7bdcfc8038..82a7c201eb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java @@ -73,4 +73,16 @@ public String formatSpeed(long bytes) { return supportedLocale.i18n("download.speed.megabyte_per_second", (double) bytes / (1024 * 1024)); } } + + public String formatSize(long bytes) { + if (bytes < 1024) { + return supportedLocale.i18n("download.size.byte", bytes); + } else if (bytes < 1024 * 1024) { + return supportedLocale.i18n("download.size.kibibyte", (double) bytes / 1024); + } else if (bytes < 1024 * 1024 * 1024) { + return supportedLocale.i18n("download.size.megabyte", (double) bytes / (1024 * 1024)); + } else { + return supportedLocale.i18n("download.size.gibabyte", (double) bytes / (1024 * 1024 * 1024)); + } + } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 174b7a1f41..0cbb51a14d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -379,6 +379,10 @@ download.javafx.notes=We are currently downloading dependencies for HMCL from th Note: If your download speed is too slow, you can try switching to another mirror. download.javafx.component=Downloading module "%s" download.javafx.prepare=Preparing to download +download.size.byte=%d B +download.size.kibibyte=%.1f KiB +download.size.megabyte=%.1f MiB +download.size.gibabyte=%.1f GiB download.speed.byte_per_second=%d B/s download.speed.kibibyte_per_second=%.1f KiB/s download.speed.megabyte_per_second=%.1f MiB/s @@ -470,6 +474,9 @@ folder.screenshots=Screenshots folder.world=World Directory game=Games +game.clean=Clean Game Files +game.clean.loading=Loading... +game.clean.content=This operation will clean up game logs, redundant libraries, redundant resource files, etc., freeing up approximately %s of space.\nYour mods, save files, and other game data will not be affected. Do you want to continue? game.crash.feedback=Please do not share screenshots or photos of this interface with others! If you ask for help from others, please click "Export Crash Logs" and send the exported file to others for analysis. game.crash.info=Crash Info game.crash.reason=Crash Cause diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 96f7df20b1..ccc03792fd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -372,6 +372,10 @@ download.javafx=正在下載必要的執行時元件 download.javafx.notes=正在透過網路下載 HMCL 必要的執行時元件。\n點擊「切換下載源」按鈕查看詳情以及選取下載源。點擊「取消」按鈕停止並退出。\n注意:如果下載速度過慢,請嘗試切換下載源。 download.javafx.component=正在下載元件「%s」 download.javafx.prepare=準備開始下載 +download.size.byte=%d B +download.size.kibibyte=%.1f KiB +download.size.megabyte=%.1f MiB +download.size.gibabyte=%.1f GiB download.speed.byte_per_second=%d B/s download.speed.kibibyte_per_second=%.1f KiB/s download.speed.megabyte_per_second=%.1f MiB/s @@ -433,6 +437,9 @@ folder.screenshots=截圖目錄 folder.world=世界目錄 game=遊戲 +game.clean=清理遊戲文件 +game.clean.loading=统计中... +game.clean.content=本操作將會清理遊戲的日誌、冗餘庫、冗餘資源等文件,預計釋放 %s 空間。\n你的模組、存檔等遊戲數據不會受到影響。是否繼續操作? game.crash.feedback=請不要將本介面截圖或拍照給他人!如果你要求助他人,請你點擊左下角「匯出遊戲崩潰資訊」後將匯出的檔案發送給他人以供分析。\n你可以點擊下方的「幫助」前往社群尋求幫助。 game.crash.info=遊戲訊息 game.crash.reason=崩潰原因 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 2d306511af..ca2eebdf12 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -374,6 +374,10 @@ download.javafx=正在下载必要的运行时组件…… download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\n点击“切换下载源”按钮查看详情以及选择下载源。点击“取消”按钮停止并退出。\n注意:若下载速度过慢,请尝试切换下载源。 download.javafx.component=正在下载模块“%s” download.javafx.prepare=准备开始下载 +download.size.byte=%d B +download.size.kibibyte=%.1f KiB +download.size.megabyte=%.1f MiB +download.size.gibabyte=%.1f GiB download.speed.byte_per_second=%d B/s download.speed.kibibyte_per_second=%.1f KiB/s download.speed.megabyte_per_second=%.1f MiB/s @@ -437,6 +441,9 @@ folder.screenshots=截图文件夹 folder.world=世界文件夹 game=游戏 +game.clean=清理游戏文件 +game.clean.loading=统计中... +game.clean.content=本操作将会清理游戏的日志、冗余库、冗余资源等文件,预计释放 %s 空间。\n你的模组、存档等游戏数据不会受到影响。是否继续操作? game.crash.feedback=请不要将本界面截图或拍照给他人!如果你要向他人求助,请你点击左下角“导出游戏崩溃信息”后将导出的文件发送给他人以供分析。\n你可以点击下方的“帮助”前往交流群寻求帮助。 game.crash.info=游戏信息 game.crash.reason=崩溃原因