diff --git a/build.gradle.kts b/build.gradle.kts index c4288ad..ca880d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -63,7 +63,12 @@ tasks.test { } tasks.processResources { - filesMatching("**") { + filesMatching("**/*.json") { + expand( + "version" to project.version, + ) + } + filesMatching("**/*.mod.json") { expand( "version" to project.version, ) diff --git a/src/main/java/i18nupdatemod/I18nUpdateMod.java b/src/main/java/i18nupdatemod/I18nUpdateMod.java index 1afd352..0d48a09 100644 --- a/src/main/java/i18nupdatemod/I18nUpdateMod.java +++ b/src/main/java/i18nupdatemod/I18nUpdateMod.java @@ -7,6 +7,8 @@ import i18nupdatemod.core.ResourcePack; import i18nupdatemod.core.ResourcePackConverter; import i18nupdatemod.entity.GameAssetDetail; +import i18nupdatemod.core.LoadDetailUI; +import i18nupdatemod.entity.LoadStage; import i18nupdatemod.util.FileUtil; import i18nupdatemod.util.Log; @@ -24,10 +26,13 @@ public class I18nUpdateMod { public static final String MOD_ID = "i18nupdatemod"; public static String MOD_VERSION; - + public static volatile boolean shouldShutdown = false; public static final Gson GSON = new Gson(); public static void init(Path minecraftPath, String minecraftVersion, String loader) { + LoadDetailUI.show(); + LoadDetailUI.setStage(LoadStage.INIT); + try (InputStream is = I18nUpdateMod.class.getResourceAsStream("/i18nMetaData.json")) { MOD_VERSION = GSON.fromJson(new InputStreamReader(is), JsonObject.class).get("version").getAsString(); } catch (Exception e) { @@ -47,6 +52,11 @@ public static void init(Path minecraftPath, String minecraftVersion, String load Log.warning("I18nUpdateMod会从互联网获取内容不可控的资源包。"); Log.warning("这一行为违背了网易我的世界「开发者内容审核制度」:禁止上传与提审内容不一致的游戏内容。"); Log.warning("为了遵循这一制度,I18nUpdateMod不会下载任何内容。"); + + LoadDetailUI.appendLog("I18nUpdateMod会从互联网获取内容不可控的资源包。"); + LoadDetailUI.appendLog("这一行为违背了网易我的世界「开发者内容审核制度」:禁止上传与提审内容不一致的游戏内容。"); + LoadDetailUI.appendLog("为了遵循这一制度,I18nUpdateMod不会下载任何内容。"); + LoadDetailUI.appendLog("请您手动关闭此窗口"); return; } catch (ClassNotFoundException ignored) { } @@ -57,13 +67,23 @@ public static void init(Path minecraftPath, String minecraftVersion, String load try { //Get asset + if (shouldShutdown) { + return; + } GameAssetDetail assets = I18nConfig.getAssetDetail(minecraftVersion, loader); //Update resource pack + LoadDetailUI.setStage(LoadStage.DOWNLOAD_ASSET); + if (shouldShutdown) { + return; + } List languagePacks = new ArrayList<>(); boolean convertNotNeed = assets.downloads.size() == 1 && assets.downloads.get(0).targetVersion.equals(minecraftVersion); String applyFileName = assets.downloads.get(0).fileName; for (GameAssetDetail.AssetDownloadDetail it : assets.downloads) { + if (shouldShutdown) { + return; + } FileUtil.setTemporaryDirPath(Paths.get(localStorage, "." + MOD_ID, it.targetVersion)); ResourcePack languagePack = new ResourcePack(it.fileName, convertNotNeed); languagePack.checkUpdate(it.fileUrl, it.md5Url); @@ -71,22 +91,37 @@ public static void init(Path minecraftPath, String minecraftVersion, String load } //Convert resourcepack + LoadDetailUI.setStage(LoadStage.CONVERT_RESOURCE_PACK); + if (shouldShutdown) { + return; + } if (!convertNotNeed) { FileUtil.setTemporaryDirPath(Paths.get(localStorage, "." + MOD_ID, minecraftVersion)); applyFileName = assets.covertFileName; ResourcePackConverter converter = new ResourcePackConverter(languagePacks, applyFileName); converter.convert(assets.covertPackFormat, getResourcePackDescription(assets.downloads)); } + LoadDetailUI.appendLog("资源包已转换完成。"); //Apply resource pack + LoadDetailUI.setStage(LoadStage.APPLY_RESOURCE_PACK); + if (shouldShutdown) { + return; + } GameConfig config = new GameConfig(minecraftPath.resolve("options.txt")); config.addResourcePack("Minecraft-Mod-Language-Modpack", (minecraftMajorVersion <= 12 ? "" : "file/") + applyFileName); config.writeToFile(); + LoadDetailUI.appendLog("资源包已应用。"); + LoadDetailUI.setStage(LoadStage.FINISH); } catch (Exception e) { Log.warning(String.format("Failed to update resource pack: %s", e)); + LoadDetailUI.appendLog(String.format("I18n Update Mod 运行失败: %s", e)); + LoadDetailUI.appendLog("请您手动关闭此窗口"); + return; // e.printStackTrace(); } + LoadDetailUI.hide(); } private static String getResourcePackDescription(List downloads) { diff --git a/src/main/java/i18nupdatemod/core/I18nConfig.java b/src/main/java/i18nupdatemod/core/I18nConfig.java index b0169c7..63b0815 100644 --- a/src/main/java/i18nupdatemod/core/I18nConfig.java +++ b/src/main/java/i18nupdatemod/core/I18nConfig.java @@ -1,10 +1,7 @@ package i18nupdatemod.core; import com.google.gson.Gson; -import i18nupdatemod.entity.AssetMetaData; -import i18nupdatemod.entity.GameAssetDetail; -import i18nupdatemod.entity.GameMetaData; -import i18nupdatemod.entity.I18nMetaData; +import i18nupdatemod.entity.*; import i18nupdatemod.util.Log; import i18nupdatemod.util.Version; import i18nupdatemod.util.VersionRange; @@ -62,8 +59,10 @@ public static GameAssetDetail getAssetDetail(String minecraftVersion, String loa GameMetaData convert = getGameMetaData(minecraftVersion); GameAssetDetail ret = new GameAssetDetail(); + LoadDetailUI.appendLog("正在获取最快的镜像源..."); String assetRoot = getFastestUrl(); Log.debug("Using asset root: " + assetRoot); + LoadDetailUI.appendLog("即将从 " + assetRoot + " 下载资源包"); if (assetRoot.equals("https://raw.githubusercontent.com/")) { ret.downloads = createDownloadDetailsFromGit(convert, loader); diff --git a/src/main/java/i18nupdatemod/core/LoadDetailUI.java b/src/main/java/i18nupdatemod/core/LoadDetailUI.java new file mode 100644 index 0000000..63b4841 --- /dev/null +++ b/src/main/java/i18nupdatemod/core/LoadDetailUI.java @@ -0,0 +1,152 @@ +package i18nupdatemod.core; + +import i18nupdatemod.I18nUpdateMod; +import i18nupdatemod.entity.LoadStage; +import i18nupdatemod.util.Log; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.net.URL; + +public class LoadDetailUI { + private static volatile LoadDetailUI instance; + private final JFrame frame; + private final JProgressBar statusBar; + private final JTextArea logArea; + private final boolean useGUI; + + private LoadDetailUI() { + useGUI = !Boolean.parseBoolean(System.getProperty("java.awt.headless", "false")); + + if (!useGUI) { + frame = null; + statusBar = null; + logArea = null; + return; + } + + frame = new JFrame(); + frame.setTitle("I18nUpdateMod-资源包下载进度"); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.setSize(854, 480); + frame.setLocationRelativeTo(null); + frame.setLayout(new BorderLayout()); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + shutdown(); + } + }); + + + URL iconURL = getClass().getResource("/icons/CFPA.png"); + if (iconURL != null) { + Image icon = Toolkit.getDefaultToolkit().getImage(iconURL); + frame.setIconImage(icon); + } + + // 主面板 + JPanel panel = new JPanel(); + panel.setBackground(new Color(220, 220, 220)); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + + // 状态栏 + statusBar = new JProgressBar(); + statusBar.setString(LoadStage.getDescription(LoadStage.INIT)); + statusBar.setStringPainted(true); + statusBar.setMaximum(LoadStage.values().length - 1); + statusBar.setValue(0); + statusBar.setForeground(new Color(102, 255, 102)); + panel.add(statusBar); + panel.add(Box.createVerticalStrut(10)); + + // 日志输出区 + logArea = new JTextArea(6, 30); + logArea.setEditable(false); + logArea.setFont(new Font("Monospaced", Font.PLAIN, 13)); + JScrollPane scrollPane = new JScrollPane(logArea); + scrollPane.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + panel.add(scrollPane); + panel.add(Box.createVerticalStrut(10)); + + // 提示文字 + JLabel tip = new JLabel("如遇到进度卡住,可以点击下方按钮/关闭此窗口停止此次下载。"); + tip.setAlignmentX(Component.CENTER_ALIGNMENT); + panel.add(tip); + panel.add(Box.createVerticalStrut(10)); + + // 停止按钮 + JButton stopButton = new JButton("停止此次下载"); + stopButton.setFocusPainted(false); + stopButton.setBackground(Color.WHITE); + stopButton.setAlignmentX(Component.CENTER_ALIGNMENT); + stopButton.addActionListener((ActionEvent e) -> shutdown()); + panel.add(stopButton); + + frame.add(panel, BorderLayout.CENTER); + } + + public static LoadDetailUI getInstance() { + if (instance == null) { + synchronized (LoadDetailUI.class) { + if (instance == null) { + instance = new LoadDetailUI(); + } + } + } + return instance; + } + + public static void show() { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.frame == null) { + return; + } + SwingUtilities.invokeLater(() -> gui.frame.setVisible(true)); + } + + public static void hide() { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.frame == null) { + return; + } + SwingUtilities.invokeLater(() -> gui.frame.setVisible(false)); + } + + private void shutdown() { + I18nUpdateMod.shouldShutdown = true; + Log.info("User shutdown task"); + if (!useGUI) { + return; + } + hide(); + } + + public static void setStage(LoadStage stage) { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.statusBar == null || gui.logArea == null) { + return; + } + SwingUtilities.invokeLater(() -> { + gui.statusBar.setString(LoadStage.getDescription(stage)); + gui.statusBar.setValue(stage.getValue()); + gui.logArea.append("当前阶段: " + LoadStage.getDescription(stage) + "\n"); + gui.logArea.setCaretPosition(gui.logArea.getDocument().getLength()); + }); + } + + public static void appendLog(String log) { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.logArea == null) { + return; + } + SwingUtilities.invokeLater(() -> { + gui.logArea.append(log + "\n"); + gui.logArea.setCaretPosition(gui.logArea.getDocument().getLength()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/i18nupdatemod/core/ResourcePack.java b/src/main/java/i18nupdatemod/core/ResourcePack.java index 6c890e5..698dc11 100644 --- a/src/main/java/i18nupdatemod/core/ResourcePack.java +++ b/src/main/java/i18nupdatemod/core/ResourcePack.java @@ -41,11 +41,14 @@ public ResourcePack(String filename, boolean saveToGame) { public void checkUpdate(String fileUrl, String md5Url) throws IOException, URISyntaxException, NoSuchAlgorithmException { if (isUpToDate(md5Url)) { + LoadDetailUI.appendLog(filename + " 无需更新"); Log.debug("Already up to date."); return; } //In this time, we can only download full file + LoadDetailUI.appendLog("正在下载 " + filename); downloadFull(fileUrl, md5Url); + LoadDetailUI.appendLog(filename + " 下载完成"); //In the future, we will download patch file and merge local file } diff --git a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java index 0a97712..9d4928e 100644 --- a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java +++ b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java @@ -40,6 +40,7 @@ public void convert(int packFormat, String description) throws Exception { // zos.setMethod(ZipOutputStream.STORED); for (Path p : sourcePath) { Log.info("Converting: " + p); + LoadDetailUI.appendLog("正在转换 " + p); try (ZipFile zf = new ZipFile(p.toFile(), StandardCharsets.UTF_8)) { for (Enumeration e = zf.entries(); e.hasMoreElements(); ) { ZipEntry ze = e.nextElement(); diff --git a/src/main/java/i18nupdatemod/entity/LoadStage.java b/src/main/java/i18nupdatemod/entity/LoadStage.java new file mode 100644 index 0000000..31bd883 --- /dev/null +++ b/src/main/java/i18nupdatemod/entity/LoadStage.java @@ -0,0 +1,36 @@ +package i18nupdatemod.entity; + +public enum LoadStage { + INIT(0), + DOWNLOAD_ASSET(1), + CONVERT_RESOURCE_PACK(2), + APPLY_RESOURCE_PACK(3), + FINISH(4); + + private final int value; + + LoadStage(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static String getDescription(LoadStage stage) { + switch (stage) { + case INIT: + return "初始化"; + case DOWNLOAD_ASSET: + return "更新资源包"; + case CONVERT_RESOURCE_PACK: + return "转换资源包"; + case APPLY_RESOURCE_PACK: + return "应用资源包"; + case FINISH: + return "完成"; + default: + return "未知"; + } + } +} diff --git a/src/main/resources/icons/CFPA.png b/src/main/resources/icons/CFPA.png new file mode 100644 index 0000000..88d4fdb Binary files /dev/null and b/src/main/resources/icons/CFPA.png differ diff --git a/src/main/resources/icons/CFPA_with_title.png b/src/main/resources/icons/CFPA_with_title.png new file mode 100644 index 0000000..b46d3cc Binary files /dev/null and b/src/main/resources/icons/CFPA_with_title.png differ