feat: 添加系统托盘、静默启动功能

This commit is contained in:
octopus_yan 2024-09-17 02:37:45 +08:00
parent 442940cf05
commit 08f0473814
15 changed files with 381 additions and 4 deletions

View File

@ -3,6 +3,7 @@ package cn.octopusyan.alistgui;
import cn.octopusyan.alistgui.config.Constants; import cn.octopusyan.alistgui.config.Constants;
import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.config.Context;
import cn.octopusyan.alistgui.manager.ConfigManager; import cn.octopusyan.alistgui.manager.ConfigManager;
import cn.octopusyan.alistgui.manager.SystemTrayManager;
import cn.octopusyan.alistgui.manager.http.HttpConfig; import cn.octopusyan.alistgui.manager.http.HttpConfig;
import cn.octopusyan.alistgui.manager.http.HttpUtil; import cn.octopusyan.alistgui.manager.http.HttpUtil;
import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager; import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager;
@ -84,6 +85,13 @@ public class Application extends javafx.application.Application {
primaryStage.setScene(scene); primaryStage.setScene(scene);
primaryStage.show(); primaryStage.show();
// 静默启动
if (ConfigManager.silentStartup()) {
Platform.setImplicitExit(false);
primaryStage.hide();
SystemTrayManager.show();
}
logger.info("application start over ..."); logger.info("application start over ...");
} }

View File

@ -3,10 +3,13 @@ package cn.octopusyan.alistgui.controller;
import atlantafx.base.controls.ModalPane; import atlantafx.base.controls.ModalPane;
import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.base.BaseController;
import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.config.Context;
import cn.octopusyan.alistgui.manager.ConfigManager;
import cn.octopusyan.alistgui.manager.SystemTrayManager;
import cn.octopusyan.alistgui.util.WindowsUtil; import cn.octopusyan.alistgui.util.WindowsUtil;
import cn.octopusyan.alistgui.viewModel.RootViewModel; import cn.octopusyan.alistgui.viewModel.RootViewModel;
import com.gluonhq.emoji.EmojiData; import com.gluonhq.emoji.EmojiData;
import com.gluonhq.emoji.util.EmojiImageUtils; import com.gluonhq.emoji.util.EmojiImageUtils;
import javafx.application.Platform;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -107,7 +110,15 @@ public class RootController extends BaseController<RootViewModel> {
*/ */
@Override @Override
public void initViewAction() { public void initViewAction() {
closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().close()); closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> {
if (ConfigManager.closeToTray()) {
SystemTrayManager.show();
} else {
SystemTrayManager.hide();
}
Platform.setImplicitExit(!ConfigManager.closeToTray());
getWindow().close();
});
minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().setIconified(true)); minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().setIconified(true));
alwaysOnTopIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> { alwaysOnTopIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> {
boolean newVal = !getWindow().isAlwaysOnTop(); boolean newVal = !getWindow().isAlwaysOnTop();

View File

@ -37,6 +37,8 @@ public class SetupController extends BaseController<SetupViewModel> implements I
@FXML @FXML
public CheckBox silentStartupCheckBox; public CheckBox silentStartupCheckBox;
@FXML @FXML
public CheckBox closeToTrayCheckBox;
@FXML
public ComboBox<Locale> languageComboBox; public ComboBox<Locale> languageComboBox;
@FXML @FXML
public ComboBox<Theme> themeComboBox; public ComboBox<Theme> themeComboBox;
@ -92,6 +94,7 @@ public class SetupController extends BaseController<SetupViewModel> implements I
// //
autoStartCheckBox.selectedProperty().bindBidirectional(viewModel.autoStartProperty()); autoStartCheckBox.selectedProperty().bindBidirectional(viewModel.autoStartProperty());
silentStartupCheckBox.selectedProperty().bindBidirectional(viewModel.silentStartupProperty()); silentStartupCheckBox.selectedProperty().bindBidirectional(viewModel.silentStartupProperty());
closeToTrayCheckBox.selectedProperty().bindBidirectional(viewModel.closeToTrayProperty());
proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty()); proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty());
proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty()); proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty());

View File

@ -237,6 +237,16 @@ public class ConfigManager {
guiConfig.setSilentStartup(startup); guiConfig.setSilentStartup(startup);
} }
// --------------------------------{ 最小化到托盘 }------------------------------------------
public static boolean closeToTray() {
return guiConfig.getCloseToTray();
}
public static void closeToTray(boolean check) {
guiConfig.setCloseToTray(check);
}
// --------------------------------{ 版本检查 }------------------------------------------ // --------------------------------{ 版本检查 }------------------------------------------
public static UpgradeConfig upgradeConfig() { public static UpgradeConfig upgradeConfig() {

View File

@ -0,0 +1,186 @@
package cn.octopusyan.alistgui.manager;
import cn.octopusyan.alistgui.Application;
import cn.octopusyan.alistgui.config.Constants;
import cn.octopusyan.alistgui.config.Context;
import cn.octopusyan.alistgui.util.WindowsUtil;
import cn.octopusyan.alistgui.view.PopupMenu;
import javafx.application.Platform;
import javafx.scene.control.MenuItem;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.List;
/**
* 系统托盘管理
*
* @author octopus_yan
*/
@Slf4j
public class SystemTrayManager {
// 托盘工具
private static final SystemTray systemTray;
private static TrayIcon trayIcon;
private static PopupMenu popupMenu;
static {
//检查系统是否支持托盘
if (!SystemTray.isSupported()) {
//系统托盘不支持
log.info("{}:系统托盘不支持", Thread.currentThread().getStackTrace()[1].getClassName());
systemTray = null;
} else {
systemTray = SystemTray.getSystemTray();
}
}
public static void toolTip(String toptip) {
if (trayIcon == null) return;
trayIcon.setToolTip(toptip);
}
public static void icon(String path) {
if (trayIcon == null) return;
icon(WindowsUtil.class.getResource(path));
}
public static void icon(URL url) {
if (trayIcon == null) return;
icon(Toolkit.getDefaultToolkit().getImage(url));
}
public static void icon(Image image) {
if (trayIcon == null) return;
trayIcon.setImage(image);
}
public static boolean isShowing() {
if (systemTray == null) return false;
return List.of(systemTray.getTrayIcons()).contains(trayIcon);
}
public static void show() {
// 是否启用托盘
if (!ConfigManager.closeToTray() || systemTray == null) {
if (trayIcon != null && isShowing()) {
hide();
}
return;
}
initTrayIcon();
try {
if (!isShowing())
systemTray.add(trayIcon);
} catch (AWTException e) {
//系统托盘添加失败
log.error("{}:系统添加失败", Thread.currentThread().getStackTrace()[1].getClassName(), e);
}
}
public static void hide() {
if (systemTray == null) return;
systemTray.remove(trayIcon);
}
//========================================={ private }===========================================
private static void initTrayIcon() {
if (trayIcon != null) return;
// 系统托盘图标
Image image = Toolkit.getDefaultToolkit().getImage(WindowsUtil.class.getResource("/assets/logo-disabled.png"));
trayIcon = new TrayIcon(image);
// 设置图标尺寸自动适应
trayIcon.setImageAutoSize(true);
// 弹出式菜单组件
// trayIcon.setPopupMenu(getMenu());
// 鼠标移到系统托盘,会显示提示文本
toolTip(Constants.APP_TITLE);
// 鼠标监听
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent event) {
maybeShowPopup(event);
}
@Override
public void mousePressed(MouseEvent event) {
maybeShowPopup(event);
}
private void maybeShowPopup(MouseEvent event) {
// popup menu trigger event
if (event.isPopupTrigger()) {
// 弹出菜单
Platform.runLater(() -> {
initPopupMenu();
popupMenu.show(event);
});
} else if (event.getButton() == MouseEvent.BUTTON1) {
// 显示 PrimaryStage
Platform.runLater(() -> Application.getPrimaryStage().show());
}
}
});
}
/**
* 构建托盘菜单
*/
private static void initPopupMenu() {
if (popupMenu != null) return;
MenuItem start = PopupMenu.menuItem(getString("main.control.start"), _ -> AListManager.openScheme());
MenuItem browser = PopupMenu.menuItem(getString("main.more.browser"), _ -> AListManager.openScheme());
browser.setDisable(true);
AListManager.runningProperty().addListener((_, _, newValue) -> {
start.setText(getString(STR."main.control.\{newValue ? "stop" : "start"}"));
browser.disableProperty().set(!newValue);
toolTip(STR."AList \{newValue ? "running" : "stopped"}");
icon(STR."/assets/logo\{newValue ? "" : "-disabled"}.png");
});
popupMenu = new PopupMenu()
.addItem(new MenuItem(Constants.APP_TITLE), _ -> stage().show())
.addSeparator()
.addCaptionItem("AList")
.addItem(start, _ -> {
if (AListManager.isRunning()) {
AListManager.stop();
} else {
AListManager.start();
}
})
.addItem(getString("main.control.restart"), _ -> AListManager.restart())
.addMenu(getString("main.control.more"), browser,
PopupMenu.menuItem(getString("main.more.open-config"), _ -> AListManager.openConfig()),
PopupMenu.menuItem(getString("main.more.open-log"), _ -> AListManager.openLogFolder()))
.addSeparator()
.addExitItem();
}
private static String getString(String key) {
return Context.getLanguageBinding(key).get();
}
private static Stage stage() {
return WindowsUtil.getStage();
}
}

View File

@ -18,6 +18,7 @@ public class GuiConfig {
private Boolean autoStart = false; private Boolean autoStart = false;
private Boolean silentStartup = false; private Boolean silentStartup = false;
private Boolean closeToTray = true;
@JsonProperty("proxy") @JsonProperty("proxy")
private ProxyInfo proxyInfo; private ProxyInfo proxyInfo;
@JsonProperty("proxy.testUrl") @JsonProperty("proxy.testUrl")

View File

@ -2,17 +2,25 @@ package cn.octopusyan.alistgui.util;
import cn.octopusyan.alistgui.Application; import cn.octopusyan.alistgui.Application;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.stage.Screen;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* 工具
*
* @author octopus_yan * @author octopus_yan
*/ */
public class WindowsUtil { public class WindowsUtil {
public static final Map<Pane, Double> paneXOffset = new HashMap<>(); // 获取系统缩放比
public static final Map<Pane, Double> paneYOffset = new HashMap<>(); public static final double scaleX = Screen.getPrimary().getOutputScaleX();
public static final double scaleY = Screen.getPrimary().getOutputScaleY();
private static final Map<Pane, Double> paneXOffset = new HashMap<>();
private static final Map<Pane, Double> paneYOffset = new HashMap<>();
public static void bindShadow(Pane pane) { public static void bindShadow(Pane pane) {
pane.setStyle(""" pane.setStyle("""
@ -29,6 +37,13 @@ public class WindowsUtil {
bindDragged(pane, stage); bindDragged(pane, stage);
} }
public static void unbindDragged(Pane pane) {
pane.setOnMousePressed(null);
pane.setOnMouseDragged(null);
paneXOffset.remove(pane);
paneYOffset.remove(pane);
}
public static void bindDragged(Pane pane, Stage stage) { public static void bindDragged(Pane pane, Stage stage) {
pane.setOnMousePressed(event -> { pane.setOnMousePressed(event -> {
paneXOffset.put(pane, stage.getX() - event.getScreenX()); paneXOffset.put(pane, stage.getX() - event.getScreenX());
@ -40,11 +55,15 @@ public class WindowsUtil {
}); });
} }
public static Stage getStage() {
return Application.getPrimaryStage();
}
public static Stage getStage(Pane pane) { public static Stage getStage(Pane pane) {
try { try {
return (Stage) pane.getScene().getWindow(); return (Stage) pane.getScene().getWindow();
} catch (Throwable e) { } catch (Throwable e) {
return Application.getPrimaryStage(); return getStage();
} }
} }
} }

View File

@ -0,0 +1,117 @@
package cn.octopusyan.alistgui.view;
import atlantafx.base.controls.CaptionMenuItem;
import cn.octopusyan.alistgui.config.Constants;
import cn.octopusyan.alistgui.util.WindowsUtil;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* 托盘图标 菜单
*
* @author octopus_yan
*/
public class PopupMenu {
// 用来隐藏弹出窗口的任务栏图标
private static final Stage utilityStage = new Stage();
// 菜单栏
private final ContextMenu root = new ContextMenu();
static {
utilityStage.initStyle(StageStyle.UTILITY);
utilityStage.setScene(new Scene(new Region()));
utilityStage.setOpacity(0);
}
public PopupMenu() {
root.focusedProperty().addListener((_, _, focused) -> {
if (!focused)
Platform.runLater(() -> {
root.hide();
utilityStage.hide();
});
});
}
public PopupMenu addItem(String label, EventHandler<ActionEvent> handler) {
return addItem(new MenuItem(label), handler);
}
public PopupMenu addItem(MenuItem node, EventHandler<ActionEvent> handler) {
node.setOnAction(handler);
return addItem(node);
}
public PopupMenu addSeparator() {
return addItem(new SeparatorMenuItem());
}
public PopupMenu addCaptionItem() {
return addCaptionItem(null);
}
public PopupMenu addCaptionItem(String title) {
return addItem(new CaptionMenuItem(title));
}
public PopupMenu addMenu(String label, MenuItem... items) {
return addMenu(new Menu(label), items);
}
public PopupMenu addMenu(Menu menu, MenuItem... items) {
menu.getItems().addAll(items);
return addItem(menu);
}
public PopupMenu addTitleItem() {
return addTitleItem(Constants.APP_TITLE);
}
public PopupMenu addTitleItem(String label) {
return addExitItem(label);
}
public PopupMenu addExitItem() {
return addExitItem("Exit");
}
public PopupMenu addExitItem(String label) {
return addItem(label, _ -> Platform.exit());
}
private PopupMenu addItem(MenuItem node) {
root.getItems().add(node);
return this;
}
public void show(java.awt.event.MouseEvent event) {
// 必须调用show才会隐藏任务栏图标
utilityStage.show();
if (root.isShowing())
root.hide();
root.show(utilityStage,
event.getX() / WindowsUtil.scaleX,
event.getY() / WindowsUtil.scaleY
);
// 获取焦点 (失去焦点隐藏自身)
root.requestFocus();
}
public static MenuItem menuItem(String label, EventHandler<ActionEvent> handler) {
MenuItem menuItem = new MenuItem(label);
menuItem.setOnAction(handler);
return menuItem;
}
}

View File

@ -26,6 +26,7 @@ import java.util.Locale;
public class SetupViewModel extends BaseViewModel { public class SetupViewModel extends BaseViewModel {
private final BooleanProperty autoStart = new SimpleBooleanProperty(ConfigManager.autoStart()); private final BooleanProperty autoStart = new SimpleBooleanProperty(ConfigManager.autoStart());
private final BooleanProperty silentStartup = new SimpleBooleanProperty(ConfigManager.silentStartup()); private final BooleanProperty silentStartup = new SimpleBooleanProperty(ConfigManager.silentStartup());
private final BooleanProperty closeToTray = new SimpleBooleanProperty(ConfigManager.closeToTray());
private final ObjectProperty<Theme> theme = new SimpleObjectProperty<>(ConfigManager.theme()); private final ObjectProperty<Theme> theme = new SimpleObjectProperty<>(ConfigManager.theme());
private final StringProperty proxyHost = new SimpleStringProperty(ConfigManager.proxyHost()); private final StringProperty proxyHost = new SimpleStringProperty(ConfigManager.proxyHost());
private final StringProperty proxyPort = new SimpleStringProperty(ConfigManager.proxyPort()); private final StringProperty proxyPort = new SimpleStringProperty(ConfigManager.proxyPort());
@ -50,6 +51,18 @@ public class SetupViewModel extends BaseViewModel {
} }
ConfigManager.autoStart(newValue); ConfigManager.autoStart(newValue);
}); });
silentStartup.addListener((_, _, newValue) -> {
// 开启时检查托盘选项
if (newValue && !closeToTray.get()) closeToTray.set(true);
ConfigManager.silentStartup(newValue);
});
closeToTray.addListener((_, _, newValue) -> {
// 开启时检查托盘选项
if (!newValue && silentStartup.get()) silentStartup.set(false);
ConfigManager.closeToTray(newValue);
});
proxySetup.addListener((_, _, newValue) -> ConfigManager.proxySetup(newValue)); proxySetup.addListener((_, _, newValue) -> ConfigManager.proxySetup(newValue));
proxyTestUrl.addListener((_, _, newValue) -> ConfigManager.proxyTestUrl(newValue)); proxyTestUrl.addListener((_, _, newValue) -> ConfigManager.proxyTestUrl(newValue));
proxyHost.addListener((_, _, newValue) -> ConfigManager.proxyHost(newValue)); proxyHost.addListener((_, _, newValue) -> ConfigManager.proxyHost(newValue));
@ -69,6 +82,10 @@ public class SetupViewModel extends BaseViewModel {
return silentStartup; return silentStartup;
} }
public BooleanProperty closeToTrayProperty() {
return closeToTray;
}
public ObjectProperty<Locale> languageProperty() { public ObjectProperty<Locale> languageProperty() {
return language; return language;
} }

View File

@ -1,4 +1,5 @@
module cn.octopusyan.alistgui { module cn.octopusyan.alistgui {
requires java.desktop;
requires java.net.http; requires java.net.http;
requires javafx.controls; requires javafx.controls;
requires javafx.fxml; requires javafx.fxml;

View File

Before

Width:  |  Height:  |  Size: 960 B

After

Width:  |  Height:  |  Size: 960 B

View File

@ -12,6 +12,7 @@
</padding> </padding>
<CheckBox fx:id="autoStartCheckBox" text="%setup.auto-start.label"/> <CheckBox fx:id="autoStartCheckBox" text="%setup.auto-start.label"/>
<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"/>
<HBox alignment="CENTER_LEFT" spacing="10"> <HBox alignment="CENTER_LEFT" spacing="10">
<Label text="%setup.theme"/> <Label text="%setup.theme"/>
<ComboBox fx:id="themeComboBox"/> <ComboBox fx:id="themeComboBox"/>

View File

@ -48,6 +48,7 @@ admin.pwd.title=\u7BA1\u7406\u5458\u5BC6\u7801
admin.pwd.toptip=\u65B0\u5BC6\u7801\u53EA\u4F1A\u663E\u793A\u4E00\u6B21 admin.pwd.toptip=\u65B0\u5BC6\u7801\u53EA\u4F1A\u663E\u793A\u4E00\u6B21
admin.pwd.user-field=\u7528\u6237\uFF1A admin.pwd.user-field=\u7528\u6237\uFF1A
admin.pwd.pwd-field=\u5BC6\u7801\uFF1A admin.pwd.pwd-field=\u5BC6\u7801\uFF1A
setup.close-to-tray.label=\u5173\u95ED\u65F6\u6700\u5C0F\u5316\u5230\u6258\u76D8

View File

@ -48,5 +48,6 @@ admin.pwd.title=Admin Password
admin.pwd.toptip=The new password will only be displayed once admin.pwd.toptip=The new password will only be displayed once
admin.pwd.user-field=User: admin.pwd.user-field=User:
admin.pwd.pwd-field=Password : admin.pwd.pwd-field=Password :
setup.close-to-tray.label=Minimize to tray when closed

View File

@ -48,5 +48,6 @@ admin.pwd.title=\u7BA1\u7406\u5458\u5BC6\u7801
admin.pwd.toptip=\u65B0\u5BC6\u7801\u53EA\u4F1A\u663E\u793A\u4E00\u6B21 admin.pwd.toptip=\u65B0\u5BC6\u7801\u53EA\u4F1A\u663E\u793A\u4E00\u6B21
admin.pwd.user-field=\u7528\u6237\uFF1A admin.pwd.user-field=\u7528\u6237\uFF1A
admin.pwd.pwd-field=\u5BC6\u7801\uFF1A admin.pwd.pwd-field=\u5BC6\u7801\uFF1A
setup.close-to-tray.label=\u5173\u95ED\u65F6\u6700\u5C0F\u5316\u5230\u6258\u76D8