feat(mod): mod update.
This commit is contained in:
@@ -60,7 +60,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
||||
mods.forEach(it -> {
|
||||
try {
|
||||
modManager.addMod(it);
|
||||
modManager.addMod(it.toPath());
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
Task.runAsync(() -> {
|
||||
for (File file : res) {
|
||||
try {
|
||||
modManager.addMod(file);
|
||||
modManager.addMod(file.toPath());
|
||||
succeeded.add(file.getName());
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to add mod " + file, e);
|
||||
@@ -196,6 +196,15 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
});
|
||||
}
|
||||
|
||||
public void rollback(LocalModFile from, LocalModFile to) {
|
||||
try {
|
||||
modManager.rollback(from, to);
|
||||
refresh();
|
||||
} catch (IOException ex) {
|
||||
Controllers.showToast(i18n("message.failed"));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isModded() {
|
||||
return modded.get();
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -43,6 +40,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
@@ -56,6 +54,7 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
@@ -259,9 +258,13 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
static class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
private static Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||
private static Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
|
||||
class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton restoreButton = new JFXButton();
|
||||
JFXButton infoButton = new JFXButton();
|
||||
JFXButton revealButton = new JFXButton();
|
||||
BooleanProperty booleanProperty;
|
||||
@@ -276,13 +279,18 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
restoreButton.getStyleClass().add("toggle-icon4");
|
||||
restoreButton.setGraphic(FXUtils.limitingSize(SVG.restore(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
|
||||
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
container.getChildren().setAll(checkBox, content, revealButton, infoButton);
|
||||
container.getChildren().setAll(checkBox, content, restoreButton, revealButton, infoButton);
|
||||
|
||||
StackPane.setMargin(container, new Insets(8));
|
||||
getContainer().getChildren().setAll(container);
|
||||
@@ -302,6 +310,17 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
restoreButton.setVisible(!dataItem.getModInfo().getMod().getOldFiles().isEmpty());
|
||||
restoreButton.setOnMouseClicked(e -> {
|
||||
menu.get().getContent().setAll(dataItem.getModInfo().getMod().getOldFiles().stream()
|
||||
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(), () -> {
|
||||
getSkinnable().rollback(dataItem.getModInfo(), localModFile);
|
||||
}))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
|
||||
popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight());
|
||||
});
|
||||
revealButton.setOnMouseClicked(e -> {
|
||||
FXUtils.showFileInExplorer(dataItem.getModInfo().getFile());
|
||||
});
|
||||
|
||||
@@ -32,15 +32,16 @@ import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseAddon;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
@@ -120,13 +121,26 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
private void updateMods() {
|
||||
ModUpdateTask task = new ModUpdateTask(
|
||||
modManager,
|
||||
objects.stream()
|
||||
.filter(o -> o.enabled.get())
|
||||
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0)))
|
||||
.collect(Collectors.toList()));
|
||||
Controllers.taskDialog(
|
||||
new ModUpdateTask(
|
||||
modManager,
|
||||
objects.stream()
|
||||
.filter(o -> o.enabled.get())
|
||||
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0)))
|
||||
.collect(Collectors.toList())),
|
||||
task.whenComplete(Schedulers.javafx(), exception -> {
|
||||
fireEvent(new PageCloseEvent());
|
||||
if (!task.getFailedMods().isEmpty()) {
|
||||
Controllers.dialog(i18n("mods.check_updates.failed") + "\n" +
|
||||
task.getFailedMods().stream().map(LocalModFile::getFileName).collect(Collectors.joining("\n")),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
|
||||
if (exception == null) {
|
||||
Controllers.dialog(i18n("install.success"));
|
||||
}
|
||||
}),
|
||||
i18n("mods.check_updates.update"),
|
||||
t -> {
|
||||
});
|
||||
@@ -249,6 +263,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
|
||||
public static class ModUpdateTask extends Task<Void> {
|
||||
private final Collection<Task<?>> dependents;
|
||||
private final List<LocalModFile> failedMods = new ArrayList<>();
|
||||
|
||||
ModUpdateTask(ModManager modManager, List<Pair<LocalModFile, RemoteMod.Version>> mods) {
|
||||
setStage("mods.check_updates.update");
|
||||
@@ -257,16 +272,33 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
dependents = mods.stream()
|
||||
.map(mod -> {
|
||||
return Task
|
||||
.supplyAsync(() -> {
|
||||
return null;
|
||||
.runAsync(Schedulers.javafx(), () -> {
|
||||
mod.getKey().setOld(true);
|
||||
})
|
||||
.thenComposeAsync(() -> {
|
||||
FileDownloadTask task = new FileDownloadTask(
|
||||
new URL(mod.getValue().getFile().getUrl()),
|
||||
modManager.getModsDirectory().resolve(mod.getValue().getFile().getFilename()).toFile());
|
||||
|
||||
task.setName(mod.getValue().getName());
|
||||
return task;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
// restore state if failed
|
||||
mod.getKey().setOld(false);
|
||||
failedMods.add(mod.getKey());
|
||||
}
|
||||
})
|
||||
.setName(mod.getKey().getName())
|
||||
.setSignificance(TaskSignificance.MAJOR)
|
||||
.withCounter("mods.check_updates.update");
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<LocalModFile> getFailedMods() {
|
||||
return failedMods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
|
||||
@@ -486,6 +486,7 @@ message.default=Default
|
||||
message.doing=Please wait
|
||||
message.downloading=Downloading...
|
||||
message.error=Error
|
||||
message.failed=Operation failed
|
||||
message.info=Info
|
||||
message.success=Job completed successfully
|
||||
message.unknown=Unknown
|
||||
@@ -583,6 +584,7 @@ mods.add.success=Successfully installed mods %s.
|
||||
mods.category=Category
|
||||
mods.check_updates=Check updates
|
||||
mods.check_updates.current_version=Current
|
||||
mods.check_updates.failed=Failed to download some of files
|
||||
mods.check_updates.file=File
|
||||
mods.check_updates.source=Source
|
||||
mods.check_updates.target_version=Target
|
||||
@@ -602,6 +604,7 @@ mods.mcmod.search=Search in MCMOD
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Name
|
||||
mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader)
|
||||
mods.restore=Restore
|
||||
mods.url=Official Page
|
||||
|
||||
multiplayer=Multiplayer
|
||||
|
||||
@@ -486,6 +486,7 @@ message.default=預設
|
||||
message.doing=請耐心等待
|
||||
message.downloading=正在下載…
|
||||
message.error=錯誤
|
||||
message.failed=操作失敗
|
||||
message.info=資訊
|
||||
message.success=完成
|
||||
message.unknown=未知
|
||||
@@ -583,6 +584,7 @@ mods.add.success=成功新增模組 %s。
|
||||
mods.category=類別
|
||||
mods.check_updates=檢查模組更新
|
||||
mods.check_updates.current_version=當前版本
|
||||
mods.check_updates.failed=部分文件下載失敗
|
||||
mods.check_updates.file=文件
|
||||
mods.check_updates.source=來源
|
||||
mods.check_updates.target_version=目標版本
|
||||
@@ -602,6 +604,7 @@ mods.mcmod.search=MC 百科蒐索
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=名稱
|
||||
mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。
|
||||
mods.restore=回退
|
||||
mods.url=官方頁面
|
||||
|
||||
multiplayer=多人聯機
|
||||
|
||||
@@ -486,6 +486,7 @@ message.default=默认
|
||||
message.doing=请耐心等待
|
||||
message.downloading=正在下载
|
||||
message.error=错误
|
||||
message.failed=操作失败
|
||||
message.info=提示
|
||||
message.success=已完成
|
||||
message.unknown=未知
|
||||
@@ -583,6 +584,7 @@ mods.add.success=成功添加模组 %s。
|
||||
mods.category=类别
|
||||
mods.check_updates=检查模组更新
|
||||
mods.check_updates.current_version=当前版本
|
||||
mods.check_updates.failed=部分文件下载失败
|
||||
mods.check_updates.file=文件
|
||||
mods.check_updates.source=来源
|
||||
mods.check_updates.target_version=目标版本
|
||||
@@ -602,6 +604,7 @@ mods.mcmod.search=MC 百科搜索
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=名称
|
||||
mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。
|
||||
mods.restore=回退
|
||||
mods.url=官方页面
|
||||
|
||||
multiplayer=多人联机
|
||||
|
||||
@@ -17,27 +17,21 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Immutable
|
||||
@@ -64,14 +58,14 @@ public final class FabricModMetadata {
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||
Path mcmod = fs.getPath("fabric.mod.json");
|
||||
if (Files.notExists(mcmod))
|
||||
throw new IOException("File " + modFile + " is not a Fabric mod.");
|
||||
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
|
||||
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
|
||||
return new LocalModFile(modFile, ModLoaderType.FABRIC, metadata.id, metadata.name, new LocalModFile.Description(metadata.description),
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalModFile.Description(metadata.description),
|
||||
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
@@ -116,8 +115,8 @@ public final class ForgeNewModMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||
Path modstoml = fs.getPath("META-INF/mods.toml");
|
||||
if (Files.notExists(modstoml))
|
||||
throw new IOException("File " + modFile + " is not a Forge 1.13+ mod.");
|
||||
@@ -132,10 +131,10 @@ public final class ForgeNewModMetadata {
|
||||
Manifest manifest = new Manifest(Files.newInputStream(manifestMF));
|
||||
jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile.getPath());
|
||||
LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile);
|
||||
}
|
||||
}
|
||||
return new LocalModFile(modFile, ModLoaderType.FORGE, mod.getModId(), mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
|
||||
mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||
mod.getDisplayURL(),
|
||||
metadata.getLogoFile());
|
||||
|
||||
@@ -20,12 +20,12 @@ package org.jackhuang.hmcl.mod;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
@@ -120,8 +120,8 @@ public final class ForgeOldModMetadata {
|
||||
return authors;
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||
Path mcmod = fs.getPath("mcmod.info");
|
||||
if (Files.notExists(mcmod))
|
||||
throw new IOException("File " + modFile + " is not a Forge mod.");
|
||||
@@ -138,7 +138,7 @@ public final class ForgeOldModMetadata {
|
||||
authors = String.join(", ", metadata.getAuthorList());
|
||||
if (StringUtils.isBlank(authors))
|
||||
authors = metadata.getCredits();
|
||||
return new LocalModFile(modFile, ModLoaderType.FORGE, metadata.getModId(), metadata.getName(), new LocalModFile.Description(metadata.getDescription()),
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()),
|
||||
authors, metadata.getVersion(), metadata.getGameVersion(),
|
||||
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url,
|
||||
metadata.getLogoFile());
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
@@ -108,15 +107,15 @@ public final class LiteModMetadata {
|
||||
return updateURI;
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
||||
try (ZipFile zipFile = new ZipFile(modFile)) {
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||
try (ZipFile zipFile = new ZipFile(modFile.toFile())) {
|
||||
ZipEntry entry = zipFile.getEntry("litemod.json");
|
||||
if (entry == null)
|
||||
throw new IOException("File " + modFile + "is not a LiteLoader mod.");
|
||||
LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class);
|
||||
if (metadata == null)
|
||||
throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
|
||||
return new LocalModFile(modFile, ModLoaderType.FORGE, null, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(),
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(),
|
||||
metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), "");
|
||||
}
|
||||
}
|
||||
|
||||
63
HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java
Normal file
63
HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.mod;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
|
||||
public class LocalMod {
|
||||
|
||||
private final String id;
|
||||
private final ModLoaderType modLoaderType;
|
||||
private final HashSet<LocalModFile> files = new HashSet<>();
|
||||
private final HashSet<LocalModFile> oldFiles = new HashSet<>();
|
||||
|
||||
public LocalMod(String id, ModLoaderType modLoaderType) {
|
||||
this.id = id;
|
||||
this.modLoaderType = modLoaderType;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ModLoaderType getModLoaderType() {
|
||||
return modLoaderType;
|
||||
}
|
||||
|
||||
public HashSet<LocalModFile> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public HashSet<LocalModFile> getOldFiles() {
|
||||
return oldFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LocalMod localMod = (LocalMod) o;
|
||||
return Objects.equals(id, localMod.id) && modLoaderType == localMod.modLoaderType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, modLoaderType);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 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
|
||||
@@ -20,10 +20,8 @@ package org.jackhuang.hmcl.mod;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -37,8 +35,8 @@ import java.util.stream.Collectors;
|
||||
public final class LocalModFile implements Comparable<LocalModFile> {
|
||||
|
||||
private Path file;
|
||||
private final ModLoaderType modLoaderType;
|
||||
private final String id;
|
||||
private final ModManager modManager;
|
||||
private final LocalMod mod;
|
||||
private final String name;
|
||||
private final Description description;
|
||||
private final String authors;
|
||||
@@ -49,14 +47,14 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
||||
private final String logoPath;
|
||||
private final BooleanProperty activeProperty;
|
||||
|
||||
public LocalModFile(File file, ModLoaderType modLoaderType, String id, String name, Description description) {
|
||||
this(file, modLoaderType, id, name, description, "", "", "", "", "");
|
||||
public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description) {
|
||||
this(modManager, mod, file, name, description, "", "", "", "", "");
|
||||
}
|
||||
|
||||
public LocalModFile(File file, ModLoaderType modLoaderType, String id, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath) {
|
||||
this.file = file.toPath();
|
||||
this.modLoaderType = modLoaderType;
|
||||
this.id = id;
|
||||
public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath) {
|
||||
this.modManager = modManager;
|
||||
this.mod = mod;
|
||||
this.file = file;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.authors = authors;
|
||||
@@ -65,23 +63,39 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
||||
this.url = url;
|
||||
this.logoPath = logoPath;
|
||||
|
||||
activeProperty = new SimpleBooleanProperty(this, "active", !ModManager.isDisabled(file)) {
|
||||
activeProperty = new SimpleBooleanProperty(this, "active", !modManager.isDisabled(file)) {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
if (isOld()) return;
|
||||
|
||||
Path path = LocalModFile.this.file.toAbsolutePath();
|
||||
|
||||
try {
|
||||
if (get())
|
||||
LocalModFile.this.file = ModManager.enableMod(path);
|
||||
LocalModFile.this.file = modManager.enableMod(path);
|
||||
else
|
||||
LocalModFile.this.file = ModManager.disableMod(path);
|
||||
LocalModFile.this.file = modManager.disableMod(path);
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to invert state of mod file " + path, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fileName = StringUtils.substringBeforeLast(isActive() ? file.getName() : FileUtils.getNameWithoutExtension(file), '.');
|
||||
fileName = FileUtils.getNameWithoutExtension(ModManager.getModName(file));
|
||||
|
||||
if (isOld()) {
|
||||
mod.getOldFiles().add(this);
|
||||
} else {
|
||||
mod.getFiles().add(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ModManager getModManager() {
|
||||
return modManager;
|
||||
}
|
||||
|
||||
public LocalMod getMod() {
|
||||
return mod;
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
@@ -89,11 +103,11 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
||||
}
|
||||
|
||||
public ModLoaderType getModLoaderType() {
|
||||
return modLoaderType;
|
||||
return mod.getModLoaderType();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
return mod.getId();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -140,12 +154,29 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public boolean isOld() {
|
||||
return modManager.isOld(file);
|
||||
}
|
||||
|
||||
public void setOld(boolean old) throws IOException {
|
||||
file = modManager.setOld(this, old);
|
||||
|
||||
if (old) {
|
||||
mod.getFiles().remove(this);
|
||||
mod.getOldFiles().add(this);
|
||||
} else {
|
||||
mod.getOldFiles().remove(this);
|
||||
mod.getFiles().add(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException {
|
||||
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
|
||||
if (!currentVersion.isPresent()) return null;
|
||||
List<RemoteMod.Version> remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid())
|
||||
.filter(version -> version.getGameVersions().contains(gameVersion))
|
||||
.filter(version -> version.getLoaders().contains(modLoaderType))
|
||||
.filter(version -> version.getLoaders().contains(getModLoaderType()))
|
||||
.filter(version -> version.getDatePublished().compareTo(currentVersion.get().getDatePublished()) > 0)
|
||||
.sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed())
|
||||
.collect(Collectors.toList());
|
||||
if (remoteVersions.isEmpty()) return null;
|
||||
|
||||
@@ -23,16 +23,18 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class ModManager {
|
||||
private final GameRepository repository;
|
||||
private final String id;
|
||||
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
|
||||
private final HashMap<LocalMod, LocalMod> localMods = new HashMap<>();
|
||||
|
||||
private boolean loaded = false;
|
||||
|
||||
@@ -49,60 +51,68 @@ public final class ModManager {
|
||||
return id;
|
||||
}
|
||||
|
||||
private Path getModsDirectory() {
|
||||
public Path getModsDirectory() {
|
||||
return repository.getRunDirectory(id).toPath().resolve("mods");
|
||||
}
|
||||
|
||||
private void addModInfo(File file) {
|
||||
public LocalMod getLocalMod(String id, ModLoaderType modLoaderType) {
|
||||
return localMods.computeIfAbsent(new LocalMod(id, modLoaderType), x -> x);
|
||||
}
|
||||
|
||||
private void addModInfo(Path file) {
|
||||
try {
|
||||
localModFiles.add(getModInfo(file));
|
||||
LocalModFile localModFile = getModInfo(file);
|
||||
if (!localModFile.isOld()) {
|
||||
localModFiles.add(localModFile);
|
||||
}
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalModFile getModInfo(File modFile) {
|
||||
File file = isDisabled(modFile) ? new File(modFile.getAbsoluteFile().getParentFile(), FileUtils.getNameWithoutExtension(modFile)) : modFile;
|
||||
String description, extension = FileUtils.getExtension(file);
|
||||
switch (extension) {
|
||||
case "zip":
|
||||
case "jar":
|
||||
try {
|
||||
return ForgeOldModMetadata.fromFile(modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
public LocalModFile getModInfo(Path modFile) {
|
||||
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||
String description;
|
||||
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
|
||||
try {
|
||||
return ForgeOldModMetadata.fromFile(this, modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return ForgeNewModMetadata.fromFile(modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
return ForgeNewModMetadata.fromFile(this, modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return FabricModMetadata.fromFile(modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
return FabricModMetadata.fromFile(this, modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return PackMcMeta.fromFile(modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
return PackMcMeta.fromFile(this, modFile);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
description = "";
|
||||
break;
|
||||
case "litemod":
|
||||
try {
|
||||
return LiteModMetadata.fromFile(modFile);
|
||||
} catch (Exception ignore) {
|
||||
description = "LiteLoader Mod";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
|
||||
description = "";
|
||||
} else if (fileName.endsWith(".litemod")) {
|
||||
try {
|
||||
return LiteModMetadata.fromFile(this, modFile);
|
||||
} catch (Exception ignore) {
|
||||
description = "LiteLoader Mod";
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
|
||||
}
|
||||
return new LocalModFile(modFile, ModLoaderType.UNKNOWN, null, FileUtils.getNameWithoutExtension(modFile), new LocalModFile.Description(description));
|
||||
return new LocalModFile(this,
|
||||
getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN),
|
||||
modFile,
|
||||
FileUtils.getNameWithoutExtension(modFile),
|
||||
new LocalModFile.Description(description));
|
||||
}
|
||||
|
||||
public void refreshMods() throws IOException {
|
||||
localModFiles.clear();
|
||||
localMods.clear();
|
||||
if (Files.isDirectory(getModsDirectory())) {
|
||||
try (DirectoryStream<Path> modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) {
|
||||
for (Path subitem : modsDirectoryStream) {
|
||||
@@ -110,11 +120,11 @@ public final class ModManager {
|
||||
// If the folder name is game version, forge will search mod in this subdirectory
|
||||
try (DirectoryStream<Path> subitemDirectoryStream = Files.newDirectoryStream(subitem)) {
|
||||
for (Path subsubitem : subitemDirectoryStream) {
|
||||
addModInfo(subsubitem.toFile());
|
||||
addModInfo(subsubitem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addModInfo(subitem.toFile());
|
||||
addModInfo(subitem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,18 +138,17 @@ public final class ModManager {
|
||||
return localModFiles;
|
||||
}
|
||||
|
||||
public void addMod(File file) throws IOException {
|
||||
public void addMod(Path file) throws IOException {
|
||||
if (!isFileNameMod(file))
|
||||
throw new IllegalArgumentException("File " + file + " is not a valid mod file.");
|
||||
|
||||
if (!loaded)
|
||||
refreshMods();
|
||||
|
||||
File modsDirectory = new File(repository.getRunDirectory(id), "mods");
|
||||
if (!FileUtils.makeDirectory(modsDirectory))
|
||||
throw new IOException("Cannot make directory " + modsDirectory);
|
||||
Path modsDirectory = getModsDirectory();
|
||||
Files.createDirectories(modsDirectory);
|
||||
|
||||
File newFile = new File(modsDirectory, file.getName());
|
||||
Path newFile = modsDirectory.resolve(file.getFileName());
|
||||
FileUtils.copyFile(file, newFile);
|
||||
|
||||
addModInfo(newFile);
|
||||
@@ -151,32 +160,105 @@ public final class ModManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static Path disableMod(Path file) throws IOException {
|
||||
Path disabled = file.getParent().resolve(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
||||
public void rollback(LocalModFile from, LocalModFile to) throws IOException {
|
||||
if (!loaded) {
|
||||
throw new IllegalStateException("ModManager Not loaded");
|
||||
}
|
||||
if (!localModFiles.contains(from)) {
|
||||
throw new IllegalStateException("Rolling back an unknown mod " + from.getFileName());
|
||||
}
|
||||
if (from.isOld()) {
|
||||
throw new IllegalArgumentException("Rolling back an old mod " + from.getFileName());
|
||||
}
|
||||
if (!to.isOld()) {
|
||||
throw new IllegalArgumentException("Rolling back to an old path " + to.getFileName());
|
||||
}
|
||||
if (from.getFileName().equals(to.getFileName())) {
|
||||
// We cannot roll back to the mod with the same name.
|
||||
return;
|
||||
}
|
||||
|
||||
LocalMod mod = Objects.requireNonNull(from.getMod());
|
||||
if (mod != to.getMod()) {
|
||||
throw new IllegalArgumentException("Rolling back mod " + from.getFileName() + " to a different mod " + to.getFileName());
|
||||
}
|
||||
if (!mod.getFiles().contains(from)
|
||||
|| !mod.getOldFiles().contains(to)) {
|
||||
throw new IllegalStateException("LocalMod state corrupt");
|
||||
}
|
||||
|
||||
boolean active = from.isActive();
|
||||
from.setActive(true);
|
||||
from.setOld(true);
|
||||
to.setOld(false);
|
||||
to.setActive(active);
|
||||
}
|
||||
|
||||
private Path backupMod(Path file) throws IOException {
|
||||
Path newPath = file.resolveSibling(
|
||||
StringUtils.addSuffix(
|
||||
StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION),
|
||||
OLD_EXTENSION
|
||||
)
|
||||
);
|
||||
if (Files.exists(file)) {
|
||||
Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
private Path restoreMod(Path file) throws IOException {
|
||||
Path newPath = file.resolveSibling(
|
||||
StringUtils.removeSuffix(FileUtils.getName(file), OLD_EXTENSION)
|
||||
);
|
||||
if (Files.exists(file)) {
|
||||
Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
public Path setOld(LocalModFile modFile, boolean old) throws IOException {
|
||||
Path newPath;
|
||||
if (old) {
|
||||
newPath = backupMod(modFile.getFile());
|
||||
localModFiles.remove(modFile);
|
||||
} else {
|
||||
newPath = restoreMod(modFile.getFile());
|
||||
localModFiles.add(modFile);
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
public Path disableMod(Path file) throws IOException {
|
||||
if (isOld(file)) return file; // no need to disable an old mod.
|
||||
Path disabled = file.resolveSibling(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
||||
if (Files.exists(file))
|
||||
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
|
||||
return disabled;
|
||||
}
|
||||
|
||||
public static Path enableMod(Path file) throws IOException {
|
||||
Path enabled = file.getParent().resolve(StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
||||
public Path enableMod(Path file) throws IOException {
|
||||
if (isOld(file)) return file;
|
||||
Path enabled = file.resolveSibling(StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
||||
if (Files.exists(file))
|
||||
Files.move(file, enabled, StandardCopyOption.REPLACE_EXISTING);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public static boolean isOld(File file) {
|
||||
return file.getPath().endsWith(OLD_EXTENSION);
|
||||
public static String getModName(Path file) {
|
||||
return StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||
}
|
||||
|
||||
public static boolean isDisabled(File file) {
|
||||
return file.getPath().endsWith(DISABLED_EXTENSION);
|
||||
public boolean isOld(Path file) {
|
||||
return FileUtils.getName(file).endsWith(OLD_EXTENSION);
|
||||
}
|
||||
|
||||
public static boolean isFileNameMod(File file) {
|
||||
String name = file.getName();
|
||||
if (isDisabled(file))
|
||||
name = FileUtils.getNameWithoutExtension(file);
|
||||
public boolean isDisabled(Path file) {
|
||||
return FileUtils.getName(file).endsWith(DISABLED_EXTENSION);
|
||||
}
|
||||
|
||||
public static boolean isFileNameMod(Path file) {
|
||||
String name = getModName(file);
|
||||
return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.FileSystem;
|
||||
@@ -144,13 +143,19 @@ public class PackMcMeta implements Validation {
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||
Path mcmod = fs.getPath("pack.mcmeta");
|
||||
if (Files.notExists(mcmod))
|
||||
throw new IOException("File " + modFile + " is not a resource pack.");
|
||||
PackMcMeta metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), PackMcMeta.class);
|
||||
return new LocalModFile(modFile, ModLoaderType.PACK, null, FileUtils.getNameWithoutExtension(modFile), metadata.pack.description, "", "", "", "", "");
|
||||
return new LocalModFile(
|
||||
modManager,
|
||||
modManager.getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.PACK),
|
||||
modFile,
|
||||
FileUtils.getNameWithoutExtension(modFile),
|
||||
metadata.pack.description,
|
||||
"", "", "", "", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +507,7 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
this,
|
||||
Integer.toString(projectId),
|
||||
getDisplayName(),
|
||||
null,
|
||||
getFileName(),
|
||||
null,
|
||||
getParsedFileDate(),
|
||||
versionType,
|
||||
|
||||
@@ -75,6 +75,10 @@ public final class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getNameWithoutExtension(String fileName) {
|
||||
return StringUtils.substringBeforeLast(fileName, '.');
|
||||
}
|
||||
|
||||
public static String getNameWithoutExtension(File file) {
|
||||
return StringUtils.substringBeforeLast(file.getName(), '.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user