Compare commits

...

7 Commits

9 changed files with 112 additions and 13 deletions

View File

@ -121,10 +121,12 @@ jobs:
mkdir zipball && cp target/*.zip zipball mkdir zipball && cp target/*.zip zipball
- name: Upload AListGUI to Github - name: Upload AListGUI to Github
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: AListGUI-windows name: AListGUI-windows
path: zipball path: zipball
retention-days: 5
release: release:
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')

2
.gitignore vendored
View File

@ -6,8 +6,8 @@ target/
!.mvn/wrapper/maven-wrapper.jar !.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/ !**/src/main/**/target/
!**/src/test/**/target/ !**/src/test/**/target/
/config/
/bin/ /bin/
gui.yaml
### IntelliJ IDEA ### ### IntelliJ IDEA ###
.idea/ .idea/

View File

@ -6,7 +6,7 @@
<groupId>cn.octopusyan</groupId> <groupId>cn.octopusyan</groupId>
<artifactId>alist-gui</artifactId> <artifactId>alist-gui</artifactId>
<version>1.0.1</version> <version>1.0.2</version>
<name>alist-gui</name> <name>alist-gui</name>
<organization> <organization>
@ -238,6 +238,7 @@
<mainClass>cn.octopusyan.alistgui/${exec.mainClass}</mainClass> <mainClass>cn.octopusyan.alistgui/${exec.mainClass}</mainClass>
<options> <options>
<option>--enable-preview</option> <option>--enable-preview</option>
<!-- <option>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005</option>-->
</options> </options>
</configuration> </configuration>
</execution> </execution>

View File

@ -18,11 +18,12 @@ import lombok.Getter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.*;
import java.net.InetSocketAddress; import java.net.*;
import java.net.ProxySelector;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
public class Application extends javafx.application.Application { public class Application extends javafx.application.Application {
private static final Logger logger = LoggerFactory.getLogger(Application.class); private static final Logger logger = LoggerFactory.getLogger(Application.class);
@ -32,6 +33,10 @@ public class Application extends javafx.application.Application {
@Override @Override
public void init() { public void init() {
logger.info("application init ..."); logger.info("application init ...");
// 单例模式检查
makeSingle();
// 初始化客户端配置 // 初始化客户端配置
ConfigManager.load(); 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)); primaryStage.setTitle(String.format("%s v%s", Constants.APP_TITLE, Constants.APP_VERSION));
Scene scene = Context.initScene(); Scene scene = Context.initScene();
primaryStage.setScene(scene); primaryStage.setScene(scene);
primaryStage.show(); primaryStage.show();
// 静默启动 // 静默启动
@ -113,4 +119,84 @@ public class Application extends javafx.application.Application {
Platform.exit(); Platform.exit();
System.exit(0); 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 <a href='https://www.cnblogs.com/shihaiming/p/13553278.html'>JavaFX单实例运行应用程序</url>
*/
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();
}
}
} }

View File

@ -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 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 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."\{DATA_DIR_PATH}\{File.separator}gui.yaml";
public static final String GUI_CONFIG_PATH = STR."\{CONFIG_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 BAK_FILE_PATH = STR."\{Constants.TMP_DIR_PATH}\{File.separator}bak";
public static final String REG_AUTO_RUN = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"; public static final String REG_AUTO_RUN = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";

View File

@ -114,12 +114,13 @@ public class RootController extends BaseController<RootViewModel> {
@Override @Override
public void initViewAction() { public void initViewAction() {
closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> { closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> {
Platform.setImplicitExit(!ConfigManager.closeToTray());
if (ConfigManager.closeToTray()) { if (ConfigManager.closeToTray()) {
SystemTrayManager.show(); SystemTrayManager.show();
} else { } else {
SystemTrayManager.hide(); SystemTrayManager.hide();
Platform.exit();
} }
Platform.setImplicitExit(!ConfigManager.closeToTray());
getWindow().close(); getWindow().close();
}); });
minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().setIconified(true)); minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().setIconified(true));

View File

@ -37,8 +37,14 @@ public class SetupController extends BaseController<SetupViewModel> implements I
public CheckBox silentStartupCheckBox; public CheckBox silentStartupCheckBox;
@I18n(key = "setup.close-to-tray.label") @I18n(key = "setup.close-to-tray.label")
public CheckBox closeToTrayCheckBox; public CheckBox closeToTrayCheckBox;
public ComboBox<Locale> languageComboBox; @I18n(key = "setup.theme")
public Label themeLabel;
public ComboBox<Theme> themeComboBox; public ComboBox<Theme> themeComboBox;
@I18n(key = "setup.language")
public Label languageLabel;
public ComboBox<Locale> languageComboBox;
@I18n(key = "setup.proxy")
public Label proxySetupLabel;
public ComboBox<ProxySetup> proxySetupComboBox; public ComboBox<ProxySetup> proxySetupComboBox;
public Pane proxySetupPane; public Pane proxySetupPane;
@I18n(key = "setup.proxy.test") @I18n(key = "setup.proxy.test")

View File

@ -84,10 +84,10 @@ public class ConfigManager {
File parent = FileUtil.getParent(src, 1); File parent = FileUtil.getParent(src, 1);
if (!parent.exists()) { if (!parent.exists()) {
boolean wasSuccessful = parent.mkdirs(); boolean wasSuccessful = parent.mkdirs();
objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance());
if (!wasSuccessful) if (!wasSuccessful)
logger.error("{} 创建失败", src.getAbsolutePath()); logger.error("{} 创建失败", src.getAbsolutePath());
} }
objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance());
} }
public static void save() { public static void save() {
@ -278,6 +278,10 @@ public class ConfigManager {
} }
public static String guiVersion() { public static String guiVersion() {
// 覆盖配置文件读取的版本号
if (!Constants.APP_VERSION.equals(gui().getVersion())) {
guiVersion(Constants.APP_VERSION);
}
return gui().getVersion(); return gui().getVersion();
} }

View File

@ -14,15 +14,15 @@
<CheckBox fx:id="silentStartupCheckBox" text="%setup.silent-startup.label"/> <CheckBox fx:id="silentStartupCheckBox" text="%setup.silent-startup.label"/>
<CheckBox fx:id="closeToTrayCheckBox" text="%setup.close-to-tray.label"/> <CheckBox fx:id="closeToTrayCheckBox" text="%setup.close-to-tray.label"/>
<HBox alignment="CENTER_LEFT" spacing="10"> <HBox alignment="CENTER_LEFT" spacing="10">
<Label text="%setup.theme"/> <Label fx:id="themeLabel" text="%setup.theme"/>
<ComboBox fx:id="themeComboBox"/> <ComboBox fx:id="themeComboBox"/>
</HBox> </HBox>
<HBox alignment="CENTER_LEFT" spacing="10"> <HBox alignment="CENTER_LEFT" spacing="10">
<Label text="%setup.language"/> <Label fx:id="languageLabel" text="%setup.language"/>
<ComboBox fx:id="languageComboBox"/> <ComboBox fx:id="languageComboBox"/>
</HBox> </HBox>
<HBox alignment="CENTER_LEFT" spacing="20"> <HBox alignment="CENTER_LEFT" spacing="20">
<Label styleClass="proxy-label" text="%setup.proxy"/> <Label fx:id="proxySetupLabel" styleClass="proxy-label" text="%setup.proxy"/>
<ComboBox fx:id="proxySetupComboBox"/> <ComboBox fx:id="proxySetupComboBox"/>
<Button fx:id="proxyCheck" onAction="#proxyTest" text="%setup.proxy.test"/> <Button fx:id="proxyCheck" onAction="#proxyTest" text="%setup.proxy.test"/>
</HBox> </HBox>