diff --git a/pom.xml b/pom.xml index 82ee086..731a8c1 100644 --- a/pom.xml +++ b/pom.xml @@ -79,20 +79,6 @@ ${logback.version} - - - org.junit.jupiter - junit-jupiter-api - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - @@ -205,6 +191,7 @@ ${cssSrcPath}/main-view.scss:${cssTargetPath}/main-view.css ${cssSrcPath}/setup-view.scss:${cssTargetPath}/setup-view.css ${cssSrcPath}/about-view.scss:${cssTargetPath}/about-view.css + ${cssSrcPath}/admin-panel.scss:${cssTargetPath}/admin-panel.css --no-source-map diff --git a/src/main/java/cn/octopusyan/alistgui/Application.java b/src/main/java/cn/octopusyan/alistgui/Application.java index 8b5afc6..18f4245 100644 --- a/src/main/java/cn/octopusyan/alistgui/Application.java +++ b/src/main/java/cn/octopusyan/alistgui/Application.java @@ -7,7 +7,7 @@ import cn.octopusyan.alistgui.manager.http.HttpConfig; import cn.octopusyan.alistgui.manager.http.HttpUtil; import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager; import cn.octopusyan.alistgui.util.ProcessesUtil; -import cn.octopusyan.alistgui.util.alert.AlertUtil; +import cn.octopusyan.alistgui.view.alert.AlertUtil; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.image.Image; @@ -61,6 +61,8 @@ public class Application extends javafx.application.Application { Application.primaryStage = primaryStage; + Context.setApplication(this); + // 初始化弹窗工具 AlertUtil.initOwner(primaryStage); diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/BaseBuilder.java b/src/main/java/cn/octopusyan/alistgui/base/BaseBuilder.java similarity index 55% rename from src/main/java/cn/octopusyan/alistgui/util/alert/BaseBuilder.java rename to src/main/java/cn/octopusyan/alistgui/base/BaseBuilder.java index ac2bfdd..cd437f3 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/BaseBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/base/BaseBuilder.java @@ -1,11 +1,16 @@ -package cn.octopusyan.alistgui.util.alert; +package cn.octopusyan.alistgui.base; +import cn.octopusyan.alistgui.manager.ConfigManager; +import javafx.application.Platform; +import javafx.scene.Node; import javafx.scene.control.Dialog; import javafx.stage.Window; +import lombok.Getter; /** * @author octopus_yan */ +@Getter public abstract class BaseBuilder, D extends Dialog> { protected D dialog; @@ -30,12 +35,19 @@ public abstract class BaseBuilder, D extends Dialog< return (T) this; } - public D getDialog() { - return dialog; - } - public void show() { - dialog.showAndWait(); + + Node dialogPane = dialog.getDialogPane().getContent(); + if (dialogPane != null && ConfigManager.theme().isDarkMode()) { + dialogPane.setStyle(STR.""" + \{dialogPane.getStyle()} + -fx-border-color: rgb(209, 209, 214, 0.5); + -fx-border-width: 1; + -fx-border-radius: 10; + """); + } + + Platform.runLater(() -> dialog.showAndWait()); } public void close() { diff --git a/src/main/java/cn/octopusyan/alistgui/base/BaseController.java b/src/main/java/cn/octopusyan/alistgui/base/BaseController.java index f7618cf..c1e48e7 100644 --- a/src/main/java/cn/octopusyan/alistgui/base/BaseController.java +++ b/src/main/java/cn/octopusyan/alistgui/base/BaseController.java @@ -2,15 +2,18 @@ package cn.octopusyan.alistgui.base; import cn.octopusyan.alistgui.Application; import cn.octopusyan.alistgui.config.Context; -import cn.octopusyan.alistgui.manager.WindowsUtil; import cn.octopusyan.alistgui.util.FxmlUtil; +import cn.octopusyan.alistgui.util.WindowsUtil; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.layout.Pane; import javafx.stage.Stage; +import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.net.URL; import java.util.ResourceBundle; @@ -19,19 +22,34 @@ import java.util.ResourceBundle; * * @author octopus_yan@foxmail.com */ -public abstract class BaseController

implements Initializable { +public abstract class BaseController implements Initializable { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + @Getter + protected final VM viewModel; public BaseController() { //初始化时保存当前Controller实例 Context.getControllers().put(this.getClass().getSimpleName(), this); + + // view model + VM vm = null; + Type superclass = getClass().getGenericSuperclass(); + if (superclass instanceof ParameterizedType type) { + Class clazz = (Class) type.getActualTypeArguments()[0]; + try { + vm = clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + logger.error("", e); + } + } + viewModel = vm; } @FXML @Override public void initialize(URL url, ResourceBundle resourceBundle) { // 全局窗口拖拽 - if (dragWindow()) { + if (dragWindow() && getRootPanel() != null) { // 窗口拖拽 WindowsUtil.bindDragged(getRootPanel()); } @@ -60,7 +78,7 @@ public abstract class BaseController

implements Initializable { * * @return 根布局对象 */ - public abstract P getRootPanel(); + public abstract Pane getRootPanel(); /** * 获取根布局 diff --git a/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java b/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java index f37096e..eaf99a9 100644 --- a/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java +++ b/src/main/java/cn/octopusyan/alistgui/base/BaseTask.java @@ -1,8 +1,8 @@ package cn.octopusyan.alistgui.base; import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager; -import javafx.application.Platform; import javafx.concurrent.Task; +import lombok.Getter; /** * @author octopus_yan @@ -10,6 +10,12 @@ import javafx.concurrent.Task; public abstract class BaseTask extends Task { private final ThreadPoolManager Executor = ThreadPoolManager.getInstance(); protected Listener listener; + @Getter + private final String name; + + protected BaseTask(String name) { + this.name = name; + } @Override protected Void call() throws Exception { @@ -18,10 +24,6 @@ public abstract class BaseTask extends Task { return null; } - protected void runLater(Runnable runnable) { - Platform.runLater(runnable); - } - protected abstract void task() throws Exception; public void onListen(Listener listener) { diff --git a/src/main/java/cn/octopusyan/alistgui/base/BaseViewModel.java b/src/main/java/cn/octopusyan/alistgui/base/BaseViewModel.java new file mode 100644 index 0000000..a3b3e23 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/base/BaseViewModel.java @@ -0,0 +1,13 @@ +package cn.octopusyan.alistgui.base; + +import lombok.Data; + +/** + * View Model + * + * @author octopus_yan + */ +@Data +public abstract class BaseViewModel { + +} diff --git a/src/main/java/cn/octopusyan/alistgui/config/Context.java b/src/main/java/cn/octopusyan/alistgui/config/Context.java index 3df7a91..0db021e 100644 --- a/src/main/java/cn/octopusyan/alistgui/config/Context.java +++ b/src/main/java/cn/octopusyan/alistgui/config/Context.java @@ -1,6 +1,7 @@ package cn.octopusyan.alistgui.config; import atlantafx.base.theme.Theme; +import cn.octopusyan.alistgui.Application; import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.controller.AboutController; import cn.octopusyan.alistgui.controller.MainController; @@ -9,6 +10,7 @@ import cn.octopusyan.alistgui.controller.SetupController; import cn.octopusyan.alistgui.manager.ConfigManager; import cn.octopusyan.alistgui.manager.ConsoleLog; import cn.octopusyan.alistgui.util.FxmlUtil; +import cn.octopusyan.alistgui.util.ProcessesUtil; import javafx.application.Platform; import javafx.beans.binding.StringBinding; import javafx.beans.property.IntegerProperty; @@ -24,7 +26,7 @@ import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; +import java.io.File; import java.net.URL; import java.util.*; @@ -34,9 +36,11 @@ import java.util.*; * @author octopus_yan */ public class Context { + @Getter + private static Application application; private static final Logger log = LoggerFactory.getLogger(Context.class); private static Scene scene; - private static final IntegerProperty currentViewIndexProperty = new SimpleIntegerProperty(0); + private static final IntegerProperty currentViewIndex = new SimpleIntegerProperty(0); private static final ObjectProperty theme = new SimpleObjectProperty<>(ConfigManager.theme()); /** @@ -84,6 +88,10 @@ public class Context { }; } + public static void setApplication(Application application) { + Context.application = application; + } + public static ObjectProperty themeProperty() { return theme; } @@ -121,8 +129,7 @@ public class Context { LANGUAGE_RESOURCE_FACTORY.setResourceBundle(ResourceBundle.getBundle(LANGUAGE_RESOURCE_NAME, locale)); log.info("language changed to {}", locale); - if (ConsoleLog.isInit()) - ConsoleLog.info(STR."language changed to \{locale}"); + ConsoleLog.info(STR."language changed to \{locale}"); } /** @@ -147,13 +154,6 @@ public class Context { * 初始化 语言 */ private static void initI18n() { - currentLocaleProperty().addListener((_, _, locale) -> Platform.runLater(() -> { - try { - loadScene(); - } catch (IOException e) { - log.error("", e); - } - })); } /** @@ -171,45 +171,58 @@ public class Context { * 初始化场景 * * @return Scene - * @throws IOException 如果在加载过程中发生错误 */ - public static Scene initScene() throws IOException { - initI18n(); + public static Scene initScene() { + // locale监听; 切换后,重新加载界面 + currentLocaleProperty().addListener((_, _, locale) -> Platform.runLater(Context::loadScene)); + // 加载 loadScene(); return scene; } - private static void loadScene() throws IOException { - FXMLLoader loader = FxmlUtil.load("root-view"); - loader.setControllerFactory(Context.getControlFactory()); - //底层面板 - Pane root = loader.load(); - Optional.ofNullable(scene).ifPresentOrElse( - s -> s.setRoot(root), - () -> { - scene = new Scene(root, root.getPrefWidth() + 20, root.getPrefHeight() + 20, Color.TRANSPARENT); - URL resource = Objects.requireNonNull(Context.class.getResource("/css/root-view.css")); - scene.getStylesheets().addAll(resource.toExternalForm()); - scene.setFill(Color.TRANSPARENT); - } - ); + private static void loadScene() { + try { + FXMLLoader loader = FxmlUtil.load("root-view"); + loader.setControllerFactory(Context.getControlFactory()); + //底层面板 + Pane root = loader.load(); + Optional.ofNullable(scene).ifPresentOrElse( + s -> s.setRoot(root), + () -> { + scene = new Scene(root, root.getPrefWidth() + 20, root.getPrefHeight() + 20, Color.TRANSPARENT); + URL resource = Objects.requireNonNull(Context.class.getResource("/css/root-view.css")); + scene.getStylesheets().addAll(resource.toExternalForm()); + scene.setFill(Color.TRANSPARENT); + } + ); + } catch (Throwable e) { + log.error("loadScene error", e); + } } - /** - * 设置当前展示的界面 - * - * @param index 界面Index - */ - public static void setCurrentViewIndex(Number index) { - currentViewIndexProperty.setValue(index); + public static int currentViewIndex() { + return currentViewIndex.get(); } - /** - * 获取当前展示的界面Index - * - * @return 界面Index - */ - public static Integer getCurrentViewIndex() { - return currentViewIndexProperty.get(); + public static IntegerProperty currentViewIndexProperty() { + return currentViewIndex; + } + + public static void openUrl(String url) { + getApplication().getHostServices().showDocument(url); + } + + public static void openFolder(File file) { + openFile(file); + } + + public static void openFile(File file) { + if (!file.exists()) return; + + if (file.isDirectory()) { + ProcessesUtil.init(file.getAbsolutePath()).exec("explorer.exe ."); + } else { + ProcessesUtil.init(file.getParentFile().getAbsolutePath()).exec(STR."explorer.exe /select,\{file.getName()}"); + } } } \ No newline at end of file diff --git a/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java b/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java index 2c99384..17c45f5 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/AboutController.java @@ -2,9 +2,9 @@ package cn.octopusyan.alistgui.controller; import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.manager.ConfigManager; +import cn.octopusyan.alistgui.view.alert.AlertUtil; import cn.octopusyan.alistgui.viewModel.AboutViewModule; import javafx.fxml.FXML; -import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.VBox; import org.slf4j.Logger; @@ -15,7 +15,7 @@ import org.slf4j.LoggerFactory; * * @author octopus_yan */ -public class AboutController extends BaseController { +public class AboutController extends BaseController { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); @FXML @@ -23,10 +23,6 @@ public class AboutController extends BaseController { @FXML public Label aListVersion; - @FXML - public Button checkAppVersion; - - private final AboutViewModule viewModule = new AboutViewModule(); @Override public VBox getRootPanel() { @@ -45,11 +41,18 @@ public class AboutController extends BaseController { @Override public void initViewAction() { - aListVersion.textProperty().bindBidirectional(viewModule.aListVersionProperty()); + aListVersion.textProperty().bindBidirectional(viewModel.aListVersionProperty()); } @FXML public void checkAListUpdate() { - viewModule.checkUpdate(ConfigManager.aList()); + viewModel.checkUpdate(ConfigManager.aList()); } + + @FXML + public void checkGuiUpdate() { + // TODO 检查 gui 版本 + AlertUtil.info("待开发。。。").show(); + } + } diff --git a/src/main/java/cn/octopusyan/alistgui/controller/MainController.java b/src/main/java/cn/octopusyan/alistgui/controller/MainController.java index 9be02b9..a345d1d 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/MainController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/MainController.java @@ -4,20 +4,25 @@ import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.manager.AListManager; import cn.octopusyan.alistgui.manager.ConsoleLog; -import javafx.application.Platform; +import cn.octopusyan.alistgui.util.FxmlUtil; +import cn.octopusyan.alistgui.viewModel.MainViewModel; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; +import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + /** * 主界面控制器 * * @author octopus_yan */ -public class MainController extends BaseController { +public class MainController extends BaseController { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); @FXML @@ -30,6 +35,10 @@ public class MainController extends BaseController { public Button statusLabel; @FXML public Button startButton; + @FXML + public MenuItem browserButton; + + private PasswordController controller; @Override public VBox getRootPanel() { @@ -43,31 +52,18 @@ public class MainController extends BaseController { @Override public void initViewStyle() { - AListManager.runningProperty().addListener(_ -> setStartButton(AListManager.isRunning())); } @Override public void initViewAction() { - AListManager.runningProperty().addListener((_, _, running) -> setStartButton(running)); - } - - private void setStartButton(boolean running) { - String removeStyle = running ? "success" : "danger"; - String addStyle = running ? "danger" : "success"; - String button = Context.getLanguageBinding(STR."main.control.\{running ? "stop" : "start"}").get(); - String status = Context.getLanguageBinding(STR."main.status.label-\{running ? "running" : "stop"}").get(); - - Platform.runLater(() -> { - startButton.getStyleClass().remove(removeStyle); - startButton.getStyleClass().add(addStyle); - startButton.textProperty().set(button); - - statusLabel.getStyleClass().remove(addStyle); - statusLabel.getStyleClass().add(removeStyle); - statusLabel.textProperty().set(status); - }); + viewModel.startBtnStyleCssProperty().bindContentBidirectional(startButton.getStyleClass()); + viewModel.statusLabelStyleCssProperty().bindContentBidirectional(statusLabel.getStyleClass()); + viewModel.startBtnTextProperty().bindBidirectional(startButton.textProperty()); + viewModel.statusLabelTextProperty().bindBidirectional(statusLabel.textProperty()); + viewModel.browserButtonDisableProperty().bindBidirectional(browserButton.disableProperty()); } + // start button @FXML public void start() { if (AListManager.isRunning()) { @@ -77,8 +73,41 @@ public class MainController extends BaseController { } } + // password button + @FXML + public void adminPassword() throws IOException { + if (controller == null) { + FXMLLoader load = FxmlUtil.load("admin-panel"); + load.load(); + controller = load.getController(); + } + controller.show(); + } + + // restart button @FXML public void restart() { AListManager.restart(); } + + // more button + + @FXML + public void openInBrowser() { + AListManager.openScheme(); + } + + @FXML + public void openLogFolder() { + AListManager.openLogFolder(); + } + + @FXML + public void openConfig() { + AListManager.openConfig(); + } + + private String getText(String key) { + return Context.getLanguageBinding(key).get(); + } } diff --git a/src/main/java/cn/octopusyan/alistgui/controller/PasswordController.java b/src/main/java/cn/octopusyan/alistgui/controller/PasswordController.java new file mode 100644 index 0000000..64134b9 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/controller/PasswordController.java @@ -0,0 +1,127 @@ +package cn.octopusyan.alistgui.controller; + +import atlantafx.base.controls.Popover; +import cn.hutool.core.swing.clipboard.ClipboardUtil; +import cn.octopusyan.alistgui.base.BaseController; +import cn.octopusyan.alistgui.config.Context; +import cn.octopusyan.alistgui.manager.AListManager; +import cn.octopusyan.alistgui.viewModel.AdminPanelViewModel; +import javafx.beans.value.ChangeListener; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; +import javafx.scene.text.Text; +import org.apache.commons.lang3.StringUtils; + +/** + * 管理员密码 + * + * @author octopus_yan + */ +public class PasswordController extends BaseController { + @FXML + private AnchorPane adminPanel; + + @FXML + public TextField usernameField; + @FXML + public Button copyUsername; + @FXML + public PasswordField passwordField; + @FXML + public Button refreshPassword; + @FXML + public Button savePassword; + @FXML + public Button copyPassword; + + private RootController root; + + private final Popover pop = new Popover(new Text(Context.getLanguageBinding("msg.alist.pwd.copy").get())); + + @Override + public Pane getRootPanel() { + return adminPanel; + } + + @Override + public void initData() { + root = (RootController) Context.getControllers().get("RootController"); + } + + @Override + public void initViewStyle() { + pop.setArrowLocation(Popover.ArrowLocation.BOTTOM_CENTER); + } + + @Override + public void initViewAction() { + passwordField.textProperty().bindBidirectional(viewModel.passwordProperty()); + passwordField.setOnMouseClicked(event -> { + // 点击密码框时,设置为可修改状态 + passwordField.setEditable(true); + refreshPassword.setVisible(true); + refreshPassword.setManaged(true); + }); + ChangeListener changeListener = (_, _, focused) -> { + if (!focused && !refreshPassword.isFocused() + && !copyPassword.isFocused() + && StringUtils.equals(passwordField.getText(), AListManager.passwordProperty().get())) { + // 当密码栏失去焦点,如果密码未变更,设置为不可用状态 + passwordField.setEditable(false); + refreshPassword.setVisible(false); + refreshPassword.setManaged(false); + savePassword.setVisible(false); + savePassword.setManaged(false); + } + }; + passwordField.focusedProperty().addListener(changeListener); + refreshPassword.focusedProperty().addListener(changeListener); + savePassword.focusedProperty().addListener(changeListener); + copyPassword.focusedProperty().addListener(changeListener); + // 监听密码修改,展示保存按钮 + passwordField.textProperty().addListener((_, _, newValue) -> { + boolean equals = StringUtils.equals(newValue, AListManager.passwordProperty().get()); + savePassword.setVisible(!equals); + savePassword.setManaged(!equals); + }); + } + + public void show() { + root.showModal(getRootPanel(), true); + } + + @FXML + public void close() { + passwordField.setText(AListManager.passwordProperty().get()); + root.hideModal(); + } + + @FXML + public void copyUsername() { + usernameField.copy(); + pop.show(copyUsername); + } + + @FXML + public void savePassword(ActionEvent event) { + Object source = event.getSource(); + if (refreshPassword.equals(source)) { + AListManager.resetPassword(); + return; + } + AListManager.resetPassword(passwordField.getText()); + savePassword.setVisible(false); + savePassword.setManaged(false); + } + + @FXML + public void copyPassword() { + ClipboardUtil.setStr(AListManager.password()); + pop.show(copyPassword); + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/controller/RootController.java b/src/main/java/cn/octopusyan/alistgui/controller/RootController.java index 408b5d7..02265f7 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/RootController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/RootController.java @@ -1,32 +1,35 @@ package cn.octopusyan.alistgui.controller; +import atlantafx.base.controls.ModalPane; import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.config.Context; +import cn.octopusyan.alistgui.util.WindowsUtil; +import cn.octopusyan.alistgui.viewModel.RootViewModel; import com.gluonhq.emoji.EmojiData; import com.gluonhq.emoji.util.EmojiImageUtils; import javafx.css.PseudoClass; import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.TabPane; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; +import javafx.scene.layout.StackPane; import org.kordamp.ikonli.javafx.FontIcon; +import java.util.Locale; + /** - * 主页面控制器 + * Root 页面控制器 * * @author octopus_yan@foxmail.com */ -public class RootController extends BaseController { - - private double xOffset; - private double yOffset; - +public class RootController extends BaseController { // 布局 @FXML - private VBox rootPane; + private StackPane rootPane; @FXML private HBox windowHeader; @FXML @@ -48,13 +51,15 @@ public class RootController extends BaseController { @FXML public Button sponsor; + private final ModalPane modalPane = new ModalPane(); + /** * 获取根布局 * * @return 根布局对象 */ @Override - public VBox getRootPanel() { + public StackPane getRootPanel() { return rootPane; } @@ -63,7 +68,7 @@ public class RootController extends BaseController { */ @Override public void initData() { - tabPane.getSelectionModel().select(Context.getCurrentViewIndex()); + tabPane.getSelectionModel().select(viewModel.currentViewIndexProperty().get()); } /** @@ -85,6 +90,16 @@ public class RootController extends BaseController { ImageView juice = EmojiImageUtils.emojiView(icon, 25); sponsor.setGraphic(juice); }); + + getRootPanel().getChildren().add(modalPane); + modalPane.setId("modalPane"); + // reset side and transition to reuse a single modal pane between different examples + modalPane.displayProperty().addListener((obs, old, val) -> { + if (!val) { + modalPane.setAlignment(Pos.CENTER); + modalPane.usePredefinedTransitionFactories(null); + } + }); } /** @@ -100,17 +115,29 @@ public class RootController extends BaseController { getWindow().setAlwaysOnTop(newVal); }); - windowHeader.setOnMousePressed(event -> { - xOffset = getWindow().getX() - event.getScreenX(); - yOffset = getWindow().getY() - event.getScreenY(); - }); - windowHeader.setOnMouseDragged(event -> { - getWindow().setX(event.getScreenX() + xOffset); - getWindow().setY(event.getScreenY() + yOffset); - }); + WindowsUtil.bindDragged(windowHeader); - tabPane.getSelectionModel() - .selectedIndexProperty() - .addListener((_, _, newValue) -> Context.setCurrentViewIndex(newValue)); + viewModel.currentViewIndexProperty().bind(tabPane.getSelectionModel().selectedIndexProperty()); + } + + @FXML + public void openDocument() { + String locale = Context.getCurrentLocale().equals(Locale.ENGLISH) ? "" : "zh/"; + Context.openUrl(STR."https://alist.nn.ci/\{locale}"); + } + + @FXML + public void openGithub() { + Context.openUrl("https://github.com/alist-org/alist"); + } + + public void showModal(Node node, boolean persistent) { + modalPane.show(node); + modalPane.setPersistent(persistent); + } + + public void hideModal() { + modalPane.hide(false); + modalPane.setPersistent(false); } } diff --git a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java index ac81b78..67847f3 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java @@ -27,7 +27,7 @@ import java.util.Locale; * * @author octopus_yan */ -public class SetupController extends BaseController implements Initializable { +public class SetupController extends BaseController implements Initializable { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); @FXML @@ -51,8 +51,6 @@ public class SetupController extends BaseController implements Initializab @FXML public TextField proxyPort; - private final SetupViewModel viewModule = new SetupViewModel(); - @Override public VBox getRootPanel() { return setupView; @@ -91,20 +89,19 @@ public class SetupController extends BaseController implements Initializab @Override public void initViewAction() { - autoStartCheckBox.selectedProperty().bindBidirectional(viewModule.autoStartProperty()); - silentStartupCheckBox.selectedProperty().bindBidirectional(viewModule.silentStartupProperty()); - languageComboBox.getSelectionModel().selectedItemProperty() - .subscribe(locale -> viewModule.languageProperty().set(locale)); - themeComboBox.getSelectionModel().selectedItemProperty() - .subscribe(theme -> viewModule.themeProperty().set(theme)); - proxySetupComboBox.getSelectionModel().selectedItemProperty() - .subscribe((setup) -> viewModule.proxySetupProperty().set(setup)); - proxyHost.textProperty().bindBidirectional(viewModule.proxyHostProperty()); - proxyPort.textProperty().bindBidirectional(viewModule.proxyPortProperty()); + // + autoStartCheckBox.selectedProperty().bindBidirectional(viewModel.autoStartProperty()); + silentStartupCheckBox.selectedProperty().bindBidirectional(viewModel.silentStartupProperty()); + proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty()); + proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty()); + + viewModel.languageProperty().bind(languageComboBox.getSelectionModel().selectedItemProperty()); + viewModel.themeProperty().bind(themeComboBox.getSelectionModel().selectedItemProperty()); + viewModel.proxySetupProperty().bind(proxySetupComboBox.getSelectionModel().selectedItemProperty()); } @FXML public void proxyTest() { - viewModule.proxyTest(); + viewModel.proxyTest(); } } diff --git a/src/main/java/cn/octopusyan/alistgui/manager/AListManager.java b/src/main/java/cn/octopusyan/alistgui/manager/AListManager.java index 6b9c91f..5312a06 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/AListManager.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/AListManager.java @@ -2,23 +2,87 @@ package cn.octopusyan.alistgui.manager; import cn.octopusyan.alistgui.config.Constants; import cn.octopusyan.alistgui.config.Context; +import cn.octopusyan.alistgui.model.AListConfig; +import cn.octopusyan.alistgui.task.CheckUpdateTask; +import cn.octopusyan.alistgui.task.DownloadTask; +import cn.octopusyan.alistgui.task.listener.TaskListener; +import cn.octopusyan.alistgui.util.DownloadUtil; import cn.octopusyan.alistgui.util.ProcessesUtil; +import cn.octopusyan.alistgui.view.alert.AlertUtil; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; /** * AList 管理 * * @author octopus_yan */ +@Slf4j public class AListManager { + public static final String DATA_DIR = STR."\{Constants.BIN_DIR_PATH}\{File.separator}data"; + public static final String LOG_DIR = STR."\{DATA_DIR}\{File.separator}log"; + public static final String CONFIG_FILE = STR."\{DATA_DIR}\{File.separator}config.json"; + public static final String START_COMMAND = STR."\{Constants.ALIST_FILE} server"; + public static final String PWD_SET_COMMAND = STR."\{Constants.ALIST_FILE} admin set"; + public static final String PWD_RANDOM_COMMAND = STR."\{Constants.ALIST_FILE} admin random"; + + public static final String DEFAULT_SCHEME = "0.0.0.0:5244"; + public static final String PASSWORD_MSG_REG = ".*password( is)?: (.*)$"; + public static AListConfig aListConfig; + + public static final File configFile = new File(CONFIG_FILE); + private static final ProcessesUtil util; private static final BooleanProperty running = new SimpleBooleanProperty(false); + private static final StringProperty password = new SimpleStringProperty("******"); + private static DownloadTask downloadTask; + + private static final ProcessesUtil.OnExecuteListener runningListener; static { util = ProcessesUtil.init(Constants.BIN_DIR_PATH); + loadConfig(); + runningListener = new ProcessesUtil.OnExecuteListener() { + @Override + public void onExecute(String msg) { + if (hasConfig() && aListConfig == null) loadConfig(); + + if (msg.contains("start HTTP server")) { + Platform.runLater(() -> running.set(true)); + } + + ConsoleLog.msg(msg); + } + + @Override + public void onExecuteSuccess(boolean success) { + Platform.runLater(() -> running.set(false)); + } + + @Override + public void onExecuteError(Exception e) { + Platform.runLater(() -> running.set(false)); + log.error("AList error", e); + ConsoleLog.error("AList", e.getMessage()); + } + }; + } + +//==============================={ Property }==================================== + + private static void loadConfig() { + if (hasConfig()) { + aListConfig = ConfigManager.loadConfig(CONFIG_FILE, AListConfig.class); + } } public static BooleanProperty runningProperty() { @@ -29,31 +93,50 @@ public class AListManager { return running.get(); } + public static boolean hasConfig() { + return configFile.exists() && aListConfig != null; + } + + public static String scheme() { + return hasConfig() ? + STR."\{aListConfig.getScheme().getAddress()}:\{aListConfig.getScheme().getHttpPort()}" + : DEFAULT_SCHEME; + } + + public static StringProperty passwordProperty() { + return password; + } + + public static String password() { + return password.get(); + } + +//================================{ action }==================================== + + public static void openConfig() { + Context.openFile(new File(CONFIG_FILE)); + } + + public static void openLogFolder() { + Context.openFolder(new File(LOG_DIR)); + } + + public static void openScheme() { + Context.openUrl(STR."http://\{scheme()}"); + } + public static void start() { - ConsoleLog.info(getText("alist.status.start")); - if (running.get()) { + if (!checkAList()) return; + + if (running.get() || util.isRunning()) { ConsoleLog.warning(getText("alist.status.start.running")); return; } - running.set(true); - util.exec(START_COMMAND, new ProcessesUtil.OnExecuteListener() { - @Override - public void onExecute(String msg) { - if (ConsoleLog.isInit()) - ConsoleLog.msg(msg); - } + ConsoleLog.info(getText("alist.status.start")); - @Override - public void onExecuteSuccess(int exitValue) { - running.set(false); - } - - @Override - public void onExecuteError(Exception e) { - running.set(false); - } - }); + loadConfig(); + util.exec(START_COMMAND, runningListener); } public static void stop() { @@ -65,21 +148,96 @@ public class AListManager { util.destroy(); } - static ChangeListener changeListener; + static ChangeListener restartListener; public static void restart() { - stop(); - changeListener = (_, _, run) -> { - if (run) return; + if (!running.get()) { start(); - if (changeListener != null) { - running.removeListener(changeListener); + } else { + stop(); + + restartListener = (_, _, run) -> { + if (run) return; + running.removeListener(restartListener); + start(); + }; + running.addListener(restartListener); + } + } + + public static void resetPassword() { + resetPassword(""); + } + + static ChangeListener resetPasswordListener; + + public static void resetPassword(String pwd) { + String command = StringUtils.isNoneEmpty(pwd) ? + STR."\{PWD_SET_COMMAND} \{pwd}" : PWD_RANDOM_COMMAND; + + if (isRunning()) { + util.exec(command, ConsoleLog::msg); + return; + } + + start(); + resetPasswordListener = (_, _, newValue) -> { + if (newValue) { + running.removeListener(resetPasswordListener); + util.exec(command, ConsoleLog::msg); } }; - running.addListener(changeListener); + running.addListener(resetPasswordListener); + + } + +//============================={ private }==================================== + + /** + * TODO 点击开始时检查 aList 执行文件 + */ + private static boolean checkAList() { + if (new File(Constants.ALIST_FILE).exists()) return true; + + if (downloadTask != null && downloadTask.isRunning()) { + ConsoleLog.warning("AList Downloading ..."); + return false; + } + + var task = new CheckUpdateTask(ConfigManager.aList()); + task.onListen(new TaskListener.UpgradeUpgradeListener(task) { + @Override + public void onChecked(boolean hasUpgrade, String version) { + Platform.runLater(() -> showDownload(version)); + } + }); + task.execute(); + + return false; + } + + private static void showDownload(String version) { + String content = STR.""" + \{getText("msg.alist.download.notfile")} + \{Context.getLanguageBinding("update.remote").get()} : \{version} + """; + downloadTask = DownloadUtil.startDownload(ConfigManager.aList(), version, () -> { + DownloadUtil.unzip(ConfigManager.aList()); + Platform.runLater(() -> ConfigManager.aListVersion(version)); + restart(); + }); + AlertUtil.confirm() + .title("Download ALst") + .header(null) + .content(content) + .show(downloadTask::execute); } private static String getText(String code) { return Context.getLanguageBinding(code).get(); } + + public static void tmpPassword(String pwd) { + Platform.runLater(() -> password.set(pwd)); + } } diff --git a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java index 6122a7d..c6a7404 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import javafx.application.Platform; +import javafx.beans.property.StringProperty; import org.apache.commons.lang3.LocaleUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -66,7 +67,7 @@ public class ConfigManager { guiConfig = loadConfig(Constants.GUI_CONFIG_PATH, GuiConfig.class); } - private static T loadConfig(String path, Class clazz) { + public static T loadConfig(String path, Class clazz) { File src = new File(path); try { if (!src.exists()) { @@ -250,8 +251,12 @@ public class ConfigManager { return aList().getVersion(); } + public static StringProperty aListVersionProperty() { + return aList().versionProperty(); + } + public static void aListVersion(String version) { - aList().setVersion(version); + aListVersionProperty().set(version); } public static Gui gui() { diff --git a/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java b/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java index f3490e9..cf1cc72 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/ConsoleLog.java @@ -1,12 +1,17 @@ package cn.octopusyan.alistgui.manager; +import atlantafx.base.controls.Popover; import atlantafx.base.util.BBCodeParser; +import cn.hutool.core.swing.clipboard.ClipboardUtil; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; +import cn.octopusyan.alistgui.config.Context; import javafx.application.Platform; -import javafx.scene.Node; +import javafx.scene.control.Hyperlink; import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -14,6 +19,7 @@ import org.apache.commons.lang3.time.DateFormatUtils; import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -27,8 +33,9 @@ public class ConsoleLog { public static final String format = "yyyy/MM/dd hh:mm:ss"; private volatile static ConsoleLog log; 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 final static String CONSOLE_COLOR_PREFIX = "^\033["; + private final static String CONSOLE_MSG_REX = "^\033\\[(\\d+)m(.*)\033\\[0m(.*)$"; + private final static String URL_IP_REX = "^((ht|f)tps?:\\/\\/)?[\\w-+&@#/%?=~_|!:,.;]*[\\w-+&@#/%=~_|]+(:\\d{1,5})?\\/?$"; private ConsoleLog(ScrollPane logAreaSp, VBox textArea) { this.textArea = textArea; @@ -78,37 +85,104 @@ public class ConsoleLog { } public static void msg(String message, Object... param) { - if (message.contains("\033[")) { - message = resetConsoleColor(message); - } + if (StringUtils.isEmpty(message) || !isInit()) return; + message = message.strip(); + message = StrUtil.format(message, param); - Node text = BBCodeParser.createFormattedText(STR."\{StrUtil.format(message, param)}"); - Platform.runLater(() -> log.textArea.getChildren().add(text)); + // 多颜色消息处理 + if (StringUtils.countMatches(message, CONSOLE_COLOR_PREFIX) > 1) { + String[] split = message.replace(CONSOLE_MSG_REX, "\n%s".formatted(CONSOLE_COLOR_PREFIX)).split("\n"); + List msgs = Arrays.stream(split).toList(); + for (String msg : msgs) { + msg(msg); + } + return; + } + message = setPwdText(message); + message = resetConsoleColor(message); + + print(message); } public static void printLog(String tag, Level level, String message, Object... param) { + if (!isInit()) return; + // 时间 String time = DateFormatUtils.format(new Date(), format); time = STR."[color=-color-accent-emphasis]\{time}[/color]"; - + // 级别 String levelStr = resetLevelColor(level); - + // 标签 tag = StringUtils.isEmpty(tag) ? "" : STR."\{tag}: "; - + // 消息 message = STR."[color=-color-fg-muted]\{StrUtil.format(message, param)}[/color]"; - Node text; - String input = STR."\{time} \{levelStr} \{tag}\{message}"; + // 拼接后输出 + String input = STR."\{time} \{levelStr} - \{tag}\{message}"; - if (input.contains("\n")) { - text = BBCodeParser.createLayout(input); - } else { - text = BBCodeParser.createFormattedText(input); + print(input); + } + + private static void print(String message) { + + // 标记链接 + String regex = STR.".*(\{AListManager.scheme()}|\{URL_IP_REX}).*"; + if (ReUtil.isMatch(regex, message)) { + String text = ReUtil.get(regex, message, 1); + String url = text.startsWith("http") ? STR."http://\{text}" : text; + url = url.replace("0.0.0.0", "127.0.0.1"); + message = message.replace(text, STR."[url=\{url}]\{text}[/url]"); } + + TextFlow text = BBCodeParser.createFormattedText(STR."\{message}"); + // 处理链接 + setLink(text); + Platform.runLater(() -> log.textArea.getChildren().add(text)); } -//============================{ 私有方法 }================================ +//==========================================={ 私有方法 }=================================================== + + /** + * 将密码标记为link,一起处理点击事件 + * + * @param msg 输出信息 + * @return 处理后的信息 + */ + private static String setPwdText(String msg) { + if (!ReUtil.isMatch(AListManager.PASSWORD_MSG_REG, msg)) return msg; + + String password = ReUtil.get(AListManager.PASSWORD_MSG_REG, msg, 2); + AListManager.tmpPassword(password); + return msg.replace(password, STR."[url=\{password}]\{password}[/url]"); + } + + /** + * 处理文本流中的链接 + * + * @param text 文本流 + */ + private static void setLink(TextFlow text) { + + text.getChildren().forEach(child -> { + switch (child) { + case Hyperlink link -> link.setOnAction(_ -> { + String linkText = link.getUserData().toString(); + if (ReUtil.isMatch(URL_IP_REX, linkText)) { + Context.getApplication().getHostServices().showDocument(linkText); + } else { + ClipboardUtil.setStr(linkText); + var pop = new Popover(new Text(Context.getLanguageBinding("msg.alist.pwd.copy").get())); + pop.show(link); + } + link.setVisited(false); + }); + case TextFlow flow -> setLink(flow); + default -> { + } + } + }); + } /** * 控制台输出颜色 @@ -117,16 +191,13 @@ public class ConsoleLog { * @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 ""; + if (!msg.contains("\033[")) return msg; + + String colorCode = ReUtil.get(CONSOLE_MSG_REX, msg, 1); + String color = StringUtils.lowerCase(Color.valueOf(Integer.parseInt(colorCode)).getColor()); + String colorMsg = ReUtil.get(CONSOLE_MSG_REX, msg, 2); + msg = ReUtil.get(CONSOLE_MSG_REX, msg, 3); + return color(color, colorMsg) + msg; } /** @@ -149,8 +220,8 @@ public class ConsoleLog { @RequiredArgsConstructor public enum Level { INFO("INFO", null), - WARN("WARN", "color=-color-chart-2"), - ERROR("ERROR", "color=-color-chart-3"), + WARN("WARN", "-color-danger-emphasis"), + ERROR("ERROR", "-color-danger-fg"), ; private final String 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 65427bc..10fb04e 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/http/HttpUtil.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/http/HttpUtil.java @@ -1,11 +1,14 @@ package cn.octopusyan.alistgui.manager.http; import cn.octopusyan.alistgui.enums.ProxySetup; +import cn.octopusyan.alistgui.manager.http.handler.BodyHandler; import cn.octopusyan.alistgui.model.ProxyInfo; import cn.octopusyan.alistgui.util.JsonUtil; import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ProxySelector; @@ -15,15 +18,19 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.time.Duration; import java.util.Map; import java.util.Optional; +import java.util.function.BiConsumer; /** * 网络请求封装 * * @author octopus_yan@foxmail.com */ +@Slf4j public class HttpUtil { private volatile static HttpUtil util; @Getter @@ -107,6 +114,30 @@ public class HttpUtil { return response.body(); } + public void download(String url, String savePath, BiConsumer listener) throws IOException, InterruptedException { + HttpRequest request = getRequest(url, null).build(); + // 检查bin目录 + File binDir = new File(savePath); + if (!binDir.exists()) { + log.debug(STR."dir [\{savePath}] not exists"); + //noinspection ResultOfMethodCallIgnored + binDir.mkdirs(); + log.debug(STR."created dir [\{savePath}]"); + } + + // 下载处理器 + var handler = BodyHandler.create( + Path.of(savePath), + StandardOpenOption.CREATE, StandardOpenOption.WRITE + ); + + // 下载监听 + if (listener != null) + handler.listener(listener); + + HttpResponse response = httpClient.send(request, handler); + } + private HttpRequest.Builder getRequest(String uri, JsonNode header) { HttpRequest.Builder request = HttpRequest.newBuilder(); // 请求地址 @@ -141,7 +172,7 @@ public class HttpUtil { } } if (!formParams.isEmpty()) { - formParams = new StringBuilder("?" + formParams.substring(1)); + formParams = new StringBuilder(STR."?\{formParams.substring(1)}"); } return formParams.toString(); diff --git a/src/main/java/cn/octopusyan/alistgui/manager/http/handler/BodyHandler.java b/src/main/java/cn/octopusyan/alistgui/manager/http/handler/BodyHandler.java new file mode 100644 index 0000000..913aa6c --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/manager/http/handler/BodyHandler.java @@ -0,0 +1,102 @@ +package cn.octopusyan.alistgui.manager.http.handler; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.file.OpenOption; +import java.nio.file.Path; +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; + +/** + * 下载处理 + * + * @author octopus_yan + */ +@Slf4j +public 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/model/AListConfig.java b/src/main/java/cn/octopusyan/alistgui/model/AListConfig.java new file mode 100644 index 0000000..ff6870d --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/model/AListConfig.java @@ -0,0 +1,207 @@ +package cn.octopusyan.alistgui.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * alist 配置文件 model + * + * @author octopus_yan + */ +@NoArgsConstructor +@Getter +public class AListConfig { + + @JsonProperty("force") + private Boolean force; + @JsonProperty("site_url") + private String siteUrl; + @JsonProperty("cdn") + private String cdn; + @JsonProperty("jwt_secret") + private String jwtSecret; + @JsonProperty("token_expires_in") + private Integer tokenExpiresIn; + @JsonProperty("database") + private Database database; + @JsonProperty("meilisearch") + private MeiliSearch meilisearch; + @JsonProperty("scheme") + private Scheme scheme; + @JsonProperty("temp_dir") + private String tempDir; + @JsonProperty("bleve_dir") + private String bleveDir; + @JsonProperty("dist_dir") + private String distDir; + @JsonProperty("log") + private Log log; + @JsonProperty("delayed_start") + private Integer delayedStart; + @JsonProperty("max_connections") + private Integer maxConnections; + @JsonProperty("tls_insecure_skip_verify") + private Boolean tlsInsecureSkipVerify; + @JsonProperty("tasks") + private Tasks tasks; + @JsonProperty("cors") + private Cors cors; + @JsonProperty("s3") + private S3 s3; + + @NoArgsConstructor + @Getter + public static class Database { + @JsonProperty("type") + private String type; + @JsonProperty("host") + private String host; + @JsonProperty("port") + private Integer port; + @JsonProperty("user") + private String user; + @JsonProperty("password") + private String password; + @JsonProperty("name") + private String name; + @JsonProperty("db_file") + private String dbFile; + @JsonProperty("table_prefix") + private String tablePrefix; + @JsonProperty("ssl_mode") + private String sslMode; + @JsonProperty("dsn") + private String dsn; + } + + @NoArgsConstructor + @Getter + public static class MeiliSearch { + @JsonProperty("host") + private String host; + @JsonProperty("api_key") + private String apiKey; + @JsonProperty("index_prefix") + private String indexPrefix; + } + + @NoArgsConstructor + @Getter + public static class Scheme { + @JsonProperty("address") + private String address; + @JsonProperty("http_port") + private Integer httpPort; + @JsonProperty("https_port") + private Integer httpsPort; + @JsonProperty("force_https") + private Boolean forceHttps; + @JsonProperty("cert_file") + private String certFile; + @JsonProperty("key_file") + private String keyFile; + @JsonProperty("unix_file") + private String unixFile; + @JsonProperty("unix_file_perm") + private String unixFilePerm; + } + + @NoArgsConstructor + @Getter + public static class Log { + @JsonProperty("enable") + private Boolean enable; + @JsonProperty("name") + private String name; + @JsonProperty("max_size") + private Integer maxSize; + @JsonProperty("max_backups") + private Integer maxBackups; + @JsonProperty("max_age") + private Integer maxAge; + @JsonProperty("compress") + private Boolean compress; + } + + @NoArgsConstructor + @Getter + public static class Tasks { + @JsonProperty("download") + private Download download; + @JsonProperty("transfer") + private Transfer transfer; + @JsonProperty("upload") + private Upload upload; + @JsonProperty("copy") + private Copy copy; + + @NoArgsConstructor + @Getter + public static class Download { + @JsonProperty("workers") + private Integer workers; + @JsonProperty("max_retry") + private Integer maxRetry; + @JsonProperty("task_persistant") + private Boolean taskPersistant; + } + + @NoArgsConstructor + @Getter + public static class Transfer { + @JsonProperty("workers") + private Integer workers; + @JsonProperty("max_retry") + private Integer maxRetry; + @JsonProperty("task_persistant") + private Boolean taskPersistant; + } + + @NoArgsConstructor + @Getter + public static class Upload { + @JsonProperty("workers") + private Integer workers; + @JsonProperty("max_retry") + private Integer maxRetry; + @JsonProperty("task_persistant") + private Boolean taskPersistant; + } + + @NoArgsConstructor + @Getter + public static class Copy { + @JsonProperty("workers") + private Integer workers; + @JsonProperty("max_retry") + private Integer maxRetry; + @JsonProperty("task_persistant") + private Boolean taskPersistant; + } + } + + @NoArgsConstructor + @Getter + public static class Cors { + @JsonProperty("allow_origins") + private List allowOrigins; + @JsonProperty("allow_methods") + private List allowMethods; + @JsonProperty("allow_headers") + private List allowHeaders; + } + + @NoArgsConstructor + @Getter + public static class S3 { + @JsonProperty("enable") + private Boolean enable; + @JsonProperty("port") + private Integer port; + @JsonProperty("ssl") + private Boolean ssl; + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/model/upgrade/AList.java b/src/main/java/cn/octopusyan/alistgui/model/upgrade/AList.java index 9fa79b5..c1c5690 100644 --- a/src/main/java/cn/octopusyan/alistgui/model/upgrade/AList.java +++ b/src/main/java/cn/octopusyan/alistgui/model/upgrade/AList.java @@ -1,6 +1,8 @@ package cn.octopusyan.alistgui.model.upgrade; import com.fasterxml.jackson.annotation.JsonIgnore; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import lombok.Data; /** @@ -14,5 +16,17 @@ public class AList implements UpgradeApp { private final String repo = "alist"; private String releaseFile = "alist-windows-amd64.zip"; - private String version = "unknown"; + private StringProperty version = new SimpleStringProperty("unknown"); + + public StringProperty versionProperty() { + return version; + } + + public void setVersion(String version) { + this.version.set(version); + } + + public String getVersion() { + return version.get(); + } } diff --git a/src/main/java/cn/octopusyan/alistgui/task/CheckUpdateTask.java b/src/main/java/cn/octopusyan/alistgui/task/CheckUpdateTask.java new file mode 100644 index 0000000..9ba4ba3 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/task/CheckUpdateTask.java @@ -0,0 +1,43 @@ +package cn.octopusyan.alistgui.task; + +import cn.octopusyan.alistgui.base.BaseTask; +import cn.octopusyan.alistgui.manager.http.HttpUtil; +import cn.octopusyan.alistgui.model.upgrade.UpgradeApp; +import cn.octopusyan.alistgui.util.JsonUtil; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.lang3.StringUtils; + +/** + * 检查更新任务 + * + * @author octopus_yan + */ +public class CheckUpdateTask extends BaseTask { + private final UpgradeApp app; + + public CheckUpdateTask(UpgradeApp app) { + super(STR."check update \{app.getRepo()}"); + this.app = app; + } + + @Override + protected void task() throws Exception { + String responseStr = HttpUtil.getInstance().get(app.getReleaseApi(), null, null); + JsonNode response = JsonUtil.parseJsonObject(responseStr); + + // TODO 校验返回内容 + String newVersion = response.get("tag_name").asText(); + + if (listener != null && listener instanceof UpgradeListener lis) + lis.onChecked(!StringUtils.equals(app.getVersion(), newVersion), newVersion); + } + + public interface UpgradeListener extends BaseTask.Listener { + @Override + default void onSucceeded() { + // do nothing ... + } + + void onChecked(boolean hasUpgrade, String version); + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/task/DownloadTask.java b/src/main/java/cn/octopusyan/alistgui/task/DownloadTask.java index 79e3fa0..6a8104e 100644 --- a/src/main/java/cn/octopusyan/alistgui/task/DownloadTask.java +++ b/src/main/java/cn/octopusyan/alistgui/task/DownloadTask.java @@ -3,26 +3,8 @@ 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 下载任务 * @@ -33,126 +15,24 @@ public class DownloadTask extends BaseTask { private final String downloadUrl; public DownloadTask(String downloadUrl) { + super(STR."Download \{downloadUrl}"); this.downloadUrl = downloadUrl; - log.info(STR."downlaod url : \{downloadUrl}"); + } + + public void onListen(DownloadListener listener) { + super.onListen(listener); } @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 + HttpUtil.getInstance().download( + downloadUrl, + Constants.BIN_DIR_PATH, + listener instanceof DownloadListener ? ((DownloadListener) listener)::onProgress : null ); - - // 下载监听 - 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 { + public interface DownloadListener 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/ProxyCheckTask.java b/src/main/java/cn/octopusyan/alistgui/task/ProxyCheckTask.java index b9e2b9f..0dc3195 100644 --- a/src/main/java/cn/octopusyan/alistgui/task/ProxyCheckTask.java +++ b/src/main/java/cn/octopusyan/alistgui/task/ProxyCheckTask.java @@ -14,6 +14,7 @@ public class ProxyCheckTask extends BaseTask { private final String checkUrl; public ProxyCheckTask(String checkUrl) { + super(STR."ProxyCheck[\{checkUrl}]"); this.checkUrl = checkUrl; this.updateProgress(0d, 1d); } diff --git a/src/main/java/cn/octopusyan/alistgui/task/UpgradeTask.java b/src/main/java/cn/octopusyan/alistgui/task/UpgradeTask.java deleted file mode 100644 index 7ef9991..0000000 --- a/src/main/java/cn/octopusyan/alistgui/task/UpgradeTask.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.octopusyan.alistgui.task; - -import cn.octopusyan.alistgui.base.BaseTask; -import cn.octopusyan.alistgui.manager.http.HttpUtil; -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; -import com.fasterxml.jackson.databind.JsonNode; -import org.apache.commons.lang3.StringUtils; - -/** - * 检查更新任务 - * - * @author octopus_yan - */ -public class UpgradeTask extends BaseTask { - private final AboutViewModule viewModule; - private final UpgradeApp app; - - public UpgradeTask(AboutViewModule viewModel, UpgradeApp app) { - this.viewModule = viewModel; - this.app = app; - } - - @Override - protected void task() throws Exception { - String responseStr = HttpUtil.getInstance().get(app.getReleaseApi(), null, null); - JsonNode response = JsonUtil.parseJsonObject(responseStr); - - // TODO 校验返回内容 - String newVersion = response.get("tag_name").asText(); - - runLater(() -> { - 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/task/listener/TaskListener.java b/src/main/java/cn/octopusyan/alistgui/task/listener/TaskListener.java new file mode 100644 index 0000000..03088e4 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/task/listener/TaskListener.java @@ -0,0 +1,102 @@ +package cn.octopusyan.alistgui.task.listener; + +import cn.octopusyan.alistgui.base.BaseTask; +import cn.octopusyan.alistgui.manager.ConsoleLog; +import cn.octopusyan.alistgui.task.CheckUpdateTask; +import cn.octopusyan.alistgui.task.DownloadTask; +import cn.octopusyan.alistgui.view.alert.AlertUtil; +import cn.octopusyan.alistgui.view.alert.builder.ProgressBuilder; +import lombok.extern.slf4j.Slf4j; + +/** + * 任务监听器默认实现 + * + * @author octopus_yan + */ +@Slf4j +public abstract class TaskListener implements BaseTask.Listener { + private final BaseTask task; + // 加载弹窗 + final ProgressBuilder progress = AlertUtil.progress(); + + public TaskListener(BaseTask task) { + this.task = task; + progress.onCancel(task::cancel); + } + + @Override + public void onStart() { + log.info(STR."\{task.getName()} start ..."); + ConsoleLog.info(task.getName(), "start ..."); + } + + @Override + public void onRunning() { + progress.show(); + } + + @Override + public void onCancelled() { + progress.close(); + log.info(STR."\{task.getName()} cancel ..."); + ConsoleLog.info(task.getName(), "cancel ..."); + } + + @Override + public void onFailed(Throwable throwable) { + progress.close(); + log.error(STR."\{task.getName()} fail ...", throwable); + ConsoleLog.error(task.getName(), STR."fail : \{throwable.getMessage()}"); + onFail(throwable); + } + + @Override + public void onSucceeded() { + progress.close(); + log.info(STR."\{task.getName()} success ..."); + ConsoleLog.info(task.getName(), "success ..."); + onSucceed(); + } + + protected abstract void onSucceed(); + + protected void onFail(Throwable throwable) { + } + + /** + * 下载任务监听默认实现 + */ + public static abstract class DownloadListener extends TaskListener implements DownloadTask.DownloadListener { + + private volatile int lastProgress = 0; + + public DownloadListener(BaseTask task) { + super(task); + } + + @Override + public void onProgress(Long total, Long progress) { + int a = (int) (((double) progress / total) * 100); + if (a % 10 == 0) { + if (a != lastProgress) { + lastProgress = a; + ConsoleLog.info(STR."\{lastProgress} %"); + } + } + } + } + + /** + * 检查更新监听默认实现 + */ + public static abstract class UpgradeUpgradeListener extends TaskListener implements CheckUpdateTask.UpgradeListener { + public UpgradeUpgradeListener(BaseTask task) { + super(task); + } + + @Override + protected void onSucceed() { + // do nothing ... + } + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/util/DownloadUtil.java b/src/main/java/cn/octopusyan/alistgui/util/DownloadUtil.java new file mode 100644 index 0000000..406c67e --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/util/DownloadUtil.java @@ -0,0 +1,84 @@ +package cn.octopusyan.alistgui.util; + +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.config.Constants; +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.listener.TaskListener; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.InputStream; +import java.util.zip.ZipFile; + +/** + * 下载工具 + * + * @author octopus_yan + */ +@Slf4j +public class DownloadUtil { + + + /** + * 下载文件 + * + * @param app 应用 + * @param version 下载版本 + */ + public static DownloadTask startDownload(UpgradeApp app, String version, Runnable runnable) { + var task = new DownloadTask(app.getDownloadUrl(version)); + task.onListen(new TaskListener.DownloadListener(task) { + + @Override + public void onRunning() { + // 不展示进度条 + } + + @Override + public void onSucceed() { + String msg = STR."download \{app.getRepo()} success"; + log.info(msg); + ConsoleLog.info(msg); + + runnable.run(); + } + }); + return task; + } + + public static void unzip(UpgradeApp app) { + String parentPath = app instanceof AList ? Constants.BIN_DIR_PATH : Constants.DATA_DIR_PATH; + + File file = new File(parentPath + 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(parentPath, 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/util/ProcessesUtil.java b/src/main/java/cn/octopusyan/alistgui/util/ProcessesUtil.java index 6c1e586..bae8336 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/ProcessesUtil.java +++ b/src/main/java/cn/octopusyan/alistgui/util/ProcessesUtil.java @@ -4,7 +4,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.exec.*; import java.io.File; -import java.io.IOException; import java.util.HashSet; import java.util.Set; @@ -16,20 +15,12 @@ import java.util.Set; @Slf4j public class ProcessesUtil { private static final String NEW_LINE = System.lineSeparator(); + private static final int EXIT_VALUE = 1; private final Executor executor = DefaultExecutor.builder().get(); private final ShutdownHookProcessDestroyer processDestroyer; - private DefaultExecuteResultHandler handler; - private final PumpStreamHandler streamHandler; private OnExecuteListener listener; private CommandLine commandLine; - private final LogOutputStream logout = new LogOutputStream() { - @Override - protected void processLine(String line, int logLevel) { - if (listener != null) - listener.onExecute(line + NEW_LINE); - } - }; private static final Set set = new HashSet<>(); @@ -37,10 +28,17 @@ public class ProcessesUtil { * Prevent construction. */ private ProcessesUtil(String workingDirectory) { - streamHandler = new PumpStreamHandler(logout, logout); + LogOutputStream logout = new LogOutputStream() { + @Override + protected void processLine(String line, int logLevel) { + if (listener != null) + listener.onExecute(line + NEW_LINE); + } + }; + PumpStreamHandler streamHandler = new PumpStreamHandler(logout, logout); executor.setStreamHandler(streamHandler); executor.setWorkingDirectory(new File(workingDirectory)); - executor.setExitValue(1); + executor.setExitValue(EXIT_VALUE); processDestroyer = new ShutdownHookProcessDestroyer(); executor.setProcessDestroyer(processDestroyer); } @@ -51,20 +49,25 @@ public class ProcessesUtil { return util; } - public boolean exec(String command) throws IOException { + public boolean exec(String command) { commandLine = CommandLine.parse(command); - int execute = executor.execute(commandLine); - return 0 == execute; + int execute = 0; + try { + execute = executor.execute(commandLine); + } catch (Exception e) { + log.error("exec", e); + } + return execute == EXIT_VALUE; } public void exec(String command, OnExecuteListener listener) { this.listener = listener; commandLine = CommandLine.parse(command); - handler = new DefaultExecuteResultHandler() { + DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() { @Override public void onProcessComplete(int exitValue) { if (listener != null) { - listener.onExecuteSuccess(exitValue); + listener.onExecuteSuccess(exitValue == EXIT_VALUE); } } @@ -77,7 +80,7 @@ public class ProcessesUtil { }; try { executor.execute(commandLine, handler); - } catch (IOException e) { + } catch (Exception e) { if (listener != null) listener.onExecuteError(e); } } @@ -87,6 +90,10 @@ public class ProcessesUtil { processDestroyer.run(); } + public boolean isRunning() { + return !processDestroyer.isEmpty(); + } + public static void destroyAll() { set.forEach(ProcessesUtil::destroy); } @@ -94,7 +101,7 @@ public class ProcessesUtil { public interface OnExecuteListener { void onExecute(String msg); - default void onExecuteSuccess(int exitValue) { + default void onExecuteSuccess(boolean success) { } default void onExecuteError(Exception e) { diff --git a/src/main/java/cn/octopusyan/alistgui/manager/WindowsUtil.java b/src/main/java/cn/octopusyan/alistgui/util/WindowsUtil.java similarity index 75% rename from src/main/java/cn/octopusyan/alistgui/manager/WindowsUtil.java rename to src/main/java/cn/octopusyan/alistgui/util/WindowsUtil.java index 7634f4c..9dc750b 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/WindowsUtil.java +++ b/src/main/java/cn/octopusyan/alistgui/util/WindowsUtil.java @@ -1,8 +1,8 @@ -package cn.octopusyan.alistgui.manager; +package cn.octopusyan.alistgui.util; +import cn.octopusyan.alistgui.Application; import javafx.scene.layout.Pane; import javafx.stage.Stage; -import javafx.stage.Window; import java.util.HashMap; import java.util.Map; @@ -25,7 +25,11 @@ public class WindowsUtil { } public static void bindDragged(Pane pane) { - Window stage = getStage(pane); + Stage stage = getStage(pane); + bindDragged(pane, stage); + } + + public static void bindDragged(Pane pane, Stage stage) { pane.setOnMousePressed(event -> { paneXOffset.put(pane, stage.getX() - event.getScreenX()); paneYOffset.put(pane, stage.getY() - event.getScreenY()); @@ -37,6 +41,10 @@ public class WindowsUtil { } public static Stage getStage(Pane pane) { - return (Stage) pane.getScene().getWindow(); + try { + return (Stage) pane.getScene().getWindow(); + } catch (Throwable e) { + return Application.getPrimaryStage(); + } } } diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertUtil.java b/src/main/java/cn/octopusyan/alistgui/view/alert/AlertUtil.java similarity index 92% rename from src/main/java/cn/octopusyan/alistgui/util/alert/AlertUtil.java rename to src/main/java/cn/octopusyan/alistgui/view/alert/AlertUtil.java index be51aa6..9b74557 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertUtil.java +++ b/src/main/java/cn/octopusyan/alistgui/view/alert/AlertUtil.java @@ -1,5 +1,6 @@ -package cn.octopusyan.alistgui.util.alert; +package cn.octopusyan.alistgui.view.alert; +import cn.octopusyan.alistgui.view.alert.builder.*; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.stage.Stage; @@ -17,6 +18,10 @@ public class AlertUtil { AlertUtil.mOwner = stage; } + public static DefaultBuilder builder() { + return new DefaultBuilder(mOwner); + } + public static AlertBuilder info(String content) { return info().content(content).header(null); } diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertBuilder.java b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/AlertBuilder.java similarity index 95% rename from src/main/java/cn/octopusyan/alistgui/util/alert/AlertBuilder.java rename to src/main/java/cn/octopusyan/alistgui/view/alert/builder/AlertBuilder.java index 4195713..09e27fe 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/AlertBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/AlertBuilder.java @@ -1,5 +1,7 @@ -package cn.octopusyan.alistgui.util.alert; +package cn.octopusyan.alistgui.view.alert.builder; +import cn.octopusyan.alistgui.base.BaseBuilder; +import cn.octopusyan.alistgui.view.alert.AlertUtil; import javafx.scene.control.*; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/ChoiceBuilder.java b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/ChoiceBuilder.java similarity index 86% rename from src/main/java/cn/octopusyan/alistgui/util/alert/ChoiceBuilder.java rename to src/main/java/cn/octopusyan/alistgui/view/alert/builder/ChoiceBuilder.java index 7be7c17..b380475 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/ChoiceBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/ChoiceBuilder.java @@ -1,9 +1,9 @@ -package cn.octopusyan.alistgui.util.alert; +package cn.octopusyan.alistgui.view.alert.builder; +import cn.octopusyan.alistgui.base.BaseBuilder; import javafx.scene.control.ChoiceDialog; import javafx.stage.Window; -import java.util.Collection; import java.util.Optional; /** diff --git a/src/main/java/cn/octopusyan/alistgui/view/alert/builder/DefaultBuilder.java b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/DefaultBuilder.java new file mode 100644 index 0000000..fcf4317 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/DefaultBuilder.java @@ -0,0 +1,50 @@ +package cn.octopusyan.alistgui.view.alert.builder; + +import cn.octopusyan.alistgui.base.BaseBuilder; +import cn.octopusyan.alistgui.config.Context; +import cn.octopusyan.alistgui.util.WindowsUtil; +import javafx.scene.Node; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.paint.Color; +import javafx.stage.StageStyle; +import javafx.stage.Window; + +/** + * 默认弹窗 + * + * @author octopus_yan + */ +public class DefaultBuilder extends BaseBuilder> { + + public DefaultBuilder(Window mOwner) { + super(new Dialog<>(), mOwner); + + header(null); + + DialogPane dialogPane = dialog.getDialogPane(); + dialogPane.getScene().setFill(Color.TRANSPARENT); + WindowsUtil.bindDragged(dialogPane); + WindowsUtil.bindShadow(dialogPane); + WindowsUtil.getStage(dialogPane).initStyle(StageStyle.TRANSPARENT); + + dialogPane.getButtonTypes().add(new ButtonType( + Context.getLanguageBinding("label.cancel").get(), + ButtonType.CANCEL.getButtonData() + )); + + for (Node child : dialogPane.getChildren()) { + if (child instanceof ButtonBar) { + dialogPane.getChildren().remove(child); + break; + } + } + } + + public DefaultBuilder content(Node content) { + dialog.getDialogPane().setContent(content); + return this; + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/ProgressBuilder.java b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/ProgressBuilder.java similarity index 52% rename from src/main/java/cn/octopusyan/alistgui/util/alert/ProgressBuilder.java rename to src/main/java/cn/octopusyan/alistgui/view/alert/builder/ProgressBuilder.java index 707090d..1cd3fe3 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/ProgressBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/ProgressBuilder.java @@ -1,16 +1,13 @@ -package cn.octopusyan.alistgui.util.alert; +package cn.octopusyan.alistgui.view.alert.builder; import cn.octopusyan.alistgui.config.Context; -import cn.octopusyan.alistgui.manager.WindowsUtil; import javafx.beans.binding.Bindings; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.stage.StageStyle; import javafx.stage.Window; /** @@ -18,34 +15,11 @@ import javafx.stage.Window; * * @author octopus_yan */ -public class ProgressBuilder extends BaseBuilder> { +public class ProgressBuilder extends DefaultBuilder { public ProgressBuilder(Window mOwner) { - this(new Dialog<>(), mOwner); - } - - public ProgressBuilder(Dialog dialog, Window mOwner) { - super(dialog, mOwner); - - DialogPane dialogPane = dialog.getDialogPane(); - dialogPane.getScene().setFill(Color.TRANSPARENT); - WindowsUtil.bindDragged(dialogPane); - WindowsUtil.bindShadow(dialogPane); - WindowsUtil.getStage(dialogPane).initStyle(StageStyle.TRANSPARENT); - var content = getContent(); - - dialogPane.setContent(content); - dialogPane.getButtonTypes().add(new ButtonType( - Context.getLanguageBinding("label.cancel").get(), - ButtonType.CANCEL.getButtonData() - )); - - for (Node child : dialogPane.getChildren()) { - if (child instanceof ButtonBar) { - dialogPane.getChildren().remove(child); - break; - } - } + super(mOwner); + content(getContent()); } private Pane getContent() { diff --git a/src/main/java/cn/octopusyan/alistgui/util/alert/TextInputBuilder.java b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/TextInputBuilder.java similarity index 89% rename from src/main/java/cn/octopusyan/alistgui/util/alert/TextInputBuilder.java rename to src/main/java/cn/octopusyan/alistgui/view/alert/builder/TextInputBuilder.java index b89058f..cb77faa 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/alert/TextInputBuilder.java +++ b/src/main/java/cn/octopusyan/alistgui/view/alert/builder/TextInputBuilder.java @@ -1,5 +1,6 @@ -package cn.octopusyan.alistgui.util.alert; +package cn.octopusyan.alistgui.view.alert.builder; +import cn.octopusyan.alistgui.base.BaseBuilder; import javafx.scene.control.TextInputDialog; import javafx.stage.Window; diff --git a/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java b/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java index a7d35c0..c5dbdef 100644 --- a/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/AboutViewModule.java @@ -1,35 +1,27 @@ 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.base.BaseViewModel; 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 cn.octopusyan.alistgui.task.CheckUpdateTask; +import cn.octopusyan.alistgui.task.listener.TaskListener; +import cn.octopusyan.alistgui.util.DownloadUtil; +import cn.octopusyan.alistgui.view.alert.AlertUtil; +import cn.octopusyan.alistgui.view.alert.builder.AlertBuilder; import javafx.application.Platform; import javafx.beans.property.*; import lombok.extern.slf4j.Slf4j; -import java.io.File; -import java.io.InputStream; -import java.util.zip.ZipFile; - /** * 关于 * * @author octopus_yan */ @Slf4j -public class AboutViewModule { +public class AboutViewModule extends BaseViewModel { private final StringProperty aListVersion = new SimpleStringProperty(ConfigManager.aListVersion()); private final StringProperty aListNewVersion = new SimpleStringProperty(""); private final BooleanProperty aListUpgrade = new SimpleBooleanProperty(false); @@ -38,7 +30,7 @@ public class AboutViewModule { private final BooleanProperty guiUpgrade = new SimpleBooleanProperty(false); public AboutViewModule() { - aListVersion.addListener((_, _, newValue) -> ConfigManager.aListVersion(newValue)); + aListVersion.bindBidirectional(ConfigManager.aListVersionProperty()); } public Property aListVersionProperty() { @@ -83,10 +75,12 @@ public class AboutViewModule { 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) @@ -95,131 +89,50 @@ public class AboutViewModule { \{newLabel} : \{newVersion} """) .show(() -> { - if (upgrade) startDownload(app, newVersion); + // 可升级,且点击了确定后,开始下载任务 + if (upgrade) + DownloadUtil.startDownload(app, newVersion, () -> { + // 下载完成后,解压并删除文件 + DownloadUtil.unzip(app); + // 设置应用版本 + Platform.runLater(() -> aListVersion.setValue(aListNewVersion.getValue())); + }).execute(); }); }); } 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); + var task = new CheckUpdateTask(app); // 任务监听 - task.onListen(new BaseTask.Listener() { + task.onListen(new TaskListener.UpgradeUpgradeListener(task) { @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() { - progress.close(); + protected void onSucceed() { 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()}"); + public void onChecked(boolean hasUpgrade, String version) { + // 版本检查结果 + Platform.runLater(() -> { + if (app instanceof AList) { + aListUpgrade.setValue(hasUpgrade); + aListNewVersion.setValue(version); + } else { + guiUpgrade.setValue(hasUpgrade); + guiNewVersion.setValue(version); + } + }); + } + + @Override + protected void onFail(Throwable throwable) { 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/AdminPanelViewModel.java b/src/main/java/cn/octopusyan/alistgui/viewModel/AdminPanelViewModel.java new file mode 100644 index 0000000..4c2f241 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/AdminPanelViewModel.java @@ -0,0 +1,23 @@ +package cn.octopusyan.alistgui.viewModel; + +import cn.octopusyan.alistgui.base.BaseViewModel; +import cn.octopusyan.alistgui.manager.AListManager; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +/** + * Admin Panel VM + * + * @author octopus_yan + */ +public class AdminPanelViewModel extends BaseViewModel { + private final StringProperty password = new SimpleStringProperty(AListManager.passwordProperty().get()); + + public AdminPanelViewModel() { + AListManager.passwordProperty().subscribe(password::set); + } + + public StringProperty passwordProperty() { + return password; + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/viewModel/MainViewModel.java b/src/main/java/cn/octopusyan/alistgui/viewModel/MainViewModel.java new file mode 100644 index 0000000..17fcf9e --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/MainViewModel.java @@ -0,0 +1,68 @@ +package cn.octopusyan.alistgui.viewModel; + +import cn.octopusyan.alistgui.base.BaseViewModel; +import cn.octopusyan.alistgui.config.Context; +import cn.octopusyan.alistgui.manager.AListManager; +import javafx.application.Platform; +import javafx.beans.property.*; +import javafx.collections.FXCollections; + +/** + * 主界面VM + * + * @author octopus_yan + */ +public class MainViewModel extends BaseViewModel { + private final ListProperty startBtnStyleCss = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final StringProperty startBtnText = new SimpleStringProperty(); + private final ListProperty statusLabelStyleCss = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final StringProperty statusLabelText = new SimpleStringProperty(); + private final BooleanProperty browserButtonDisable = new SimpleBooleanProperty(); + private final BooleanProperty running = new SimpleBooleanProperty(); + + public MainViewModel() { + running.addListener((_, _, running) -> { + resetStatus(running); + browserButtonDisable.set(!running); + }); + // 先添加监听再绑定,解决切换locale后,界面状态显示错误的问题 + running.bind(AListManager.runningProperty()); + } + + public ListProperty startBtnStyleCssProperty() { + return startBtnStyleCss; + } + + public StringProperty startBtnTextProperty() { + return startBtnText; + } + + public ListProperty statusLabelStyleCssProperty() { + return statusLabelStyleCss; + } + + public StringProperty statusLabelTextProperty() { + return statusLabelText; + } + + public BooleanProperty browserButtonDisableProperty() { + return browserButtonDisable; + } + + public void resetStatus(boolean running) { + String removeStyle = running ? "success" : "danger"; + String addStyle = running ? "danger" : "success"; + String button = Context.getLanguageBinding(STR."main.control.\{running ? "stop" : "start"}").get(); + String status = Context.getLanguageBinding(STR."main.status.label-\{running ? "running" : "stop"}").get(); + + Platform.runLater(() -> { + startBtnStyleCss.remove(removeStyle); + startBtnStyleCss.add(addStyle); + startBtnText.set(button); + + statusLabelStyleCss.remove(addStyle); + statusLabelStyleCss.add(removeStyle); + statusLabelText.set(status); + }); + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/viewModel/RootViewModel.java b/src/main/java/cn/octopusyan/alistgui/viewModel/RootViewModel.java new file mode 100644 index 0000000..cad967a --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/RootViewModel.java @@ -0,0 +1,23 @@ +package cn.octopusyan.alistgui.viewModel; + +import cn.octopusyan.alistgui.base.BaseViewModel; +import cn.octopusyan.alistgui.config.Context; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +/** + * Root VM + * + * @author octopus_yan + */ +public class RootViewModel extends BaseViewModel { + private final IntegerProperty currentViewIndex = new SimpleIntegerProperty(Context.currentViewIndex()) { + { + Context.currentViewIndexProperty().bind(this); + } + }; + + public IntegerProperty currentViewIndexProperty() { + return currentViewIndex; + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java b/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java index 0bf2610..4f58c53 100644 --- a/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java @@ -1,14 +1,14 @@ package cn.octopusyan.alistgui.viewModel; import atlantafx.base.theme.Theme; -import cn.octopusyan.alistgui.base.BaseTask; +import cn.octopusyan.alistgui.base.BaseViewModel; import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.enums.ProxySetup; import cn.octopusyan.alistgui.manager.ConfigManager; import cn.octopusyan.alistgui.manager.http.HttpUtil; import cn.octopusyan.alistgui.task.ProxyCheckTask; -import cn.octopusyan.alistgui.util.alert.AlertUtil; -import javafx.application.Platform; +import cn.octopusyan.alistgui.task.listener.TaskListener; +import cn.octopusyan.alistgui.view.alert.AlertUtil; import javafx.beans.property.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -21,7 +21,7 @@ import java.util.Locale; * @author octopus_yan */ @Slf4j -public class SetupViewModel { +public class SetupViewModel extends BaseViewModel { private final BooleanProperty autoStart = new SimpleBooleanProperty(ConfigManager.autoStart()); private final BooleanProperty silentStartup = new SimpleBooleanProperty(ConfigManager.silentStartup()); private final ObjectProperty theme = new SimpleObjectProperty<>(ConfigManager.theme()); @@ -96,24 +96,15 @@ public class SetupViewModel { private static ProxyCheckTask getProxyCheckTask(String checkUrl) { var task = new ProxyCheckTask(checkUrl); - final var progress = AlertUtil.progress(); - progress.title(Context.getLanguageBinding("proxy.test.title").get()); - task.onListen(new BaseTask.Listener() { + task.onListen(new TaskListener(task) { @Override - public void onRunning() { - progress.onCancel(task::cancel).show(); - } - - @Override - public void onSucceeded() { - Platform.runLater(progress::close); + public void onSucceed() { AlertUtil.info(Context.getLanguageBinding("proxy.test.result.success").getValue()).show(); } @Override - public void onFailed(Throwable throwable) { - Platform.runLater(progress::close); + public void onFail(Throwable throwable) { final var tmp = Context.getLanguageBinding("proxy.test.result.failed").getValue(); String throwableMessage = throwable.getMessage(); AlertUtil.error(tmp + (StringUtils.isEmpty(throwableMessage) ? "" : throwableMessage)).show(); diff --git a/src/main/resources/css/admin-panel.scss b/src/main/resources/css/admin-panel.scss new file mode 100644 index 0000000..19ea935 --- /dev/null +++ b/src/main/resources/css/admin-panel.scss @@ -0,0 +1,31 @@ +/************************************************** + * Admin Password Panel +**************************************************/ + +#admin-panel { + -fx-background-color: -color-bg-default; + -fx-background-radius: 15; + -fx-border-radius: 15; + + .header { + .label { + -fx-font-size: 20; + } + + .button, .button.ikonli-font-icon { + -fx-background-radius: 15; + } + } + + .admin-field { + -fx-spacing: 10; + + .text-field { + -fx-pref-width: 100; + } + + &-value { + //-fx-padding: 5 10; + } + } +} \ No newline at end of file diff --git a/src/main/resources/css/main-view.scss b/src/main/resources/css/main-view.scss index e23de78..2adf228 100644 --- a/src/main/resources/css/main-view.scss +++ b/src/main/resources/css/main-view.scss @@ -14,24 +14,27 @@ -fx-border-radius: 10; } -.control-menu, #startButton, #passwordButton, #restartButton, #moreButton { +.control-menu, #moreButton { -fx-font-size: 15; -fx-background-radius: 15; - -fx-padding: 10 40 10 40; + -fx-padding: 10 40; -fx-border-radius: 15; -fx-border-width: 2; } #startButton { + -color-button-bg-hover: -color-button-bg; + -color-button-bg-focused: -color-button-bg; -fx-border-color: -color-button-bg; + -fx-border-color-hover: -color-button-bg; } #passwordButton { -color-button-bg: -color-success-3; -color-button-bg-hover: -color-button-bg; - -color-button-bg-focused: -color-button-bg; + -color-button-bg-focused: -color-success-3; -color-button-bg-pressed: -color-button-bg; - -fx-border-color: -color-button-bg; + -fx-border-color: -color-success-3; } #restartButton { @@ -43,12 +46,20 @@ } #moreButton { - -color-button-bg-focused: transparent; - -color-button-bg-pressed: transparent; - -color-button-bg-hover: -color-chart-6-alpha20; - -color-button-border: -color-chart-6; - -fx-border-color: -color-chart-6; - -color-button-border-hover: -color-chart-6-alpha70; + -fx-padding: 2 30; + + -fx-background-color: transparent; + -fx-border-color: -color-chart-6-alpha70; + + &:hover { + -fx-background-color: -color-chart-6-alpha20; + -fx-border-color: -color-chart-6-alpha70; + } + + .context-menu, .menu-item { + -fx-background-radius: 15; + -fx-border-radius: 15; + } } .logArea { diff --git a/src/main/resources/css/root-view.scss b/src/main/resources/css/root-view.scss index 7e60841..bdfe4cd 100644 --- a/src/main/resources/css/root-view.scss +++ b/src/main/resources/css/root-view.scss @@ -91,23 +91,22 @@ #windowFooter { .button { -fx-font-size: 15; - -fx-background-color: transparent; -fx-text-alignment: CENTER; } - #document { - -fx-text-fill: -color-success-5; - } - - #github { - -fx-text-fill: -color-accent-5; - } - - #sponsor { - -fx-text-fill: -color-danger-emphasis; - } - .ikonli-font-icon { -fx-font-size: 15; } +} + +/************************************************** + * Modal Pane +**************************************************/ + +.modal-pane { + -fx-background-radius: 15; + + .scrollable-content { + -fx-background-radius: 15; + } } \ No newline at end of file diff --git a/src/main/resources/css/root.scss b/src/main/resources/css/root.scss index 3cd0718..a7a1b30 100644 --- a/src/main/resources/css/root.scss +++ b/src/main/resources/css/root.scss @@ -1,10 +1,12 @@ /************************************************** * Root **************************************************/ +.root { + -fx-font-size: 15; + -fx-font-weight: bolder; +} .root-pane { - -fx-font-size: 15; - -fx-font-weight: normal; -fx-background-radius: 15; -fx-border-radius: 15; diff --git a/src/main/resources/fxml/about-view.fxml b/src/main/resources/fxml/about-view.fxml index 8bb3008..be018a1 100644 --- a/src/main/resources/fxml/about-view.fxml +++ b/src/main/resources/fxml/about-view.fxml @@ -29,6 +29,6 @@