mirror of
https://github.com/octopusYan/alist-gui.git
synced 2024-11-24 04:56:42 +08:00
feat: 添加系统托盘、静默启动功能
This commit is contained in:
parent
442940cf05
commit
08f0473814
@ -3,6 +3,7 @@ package cn.octopusyan.alistgui;
|
||||
import cn.octopusyan.alistgui.config.Constants;
|
||||
import cn.octopusyan.alistgui.config.Context;
|
||||
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.HttpUtil;
|
||||
import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager;
|
||||
@ -84,6 +85,13 @@ public class Application extends javafx.application.Application {
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.show();
|
||||
|
||||
// 静默启动
|
||||
if (ConfigManager.silentStartup()) {
|
||||
Platform.setImplicitExit(false);
|
||||
primaryStage.hide();
|
||||
SystemTrayManager.show();
|
||||
}
|
||||
|
||||
logger.info("application start over ...");
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,13 @@ 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.manager.ConfigManager;
|
||||
import cn.octopusyan.alistgui.manager.SystemTrayManager;
|
||||
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.application.Platform;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Pos;
|
||||
@ -107,7 +110,15 @@ public class RootController extends BaseController<RootViewModel> {
|
||||
*/
|
||||
@Override
|
||||
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));
|
||||
alwaysOnTopIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> {
|
||||
boolean newVal = !getWindow().isAlwaysOnTop();
|
||||
|
@ -37,6 +37,8 @@ public class SetupController extends BaseController<SetupViewModel> implements I
|
||||
@FXML
|
||||
public CheckBox silentStartupCheckBox;
|
||||
@FXML
|
||||
public CheckBox closeToTrayCheckBox;
|
||||
@FXML
|
||||
public ComboBox<Locale> languageComboBox;
|
||||
@FXML
|
||||
public ComboBox<Theme> themeComboBox;
|
||||
@ -92,6 +94,7 @@ public class SetupController extends BaseController<SetupViewModel> implements I
|
||||
//
|
||||
autoStartCheckBox.selectedProperty().bindBidirectional(viewModel.autoStartProperty());
|
||||
silentStartupCheckBox.selectedProperty().bindBidirectional(viewModel.silentStartupProperty());
|
||||
closeToTrayCheckBox.selectedProperty().bindBidirectional(viewModel.closeToTrayProperty());
|
||||
proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty());
|
||||
proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty());
|
||||
|
||||
|
@ -237,6 +237,16 @@ public class ConfigManager {
|
||||
guiConfig.setSilentStartup(startup);
|
||||
}
|
||||
|
||||
// --------------------------------{ 最小化到托盘 }------------------------------------------
|
||||
|
||||
public static boolean closeToTray() {
|
||||
return guiConfig.getCloseToTray();
|
||||
}
|
||||
|
||||
public static void closeToTray(boolean check) {
|
||||
guiConfig.setCloseToTray(check);
|
||||
}
|
||||
|
||||
// --------------------------------{ 版本检查 }------------------------------------------
|
||||
|
||||
public static UpgradeConfig upgradeConfig() {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ public class GuiConfig {
|
||||
|
||||
private Boolean autoStart = false;
|
||||
private Boolean silentStartup = false;
|
||||
private Boolean closeToTray = true;
|
||||
@JsonProperty("proxy")
|
||||
private ProxyInfo proxyInfo;
|
||||
@JsonProperty("proxy.testUrl")
|
||||
|
@ -2,17 +2,25 @@ package cn.octopusyan.alistgui.util;
|
||||
|
||||
import cn.octopusyan.alistgui.Application;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工具
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
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) {
|
||||
pane.setStyle("""
|
||||
@ -29,6 +37,13 @@ public class WindowsUtil {
|
||||
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) {
|
||||
pane.setOnMousePressed(event -> {
|
||||
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) {
|
||||
try {
|
||||
return (Stage) pane.getScene().getWindow();
|
||||
} catch (Throwable e) {
|
||||
return Application.getPrimaryStage();
|
||||
return getStage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
117
src/main/java/cn/octopusyan/alistgui/view/PopupMenu.java
Normal file
117
src/main/java/cn/octopusyan/alistgui/view/PopupMenu.java
Normal 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;
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import java.util.Locale;
|
||||
public class SetupViewModel extends BaseViewModel {
|
||||
private final BooleanProperty autoStart = new SimpleBooleanProperty(ConfigManager.autoStart());
|
||||
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 StringProperty proxyHost = new SimpleStringProperty(ConfigManager.proxyHost());
|
||||
private final StringProperty proxyPort = new SimpleStringProperty(ConfigManager.proxyPort());
|
||||
@ -50,6 +51,18 @@ public class SetupViewModel extends BaseViewModel {
|
||||
}
|
||||
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));
|
||||
proxyTestUrl.addListener((_, _, newValue) -> ConfigManager.proxyTestUrl(newValue));
|
||||
proxyHost.addListener((_, _, newValue) -> ConfigManager.proxyHost(newValue));
|
||||
@ -69,6 +82,10 @@ public class SetupViewModel extends BaseViewModel {
|
||||
return silentStartup;
|
||||
}
|
||||
|
||||
public BooleanProperty closeToTrayProperty() {
|
||||
return closeToTray;
|
||||
}
|
||||
|
||||
public ObjectProperty<Locale> languageProperty() {
|
||||
return language;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
module cn.octopusyan.alistgui {
|
||||
requires java.desktop;
|
||||
requires java.net.http;
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
|
Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 960 B |
@ -12,6 +12,7 @@
|
||||
</padding>
|
||||
<CheckBox fx:id="autoStartCheckBox" text="%setup.auto-start.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">
|
||||
<Label text="%setup.theme"/>
|
||||
<ComboBox fx:id="themeComboBox"/>
|
||||
|
@ -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.user-field=\u7528\u6237\uFF1A
|
||||
admin.pwd.pwd-field=\u5BC6\u7801\uFF1A
|
||||
setup.close-to-tray.label=\u5173\u95ED\u65F6\u6700\u5C0F\u5316\u5230\u6258\u76D8
|
||||
|
||||
|
||||
|
||||
|
@ -48,5 +48,6 @@ admin.pwd.title=Admin Password
|
||||
admin.pwd.toptip=The new password will only be displayed once
|
||||
admin.pwd.user-field=User:
|
||||
admin.pwd.pwd-field=Password :
|
||||
setup.close-to-tray.label=Minimize to tray when closed
|
||||
|
||||
|
||||
|
@ -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.user-field=\u7528\u6237\uFF1A
|
||||
admin.pwd.pwd-field=\u5BC6\u7801\uFF1A
|
||||
setup.close-to-tray.label=\u5173\u95ED\u65F6\u6700\u5C0F\u5316\u5230\u6258\u76D8
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user