diff --git a/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java b/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java index 13e87e4..f37096e 100644 --- a/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java +++ b/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java @@ -9,9 +9,11 @@ import javafx.concurrent.Task; */ public abstract class BaseTask extends Task { private final ThreadPoolManager Executor = ThreadPoolManager.getInstance(); + protected Listener listener; @Override protected Void call() throws Exception { + if (listener != null) listener.onStart(); task(); return null; } @@ -23,7 +25,8 @@ public abstract class BaseTask extends Task { protected abstract void task() throws Exception; public void onListen(Listener listener) { - if (listener == null) return; + this.listener = listener; + if (this.listener == null) return; setOnRunning(_ -> listener.onRunning()); setOnCancelled(_ -> listener.onCancelled()); @@ -36,6 +39,9 @@ public abstract class BaseTask extends Task { } public interface Listener { + default void onStart() { + } + default void onRunning() { } diff --git a/src/main/java/cn/octopusyan/alistgui/config/Context.java b/src/main/java/cn/octopusyan/alistgui/config/Context.java index 8f72290..3df7a91 100644 --- a/src/main/java/cn/octopusyan/alistgui/config/Context.java +++ b/src/main/java/cn/octopusyan/alistgui/config/Context.java @@ -1,5 +1,6 @@ package cn.octopusyan.alistgui.config; +import atlantafx.base.theme.Theme; import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.controller.AboutController; import cn.octopusyan.alistgui.controller.MainController; @@ -36,6 +37,7 @@ public class Context { private static final Logger log = LoggerFactory.getLogger(Context.class); private static Scene scene; private static final IntegerProperty currentViewIndexProperty = new SimpleIntegerProperty(0); + private static final ObjectProperty theme = new SimpleObjectProperty<>(ConfigManager.theme()); /** * 控制器集合 @@ -82,6 +84,10 @@ public class Context { }; } + public static ObjectProperty themeProperty() { + return theme; + } + // 获取当前所选时区属性 public static ObjectProperty currentLocaleProperty() { return currentLocale; diff --git a/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java b/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java index da7a22f..2c99384 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java @@ -1,6 +1,7 @@ package cn.octopusyan.alistgui.controller; import cn.octopusyan.alistgui.base.BaseController; +import cn.octopusyan.alistgui.manager.ConfigManager; import cn.octopusyan.alistgui.viewModel.AboutViewModule; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -49,6 +50,6 @@ public class AboutController extends BaseController { @FXML public void checkAListUpdate() { - viewModule.checkAListUpdate(); + viewModule.checkUpdate(ConfigManager.aList()); } } diff --git a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java index 3b82020..ac81b78 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java @@ -1,5 +1,6 @@ package cn.octopusyan.alistgui.controller; +import atlantafx.base.theme.Theme; import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.enums.ProxySetup; @@ -14,6 +15,7 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; +import javafx.util.StringConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,7 +39,7 @@ public class SetupController extends BaseController implements Initializab @FXML public ComboBox languageComboBox; @FXML - public ComboBox themeComboBox; + public ComboBox themeComboBox; @FXML public ComboBox proxySetupComboBox; @FXML @@ -59,8 +61,20 @@ public class SetupController extends BaseController implements Initializab @Override public void initData() { languageComboBox.setItems(FXCollections.observableList(Context.SUPPORT_LANGUAGE_LIST)); - themeComboBox.setItems(FXCollections.observableList(ConfigManager.THEME_NAME_LIST)); + themeComboBox.setItems(FXCollections.observableList(ConfigManager.THEME_LIST)); proxySetupComboBox.setItems(FXCollections.observableList(List.of(ProxySetup.values()))); + + themeComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Theme object) { + return object.getName(); + } + + @Override + public Theme fromString(String string) { + return ConfigManager.THEME_MAP.get(string); + } + }); } @Override @@ -71,7 +85,7 @@ public class SetupController extends BaseController implements Initializab }); languageComboBox.getSelectionModel().select(ConfigManager.language()); - themeComboBox.getSelectionModel().select(ConfigManager.themeName()); + themeComboBox.getSelectionModel().select(ConfigManager.theme()); proxySetupComboBox.getSelectionModel().select(ConfigManager.proxySetup()); } diff --git a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java index 5fcc66e..17d7b37 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java @@ -3,6 +3,7 @@ package cn.octopusyan.alistgui.manager; import atlantafx.base.theme.*; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.net.NetUtil; import cn.hutool.core.util.NumberUtil; import cn.octopusyan.alistgui.Application; import cn.octopusyan.alistgui.config.Constants; @@ -12,6 +13,7 @@ import cn.octopusyan.alistgui.model.GuiConfig; import cn.octopusyan.alistgui.model.ProxyInfo; import cn.octopusyan.alistgui.model.UpgradeConfig; import cn.octopusyan.alistgui.model.upgrade.AList; +import cn.octopusyan.alistgui.model.upgrade.Gui; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -22,11 +24,14 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.util.Arrays; +import java.net.InetSocketAddress; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.regex.Matcher; +import java.util.stream.Collectors; /** * 客户端设置 @@ -37,22 +42,17 @@ public class ConfigManager { private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class); public static ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); public static final Locale DEFAULT_LANGUAGE = Locale.SIMPLIFIED_CHINESE; - private static GuiConfig guiConfig; - private static UpgradeConfig upgradeConfig; - - public static final String DEFAULT_THEME = "Primer Light"; - public static List THEME_LIST = Arrays.asList( + public static final String DEFAULT_THEME = new PrimerLight().getName(); + public static List THEME_LIST = List.of( new PrimerLight(), new PrimerDark(), new NordLight(), new NordDark(), new CupertinoLight(), new CupertinoDark(), new Dracula() ); - public static List THEME_NAME_LIST = Arrays.asList( - "Primer Light", "Primer Dark", - "Nord Light", "Nord Dark", - "Cupertino Light", "Cupertino Dark", - "Dracula" - ); + public static Map THEME_MAP = THEME_LIST.stream() + .collect(Collectors.toMap(Theme::getName, Function.identity())); + + private static GuiConfig guiConfig; static { objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); @@ -61,7 +61,6 @@ public class ConfigManager { public static void load() { guiConfig = loadConfig(Constants.GUI_CONFIG_PATH, GuiConfig.class); - upgradeConfig = loadConfig(Constants.UPGRADE_PATH, UpgradeConfig.class); } private static T loadConfig(String path, Class clazz) { @@ -77,6 +76,24 @@ public class ConfigManager { return null; } + private static void checkFile(File src, Class clazz) throws Exception { + File parent = FileUtil.getParent(src, 1); + if (!parent.exists()) { + boolean wasSuccessful = parent.mkdirs(); + objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance()); + if (!wasSuccessful) + logger.error("{} 创建失败", src.getAbsolutePath()); + } + } + + public static void save() { + try { + objectMapper.writeValue(new File(Constants.GUI_CONFIG_PATH), guiConfig); + } catch (IOException e) { + logger.error("save config error", e); + } + } + // --------------------------------{ 主题 }------------------------------------------ public static String themeName() { @@ -84,82 +101,15 @@ public class ConfigManager { } public static Theme theme() { - return THEME_LIST.get(THEME_NAME_LIST.indexOf(themeName())); + return THEME_MAP.get(themeName()); } - public static void themeName(String themeName) { - int themeIndex = THEME_NAME_LIST.indexOf(themeName); - if (themeIndex < 0) return; - - guiConfig.setTheme(themeName); - Application.setUserAgentStylesheet(theme().getUserAgentStylesheet()); + public static void theme(Theme theme) { + Application.setUserAgentStylesheet(theme.getUserAgentStylesheet()); + guiConfig.setTheme(theme.getName()); } - public static boolean hasProxy() { - if (guiConfig == null) - return false; - ProxyInfo proxyInfo = getProxyInfo(); - return proxyInfo != null - && StringUtils.isNoneEmpty(proxyInfo.getHost()) - && StringUtils.isNoneEmpty(proxyInfo.getPort()) - && Integer.parseInt(proxyInfo.getPort()) > 0; - } - - public static ProxyInfo getProxyInfo() { - return guiConfig.getProxyInfo(); - } - - private static void setProxyInfo(ProxyInfo info) { - guiConfig.setProxyInfo(info); - } - - public static String proxyHost() { - if (getProxyInfo() == null) return null; - return getProxyInfo().getHost(); - } - - public static void proxyHost(String host) { - final Matcher matcher = PatternPool.IPV4.matcher(host); - if (matcher.matches()) { - if (getProxyInfo() == null) - setProxyInfo(new ProxyInfo()); - getProxyInfo().setHost(host); - } - } - - public static String proxyPort() { - if (getProxyInfo() == null) return null; - return getProxyInfo().getPort(); - } - - public static int getProxyPort() { - return Integer.parseInt(getProxyInfo().getPort()); - } - - public static void proxyPort(String port) { - if (NumberUtil.isNumber(port)) { - if (getProxyInfo() == null) - setProxyInfo(new ProxyInfo()); - getProxyInfo().setPort(port); - } - } - - public static Locale language() { - String language = guiConfig.getLanguage(); - return LocaleUtils.toLocale(Optional.ofNullable(language).orElse(DEFAULT_LANGUAGE.toString())); - } - - public static void language(Locale locale) { - guiConfig.setLanguage(locale.toString()); - } - - public static boolean autoStart() { - return guiConfig.getAutoStart(); - } - - public static void autoStart(Boolean autoStart) { - guiConfig.setAutoStart(autoStart); - } +// --------------------------------{ 网络代理 }------------------------------------------ public static ProxySetup proxySetup() { return ProxySetup.valueOf(StringUtils.upperCase(guiConfig.getProxySetup())); @@ -186,6 +136,94 @@ public class ConfigManager { } } + public static boolean hasProxy() { + if (guiConfig == null) + return false; + ProxyInfo proxyInfo = getProxyInfo(); + return proxyInfo != null + && StringUtils.isNoneEmpty(proxyInfo.getHost()) + && StringUtils.isNoneEmpty(proxyInfo.getPort()) + && Integer.parseInt(proxyInfo.getPort()) > 0; + } + + public static ProxyInfo getProxyInfo() { + ProxyInfo proxyInfo = guiConfig.getProxyInfo(); + + if (proxyInfo == null) + setProxyInfo(new ProxyInfo()); + + return guiConfig.getProxyInfo(); + } + + private static void setProxyInfo(ProxyInfo info) { + guiConfig.setProxyInfo(info); + } + + public static String proxyHost() { + return getProxyInfo().getHost(); + } + + public static void proxyHost(String host) { + final Matcher matcher = PatternPool.IPV4.matcher(host); + if (!matcher.matches()) return; + + getProxyInfo().setHost(host); + + checkProxy(); + } + + public static String proxyPort() { + return getProxyInfo().getPort(); + } + + public static int getProxyPort() { + return Integer.parseInt(proxyPort()); + } + + public static void proxyPort(String port) { + if (!NumberUtil.isNumber(port)) return; + + getProxyInfo().setPort(port); + + checkProxy(); + } + + private static void checkProxy() { + if (!hasProxy()) return; + + try { + InetSocketAddress address = NetUtil.createAddress(proxyHost(), getProxyPort()); + if (NetUtil.isOpen(address, 2000)) { + HttpUtil.getInstance().proxy(proxySetup(), ConfigManager.getProxyInfo()); + } + } catch (Exception e) { + logger.error(STR."host=\{proxyHost()},port=\{proxyPort()}", e); + } + } + +// --------------------------------{ 语言 }------------------------------------------ + + public static Locale language() { + String language = guiConfig.getLanguage(); + return LocaleUtils.toLocale(Optional.ofNullable(language).orElse(DEFAULT_LANGUAGE.toString())); + } + + public static void language(Locale locale) { + guiConfig.setLanguage(locale.toString()); + } + +// --------------------------------{ 开机自启 }------------------------------------------ + + public static boolean autoStart() { + return guiConfig.getAutoStart(); + } + + public static void autoStart(Boolean autoStart) { + guiConfig.setAutoStart(autoStart); + } + +// --------------------------------{ 静默启动 }------------------------------------------ + public static boolean silentStartup() { return guiConfig.getSilentStartup(); } @@ -194,35 +232,33 @@ public class ConfigManager { guiConfig.setSilentStartup(startup); } +// --------------------------------{ 版本检查 }------------------------------------------ + + public static UpgradeConfig upgradeConfig() { + return guiConfig.getUpgradeConfig(); + } + public static AList aList() { - return upgradeConfig.getAList(); + return upgradeConfig().getAList(); } public static String aListVersion() { - return upgradeConfig.getAList().getVersion(); + return aList().getVersion(); } public static void aListVersion(String version) { - upgradeConfig.getAList().setVersion(version); + aList().setVersion(version); } - - private static void checkFile(File src, Class clazz) throws Exception { - File parent = FileUtil.getParent(src, 1); - if (!parent.exists()) { - boolean wasSuccessful = parent.mkdirs(); - objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance()); - if (!wasSuccessful) - logger.error("{} 创建失败", src.getAbsolutePath()); - } + public static Gui gui() { + return upgradeConfig().getGui(); } - public static void save() { - try { - objectMapper.writeValue(new File(Constants.GUI_CONFIG_PATH), guiConfig); - objectMapper.writeValue(new File(Constants.UPGRADE_PATH), upgradeConfig); - } catch (IOException e) { - logger.error("save config error", e); - } + public static String guiVersion() { + return gui().getVersion(); + } + + public static void guiVersion(String version) { + gui().setVersion(version); } } diff --git a/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java b/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java index 25886b1..f3490e9 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java @@ -1,15 +1,22 @@ package cn.octopusyan.alistgui.manager; import atlantafx.base.util.BBCodeParser; +import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; +import java.util.Arrays; import java.util.Date; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 模拟控制台输出 @@ -19,18 +26,14 @@ import java.util.Date; public class ConsoleLog { public static final String format = "yyyy/MM/dd hh:mm:ss"; private volatile static ConsoleLog log; - private final ScrollPane logAreaSp; private final VBox textArea; + private final static String CONSOLE_COLOR_PREFIX = "^\033\\[(\\d+)m(.*)\033\\[0m(.*)$"; + private final static String CONSOLE_COLOR_SUFFIX = "\033[0m"; private ConsoleLog(ScrollPane logAreaSp, VBox textArea) { this.textArea = textArea; - this.logAreaSp = logAreaSp; - textArea.maxWidthProperty().bind(logAreaSp.prefWidthProperty()); - - textArea.heightProperty().subscribe(() -> { - logAreaSp.vvalueProperty().setValue(1); - }); + textArea.heightProperty().subscribe(() -> logAreaSp.vvalueProperty().setValue(1)); } public static ConsoleLog getInstance() { @@ -63,41 +66,120 @@ public class ConsoleLog { } public static void info(String tag, String message, Object... param) { - printLog(tag, "INFO", message, param); + printLog(tag, Level.INFO, message, param); } public static void warning(String tag, String message, Object... param) { - printLog(tag, "[color=-color-chart-2]WARN[/color]", message, param); + printLog(tag, Level.WARN, message, param); } public static void error(String tag, String message, Object... param) { - printLog(tag, "[color=-color-chart-3]ERROR[/color]", message, param); + printLog(tag, Level.ERROR, message, param); } public static void msg(String message, Object... param) { - Node text = BBCodeParser.createFormattedText(STR."\{StrUtil.format(message, param)}\n\n"); + if (message.contains("\033[")) { + message = resetConsoleColor(message); + } + + Node text = BBCodeParser.createFormattedText(STR."\{StrUtil.format(message, param)}"); Platform.runLater(() -> log.textArea.getChildren().add(text)); } - public static void printLog(String tag, String level, String message, Object... param) { + public static void printLog(String tag, Level level, String message, Object... param) { String time = DateFormatUtils.format(new Date(), format); time = STR."[color=-color-accent-emphasis]\{time}[/color]"; - String levelStr = StringUtils.substringBetween(level, "]", "["); - if (levelStr == null) levelStr = level; - level = StringUtils.replace(level, levelStr, StringUtils.center(levelStr, 5)); + String levelStr = resetLevelColor(level); tag = StringUtils.isEmpty(tag) ? "" : STR."\{tag}: "; - String msg = STR."[color=-color-fg-muted]\{StrUtil.format(message, param)}[/color]"; + message = STR."[color=-color-fg-muted]\{StrUtil.format(message, param)}[/color]"; Node text; - if (msg.contains("\n")) { - text = BBCodeParser.createLayout(STR."\{time} \{level} \{tag}\{msg}"); + String input = STR."\{time} \{levelStr} \{tag}\{message}"; + + if (input.contains("\n")) { + text = BBCodeParser.createLayout(input); } else { - text = BBCodeParser.createFormattedText(STR."\{time} \{level} \{tag}\{msg}"); + text = BBCodeParser.createFormattedText(input); } Platform.runLater(() -> log.textArea.getChildren().add(text)); } + +//============================{ 私有方法 }================================ + + /** + * 控制台输出颜色 + * + * @param msg alist 输出消息 + * @return bbcode 颜色文本 + */ + private static String resetConsoleColor(String msg) { + try { + String colorCode = ReUtil.get(CONSOLE_COLOR_PREFIX, msg, 1); + String color = StringUtils.lowerCase(Color.valueOf(Integer.parseInt(colorCode)).getColor()); + String colorMsg = ReUtil.get(CONSOLE_COLOR_PREFIX, msg, 2); + msg = ReUtil.get(CONSOLE_COLOR_PREFIX, msg, 3); + return color(color, colorMsg) + msg; + } catch (Throwable e) { + e.printStackTrace(); + } + return ""; + } + + /** + * @param level 级别 + * @return bbcode 颜色 + */ + private static String resetLevelColor(Level level) { + return color(level.getColor(), level.getCode()); + } + + private static String color(String color, String msg) { + String PREFIX = STR."\{StringUtils.isEmpty(color) ? "" : STR."[color=\{color}]"}"; + String SUFFIX = STR."\{StringUtils.isEmpty(color) ? "" : "[/color]"}"; + return STR."\{PREFIX}\{msg}\{SUFFIX}"; + } + +//============================{ 枚举 }================================ + + @Getter + @RequiredArgsConstructor + public enum Level { + INFO("INFO", null), + WARN("WARN", "color=-color-chart-2"), + ERROR("ERROR", "color=-color-chart-3"), + ; + + private final String code; + private final String color; + } + + @RequiredArgsConstructor + @Getter + enum Color { + BLACK(30, "-color-fg-default"), + RED(31, "-color-danger-fg"), + GREEN(32, "-color-success-fg"), + YELLOW(33, "-color-warning-emphasis"), + BLUE(34, "-color-accent-fg"), + PINKISH_RED(35, "-color-danger-4"), + CYAN(36, "-color-accent-emphasis"), + WHITE(37, "-color-bg-default"); + + private final int code; + private final String color; + + public static final Map NAME_CODE = Arrays.stream(Color.values()) + .collect(Collectors.toMap(Color::name, Function.identity())); + + public static final Map CODE_NAME = Arrays.stream(Color.values()) + .collect(Collectors.toMap(Color::getCode, Function.identity())); + + public static Color valueOf(int code) { + return CODE_NAME.get(code); + } + } } diff --git a/src/main/java/cn/octopusyan/alistgui/manager/http/HttpUtil.java b/src/main/java/cn/octopusyan/alistgui/manager/http/HttpUtil.java index 09ddf18..65427bc 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/http/HttpUtil.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/http/HttpUtil.java @@ -4,6 +4,7 @@ import cn.octopusyan.alistgui.enums.ProxySetup; import cn.octopusyan.alistgui.model.ProxyInfo; import cn.octopusyan.alistgui.util.JsonUtil; import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; import java.io.IOException; import java.net.InetSocketAddress; @@ -25,6 +26,7 @@ import java.util.Optional; */ public class HttpUtil { private volatile static HttpUtil util; + @Getter private volatile HttpClient httpClient; private final HttpConfig httpConfig; diff --git a/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java b/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java index 7f63c7a..a82a895 100644 --- a/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java +++ b/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java @@ -2,6 +2,7 @@ package cn.octopusyan.alistgui.model; import cn.octopusyan.alistgui.enums.ProxySetup; import cn.octopusyan.alistgui.manager.ConfigManager; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,9 +18,13 @@ public class GuiConfig { private Boolean autoStart = false; private Boolean silentStartup = false; + @JsonProperty("proxy") private ProxyInfo proxyInfo; + @JsonProperty("proxy.testUrl") + private String proxyTestUrl = "http://"; private String proxySetup = ProxySetup.NO_PROXY.getName(); private String language = ConfigManager.DEFAULT_LANGUAGE.toString(); private String theme = ConfigManager.DEFAULT_THEME; - private String proxyTestUrl = "http://"; + @JsonProperty("upgrade") + private UpgradeConfig upgradeConfig = new UpgradeConfig(); } diff --git a/src/main/java/cn/octopusyan/alistgui/model/UpgradeConfig.java b/src/main/java/cn/octopusyan/alistgui/model/UpgradeConfig.java index 146692b..ce08980 100644 --- a/src/main/java/cn/octopusyan/alistgui/model/UpgradeConfig.java +++ b/src/main/java/cn/octopusyan/alistgui/model/UpgradeConfig.java @@ -11,8 +11,6 @@ import lombok.Data; */ @Data public class UpgradeConfig { - public static final String RELEASE_API = "https://api.github.com/repos/{}/{}/releases/latest"; - public static final String DOWNLOAD_API = "https://github.com/{}/{}/releases/download/{}/{}"; private AList aList = new AList(); private Gui gui = new Gui(); } diff --git a/src/main/java/cn/octopusyan/alistgui/model/upgrade/UpgradeApp.java b/src/main/java/cn/octopusyan/alistgui/model/upgrade/UpgradeApp.java index ad09c3d..ee23501 100644 --- a/src/main/java/cn/octopusyan/alistgui/model/upgrade/UpgradeApp.java +++ b/src/main/java/cn/octopusyan/alistgui/model/upgrade/UpgradeApp.java @@ -1,9 +1,21 @@ package cn.octopusyan.alistgui.model.upgrade; +import com.fasterxml.jackson.annotation.JsonIgnore; + /** * @author octopus_yan */ public interface UpgradeApp { + @JsonIgnore + default String getReleaseApi() { + return STR."https://api.github.com/repos/\{getOwner()}/\{getRepo()}/releases/latest"; + } + + @JsonIgnore + default String getDownloadUrl(String version) { + return STR."https://github.com/\{getOwner()}/\{getRepo()}/releases/download/\{version}/\{getReleaseFile()}"; + } + String getOwner(); String getRepo(); diff --git a/src/main/java/cn/octopusyan/alistgui/task/DownloadTask.java b/src/main/java/cn/octopusyan/alistgui/task/DownloadTask.java new file mode 100644 index 0000000..79e3fa0 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/task/DownloadTask.java @@ -0,0 +1,158 @@ +package cn.octopusyan.alistgui.task; + +import cn.octopusyan.alistgui.base.BaseTask; +import cn.octopusyan.alistgui.config.Constants; +import cn.octopusyan.alistgui.manager.http.HttpUtil; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * TODO 下载任务 + * + * @author octopus_yan + */ +@Slf4j +public class DownloadTask extends BaseTask { + private final String downloadUrl; + + public DownloadTask(String downloadUrl) { + this.downloadUrl = downloadUrl; + log.info(STR."downlaod url : \{downloadUrl}"); + } + + @Override + protected void task() throws Exception { + HttpClient client = HttpUtil.getInstance().getHttpClient(); + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(downloadUrl)).build(); + + // 检查bin目录 + File binDir = new File(Constants.BIN_DIR_PATH); + if (!binDir.exists()) { + log.debug(STR."dir [\{Constants.BIN_DIR_PATH}] not exists"); + //noinspection ResultOfMethodCallIgnored + binDir.mkdirs(); + log.debug(STR."created dir [\{Constants.BIN_DIR_PATH}]"); + } + + // 下载处理器 + var handler = BodyHandler.create( + Path.of(Constants.BIN_DIR_PATH), + StandardOpenOption.CREATE, StandardOpenOption.WRITE + ); + + // 下载监听 + handler.listener((total, progress) -> { + // 输出进度 + if (listener != null && listener instanceof Listener lis) + lis.onProgress(total, progress); + }); + + log.info("download start"); + HttpResponse response = client.send(request, handler); + log.info("download success"); + } + + public interface Listener extends BaseTask.Listener { + void onProgress(Long total, Long progress); + } + + /** + * 自定义下载返回处理 + */ + private static class BodyHandler implements HttpResponse.BodyHandler { + private final HttpResponse.BodyHandler handler; + private BiConsumer consumer; + + private BodyHandler(HttpResponse.BodyHandler handler) { + this.handler = handler; + } + + public static BodyHandler create(Path directory, OpenOption... openOptions) { + return new BodyHandler(HttpResponse.BodyHandlers.ofFileDownload(directory, openOptions)); + } + + @Override + public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseInfo) { + AtomicLong length = new AtomicLong(-1); + // 获取文件大小 + Optional string = responseInfo.headers().firstValue("content-length"); + string.ifPresentOrElse(s -> { + length.set(Long.parseLong(s)); + log.debug(STR."========={content-length = \{s}}========="); + }, () -> { + String msg = "response not has header [content-length]"; + log.error(msg); + }); + + BodySubscriber subscriber = new BodySubscriber(handler.apply(responseInfo)); + subscriber.setConsumer(progress -> consumer.accept(length.get(), progress)); + + return subscriber; + } + + public void listener(BiConsumer consumer) { + this.consumer = consumer; + } + } + + public static class BodySubscriber implements HttpResponse.BodySubscriber { + private final HttpResponse.BodySubscriber subscriber; + private final AtomicLong progress = new AtomicLong(0); + @Setter + private Consumer consumer; + + public BodySubscriber(HttpResponse.BodySubscriber subscriber) { + this.subscriber = subscriber; + } + + @Override + public CompletionStage getBody() { + return subscriber.getBody(); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscriber.onSubscribe(subscription); + } + + @Override + public void onNext(List item) { + subscriber.onNext(item); + + // 记录进度 + for (ByteBuffer byteBuffer : item) { + progress.addAndGet(byteBuffer.limit()); + } + consumer.accept(progress.get()); + } + + @Override + public void onError(Throwable throwable) { + subscriber.onError(throwable); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + + consumer.accept(progress.get()); + } + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/task/UpgradeTask.java b/src/main/java/cn/octopusyan/alistgui/task/UpgradeTask.java index e5d3c7b..7ef9991 100644 --- a/src/main/java/cn/octopusyan/alistgui/task/UpgradeTask.java +++ b/src/main/java/cn/octopusyan/alistgui/task/UpgradeTask.java @@ -1,9 +1,8 @@ package cn.octopusyan.alistgui.task; -import cn.hutool.core.util.StrUtil; import cn.octopusyan.alistgui.base.BaseTask; import cn.octopusyan.alistgui.manager.http.HttpUtil; -import cn.octopusyan.alistgui.model.UpgradeConfig; +import cn.octopusyan.alistgui.model.upgrade.AList; import cn.octopusyan.alistgui.model.upgrade.UpgradeApp; import cn.octopusyan.alistgui.util.JsonUtil; import cn.octopusyan.alistgui.viewModel.AboutViewModule; @@ -16,31 +15,30 @@ import org.apache.commons.lang3.StringUtils; * @author octopus_yan */ public class UpgradeTask extends BaseTask { - private final AboutViewModule vm; + private final AboutViewModule viewModule; private final UpgradeApp app; public UpgradeTask(AboutViewModule viewModel, UpgradeApp app) { - this.vm = viewModel; + this.viewModule = viewModel; this.app = app; } @Override protected void task() throws Exception { - String releaseApi = StrUtil.format(UpgradeConfig.RELEASE_API, app.getOwner(), app.getRepo()); - String responseStr = HttpUtil.getInstance().get(releaseApi, null, null); + String responseStr = HttpUtil.getInstance().get(app.getReleaseApi(), null, null); JsonNode response = JsonUtil.parseJsonObject(responseStr); + // TODO 校验返回内容 String newVersion = response.get("tag_name").asText(); - String downloadUrl = StrUtil.format(UpgradeConfig.DOWNLOAD_API, - app.getOwner(), - app.getRepo(), - newVersion, - app.getReleaseFile() - ); runLater(() -> { - vm.aListUpgradeProperty().setValue(StringUtils.equals(app.getVersion(), newVersion)); - vm.aListNewVersionProperty().setValue(newVersion); + if (app instanceof AList) { + viewModule.aListUpgradeProperty().setValue(!StringUtils.equals(app.getVersion(), newVersion)); + viewModule.aListNewVersionProperty().setValue(newVersion); + } else { + viewModule.guiUpgradeProperty().setValue(!StringUtils.equals(app.getVersion(), newVersion)); + viewModule.guiNewVersionProperty().setValue(newVersion); + } }); } } diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertBuilder.java b/src/main/java/cn/octopusyan/alistgui/util/alert/AlertBuilder.java index f1aafd1..4195713 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/util/alert/AlertBuilder.java @@ -29,6 +29,11 @@ public class AlertBuilder extends BaseBuilder { return this; } + public AlertBuilder buttons(ButtonType... buttons) { + dialog.getButtonTypes().addAll(buttons); + return this; + } + public AlertBuilder exception(Exception ex) { dialog.setTitle("Exception Dialog"); dialog.setHeaderText(ex.getClass().getSimpleName()); @@ -71,8 +76,9 @@ public class AlertBuilder extends BaseBuilder { return Arrays.stream(buttons).map((type) -> { ButtonBar.ButtonData buttonData = ButtonBar.ButtonData.OTHER; - if ("cancel".equals(StringUtils.lowerCase(type)) || "取消".equals(type)) - buttonData = ButtonBar.ButtonData.CANCEL_CLOSE; + if ("cancel".equals(StringUtils.lowerCase(type)) || "取消".equals(type)) { + return ButtonType.CANCEL; + } return new ButtonType(type, buttonData); }).collect(Collectors.toList()); } diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertUtil.java b/src/main/java/cn/octopusyan/alistgui/util/alert/AlertUtil.java index 722a440..be51aa6 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertUtil.java +++ b/src/main/java/cn/octopusyan/alistgui/util/alert/AlertUtil.java @@ -53,6 +53,10 @@ public class AlertUtil { return confirm().buttons(buttons); } + public static AlertBuilder confirm(ButtonType... buttons) { + return confirm().buttons(buttons); + } + public static AlertBuilder alert(Alert.AlertType type) { return new AlertBuilder(mOwner, type); } @@ -77,7 +81,8 @@ public class AlertUtil { public interface OnChoseListener { void confirm(); - void cancelOrClose(ButtonType buttonType); + default void cancelOrClose(ButtonType buttonType) { + } } public interface OnClickListener { diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/BaseBuilder.java b/src/main/java/cn/octopusyan/alistgui/util/alert/BaseBuilder.java index e19dbae..ac2bfdd 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/BaseBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/util/alert/BaseBuilder.java @@ -1,14 +1,8 @@ package cn.octopusyan.alistgui.util.alert; -import javafx.application.Platform; -import javafx.collections.ObservableList; import javafx.scene.control.Dialog; -import javafx.scene.image.Image; -import javafx.stage.Stage; import javafx.stage.Window; -import java.util.Objects; - /** * @author octopus_yan */ @@ -17,7 +11,6 @@ public abstract class BaseBuilder, D extends Dialog< public BaseBuilder(D dialog, Window mOwner) { this.dialog = dialog; - icon("/assets/logo.png"); if (mOwner != null) this.dialog.initOwner(mOwner); } @@ -37,18 +30,6 @@ public abstract class BaseBuilder, D extends Dialog< return (T) this; } - public T icon(String path) { - return icon(new Image(Objects.requireNonNull(this.getClass().getResource(path)).toString())); - } - - public T icon(Image image) { - ObservableList icons = getStage().getIcons(); - if (icons.isEmpty()) { - Platform.runLater(() -> icons.add(image)); - } - return (T) this; - } - public D getDialog() { return dialog; } @@ -58,11 +39,7 @@ public abstract class BaseBuilder, D extends Dialog< } public void close() { - if(dialog.isShowing()) + if (dialog.isShowing()) dialog.close(); } - - private Stage getStage() { - return (Stage) dialog.getDialogPane().getScene().getWindow(); - } } diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/ProgressBuilder.java b/src/main/java/cn/octopusyan/alistgui/util/alert/ProgressBuilder.java index 7c88b05..707090d 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/ProgressBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/util/alert/ProgressBuilder.java @@ -52,7 +52,7 @@ public class ProgressBuilder extends BaseBuilder> HBox hBox = new HBox(); hBox.setPrefWidth(350); hBox.setAlignment(Pos.CENTER); - hBox.setPadding(new Insets(0, 0, 0, 0)); + hBox.setPadding(new Insets(10, 0, 10, 0)); // 取消按钮 Button cancel = new Button(Context.getLanguageBinding("label.cancel").get()); diff --git a/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java b/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java index 864124f..a7d35c0 100644 --- a/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java @@ -1,14 +1,28 @@ package cn.octopusyan.alistgui.viewModel; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ZipUtil; import cn.octopusyan.alistgui.base.BaseTask; +import cn.octopusyan.alistgui.config.Constants; import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.manager.ConfigManager; +import cn.octopusyan.alistgui.manager.ConsoleLog; +import cn.octopusyan.alistgui.model.upgrade.AList; +import cn.octopusyan.alistgui.model.upgrade.UpgradeApp; +import cn.octopusyan.alistgui.task.DownloadTask; import cn.octopusyan.alistgui.task.UpgradeTask; +import cn.octopusyan.alistgui.util.alert.AlertBuilder; import cn.octopusyan.alistgui.util.alert.AlertUtil; +import javafx.application.Platform; import javafx.beans.property.*; -import javafx.scene.control.ButtonType; import lombok.extern.slf4j.Slf4j; +import java.io.File; +import java.io.InputStream; +import java.util.zip.ZipFile; + /** * 关于 * @@ -19,6 +33,9 @@ public class AboutViewModule { private final StringProperty aListVersion = new SimpleStringProperty(ConfigManager.aListVersion()); private final StringProperty aListNewVersion = new SimpleStringProperty(""); private final BooleanProperty aListUpgrade = new SimpleBooleanProperty(false); + private final StringProperty guiVersion = new SimpleStringProperty(ConfigManager.guiVersion()); + private final StringProperty guiNewVersion = new SimpleStringProperty(""); + private final BooleanProperty guiUpgrade = new SimpleBooleanProperty(false); public AboutViewModule() { aListVersion.addListener((_, _, newValue) -> ConfigManager.aListVersion(newValue)); @@ -36,38 +53,173 @@ public class AboutViewModule { return aListNewVersion; } + public StringProperty guiVersionProperty() { + return guiVersion; + } + + public StringProperty guiNewVersionProperty() { + return guiNewVersion; + } + + public BooleanProperty guiUpgradeProperty() { + return guiUpgrade; + } + /** - * 检查alist更新 + * 检查更新 */ - public void checkAListUpdate() { - var task = new UpgradeTask(this, ConfigManager.aList()); + public void checkUpdate(UpgradeApp app) { + + // 检查任务 + startUpgrade(app, () -> { + // 判断 检查的应用 + boolean tag = app instanceof AList; + + boolean upgrade = tag ? aListUpgrade.get() : guiUpgrade.get(); + String version = tag ? aListVersion.get() : guiVersion.get(); + String newVersion = tag ? aListNewVersion.get() : guiNewVersion.get(); + String title = Context.getLanguageBinding(STR."about.\{tag ? "alist" : "app"}.update").getValue(); + String currentLabel = Context.getLanguageBinding("update.current").get(); + String newLabel = Context.getLanguageBinding("update.remote").get(); + String header = Context.getLanguageBinding(STR."update.upgrade.\{upgrade ? "new" : "not"}").get(); + + String msg = STR."\{app.getRepo()}\{upgrade ? "" : STR." \{version}"} \{header} \{upgrade ? newVersion : ""}"; + log.info(msg); + ConsoleLog.info(msg); + + AlertBuilder builder = upgrade ? AlertUtil.confirm() : AlertUtil.info(); + builder.title(title) + .header(header) + .content(STR.""" + \{currentLabel} : \{version} + \{newLabel} : \{newVersion} + """) + .show(() -> { + if (upgrade) startDownload(app, newVersion); + }); + }); + } + + private void startUpgrade(UpgradeApp app, Runnable runnable) { + // 检查更新的任务 + var task = new UpgradeTask(this, app); + + // 加载弹窗 + final var progress = AlertUtil.progress(); + progress.title(Context.getLanguageBinding("proxy.test.title").get()); + progress.onCancel(task::cancel); + + // 任务监听 task.onListen(new BaseTask.Listener() { + + @Override + public void onStart() { + String msg = STR."start update \{app.getRepo()}..."; + log.info(msg); + ConsoleLog.info(msg); + } + + @Override + public void onRunning() { + progress.show(); + } + @Override public void onSucceeded() { - AlertUtil.confirm() - .content(STR.""" - 当前版本 : \{aListVersion.get()} - 最新版本 : \{aListNewVersion.get()} - """) - .title(Context.getLanguageBinding("about.alist.update").getValue()) - .show(new AlertUtil.OnChoseListener() { - @Override - public void confirm() { - log.info("========confirm=========="); - } - - @Override - public void cancelOrClose(ButtonType buttonType) { - log.info("========cancelOrClose=========="); - } - }); + progress.close(); + if (runnable != null) runnable.run(); } @Override public void onFailed(Throwable throwable) { + String msg = STR."\{app.getRepo()} check version error"; + log.error(msg, throwable); + ConsoleLog.error(STR."\{msg} : \{throwable.getMessage()}"); AlertUtil.exception(new Exception(throwable)).show(); } }); + // 执行任务 task.execute(); } + + /** + * 下载文件 + * + * @param app 应用 + * @param version 下载版本 + */ + private void startDownload(UpgradeApp app, String version) { + DownloadTask downloadTask = new DownloadTask(app.getDownloadUrl(version)); + downloadTask.onListen(new DownloadTask.Listener() { + + private volatile int lastProgress = 0; + + @Override + public void onStart() { + String msg = STR."download \{app.getRepo()} start"; + log.info(msg); + ConsoleLog.info(msg); + } + + @Override + public void onProgress(Long total, Long progress) { + int a = (int) (((double) progress / total) * 100); + if (a % 10 == 0) { + if (a != lastProgress) { + lastProgress = a; + String msg = STR."\{app.getRepo()} \{a} %"; + log.info(STR."download \{msg}"); + ConsoleLog.info(msg); + } + } + } + + @Override + public void onSucceeded() { + String msg = STR."download \{app.getRepo()} success"; + log.info(msg); + ConsoleLog.info(msg); + + // 解压并删除文件 + unzip(app); + + Platform.runLater(() -> aListVersion.setValue(aListNewVersion.getValue())); + } + + @Override + public void onFailed(Throwable throwable) { + log.error("下载失败", throwable); + ConsoleLog.error(STR."下载失败 => \{throwable.getMessage()}"); + } + }); + downloadTask.execute(); + } + + private void unzip(UpgradeApp app) { + File file = new File(Constants.BIN_DIR_PATH + File.separator + app.getReleaseFile()); + ZipFile zipFile = ZipUtil.toZipFile(file, CharsetUtil.defaultCharset()); + ZipUtil.read(zipFile, zipEntry -> { + String path = zipEntry.getName(); + if (FileUtil.isWindows()) { + // Win系统下 + path = StrUtil.replace(path, "*", "_"); + } + + final File outItemFile = FileUtil.file(Constants.BIN_DIR_PATH, path); + if (zipEntry.isDirectory()) { + // 目录 + //noinspection ResultOfMethodCallIgnored + outItemFile.mkdirs(); + } else { + InputStream in = ZipUtil.getStream(zipFile, zipEntry); + // 文件 + FileUtil.writeFromStream(in, outItemFile, false); + + log.info(STR."unzip ==> \{outItemFile.getAbsoluteFile()}"); + } + }); + + // 解压完成后删除 + FileUtil.del(file); + } } diff --git a/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java b/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java index fef68e7..3928aef 100644 --- a/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java @@ -1,5 +1,6 @@ package cn.octopusyan.alistgui.viewModel; +import atlantafx.base.theme.Theme; import cn.octopusyan.alistgui.base.BaseTask; import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.enums.ProxySetup; @@ -22,7 +23,7 @@ import java.util.Locale; public class SetupViewModel { private final BooleanProperty autoStart = new SimpleBooleanProperty(ConfigManager.autoStart()); private final BooleanProperty silentStartup = new SimpleBooleanProperty(ConfigManager.silentStartup()); - private final StringProperty theme = new SimpleStringProperty(ConfigManager.themeName()); + private final ObjectProperty theme = new SimpleObjectProperty<>(ConfigManager.theme()); private final StringProperty proxyHost = new SimpleStringProperty(ConfigManager.proxyHost()); private final StringProperty proxyPort = new SimpleStringProperty(ConfigManager.proxyPort()); private final ObjectProperty language = new SimpleObjectProperty<>(ConfigManager.language()); @@ -31,7 +32,8 @@ public class SetupViewModel { public SetupViewModel() { - theme.addListener((_, _, newValue) -> ConfigManager.themeName(newValue)); + theme.bindBidirectional(Context.themeProperty()); + theme.addListener((_, _, newValue) -> ConfigManager.theme(newValue)); autoStart.addListener((_, _, newValue) -> ConfigManager.autoStart(newValue)); silentStartup.addListener((_, _, newValue) -> ConfigManager.silentStartup(newValue)); proxySetup.addListener((_, _, newValue) -> ConfigManager.proxySetup(newValue)); @@ -41,7 +43,7 @@ public class SetupViewModel { language.addListener((_, _, newValue) -> Context.setLanguage(newValue)); } - public StringProperty themeProperty() { + public ObjectProperty themeProperty() { return theme; } diff --git a/src/main/resources/fxml/about-view.fxml b/src/main/resources/fxml/about-view.fxml index 25fefde..8bb3008 100644 --- a/src/main/resources/fxml/about-view.fxml +++ b/src/main/resources/fxml/about-view.fxml @@ -29,6 +29,6 @@