diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3608f2..16a7cd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,10 +121,12 @@ jobs: mkdir zipball && cp target/*.zip zipball - name: Upload AListGUI to Github + if: startsWith(github.ref, 'refs/tags/v') uses: actions/upload-artifact@v4 with: name: AListGUI-windows path: zipball + retention-days: 5 release: if: startsWith(github.ref, 'refs/tags/v') diff --git a/.gitignore b/.gitignore index 5a49883..a2a9479 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ -/config/ /bin/ +gui.yaml ### IntelliJ IDEA ### .idea/ diff --git a/pom.xml b/pom.xml index b2b1ae5..8ef597e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ cn.octopusyan alist-gui - 1.0.1 + 1.0.2 alist-gui @@ -238,6 +238,7 @@ cn.octopusyan.alistgui/${exec.mainClass} + diff --git a/src/main/java/cn/octopusyan/alistgui/Application.java b/src/main/java/cn/octopusyan/alistgui/Application.java index 26169e5..79c106f 100644 --- a/src/main/java/cn/octopusyan/alistgui/Application.java +++ b/src/main/java/cn/octopusyan/alistgui/Application.java @@ -18,11 +18,12 @@ import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ProxySelector; +import java.io.*; +import java.net.*; import java.net.http.HttpClient; import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; public class Application extends javafx.application.Application { private static final Logger logger = LoggerFactory.getLogger(Application.class); @@ -32,6 +33,10 @@ public class Application extends javafx.application.Application { @Override public void init() { logger.info("application init ..."); + + // 单例模式检查 + makeSingle(); + // 初始化客户端配置 ConfigManager.load(); @@ -83,6 +88,7 @@ public class Application extends javafx.application.Application { primaryStage.setTitle(String.format("%s v%s", Constants.APP_TITLE, Constants.APP_VERSION)); Scene scene = Context.initScene(); primaryStage.setScene(scene); + primaryStage.show(); // 静默启动 @@ -113,4 +119,84 @@ public class Application extends javafx.application.Application { Platform.exit(); System.exit(0); } + + private static final int SINGLE_INSTANCE_LISTENER_PORT = 9009; + private static final String SINGLE_INSTANCE_FOCUS_MESSAGE = "focus"; + + private static final String instanceId = UUID.randomUUID().toString(); + + /** + * 我们在聚焦现有实例之前定义一个暂停 + * 因为有时启动实例的命令行或窗口 + * 可能会在第二个实例执行完成后重新获得焦点 + * 所以我们在聚焦原始窗口之前引入了一个短暂的延迟 + * 以便原始窗口可以保留焦点。 + */ + private static final int FOCUS_REQUEST_PAUSE_MILLIS = 500; + + /** + * 单实例检测 + * + * @see JavaFX单实例运行应用程序 + */ + public static void makeSingle() { + CountDownLatch instanceCheckLatch = new CountDownLatch(1); + + Thread instanceListener = new Thread(() -> { + try (ServerSocket serverSocket = new ServerSocket(SINGLE_INSTANCE_LISTENER_PORT, 10)) { + instanceCheckLatch.countDown(); + + while (true) { + try ( + Socket clientSocket = serverSocket.accept(); + BufferedReader in = new BufferedReader( + new InputStreamReader(clientSocket.getInputStream())) + ) { + String input = in.readLine(); + logger.info(STR."Received single instance listener message: \{input}"); + if (input.startsWith(SINGLE_INSTANCE_FOCUS_MESSAGE) && primaryStage != null) { + //noinspection BusyWait + Thread.sleep(FOCUS_REQUEST_PAUSE_MILLIS); + Platform.runLater(() -> { + logger.info(STR."To front \{instanceId}"); + primaryStage.setIconified(false); + primaryStage.show(); + primaryStage.toFront(); + }); + } + } catch (IOException e) { + logger.error("Single instance listener unable to process focus message from client"); + } + } + } catch (java.net.BindException b) { + logger.error("SingleInstanceApp already running"); + + try ( + Socket clientSocket = new Socket(InetAddress.getLocalHost(), SINGLE_INSTANCE_LISTENER_PORT); + PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream())) + ) { + logger.info("Requesting existing app to focus"); + out.println(STR."\{SINGLE_INSTANCE_FOCUS_MESSAGE} requested by \{instanceId}"); + } catch (IOException e) { + logger.error("", e); + } + + logger.info(STR."Aborting execution for instance \{instanceId}"); + Platform.exit(); + } catch (Exception e) { + logger.error("", e); + } finally { + instanceCheckLatch.countDown(); + } + }, "instance-listener"); + instanceListener.setDaemon(true); + instanceListener.start(); + + try { + instanceCheckLatch.await(); + } catch (InterruptedException e) { + //noinspection ResultOfMethodCallIgnored + Thread.interrupted(); + } + } } \ No newline at end of file diff --git a/src/main/java/cn/octopusyan/alistgui/config/Constants.java b/src/main/java/cn/octopusyan/alistgui/config/Constants.java index 59db2bd..7127f24 100644 --- a/src/main/java/cn/octopusyan/alistgui/config/Constants.java +++ b/src/main/java/cn/octopusyan/alistgui/config/Constants.java @@ -20,8 +20,7 @@ public class Constants { public static final String TMP_DIR_PATH = System.getProperty("java.io.tmpdir") + APP_NAME; public static final String ALIST_FILE = STR."\{BIN_DIR_PATH}\{File.separator}alist.exe"; - public static final String CONFIG_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}config"; - public static final String GUI_CONFIG_PATH = STR."\{CONFIG_DIR_PATH}\{File.separator}gui.yaml"; + public static final String GUI_CONFIG_PATH = STR."\{DATA_DIR_PATH}\{File.separator}gui.yaml"; public static final String BAK_FILE_PATH = STR."\{Constants.TMP_DIR_PATH}\{File.separator}bak"; public static final String REG_AUTO_RUN = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"; diff --git a/src/main/java/cn/octopusyan/alistgui/controller/RootController.java b/src/main/java/cn/octopusyan/alistgui/controller/RootController.java index 99c85e5..efd9e83 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/RootController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/RootController.java @@ -114,12 +114,13 @@ public class RootController extends BaseController { @Override public void initViewAction() { closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> { + Platform.setImplicitExit(!ConfigManager.closeToTray()); if (ConfigManager.closeToTray()) { SystemTrayManager.show(); } else { SystemTrayManager.hide(); + Platform.exit(); } - Platform.setImplicitExit(!ConfigManager.closeToTray()); getWindow().close(); }); minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().setIconified(true)); diff --git a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java index 5e2f529..1f12631 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java @@ -37,8 +37,14 @@ public class SetupController extends BaseController implements I public CheckBox silentStartupCheckBox; @I18n(key = "setup.close-to-tray.label") public CheckBox closeToTrayCheckBox; - public ComboBox languageComboBox; + @I18n(key = "setup.theme") + public Label themeLabel; public ComboBox themeComboBox; + @I18n(key = "setup.language") + public Label languageLabel; + public ComboBox languageComboBox; + @I18n(key = "setup.proxy") + public Label proxySetupLabel; public ComboBox proxySetupComboBox; public Pane proxySetupPane; @I18n(key = "setup.proxy.test") diff --git a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java index d3e1992..80b5c36 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java @@ -84,10 +84,10 @@ public class ConfigManager { File parent = FileUtil.getParent(src, 1); if (!parent.exists()) { boolean wasSuccessful = parent.mkdirs(); - objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance()); if (!wasSuccessful) logger.error("{} 创建失败", src.getAbsolutePath()); } + objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance()); } public static void save() { @@ -278,6 +278,10 @@ public class ConfigManager { } public static String guiVersion() { + // 覆盖配置文件读取的版本号 + if (!Constants.APP_VERSION.equals(gui().getVersion())) { + guiVersion(Constants.APP_VERSION); + } return gui().getVersion(); } diff --git a/src/main/resources/fxml/setup-view.fxml b/src/main/resources/fxml/setup-view.fxml index eb3888e..4ea0c23 100644 --- a/src/main/resources/fxml/setup-view.fxml +++ b/src/main/resources/fxml/setup-view.fxml @@ -14,15 +14,15 @@ - - -